Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ env:
RUSTFLAGS: -Dwarnings

jobs:
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [stable]

steps:
- uses: actions/checkout@master

- name: Install ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: wasm32-wasip2
override: true

- name: check
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples

- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all

check_fmt_and_docs:
name: Checking fmt and docs
runs-on: ubuntu-latest
Expand Down
49 changes: 43 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
[package]
name = "wstd"
version = "0.4.0"
license = "MIT OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception"
version.workspace = true
license.workspace = true
repository = "https://github.com/yoshuawuyts/wstd"
documentation = "https://docs.rs/wstd"
description = "An async standard library for Wasm Components and WASI 0.2"
readme = "README.md"
edition = "2021"
resolver = "2"
edition.workspace = true
keywords = ["WebAssembly", "async", "stdlib", "Components"]
categories = []
authors = [
Expand All @@ -17,8 +16,46 @@ authors = [
[features]

[dependencies]
slab.workspace = true
url.workspace = true
wasi.workspace = true
wstd-macro.workspace = true

[dev-dependencies]
anyhow.workspace = true
serde_json.workspace = true
test-programs-artifacts.workspace = true
wasmtime.workspace = true
wasmtime-wasi.workspace = true
wasmtime-wasi-http.workspace = true

[workspace]
members = [
"macro",
"test-programs",
"test-programs/artifacts",
]
resolver = "2"

[workspace.package]
version = "0.4.0"
edition = "2021"
license = "MIT OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception"

[workspace.dependencies]
anyhow = "1"
cargo_metadata = "0.18.1"
heck = "0.5"
quote = "1.0"
serde_json = "1"
slab = "0.4.9"
syn = "2.0"
test-programs = { path = "test-programs" }
test-programs-artifacts = { path = "test-programs/artifacts" }
url = "2.5.0"
wasi = "0.13.1"

[dev-dependencies]
wasmtime = "26"
wasmtime-wasi = "26"
wasmtime-wasi-http = "26"
wstd = { path = "." }
wstd-macro = { path = "macro" }
34 changes: 34 additions & 0 deletions examples/http_get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::error::Error;
use wstd::http::{Client, Method, Request, Url};
use wstd::io::AsyncRead;

#[wstd::main]
async fn main() -> Result<(), Box<dyn Error>> {
let request = Request::new(Method::Get, Url::parse("https://postman-echo.com/get")?);
let mut response = Client::new().send(request).await?;

let content_type = response
.headers()
.get(&"content-type".into())
.ok_or_else(|| "response expected to have content-type header")?;
assert_eq!(content_type.len(), 1, "one header value for content-type");
assert_eq!(content_type[0], b"application/json; charset=utf-8");

// Would much prefer read_to_end here:
let mut body_buf = vec![0; 4096];
let body_len = response.body().read(&mut body_buf).await?;
body_buf.truncate(body_len);

let val: serde_json::Value = serde_json::from_slice(&body_buf)?;
let body_url = val
.get("url")
.ok_or_else(|| "body json has url")?
.as_str()
.ok_or_else(|| "body json url is str")?;
assert!(
body_url.contains("postman-echo.com/get"),
"expected body url to contain the authority and path, got: {body_url}"
);

Ok(())
}
18 changes: 18 additions & 0 deletions examples/tcp_echo_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use wstd::io;
use wstd::iter::AsyncIterator;
use wstd::net::TcpListener;

#[wstd::main]
async fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Listening on {}", listener.local_addr()?);
println!("type `nc localhost 8080` to create a TCP client");

let mut incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let stream = stream?;
println!("Accepted from: {}", stream.peer_addr()?);
io::copy(&stream, &stream).await?;
}
Ok(())
}
12 changes: 12 additions & 0 deletions macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "wstd-macro"
version.workspace = true
edition.workspace = true
license.workspace = true

[lib]
proc-macro = true

[dependencies]
syn = { workspace = true, features = ["full"] }
quote.workspace = true
46 changes: 46 additions & 0 deletions macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, spanned::Spanned, ItemFn};

#[proc_macro_attribute]
pub fn attr_macro_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);

if input.sig.asyncness.is_none() {
return quote_spanned! { input.sig.fn_token.span()=>
compile_error!("fn must be `async fn`");
}
.into();
}

if input.sig.ident != "main" {
return quote_spanned! { input.sig.ident.span()=>
compile_error!("only `async fn main` can be used for #[wstd::main]");
}
.into();
}

if !input.sig.inputs.is_empty() {
return quote_spanned! { input.sig.inputs.span()=>
compile_error!("arguments to main are not supported");
}
.into();
}
let attrs = input.attrs;
let output = input.sig.output;
let block = input.block;
quote! {
pub fn main() #output {

#(#attrs)*
async fn __run() #output {
#block
}

::wstd::runtime::block_on(async {
__run().await
})
}
}
.into()
}
3 changes: 3 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "1.82.0"
targets = ["wasm32-wasip2"]
27 changes: 8 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,14 @@
//!
//! **TCP echo server**
//!
//! ```rust
//! use wstd::io;
//! use wstd::iter::AsyncIterator;
//! use wstd::net::TcpListener;
//! use wstd::runtime::block_on;
//! ```rust,no_run
#![doc = include_str!("../examples/tcp_echo_server.rs")]
//! ```
//!
//! fn main() -> io::Result<()> {
//! block_on(async move {
//! let listener = TcpListener::bind("127.0.0.1:8080").await?;
//! println!("Listening on {}", listener.local_addr()?);
//! println!("type `nc localhost 8080` to create a TCP client");
//! **HTTP Client**
//!
//! let mut incoming = listener.incoming();
//! while let Some(stream) = incoming.next().await {
//! let stream = stream?;
//! println!("Accepted from: {}", stream.peer_addr()?);
//! io::copy(&stream, &stream).await?;
//! }
//! Ok(())
//! })
//! }
//! ```rust,no_run
#![doc = include_str!("../examples/http_get.rs")]
//! ```
//!
//! # Design Decisions
Expand Down Expand Up @@ -64,3 +51,5 @@ pub mod net;
pub mod rand;
pub mod runtime;
pub mod time;

pub use wstd_macro::attr_macro_main as main;
9 changes: 9 additions & 0 deletions test-programs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "test-programs"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
serde_json.workspace = true
wstd.workspace = true
12 changes: 12 additions & 0 deletions test-programs/artifacts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "test-programs-artifacts"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false

[dependencies]

[build-dependencies]
cargo_metadata.workspace = true
heck.workspace = true
62 changes: 62 additions & 0 deletions test-programs/artifacts/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use heck::ToShoutySnakeCase;
use std::env::var_os;
use std::path::PathBuf;
use std::process::Command;

fn main() {
let out_dir = PathBuf::from(var_os("OUT_DIR").expect("OUT_DIR env var exists"));

let meta = cargo_metadata::MetadataCommand::new()
.exec()
.expect("cargo metadata");
let test_programs_meta = meta
.packages
.iter()
.find(|p| p.name == "test-programs")
.expect("test-programs is in cargo metadata");
let test_programs_root = test_programs_meta.manifest_path.parent().unwrap();
println!(
"cargo:rerun-if-changed={}",
test_programs_root.as_os_str().to_str().unwrap()
);

let status = Command::new("cargo")
.arg("build")
.arg("--target=wasm32-wasip2")
.arg("--package=test-programs")
.env("CARGO_TARGET_DIR", &out_dir)
.env("CARGO_PROFILE_DEV_DEBUG", "2")
.env("RUSTFLAGS", rustflags())
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.status()
.expect("cargo build test programs");
assert!(status.success());

let mut generated_code = "// THIS FILE IS GENERATED CODE\n".to_string();

for binary in test_programs_meta
.targets
.iter()
.filter(|t| t.kind == ["bin"])
{
let component_path = out_dir
.join("wasm32-wasip2")
.join("debug")
.join(format!("{}.wasm", binary.name));

let const_name = binary.name.to_shouty_snake_case();
generated_code += &format!(
"pub const {const_name}: &str = {:?};\n",
component_path.as_os_str().to_str().expect("path is str")
);
}

std::fs::write(out_dir.join("gen.rs"), generated_code).unwrap();
}

fn rustflags() -> &'static str {
match option_env!("RUSTFLAGS") {
Some(s) if s.contains("-D warnings") => "-D warnings",
_ => "",
}
}
1 change: 1 addition & 0 deletions test-programs/artifacts/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/gen.rs"));
1 change: 1 addition & 0 deletions test-programs/src/bin/http_get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include!("../../../examples/http_get.rs");
1 change: 1 addition & 0 deletions test-programs/src/bin/tcp_echo_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include!("../../../examples/tcp_echo_server.rs");
Loading