From 4ef81c31a7aa566283a5a32d0a827c8364799173 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 17 Oct 2025 10:18:43 -0700
Subject: [PATCH 01/11] add wstd-axum, for http servers written using the axum
framework
---
.github/workflows/ci.yaml | 2 +-
Cargo.toml | 8 +++++-
axum/Cargo.toml | 10 ++++++++
axum/examples/hello_world.rs | 18 ++++++++++++++
axum/macro/.gitignore | 1 +
axum/macro/Cargo.toml | 11 +++++++++
axum/macro/src/lib.rs | 47 ++++++++++++++++++++++++++++++++++++
axum/src/lib.rs | 27 +++++++++++++++++++++
8 files changed, 122 insertions(+), 2 deletions(-)
create mode 100644 axum/Cargo.toml
create mode 100644 axum/examples/hello_world.rs
create mode 100644 axum/macro/.gitignore
create mode 100644 axum/macro/Cargo.toml
create mode 100644 axum/macro/src/lib.rs
create mode 100644 axum/src/lib.rs
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index a21bef2..a52f358 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -30,7 +30,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: check
- args: --all --bins --examples
+ args: --workspace --all --bins --examples
- name: wstd tests
uses: actions-rs/cargo@v1
diff --git a/Cargo.toml b/Cargo.toml
index 0a3cce5..069ba49 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,6 +46,8 @@ serde_json.workspace = true
[workspace]
members = [
+ "axum",
+ "axum/macro",
"macro",
"test-programs",
"test-programs/artifacts",
@@ -71,6 +73,7 @@ authors = [
anyhow = "1"
async-task = "4.7"
async-trait = "*"
+axum = { version = "0.8.6", default-features = false }
bytes = "1.10.1"
cargo_metadata = "0.22"
clap = { version = "4.5.26", features = ["derive"] }
@@ -92,10 +95,13 @@ syn = "2.0"
test-log = { version = "0.2", features = ["trace"] }
test-programs = { path = "test-programs" }
test-programs-artifacts = { path = "test-programs/artifacts" }
+tower-service = "0.3.3"
ureq = { version = "2.12.1", default-features = false }
wasip2 = "1.0"
wstd = { path = "." }
-wstd-macro = { path = "macro", version = "=0.5.6" }
+wstd-axum = { path = "./axum", version = "0.5.6" }
+wstd-axum-macro = { path = "./axum/macro", version = "=0.5.6" }
+wstd-macro = { path = "./macro", version = "=0.5.6" }
[package.metadata.docs.rs]
all-features = true
diff --git a/axum/Cargo.toml b/axum/Cargo.toml
new file mode 100644
index 0000000..046608e
--- /dev/null
+++ b/axum/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "wstd-axum"
+version = "0.5.6"
+edition = "2024"
+
+[dependencies]
+axum.workspace = true
+tower-service.workspace = true
+wstd.workspace = true
+wstd-axum-macro.workspace = true
diff --git a/axum/examples/hello_world.rs b/axum/examples/hello_world.rs
new file mode 100644
index 0000000..32e96f9
--- /dev/null
+++ b/axum/examples/hello_world.rs
@@ -0,0 +1,18 @@
+//! Run with
+//!
+//! ```not_rust
+//! cargo build -p example-hello-world --target wasm32-wasip2
+//! wasmtime serve -Scli target/wasm32-wasip2/debug/example-hello-world.wasm
+//! ```
+
+use axum::{response::Html, routing::get, Router};
+
+#[wstd_axum::http_server]
+fn main() -> Router {
+ // build our application with a route
+ Router::new().route("/", get(handler))
+}
+
+async fn handler() -> Html<&'static str> {
+ Html("Hello, World!
")
+}
diff --git a/axum/macro/.gitignore b/axum/macro/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/axum/macro/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/axum/macro/Cargo.toml b/axum/macro/Cargo.toml
new file mode 100644
index 0000000..c0abd19
--- /dev/null
+++ b/axum/macro/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "wstd-axum-macro"
+version = "0.5.6"
+edition = "2024"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn.workspace = true
+quote.workspace = true
diff --git a/axum/macro/src/lib.rs b/axum/macro/src/lib.rs
new file mode 100644
index 0000000..b74c502
--- /dev/null
+++ b/axum/macro/src/lib.rs
@@ -0,0 +1,47 @@
+use proc_macro::TokenStream;
+use quote::{quote, quote_spanned};
+use syn::{ItemFn, parse_macro_input, spanned::Spanned};
+
+#[proc_macro_attribute]
+pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(item as ItemFn);
+
+ if input.sig.ident != "main" {
+ return quote_spanned! { input.sig.ident.span()=>
+ compile_error!("only `fn main` can be used for #[wstd_axum::http_server]");
+ }
+ .into();
+ }
+
+ if !input.sig.inputs.is_empty() {
+ return quote_spanned! { input.sig.inputs.span()=>
+ compile_error!("arguments to main are not supported");
+ }
+ .into();
+ }
+ let (async_, call) = if input.sig.asyncness.is_some() {
+ (quote!(async), quote!(__make_service().await))
+ } else {
+ (quote!(), quote!(__make_service()))
+ };
+ let attrs = input.attrs;
+ let output = input.sig.output;
+ let block = input.block;
+ quote! {
+ #[::wstd::http_server]
+ pub async fn main(
+ __request: ::wstd::http::Request<::wstd::http::Body>
+ ) -> ::wstd::http::error::Result<::wstd::http::Response<::wstd::http::Body>> {
+
+ #(#attrs)*
+ #async_ fn __make_service() #output {
+ #block
+ }
+
+ let __service = #call;
+
+ ::wstd_axum::serve(__request, __service).await
+ }
+ }
+ .into()
+}
diff --git a/axum/src/lib.rs b/axum/src/lib.rs
new file mode 100644
index 0000000..7bb4f14
--- /dev/null
+++ b/axum/src/lib.rs
@@ -0,0 +1,27 @@
+use axum::extract::Request;
+use axum::response::Response;
+use std::convert::Infallible;
+use tower_service::Service;
+
+pub use wstd_axum_macro::attr_macro_http_server as http_server;
+
+pub async fn serve(
+ request: wstd::http::Request,
+ mut service: S,
+) -> wstd::http::error::Result>
+where
+ S: Service + Clone + Send + 'static,
+ S::Future: Send,
+{
+ let resp = service
+ .call(
+ request.map(|incoming: wstd::http::Body| -> axum::body::Body {
+ axum::body::Body::new(incoming.into_boxed_body())
+ }),
+ )
+ .await
+ .unwrap_or_else(|err| match err {});
+ Ok(resp.map(|body: axum::body::Body| -> wstd::http::Body {
+ wstd::http::Body::from_http_body(body)
+ }))
+}
From 9142f1559043150831e9a539d61e84937c118971 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 17 Oct 2025 10:45:07 -0700
Subject: [PATCH 02/11] flatten unnecessary test-programs/artifacts crate into
test-programs
and add test for axum hello world.
rather than need separate test-programs to build the bins, just build
the examples. (I have no idea why I didn't do this in the first place.)
---
.github/workflows/ci.yaml | 2 +-
Cargo.toml | 2 -
axum/examples/hello_world.rs | 4 +-
test-programs/Cargo.toml | 16 ++--
test-programs/artifacts/Cargo.toml | 19 ----
test-programs/artifacts/build.rs | 69 --------------
test-programs/build.rs | 95 +++++++++++++++++++
test-programs/src/bin/http_server.rs | 1 -
test-programs/src/bin/tcp_echo_server.rs | 1 -
test-programs/{artifacts => }/src/lib.rs | 0
test-programs/tests/axum_hello_world.rs | 46 +++++++++
.../{artifacts => }/tests/http_server.rs | 2 +-
.../{artifacts => }/tests/tcp_echo_server.rs | 4 +-
13 files changed, 157 insertions(+), 104 deletions(-)
delete mode 100644 test-programs/artifacts/Cargo.toml
delete mode 100644 test-programs/artifacts/build.rs
create mode 100644 test-programs/build.rs
delete mode 100644 test-programs/src/bin/http_server.rs
delete mode 100644 test-programs/src/bin/tcp_echo_server.rs
rename test-programs/{artifacts => }/src/lib.rs (100%)
create mode 100644 test-programs/tests/axum_hello_world.rs
rename test-programs/{artifacts => }/tests/http_server.rs (99%)
rename test-programs/{artifacts => }/tests/tcp_echo_server.rs (96%)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index a52f358..a468cb9 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -42,7 +42,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
- args: -p test-programs-artifacts -- --nocapture
+ args: -p test-programs -- --nocapture
check_fmt_and_docs:
diff --git a/Cargo.toml b/Cargo.toml
index 069ba49..1184eda 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -50,7 +50,6 @@ members = [
"axum/macro",
"macro",
"test-programs",
- "test-programs/artifacts",
]
resolver = "2"
@@ -94,7 +93,6 @@ slab = "0.4.9"
syn = "2.0"
test-log = { version = "0.2", features = ["trace"] }
test-programs = { path = "test-programs" }
-test-programs-artifacts = { path = "test-programs/artifacts" }
tower-service = "0.3.3"
ureq = { version = "2.12.1", default-features = false }
wasip2 = "1.0"
diff --git a/axum/examples/hello_world.rs b/axum/examples/hello_world.rs
index 32e96f9..00eaa8f 100644
--- a/axum/examples/hello_world.rs
+++ b/axum/examples/hello_world.rs
@@ -1,11 +1,11 @@
//! Run with
//!
-//! ```not_rust
+//! ```sh
//! cargo build -p example-hello-world --target wasm32-wasip2
//! wasmtime serve -Scli target/wasm32-wasip2/debug/example-hello-world.wasm
//! ```
-use axum::{response::Html, routing::get, Router};
+use axum::{Router, response::Html, routing::get};
#[wstd_axum::http_server]
fn main() -> Router {
diff --git a/test-programs/Cargo.toml b/test-programs/Cargo.toml
index 94eaa2a..bd81505 100644
--- a/test-programs/Cargo.toml
+++ b/test-programs/Cargo.toml
@@ -1,14 +1,18 @@
[package]
name = "test-programs"
version = "0.1.0"
-publish = false
-license.workspace = true
edition.workspace = true
+license.workspace = true
rust-version.workspace = true
+publish = false
[dependencies]
+
+[dev-dependencies]
anyhow.workspace = true
-futures-lite.workspace = true
-http-body-util.workspace = true
-serde_json.workspace = true
-wstd.workspace = true
+test-log.workspace = true
+ureq.workspace = true
+
+[build-dependencies]
+cargo_metadata.workspace = true
+heck.workspace = true
diff --git a/test-programs/artifacts/Cargo.toml b/test-programs/artifacts/Cargo.toml
deleted file mode 100644
index 7c69049..0000000
--- a/test-programs/artifacts/Cargo.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[package]
-name = "test-programs-artifacts"
-version = "0.1.0"
-edition.workspace = true
-license.workspace = true
-rust-version.workspace = true
-publish = false
-
-[dependencies]
-
-[dev-dependencies]
-anyhow.workspace = true
-test-log.workspace = true
-test-programs-artifacts.workspace = true
-ureq.workspace = true
-
-[build-dependencies]
-cargo_metadata.workspace = true
-heck.workspace = true
diff --git a/test-programs/artifacts/build.rs b/test-programs/artifacts/build.rs
deleted file mode 100644
index 9dea95b..0000000
--- a/test-programs/artifacts/build.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-use cargo_metadata::TargetKind;
-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 wstd_root = test_programs_root.parent().unwrap();
- println!(
- "cargo:rerun-if-changed={}",
- wstd_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 == [TargetKind::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",
- _ => "",
- }
-}
diff --git a/test-programs/build.rs b/test-programs/build.rs
new file mode 100644
index 0000000..12574e6
--- /dev/null
+++ b/test-programs/build.rs
@@ -0,0 +1,95 @@
+use cargo_metadata::TargetKind;
+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 wstd_meta = meta
+ .packages
+ .iter()
+ .find(|p| *p.name == "wstd")
+ .expect("wstd is in cargo metadata");
+ let wstd_axum_meta = meta
+ .packages
+ .iter()
+ .find(|p| *p.name == "wstd-axum")
+ .expect("wstd is in cargo metadata");
+
+ let wstd_root = wstd_meta.manifest_path.parent().unwrap();
+ println!(
+ "cargo:rerun-if-changed={}",
+ wstd_root.as_os_str().to_str().unwrap()
+ );
+
+ fn build_examples(pkg: &str, out_dir: &PathBuf) {
+ let status = Command::new("cargo")
+ .arg("build")
+ .arg("--examples")
+ .arg("--target=wasm32-wasip2")
+ .arg(format!("--package={pkg}"))
+ .env("CARGO_TARGET_DIR", out_dir)
+ .env("CARGO_PROFILE_DEV_DEBUG", "2")
+ .env("RUSTFLAGS", rustflags())
+ .env_remove("CARGO_ENCODED_RUSTFLAGS")
+ .status()
+ .expect("cargo build wstd examples");
+ assert!(status.success());
+ }
+ build_examples("wstd", &out_dir);
+ build_examples("wstd-axum", &out_dir);
+
+ let mut generated_code = "// THIS FILE IS GENERATED CODE\n".to_string();
+
+ for binary in wstd_meta
+ .targets
+ .iter()
+ .filter(|t| t.kind == [TargetKind::Example])
+ {
+ let component_path = out_dir
+ .join("wasm32-wasip2")
+ .join("debug")
+ .join("examples")
+ .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")
+ );
+ }
+
+ generated_code += "pub mod axum {";
+ for binary in wstd_axum_meta
+ .targets
+ .iter()
+ .filter(|t| t.kind == [TargetKind::Example])
+ {
+ let component_path = out_dir
+ .join("wasm32-wasip2")
+ .join("debug")
+ .join("examples")
+ .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")
+ );
+ }
+ generated_code += "}"; // end `pub mod axum`
+
+ 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",
+ _ => "",
+ }
+}
diff --git a/test-programs/src/bin/http_server.rs b/test-programs/src/bin/http_server.rs
deleted file mode 100644
index e9fea26..0000000
--- a/test-programs/src/bin/http_server.rs
+++ /dev/null
@@ -1 +0,0 @@
-include!("../../../examples/http_server.rs");
diff --git a/test-programs/src/bin/tcp_echo_server.rs b/test-programs/src/bin/tcp_echo_server.rs
deleted file mode 100644
index d46adb1..0000000
--- a/test-programs/src/bin/tcp_echo_server.rs
+++ /dev/null
@@ -1 +0,0 @@
-include!("../../../examples/tcp_echo_server.rs");
diff --git a/test-programs/artifacts/src/lib.rs b/test-programs/src/lib.rs
similarity index 100%
rename from test-programs/artifacts/src/lib.rs
rename to test-programs/src/lib.rs
diff --git a/test-programs/tests/axum_hello_world.rs b/test-programs/tests/axum_hello_world.rs
new file mode 100644
index 0000000..0e82040
--- /dev/null
+++ b/test-programs/tests/axum_hello_world.rs
@@ -0,0 +1,46 @@
+use anyhow::Result;
+use std::net::TcpStream;
+use std::process::{Child, Command};
+use std::thread::sleep;
+use std::time::Duration;
+
+// Wasmtime serve will run until killed. Kill it in a drop impl so the process
+// isnt orphaned when the test suite ends (successfully, or unsuccessfully)
+struct DontOrphan(Child);
+impl Drop for DontOrphan {
+ fn drop(&mut self) {
+ let _ = self.0.kill();
+ }
+}
+
+#[test_log::test]
+fn http_server() -> Result<()> {
+ // Run wasmtime serve.
+ // Enable -Scli because we currently don't have a way to build with the
+ // proxy adapter, so we build with the default adapter.
+ let _wasmtime_process = DontOrphan(
+ Command::new("wasmtime")
+ .arg("serve")
+ .arg("-Scli")
+ .arg("--addr=127.0.0.1:8081")
+ .arg(test_programs::axum::HELLO_WORLD)
+ .spawn()?,
+ );
+
+ // Clumsily wait for the server to accept connections.
+ 'wait: loop {
+ sleep(Duration::from_millis(100));
+ if TcpStream::connect("127.0.0.1:8081").is_ok() {
+ break 'wait;
+ }
+ }
+
+ // Test each path in the server:
+
+ // TEST / handler
+ // Response body is the hard-coded default
+ let body: String = ureq::get("http://127.0.0.1:8081").call()?.into_string()?;
+ assert!(body.contains("Hello, World!
"));
+
+ Ok(())
+}
diff --git a/test-programs/artifacts/tests/http_server.rs b/test-programs/tests/http_server.rs
similarity index 99%
rename from test-programs/artifacts/tests/http_server.rs
rename to test-programs/tests/http_server.rs
index 13ce30f..df96392 100644
--- a/test-programs/artifacts/tests/http_server.rs
+++ b/test-programs/tests/http_server.rs
@@ -23,7 +23,7 @@ fn http_server() -> Result<()> {
.arg("serve")
.arg("-Scli")
.arg("--addr=127.0.0.1:8081")
- .arg(test_programs_artifacts::HTTP_SERVER)
+ .arg(test_programs::HTTP_SERVER)
.spawn()?,
);
diff --git a/test-programs/artifacts/tests/tcp_echo_server.rs b/test-programs/tests/tcp_echo_server.rs
similarity index 96%
rename from test-programs/artifacts/tests/tcp_echo_server.rs
rename to test-programs/tests/tcp_echo_server.rs
index 3cf7752..1e949cc 100644
--- a/test-programs/artifacts/tests/tcp_echo_server.rs
+++ b/test-programs/tests/tcp_echo_server.rs
@@ -6,14 +6,14 @@ fn tcp_echo_server() -> Result<()> {
use std::io::{Read, Write};
use std::net::{Shutdown, TcpStream};
- println!("testing {}", test_programs_artifacts::TCP_ECHO_SERVER);
+ println!("testing {}", test_programs::TCP_ECHO_SERVER);
// Run the component in wasmtime
// -Sinherit-network required for sockets to work
let mut wasmtime_process = Command::new("wasmtime")
.arg("run")
.arg("-Sinherit-network")
- .arg(test_programs_artifacts::TCP_ECHO_SERVER)
+ .arg(test_programs::TCP_ECHO_SERVER)
.stdout(std::process::Stdio::piped())
.spawn()?;
From 49ef951662cff204fb0b418261fb669fd5d74245 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 17 Oct 2025 11:02:04 -0700
Subject: [PATCH 03/11] add to publish
---
axum/Cargo.toml | 11 +++++++++--
axum/examples/hello_world.rs | 6 +++---
axum/macro/Cargo.toml | 11 +++++++++--
axum/macro/src/lib.rs | 2 +-
ci/publish.rs | 2 +-
5 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/axum/Cargo.toml b/axum/Cargo.toml
index 046608e..dfd4879 100644
--- a/axum/Cargo.toml
+++ b/axum/Cargo.toml
@@ -1,7 +1,14 @@
[package]
name = "wstd-axum"
-version = "0.5.6"
-edition = "2024"
+description = "Support for axum as a wasi http server via wstd"
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+keywords.workspace = true
+categories.workspace = true
+repository.workspace = true
+rust-version.workspace = true
[dependencies]
axum.workspace = true
diff --git a/axum/examples/hello_world.rs b/axum/examples/hello_world.rs
index 00eaa8f..9cb642c 100644
--- a/axum/examples/hello_world.rs
+++ b/axum/examples/hello_world.rs
@@ -1,11 +1,11 @@
//! Run with
//!
//! ```sh
-//! cargo build -p example-hello-world --target wasm32-wasip2
-//! wasmtime serve -Scli target/wasm32-wasip2/debug/example-hello-world.wasm
+//! cargo build -p wstd-axum --examples --target wasm32-wasip2
+//! wasmtime serve -Scli target/wasm32-wasip2/debug/examples/hello-world.wasm
//! ```
-use axum::{Router, response::Html, routing::get};
+use axum::{response::Html, routing::get, Router};
#[wstd_axum::http_server]
fn main() -> Router {
diff --git a/axum/macro/Cargo.toml b/axum/macro/Cargo.toml
index c0abd19..989ea42 100644
--- a/axum/macro/Cargo.toml
+++ b/axum/macro/Cargo.toml
@@ -1,7 +1,14 @@
[package]
name = "wstd-axum-macro"
-version = "0.5.6"
-edition = "2024"
+description = "Proc-macro support for axum as a wasi http server via wstd"
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+keywords.workspace = true
+categories.workspace = true
+repository.workspace = true
+rust-version.workspace = true
[lib]
proc-macro = true
diff --git a/axum/macro/src/lib.rs b/axum/macro/src/lib.rs
index b74c502..0d97263 100644
--- a/axum/macro/src/lib.rs
+++ b/axum/macro/src/lib.rs
@@ -1,6 +1,6 @@
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
-use syn::{ItemFn, parse_macro_input, spanned::Spanned};
+use syn::{parse_macro_input, spanned::Spanned, ItemFn};
#[proc_macro_attribute]
pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream {
diff --git a/ci/publish.rs b/ci/publish.rs
index 43da6d3..bd20833 100644
--- a/ci/publish.rs
+++ b/ci/publish.rs
@@ -16,7 +16,7 @@ use std::thread;
use std::time::Duration;
// note that this list must be topologically sorted by dependencies
-const CRATES_TO_PUBLISH: &[&str] = &["wstd-macro", "wstd"];
+const CRATES_TO_PUBLISH: &[&str] = &["wstd-macro", "wstd", "wstd-axum-macro", "wstd-axum"];
#[derive(Debug)]
struct Workspace {
From 4a67c8f3d98c9b182e36b131b7d1de75ce942c5c Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 17 Oct 2025 11:46:10 -0700
Subject: [PATCH 04/11] workspace edition 2024
---
Cargo.toml | 2 +-
axum/examples/hello_world.rs | 2 +-
axum/macro/src/lib.rs | 2 +-
examples/complex_http_client.rs | 10 ++--------
examples/http_client.rs | 2 +-
macro/src/lib.rs | 2 +-
src/future/delay.rs | 2 +-
src/http/body.rs | 16 ++++++++--------
src/http/fields.rs | 2 +-
src/http/method.rs | 2 +-
src/http/mod.rs | 2 +-
src/http/request.rs | 2 +-
src/http/response.rs | 2 +-
src/http/server.rs | 2 +-
src/io/streams.rs | 12 ++++++------
src/runtime/block_on.rs | 2 +-
tests/http_first_byte_timeout.rs | 2 +-
17 files changed, 30 insertions(+), 36 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 1184eda..0db5a91 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,7 +55,7 @@ resolver = "2"
[workspace.package]
version = "0.5.6"
-edition = "2021"
+edition = "2024"
license = "Apache-2.0 WITH LLVM-exception"
repository = "https://github.com/bytecodealliance/wstd"
keywords = ["WebAssembly", "async", "stdlib", "Components"]
diff --git a/axum/examples/hello_world.rs b/axum/examples/hello_world.rs
index 9cb642c..90a9cbd 100644
--- a/axum/examples/hello_world.rs
+++ b/axum/examples/hello_world.rs
@@ -5,7 +5,7 @@
//! wasmtime serve -Scli target/wasm32-wasip2/debug/examples/hello-world.wasm
//! ```
-use axum::{response::Html, routing::get, Router};
+use axum::{Router, response::Html, routing::get};
#[wstd_axum::http_server]
fn main() -> Router {
diff --git a/axum/macro/src/lib.rs b/axum/macro/src/lib.rs
index 0d97263..b74c502 100644
--- a/axum/macro/src/lib.rs
+++ b/axum/macro/src/lib.rs
@@ -1,6 +1,6 @@
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
-use syn::{parse_macro_input, spanned::Spanned, ItemFn};
+use syn::{ItemFn, parse_macro_input, spanned::Spanned};
#[proc_macro_attribute]
pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream {
diff --git a/examples/complex_http_client.rs b/examples/complex_http_client.rs
index 2d4acb0..703e931 100644
--- a/examples/complex_http_client.rs
+++ b/examples/complex_http_client.rs
@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
use clap::{ArgAction, Parser};
use std::str::FromStr;
use wstd::http::{Body, BodyExt, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri};
@@ -91,13 +91,7 @@ async fn main() -> Result<()> {
Body::empty().into_boxed_body()
};
let t = trailers.clone();
- let body = body.with_trailers(async move {
- if t.is_empty() {
- None
- } else {
- Some(Ok(t))
- }
- });
+ let body = body.with_trailers(async move { if t.is_empty() { None } else { Some(Ok(t)) } });
let request = request.body(Body::from_http_body(body))?;
// Send the request.
diff --git a/examples/http_client.rs b/examples/http_client.rs
index f9e5e4c..f4465a8 100644
--- a/examples/http_client.rs
+++ b/examples/http_client.rs
@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
use clap::{ArgAction, Parser};
use wstd::http::{Body, BodyExt, Client, Method, Request, Uri};
use wstd::io::AsyncWrite;
diff --git a/macro/src/lib.rs b/macro/src/lib.rs
index bfc488c..42efa51 100644
--- a/macro/src/lib.rs
+++ b/macro/src/lib.rs
@@ -1,6 +1,6 @@
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
-use syn::{parse_macro_input, spanned::Spanned, ItemFn};
+use syn::{ItemFn, parse_macro_input, spanned::Spanned};
#[proc_macro_attribute]
pub fn attr_macro_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
diff --git a/src/future/delay.rs b/src/future/delay.rs
index d8fcdbf..20d6753 100644
--- a/src/future/delay.rs
+++ b/src/future/delay.rs
@@ -1,6 +1,6 @@
use std::future::Future;
use std::pin::Pin;
-use std::task::{ready, Context, Poll};
+use std::task::{Context, Poll, ready};
use pin_project_lite::pin_project;
diff --git a/src/http/body.rs b/src/http/body.rs
index e30c053..c23a3c0 100644
--- a/src/http/body.rs
+++ b/src/http/body.rs
@@ -1,7 +1,7 @@
use crate::http::{
+ Error, HeaderMap,
error::Context as _,
fields::{header_map_from_wasi, header_map_to_wasi},
- Error, HeaderMap,
};
use crate::io::{AsyncInputStream, AsyncOutputStream};
use crate::runtime::{AsyncPollable, Reactor, WaitFor};
@@ -10,10 +10,10 @@ pub use ::http_body::{Body as HttpBody, Frame, SizeHint};
pub use bytes::Bytes;
use http::header::CONTENT_LENGTH;
-use http_body_util::{combinators::UnsyncBoxBody, BodyExt};
+use http_body_util::{BodyExt, combinators::UnsyncBoxBody};
use std::fmt;
-use std::future::{poll_fn, Future};
-use std::pin::{pin, Pin};
+use std::future::{Future, poll_fn};
+use std::pin::{Pin, pin};
use std::task::{Context, Poll};
use wasip2::http::types::{
FutureTrailers, IncomingBody as WasiIncomingBody, OutgoingBody as WasiOutgoingBody,
@@ -145,7 +145,7 @@ impl Body {
/// copied into memory, or an error occurs.
pub async fn contents(&mut self) -> Result<&[u8], Error> {
match &mut self.0 {
- BodyInner::Complete { ref data, .. } => Ok(data.as_ref()),
+ BodyInner::Complete { data, .. } => Ok(&*data),
inner => {
let mut prev = BodyInner::Complete {
data: Bytes::new(),
@@ -164,7 +164,7 @@ impl Body {
trailers,
};
Ok(match inner {
- BodyInner::Complete { ref data, .. } => data.as_ref(),
+ BodyInner::Complete { data, .. } => &*data,
_ => unreachable!(),
})
}
@@ -511,13 +511,13 @@ impl BodyState {
loop {
match self.stream.read(MAX_FRAME_SIZE) {
Ok(bs) if !bs.is_empty() => {
- return Poll::Ready(Some(Ok(Frame::data(Bytes::from(bs)))))
+ return Poll::Ready(Some(Ok(Frame::data(Bytes::from(bs)))));
}
Err(StreamError::Closed) => return Poll::Ready(None),
Err(StreamError::LastOperationFailed(err)) => {
return Poll::Ready(Some(Err(
Error::msg(err.to_debug_string()).context("reading incoming body stream")
- )))
+ )));
}
Ok(_empty) => {
if self.subscription.is_none() {
diff --git a/src/http/fields.rs b/src/http/fields.rs
index 35a9be7..de6df16 100644
--- a/src/http/fields.rs
+++ b/src/http/fields.rs
@@ -1,6 +1,6 @@
pub use http::header::{HeaderMap, HeaderName, HeaderValue};
-use super::{error::Context, Error};
+use super::{Error, error::Context};
use wasip2::http::types::Fields;
pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result {
diff --git a/src/http/method.rs b/src/http/method.rs
index cee31e6..d1882a8 100644
--- a/src/http/method.rs
+++ b/src/http/method.rs
@@ -1,7 +1,7 @@
use wasip2::http::types::Method as WasiMethod;
-use http::method::InvalidMethod;
pub use http::Method;
+use http::method::InvalidMethod;
pub(crate) fn to_wasi_method(value: Method) -> WasiMethod {
match value {
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 9f41125..39f0a40 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -4,7 +4,7 @@ pub use http::status::StatusCode;
pub use http::uri::{Authority, PathAndQuery, Uri};
#[doc(inline)]
-pub use body::{util::BodyExt, Body};
+pub use body::{Body, util::BodyExt};
pub use client::Client;
pub use error::{Error, ErrorCode, Result};
pub use fields::{HeaderMap, HeaderName, HeaderValue};
diff --git a/src/http/request.rs b/src/http/request.rs
index f9a0817..6694d03 100644
--- a/src/http/request.rs
+++ b/src/http/request.rs
@@ -1,10 +1,10 @@
use super::{
+ Authority, HeaderMap, PathAndQuery, Uri,
body::{Body, BodyHint},
error::{Context, Error, ErrorCode},
fields::{header_map_from_wasi, header_map_to_wasi},
method::{from_wasi_method, to_wasi_method},
scheme::{from_wasi_scheme, to_wasi_scheme},
- Authority, HeaderMap, PathAndQuery, Uri,
};
use wasip2::http::outgoing_handler::OutgoingRequest;
use wasip2::http::types::IncomingRequest;
diff --git a/src/http/response.rs b/src/http/response.rs
index f0b57e8..d0af77e 100644
--- a/src/http/response.rs
+++ b/src/http/response.rs
@@ -3,7 +3,7 @@ use wasip2::http::types::IncomingResponse;
use crate::http::body::{Body, BodyHint};
use crate::http::error::{Context, Error};
-use crate::http::fields::{header_map_from_wasi, HeaderMap};
+use crate::http::fields::{HeaderMap, header_map_from_wasi};
pub use http::response::{Builder, Response};
diff --git a/src/http/server.rs b/src/http/server.rs
index 471330e..3ac12df 100644
--- a/src/http/server.rs
+++ b/src/http/server.rs
@@ -18,7 +18,7 @@
//! [`Response`]: crate::http::Response
//! [`http_server`]: crate::http_server
-use super::{error::ErrorCode, fields::header_map_to_wasi, Body, Error, Response};
+use super::{Body, Error, Response, error::ErrorCode, fields::header_map_to_wasi};
use http::header::CONTENT_LENGTH;
use wasip2::exports::http::incoming_handler::ResponseOutparam;
use wasip2::http::types::OutgoingResponse;
diff --git a/src/io/streams.rs b/src/io/streams.rs
index e5e889f..3676d21 100644
--- a/src/io/streams.rs
+++ b/src/io/streams.rs
@@ -1,6 +1,6 @@
use super::{AsyncPollable, AsyncRead, AsyncWrite};
use crate::runtime::WaitFor;
-use std::future::{poll_fn, Future};
+use std::future::{Future, poll_fn};
use std::pin::Pin;
use std::sync::{Mutex, OnceLock};
use std::task::{Context, Poll};
@@ -64,7 +64,7 @@ impl AsyncInputStream {
// 0 bytes from Rust's `read` means end-of-stream.
Err(StreamError::Closed) => return Ok(0),
Err(StreamError::LastOperationFailed(err)) => {
- return Err(std::io::Error::other(err.to_debug_string()))
+ return Err(std::io::Error::other(err.to_debug_string()));
}
}
};
@@ -263,18 +263,18 @@ impl AsyncOutputStream {
match self.stream.write(&buf[0..writable]) {
Ok(()) => return Ok(writable),
Err(StreamError::Closed) => {
- return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset))
+ return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
}
Err(StreamError::LastOperationFailed(err)) => {
- return Err(std::io::Error::other(err.to_debug_string()))
+ return Err(std::io::Error::other(err.to_debug_string()));
}
}
}
Err(StreamError::Closed) => {
- return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset))
+ return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
}
Err(StreamError::LastOperationFailed(err)) => {
- return Err(std::io::Error::other(err.to_debug_string()))
+ return Err(std::io::Error::other(err.to_debug_string()));
}
}
}
diff --git a/src/runtime/block_on.rs b/src/runtime/block_on.rs
index 5c951ce..34f0dfa 100644
--- a/src/runtime/block_on.rs
+++ b/src/runtime/block_on.rs
@@ -1,4 +1,4 @@
-use super::{Reactor, REACTOR};
+use super::{REACTOR, Reactor};
use std::future::Future;
use std::pin::pin;
diff --git a/tests/http_first_byte_timeout.rs b/tests/http_first_byte_timeout.rs
index 4882966..5fd47be 100644
--- a/tests/http_first_byte_timeout.rs
+++ b/tests/http_first_byte_timeout.rs
@@ -1,4 +1,4 @@
-use wstd::http::{error::ErrorCode, Body, Client, Request};
+use wstd::http::{Body, Client, Request, error::ErrorCode};
#[wstd::main]
async fn main() -> Result<(), Box> {
From dda12dfa436eb5b9d930c122ba918868f203c8b5 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 17 Oct 2025 11:58:46 -0700
Subject: [PATCH 05/11] wstd-axum-macro requires syn features full
---
axum/macro/Cargo.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/axum/macro/Cargo.toml b/axum/macro/Cargo.toml
index 989ea42..2b183d5 100644
--- a/axum/macro/Cargo.toml
+++ b/axum/macro/Cargo.toml
@@ -14,5 +14,5 @@ rust-version.workspace = true
proc-macro = true
[dependencies]
-syn.workspace = true
+syn = { workspace = true, features = ["full"] }
quote.workspace = true
From 0db0f51beae2ca2dd280e54ba3754e845494c7c7 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 17 Oct 2025 12:11:36 -0700
Subject: [PATCH 06/11] workspace versioning
---
Cargo.toml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 0db5a91..8222955 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -96,8 +96,8 @@ test-programs = { path = "test-programs" }
tower-service = "0.3.3"
ureq = { version = "2.12.1", default-features = false }
wasip2 = "1.0"
-wstd = { path = "." }
-wstd-axum = { path = "./axum", version = "0.5.6" }
+wstd = { path = ".", version = "=0.5.6" }
+wstd-axum = { path = "./axum", version = "=0.5.6" }
wstd-axum-macro = { path = "./axum/macro", version = "=0.5.6" }
wstd-macro = { path = "./macro", version = "=0.5.6" }
From 9842b2aeb682b7be5e2915800414f1cc8d96a48d Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 17 Oct 2025 12:51:22 -0700
Subject: [PATCH 07/11] docs, example that doesnt use macro
---
axum/examples/hello_world_nomacro.rs | 19 ++++++++++++
axum/src/lib.rs | 41 +++++++++++++++++++++++++
test-programs/tests/axum_hello_world.rs | 15 +++++++--
3 files changed, 73 insertions(+), 2 deletions(-)
create mode 100644 axum/examples/hello_world_nomacro.rs
diff --git a/axum/examples/hello_world_nomacro.rs b/axum/examples/hello_world_nomacro.rs
new file mode 100644
index 0000000..f3068d1
--- /dev/null
+++ b/axum/examples/hello_world_nomacro.rs
@@ -0,0 +1,19 @@
+//! Run with
+//!
+//! ```sh
+//! cargo build -p wstd-axum --examples --target wasm32-wasip2
+//! wasmtime serve -Scli target/wasm32-wasip2/debug/examples/hello-world-nomacro.wasm
+//! ```
+
+use axum::{Router, response::Html, routing::get};
+use wstd::http::{Body, Error, Request, Response};
+
+#[wstd::http_server]
+async fn main(request: Request) -> Result, Error> {
+ let service = Router::new().route("/", get(handler));
+ wstd_axum::serve(request, service).await
+}
+
+async fn handler() -> Html<&'static str> {
+ Html("Hello, World!
")
+}
diff --git a/axum/src/lib.rs b/axum/src/lib.rs
index 7bb4f14..3272b91 100644
--- a/axum/src/lib.rs
+++ b/axum/src/lib.rs
@@ -1,3 +1,44 @@
+//! Support for the [`axum`] web server framework in wasi-http components, via
+//! [`wstd`].
+//!
+//! This crate is a pretty thin wrapper on [`wstd`] that allows users to
+//! use the [`axum`] crate on top of wstd's http support. This means that
+//! axum services can run anywhere the [wasi-http proxy world] is supported,
+//! e.g. in [`wasmtime serve`].
+//!
+//! Users of this crate should depend on `axum` with `default-features =
+//! false`, and opt in to any features that they require (e.g. form, json,
+//! matched-path, original-uri, query, tower-log, tracing). The axum crate
+//! features that require `hyper` or `tokio` are NOT supported (e.g. http1,
+//! http2, ws), because unlike in native applications, wasi-http components
+//! have an http implementation provided as imported interfaces (i.e.
+//! implemented the Wasm host), and do not use raw sockets inside of this
+//! program.
+//!
+//! # Examples
+//!
+//! The simplest use is via the `wstd_axum::http_server` proc macro.
+//! This macro can be applied to a sync or `async` `fn main` which returns
+//! an impl of the `tower_service::Service` trait, typically an
+//! `axum::Router`:
+//!
+//! ```rust,no_run
+#![doc = include_str!("../examples/hello_world.rs")]
+//! ```
+//!
+//! If users desire, they can instead use a `wstd::http_server` entry point
+//! and then use `wstd_axum::serve` directly. The following is equivelant
+//! to the above example:
+//!
+//! ```rust,no_run
+#![doc = include_str!("../examples/hello_world_nomacro.rs")]
+//! ```
+//!
+//! [`axum`]: https://docs.rs/axum/latest/axum/
+//! [`wstd`]: https://docs.rs/wstd/latest/wstd/
+//! [wasi-http proxy world]: https://github.com/WebAssembly/wasi-http
+//! [`wasmtime serve`]: https://wasmtime.dev/
+
use axum::extract::Request;
use axum::response::Response;
use std::convert::Infallible;
diff --git a/test-programs/tests/axum_hello_world.rs b/test-programs/tests/axum_hello_world.rs
index 0e82040..1a33956 100644
--- a/test-programs/tests/axum_hello_world.rs
+++ b/test-programs/tests/axum_hello_world.rs
@@ -14,7 +14,18 @@ impl Drop for DontOrphan {
}
#[test_log::test]
-fn http_server() -> Result<()> {
+fn hello_world() -> Result<()> {
+ run(test_programs::axum::HELLO_WORLD)
+}
+
+#[test_log::test]
+fn hello_world_nomacro() -> Result<()> {
+ run(test_programs::axum::HELLO_WORLD_NOMACRO)
+}
+
+// The hello_world.rs and hello_world_nomacro.rs are identical in
+// functionality
+fn run(guest: &str) -> Result<()> {
// Run wasmtime serve.
// Enable -Scli because we currently don't have a way to build with the
// proxy adapter, so we build with the default adapter.
@@ -23,7 +34,7 @@ fn http_server() -> Result<()> {
.arg("serve")
.arg("-Scli")
.arg("--addr=127.0.0.1:8081")
- .arg(test_programs::axum::HELLO_WORLD)
+ .arg(guest)
.spawn()?,
);
From a5e81cc831041d109301600fa5c81c45dabe7b25 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Mon, 20 Oct 2025 12:07:19 -0700
Subject: [PATCH 08/11] add my own weather example for axum
---
Cargo.toml | 1 +
axum/Cargo.toml | 7 +
axum/examples/weather.rs | 321 +++++++++++++++++++++++++++++++++++++++
3 files changed, 329 insertions(+)
create mode 100644 axum/examples/weather.rs
diff --git a/Cargo.toml b/Cargo.toml
index 8222955..408df4c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -89,6 +89,7 @@ pin-project-lite = "0.2.8"
quote = "1.0"
serde= "1"
serde_json = "1"
+serde_qs = "0.15"
slab = "0.4.9"
syn = "2.0"
test-log = { version = "0.2", features = ["trace"] }
diff --git a/axum/Cargo.toml b/axum/Cargo.toml
index dfd4879..154a021 100644
--- a/axum/Cargo.toml
+++ b/axum/Cargo.toml
@@ -15,3 +15,10 @@ axum.workspace = true
tower-service.workspace = true
wstd.workspace = true
wstd-axum-macro.workspace = true
+
+[dev-dependencies]
+anyhow.workspace = true
+futures-concurrency.workspace = true
+serde = { workspace = true, features = ["derive"] }
+serde_qs.workspace = true
+axum = { workspace = true, features = ["query", "json", "macros"] }
diff --git a/axum/examples/weather.rs b/axum/examples/weather.rs
new file mode 100644
index 0000000..5163fcc
--- /dev/null
+++ b/axum/examples/weather.rs
@@ -0,0 +1,321 @@
+//! This demo app shows a Axum based wasi-http server making an arbitrary
+//! number of http requests as part of serving a single response.
+//!
+//! Use the request query string to pass the parameters `city`, and optionally
+//! `count` (defaults to 10). This app will tell you the current weather in
+//! a set of `count` locations matching the `city` name. For example, when
+//! searching for `city=portland&count=2`, it will return Portland, OR and
+//! then Portland, ME - location results are sorted by population.
+//!
+//! This app first makes a request to `geocoding-api.open-meteo.com` to search
+//! for a set of `count` locations for a given `city` name.
+//!
+//! Then, it makes `count` requests to `api.open-meteo.com`'s forecast api to
+//! get the current temperature and rain accumulation in each of those
+//! locations.
+//!
+//! The complete set of locations and weather reports are retuned as a json
+//! array of records.
+
+use anyhow::{Context, Result, anyhow};
+use axum::extract::{Json, Query};
+use axum::http::StatusCode as AxumStatusCode;
+use axum::routing::{Router, get};
+use serde::{Deserialize, Serialize};
+use wstd::http::{Client, Request, StatusCode, Uri};
+
+/// Be polite: user-agent tells server where these results came from, so they
+/// can easily block abuse
+const USER_AGENT: &str = "wstd-axum weather example (https://github.com/bytecodealliance/wstd)";
+
+/// The axum http server serves just one route for get requests at /weather
+#[wstd_axum::http_server]
+fn main() -> Router {
+ Router::new().route("/weather", get(weather))
+}
+
+/// Named pair used as JSON response
+#[derive(Serialize)]
+struct LocationWeather {
+ location: Location,
+ weather: Weather,
+}
+
+/// Whole demo app lives at this one endpoint.
+async fn weather(
+ Query(query): Query,
+) -> axum::response::Result>> {
+ if query.count == 0 {
+ Err((AxumStatusCode::BAD_REQUEST, "nonzero count required"))?;
+ }
+ // Search for the locations in the query
+ let location_results = fetch_locations(&query)
+ .await
+ .context("searching for location")
+ .map_err(anyhow_response)?;
+
+ use futures_concurrency::future::TryJoin;
+ let results = location_results
+ .into_iter()
+ // For each location found, constuct a future which fetches the
+ // weather, then returns the record of location, weather
+ .map(|location| async move {
+ let weather = fetch_weather(&location)
+ .await
+ .with_context(|| format!("fetching weather for {}", location.qualified_name))?;
+ Ok::<_, anyhow::Error>(LocationWeather { location, weather })
+ })
+ // Collect a vec of futures
+ .collect::>()
+ // TryJoin::try_join takes a vec of futures which return a
+ // result- , and gives a future which returns a
+ // result, error>
+ .try_join()
+ // Get all of the successful items, or else the first error to
+ // resolve.
+ .await
+ .map_err(anyhow_response)?;
+ Ok(Json(results))
+}
+
+/// The query string given to this server contains a city, and optionally a
+/// count.
+#[derive(Deserialize)]
+struct WeatherQuery {
+ city: String,
+ #[serde(default = "default_count")]
+ count: u32,
+}
+/// When the count is not given in the query string, it defaults to this number
+const fn default_count() -> u32 {
+ 10
+}
+/// Default WeatherQuery for when none is given. Portland is a good enough location
+/// for me, so its good enough for the demo.
+impl Default for WeatherQuery {
+ fn default() -> Self {
+ WeatherQuery {
+ city: "Portland".to_string(),
+ count: default_count(),
+ }
+ }
+}
+
+/// Location struct contains the fields we care from the location search. We
+/// massage the geolocation API response down to these fields because we dont
+/// care about a bunch of its contents. The Serialize allows us to return this
+/// value in our server response json.
+#[derive(Debug, Serialize)]
+struct Location {
+ name: String,
+ qualified_name: String,
+ population: Option,
+ latitude: f64,
+ longitude: f64,
+}
+
+/// Fetch the locations corresponding to the query from the open-meteo
+/// geocoding API.
+async fn fetch_locations(query: &WeatherQuery) -> Result> {
+ // Utility struct describes the fields we use in the geocoding api's query
+ // string
+ #[derive(Serialize)]
+ struct GeoQuery {
+ name: String,
+ count: u32,
+ language: String,
+ format: String,
+ }
+ // Value of the fields in the query string:
+ let geo_query = GeoQuery {
+ name: query.city.clone(),
+ count: query.count,
+ language: "en".to_string(),
+ format: "json".to_string(),
+ };
+
+ // Construct the request uri using serde_qs to serialize GeoQuery into a query string.
+ let uri = Uri::builder()
+ .scheme("http")
+ .authority("geocoding-api.open-meteo.com")
+ .path_and_query(format!(
+ "/v1/search?{}",
+ serde_qs::to_string(&geo_query).context("serialize query string")?
+ ))
+ .build()?;
+ // Request is a GET request with no body. User agent is polite to provide.
+ let request = Request::get(uri)
+ .header("User-Agent", USER_AGENT)
+ .body(())?;
+
+ // Make the request
+ let resp = Client::new()
+ .send(request)
+ .await
+ .context("request to geocoding-api.open-meteo.com")
+ .context(AxumStatusCode::SERVICE_UNAVAILABLE)?;
+ // Die with 503 if geocoding api fails for some reason
+ if resp.status() != StatusCode::OK {
+ return Err(anyhow!("geocoding-api returned status {:?}", resp.status())
+ .context(AxumStatusCode::SERVICE_UNAVAILABLE));
+ }
+
+ // Utility structs with Deserialize impls to extract the fields we care
+ // about from the API's json response.
+ #[derive(Deserialize)]
+ struct Contents {
+ results: Vec
- ,
+ }
+ #[derive(Deserialize)]
+ struct Item {
+ name: String,
+ latitude: f64,
+ longitude: f64,
+ population: Option,
+ // There are up to 4 admin region strings provided, only the first one
+ // seems to be guaranteed to be delivered. If it was my API, I would
+ // have made a single field `admin` which has a list of strings, but
+ // its not my API!
+ admin1: String,
+ admin2: Option,
+ admin3: Option,
+ admin4: Option,
+ }
+ impl Item {
+ /// The API returns a set of "admin" names (for administrative
+ /// regions), pretty-print them from most specific to least specific:
+ fn qualified_name(&self) -> String {
+ let mut n = String::new();
+ if let Some(name) = &self.admin4 {
+ n.push_str(name);
+ n.push_str(", ");
+ }
+ if let Some(name) = &self.admin3 {
+ n.push_str(name);
+ n.push_str(", ");
+ }
+ if let Some(name) = &self.admin2 {
+ n.push_str(name);
+ n.push_str(", ");
+ }
+ n.push_str(&self.admin1);
+ n
+ }
+ }
+
+ // Collect the response body and parse the Contents field out of the json:
+ let contents: Contents = resp
+ .into_body()
+ .json()
+ .await
+ .context("parsing geocoding-api response")?;
+
+ // Massage the Contents into a Vec.
+ let mut results = contents
+ .results
+ .into_iter()
+ .map(|item| {
+ let qualified_name = item.qualified_name();
+ Location {
+ name: item.name,
+ latitude: item.latitude,
+ longitude: item.longitude,
+ population: item.population,
+ qualified_name,
+ }
+ })
+ .collect::>();
+ // Sort by highest population first.
+ results.sort_by(|a, b| b.population.partial_cmp(&a.population).unwrap());
+ Ok(results)
+}
+
+/// Weather struct contains the items in the weather report we care about: the
+/// temperature, how much rain, and the units for each. The Serialize allows
+/// us to return this value in our server response json.
+#[derive(Debug, Serialize)]
+struct Weather {
+ temp: f64,
+ temp_unit: String,
+ rain: f64,
+ rain_unit: String,
+}
+
+/// Fetch the weather for a given location from the open-meto forecast API.
+async fn fetch_weather(location: &Location) -> Result {
+ // Utility struct for the query string expected by the forecast API
+ #[derive(Serialize)]
+ struct ForecastQuery {
+ latitude: f64,
+ longitude: f64,
+ current: String,
+ }
+ // Value used for the forecast api query string
+ let query = ForecastQuery {
+ latitude: location.latitude,
+ longitude: location.longitude,
+ current: "temperature_2m,rain".to_string(),
+ };
+ // Construct the uri to the forecast api, serializing the query string
+ // with serde_qs.
+ let uri = Uri::builder()
+ .scheme("http")
+ .authority("api.open-meteo.com")
+ .path_and_query(format!(
+ "/v1/forecast?{}",
+ serde_qs::to_string(&query).context("serialize query string")?
+ ))
+ .build()?;
+ // Make the GET request, attaching user-agent, empty body.
+ let request = Request::get(uri)
+ .header("User-Agent", USER_AGENT)
+ .body(())?;
+ let mut resp = Client::new()
+ .send(request)
+ .await
+ .context("request to api.open-meteo.com")
+ .context(AxumStatusCode::SERVICE_UNAVAILABLE)?;
+
+ // Bubble up error if forecast api failed
+ if resp.status() != StatusCode::OK {
+ return Err(anyhow!("forecast api returned status {:?}", resp.status())
+ .context(AxumStatusCode::SERVICE_UNAVAILABLE));
+ }
+
+ // Utility structs for extracting fields from the forecast api's json
+ // response.
+ #[derive(Deserialize)]
+ struct Contents {
+ current_units: Units,
+ current: Data,
+ }
+ #[derive(Deserialize)]
+ struct Units {
+ temperature_2m: String,
+ rain: String,
+ }
+ #[derive(Deserialize)]
+ struct Data {
+ temperature_2m: f64,
+ rain: f64,
+ }
+
+ // Parse the contents of the json response
+ let contents: Contents = resp.body_mut().json().await?;
+ // Massage those structs into a single Weather
+ let weather = Weather {
+ temp: contents.current.temperature_2m,
+ temp_unit: contents.current_units.temperature_2m,
+ rain: contents.current.rain,
+ rain_unit: contents.current_units.rain,
+ };
+ Ok(weather)
+}
+
+fn anyhow_response(e: anyhow::Error) -> axum::response::ErrorResponse {
+ let code = e
+ .downcast_ref::()
+ .cloned()
+ .unwrap_or(AxumStatusCode::INTERNAL_SERVER_ERROR);
+ (code, format!("{e:?}")).into()
+}
From d0dae63ff922b303882238a44116ac6e29bbc7f4 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Mon, 20 Oct 2025 15:45:32 -0700
Subject: [PATCH 09/11] add test for axum weather example. upgrade all tests to
ureq 3
---
Cargo.toml | 2 +-
examples/http_server.rs | 8 ++-
test-programs/Cargo.toml | 2 +
test-programs/src/lib.rs | 56 +++++++++++++++++++
test-programs/tests/axum_hello_world.rs | 37 ++-----------
test-programs/tests/axum_weather.rs | 54 ++++++++++++++++++
test-programs/tests/http_server.rs | 73 +++++++++----------------
7 files changed, 149 insertions(+), 83 deletions(-)
create mode 100644 test-programs/tests/axum_weather.rs
diff --git a/Cargo.toml b/Cargo.toml
index 408df4c..ee1a87d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -95,7 +95,7 @@ syn = "2.0"
test-log = { version = "0.2", features = ["trace"] }
test-programs = { path = "test-programs" }
tower-service = "0.3.3"
-ureq = { version = "2.12.1", default-features = false }
+ureq = { version = "3.1", default-features = false, features = ["json"] }
wasip2 = "1.0"
wstd = { path = ".", version = "=0.5.6" }
wstd-axum = { path = "./axum", version = "=0.5.6" }
diff --git a/examples/http_server.rs b/examples/http_server.rs
index c37e861..8449f83 100644
--- a/examples/http_server.rs
+++ b/examples/http_server.rs
@@ -122,9 +122,11 @@ async fn http_response_status(request: Request) -> Result>
} else {
500
};
- Ok(Response::builder()
- .status(status)
- .body(String::new().into())?)
+ let mut response = Response::builder().status(status);
+ if status == 302 {
+ response = response.header("Location", "http://localhost/response-status");
+ }
+ Ok(response.body(String::new().into())?)
}
async fn http_response_fail(_request: Request) -> Result> {
diff --git a/test-programs/Cargo.toml b/test-programs/Cargo.toml
index bd81505..24844b7 100644
--- a/test-programs/Cargo.toml
+++ b/test-programs/Cargo.toml
@@ -7,10 +7,12 @@ rust-version.workspace = true
publish = false
[dependencies]
+fslock = "0.2.1"
[dev-dependencies]
anyhow.workspace = true
test-log.workspace = true
+serde_json.workspace = true
ureq.workspace = true
[build-dependencies]
diff --git a/test-programs/src/lib.rs b/test-programs/src/lib.rs
index 26a930a..e7d9289 100644
--- a/test-programs/src/lib.rs
+++ b/test-programs/src/lib.rs
@@ -1 +1,57 @@
include!(concat!(env!("OUT_DIR"), "/gen.rs"));
+
+use fslock::LockFile;
+use std::net::TcpStream;
+use std::process::{Child, Command};
+use std::thread::sleep;
+use std::time::Duration;
+
+/// Manages exclusive access to port 8081, and kills the process when dropped
+pub struct WasmtimeServe {
+ #[expect(dead_code, reason = "exists to live for as long as wasmtime process")]
+ lockfile: LockFile,
+ process: Child,
+}
+
+impl WasmtimeServe {
+ /// Run `wasmtime serve -Scli --addr=127.0.0.1:8081` for a given wasm
+ /// guest filepath.
+ ///
+ /// Takes exclusive access to a lockfile so that only one test on a host
+ /// can use port 8081 at a time.
+ ///
+ /// Kills the wasmtime process, and releases the lock, once dropped.
+ pub fn new(guest: &str) -> std::io::Result {
+ let mut lockfile = std::env::temp_dir();
+ lockfile.push("TEST_PROGRAMS_WASMTIME_SERVE.pid");
+ let mut lockfile = LockFile::open(&lockfile)?;
+ lockfile.lock_with_pid()?;
+
+ // Run wasmtime serve.
+ // Enable -Scli because we currently don't have a way to build with the
+ // proxy adapter, so we build with the default adapter.
+ let process = Command::new("wasmtime")
+ .arg("serve")
+ .arg("-Scli")
+ .arg("--addr=127.0.0.1:8081")
+ .arg(guest)
+ .spawn()?;
+ let w = WasmtimeServe { lockfile, process };
+
+ // Clumsily wait for the server to accept connections.
+ 'wait: loop {
+ sleep(Duration::from_millis(100));
+ if TcpStream::connect("127.0.0.1:8081").is_ok() {
+ break 'wait;
+ }
+ }
+ Ok(w)
+ }
+}
+// Wasmtime serve will run until killed. Kill it in a drop impl so the process
+// isnt orphaned when the test suite ends (successfully, or unsuccessfully)
+impl Drop for WasmtimeServe {
+ fn drop(&mut self) {
+ let _ = self.process.kill();
+ }
+}
diff --git a/test-programs/tests/axum_hello_world.rs b/test-programs/tests/axum_hello_world.rs
index 1a33956..39c782f 100644
--- a/test-programs/tests/axum_hello_world.rs
+++ b/test-programs/tests/axum_hello_world.rs
@@ -1,17 +1,4 @@
use anyhow::Result;
-use std::net::TcpStream;
-use std::process::{Child, Command};
-use std::thread::sleep;
-use std::time::Duration;
-
-// Wasmtime serve will run until killed. Kill it in a drop impl so the process
-// isnt orphaned when the test suite ends (successfully, or unsuccessfully)
-struct DontOrphan(Child);
-impl Drop for DontOrphan {
- fn drop(&mut self) {
- let _ = self.0.kill();
- }
-}
#[test_log::test]
fn hello_world() -> Result<()> {
@@ -27,30 +14,16 @@ fn hello_world_nomacro() -> Result<()> {
// functionality
fn run(guest: &str) -> Result<()> {
// Run wasmtime serve.
- // Enable -Scli because we currently don't have a way to build with the
- // proxy adapter, so we build with the default adapter.
- let _wasmtime_process = DontOrphan(
- Command::new("wasmtime")
- .arg("serve")
- .arg("-Scli")
- .arg("--addr=127.0.0.1:8081")
- .arg(guest)
- .spawn()?,
- );
-
- // Clumsily wait for the server to accept connections.
- 'wait: loop {
- sleep(Duration::from_millis(100));
- if TcpStream::connect("127.0.0.1:8081").is_ok() {
- break 'wait;
- }
- }
+ let _serve = test_programs::WasmtimeServe::new(guest)?;
// Test each path in the server:
// TEST / handler
// Response body is the hard-coded default
- let body: String = ureq::get("http://127.0.0.1:8081").call()?.into_string()?;
+ let body: String = ureq::get("http://127.0.0.1:8081")
+ .call()?
+ .body_mut()
+ .read_to_string()?;
assert!(body.contains("
Hello, World!
"));
Ok(())
diff --git a/test-programs/tests/axum_weather.rs b/test-programs/tests/axum_weather.rs
new file mode 100644
index 0000000..4149d0c
--- /dev/null
+++ b/test-programs/tests/axum_weather.rs
@@ -0,0 +1,54 @@
+use anyhow::Result;
+use serde_json::Value;
+
+const CITY: &str = "portland";
+const COUNT: usize = 2;
+
+#[test_log::test]
+fn weather() -> Result<()> {
+ // Run wasmtime serve.
+ let _serve = test_programs::WasmtimeServe::new(test_programs::axum::WEATHER)?;
+
+ // TEST /weather weather handler
+ let body = ureq::get(format!(
+ "http://127.0.0.1:8081/weather?city={CITY}&count={COUNT}"
+ ))
+ .call()?
+ .body_mut()
+ .read_json::()?;
+ let array = body.as_array().expect("json body is an array");
+ assert_eq!(array.len(), COUNT);
+ let item_0 = &array[0];
+ let loc_0 = item_0
+ .get("location")
+ .expect("item 0 has `location`")
+ .as_object()
+ .expect("location 0 is object");
+ let qn_0 = loc_0
+ .get("qualified_name")
+ .expect("location has qualified name")
+ .as_str()
+ .expect("name is string");
+ assert!(
+ qn_0.contains("Multnomah"),
+ "{qn_0:?} should contain substring 'Multnomah'"
+ );
+
+ let item_1 = &array[1];
+ let loc_1 = item_1
+ .get("location")
+ .expect("item 1 has `location`")
+ .as_object()
+ .expect("location 1 is object");
+ let qn_1 = loc_1
+ .get("qualified_name")
+ .expect("location has qualified name")
+ .as_str()
+ .expect("name is string");
+ assert!(
+ qn_1.contains("Cumberland"),
+ "{qn_1:?} should contain substring 'Cumberland'"
+ );
+
+ Ok(())
+}
diff --git a/test-programs/tests/http_server.rs b/test-programs/tests/http_server.rs
index df96392..a8e9ae9 100644
--- a/test-programs/tests/http_server.rs
+++ b/test-programs/tests/http_server.rs
@@ -1,45 +1,21 @@
use anyhow::Result;
-use std::net::TcpStream;
-use std::process::{Child, Command};
-use std::thread::sleep;
use std::time::{Duration, Instant};
-// Wasmtime serve will run until killed. Kill it in a drop impl so the process
-// isnt orphaned when the test suite ends (successfully, or unsuccessfully)
-struct DontOrphan(Child);
-impl Drop for DontOrphan {
- fn drop(&mut self) {
- let _ = self.0.kill();
- }
-}
-
#[test_log::test]
fn http_server() -> Result<()> {
// Run wasmtime serve.
// Enable -Scli because we currently don't have a way to build with the
// proxy adapter, so we build with the default adapter.
- let _wasmtime_process = DontOrphan(
- Command::new("wasmtime")
- .arg("serve")
- .arg("-Scli")
- .arg("--addr=127.0.0.1:8081")
- .arg(test_programs::HTTP_SERVER)
- .spawn()?,
- );
-
- // Clumsily wait for the server to accept connections.
- 'wait: loop {
- sleep(Duration::from_millis(100));
- if TcpStream::connect("127.0.0.1:8081").is_ok() {
- break 'wait;
- }
- }
+ let _serve = test_programs::WasmtimeServe::new(test_programs::HTTP_SERVER)?;
// Test each path in the server:
// TEST / http_home
// Response body is the hard-coded default
- let body: String = ureq::get("http://127.0.0.1:8081").call()?.into_string()?;
+ let body: String = ureq::get("http://127.0.0.1:8081")
+ .call()?
+ .body_mut()
+ .read_to_string()?;
assert_eq!(body, "Hello, wasi:http/proxy world!\n");
// TEST /wait-response http_wait_response
@@ -48,7 +24,8 @@ fn http_server() -> Result<()> {
let start = Instant::now();
let body: String = ureq::get("http://127.0.0.1:8081/wait-response")
.call()?
- .into_string()?;
+ .body_mut()
+ .read_to_string()?;
let duration = start.elapsed();
let sleep_report = body
.split(' ')
@@ -69,7 +46,8 @@ fn http_server() -> Result<()> {
let start = Instant::now();
let body: String = ureq::get("http://127.0.0.1:8081/wait-body")
.call()?
- .into_string()?;
+ .body_mut()
+ .read_to_string()?;
let duration = start.elapsed();
let sleep_report = body
.split(' ')
@@ -91,7 +69,8 @@ fn http_server() -> Result<()> {
let start = Instant::now();
let body: String = ureq::get("http://127.0.0.1:8081/stream-body")
.call()?
- .into_string()?;
+ .body_mut()
+ .read_to_string()?;
let duration = start.elapsed();
assert_eq!(body.lines().count(), 5, "body has 5 lines");
for (iter, line) in body.lines().enumerate() {
@@ -110,8 +89,10 @@ fn http_server() -> Result<()> {
// Send a request body, see that we got the same back in response body.
const MESSAGE: &[u8] = b"hello, echoserver!\n";
let body: String = ureq::get("http://127.0.0.1:8081/echo")
+ .force_send_body()
.send(MESSAGE)?
- .into_string()?;
+ .body_mut()
+ .read_to_string()?;
assert_eq!(body.as_bytes(), MESSAGE);
// TEST /echo-headers htto_echo_headers
@@ -127,12 +108,15 @@ fn http_server() -> Result<()> {
];
let mut request = ureq::get("http://127.0.0.1:8081/echo-headers");
for (name, value) in test_headers {
- request = request.set(name, value);
+ request = request.header(name, value);
}
let response = request.call()?;
- assert!(response.headers_names().len() >= test_headers.len());
+ assert!(response.headers().len() >= test_headers.len());
for (name, value) in test_headers {
- assert_eq!(response.header(name), Some(value));
+ assert_eq!(
+ response.headers().get(name),
+ Some(&ureq::http::HeaderValue::from_str(value).unwrap())
+ );
}
// NOT TESTED /echo-trailers htto_echo_trailers
@@ -142,16 +126,11 @@ fn http_server() -> Result<()> {
// Send request with `X-Request-Code: `. Should get back that
// status.
let response = ureq::get("http://127.0.0.1:8081/response-status")
- .set("X-Response-Status", "302")
- .call()?;
- assert_eq!(response.status(), 302);
-
- let response = ureq::get("http://127.0.0.1:8081/response-status")
- .set("X-Response-Status", "401")
+ .header("X-Response-Status", "401")
.call();
- // ureq interprets some statuses as OK, some as Err:
+ // ureq gives us a 401 in an Error::StatusCode
match response {
- Err(ureq::Error::Status(401, _)) => {}
+ Err(ureq::Error::StatusCode(401)) => {}
result => {
panic!("/response-code expected status 302, got: {result:?}");
}
@@ -161,7 +140,7 @@ fn http_server() -> Result<()> {
// Wasmtime gives a 500 error when wasi-http guest gives error instead of
// response
match ureq::get("http://127.0.0.1:8081/response-fail").call() {
- Err(ureq::Error::Status(500, _)) => {}
+ Err(ureq::Error::StatusCode(500)) => {}
result => {
panic!("/response-fail expected status 500 error, got: {result:?}");
}
@@ -171,9 +150,9 @@ fn http_server() -> Result<()> {
// Response status and headers sent off, then error in body will close
// connection
match ureq::get("http://127.0.0.1:8081/response-body-fail").call() {
- Err(ureq::Error::Transport(_transport)) => {}
+ Err(ureq::Error::Io(_transport)) => {}
result => {
- panic!("/response-body-fail expected transport error, got: {result:?}")
+ panic!("/response-body-fail expected io error, got: {result:?}")
}
}
From de99ea4afc0b615a6108851131c494b8c07a4568 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Thu, 23 Oct 2025 10:47:38 -0700
Subject: [PATCH 10/11] file locking is in std if we lie about msrv policy for
7 days
---
Cargo.toml | 2 +-
src/net/tcp_listener.rs | 2 +-
test-programs/Cargo.toml | 3 ---
test-programs/src/lib.rs | 10 +++++-----
4 files changed, 7 insertions(+), 10 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index ee1a87d..2a362d6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -61,7 +61,7 @@ repository = "https://github.com/bytecodealliance/wstd"
keywords = ["WebAssembly", "async", "stdlib", "Components"]
categories = ["wasm", "asynchronous"]
# Rust-version policy: stable N-2, same as wasmtime.
-rust-version = "1.87"
+rust-version = "1.89"
authors = [
"Yoshua Wuyts ",
"Pat Hickey ",
diff --git a/src/net/tcp_listener.rs b/src/net/tcp_listener.rs
index 9f6d67f..9a1f57a 100644
--- a/src/net/tcp_listener.rs
+++ b/src/net/tcp_listener.rs
@@ -5,7 +5,7 @@ use crate::io;
use crate::iter::AsyncIterator;
use std::net::SocketAddr;
-use super::{to_io_err, TcpStream};
+use super::{TcpStream, to_io_err};
use crate::runtime::AsyncPollable;
/// A TCP socket server, listening for connections.
diff --git a/test-programs/Cargo.toml b/test-programs/Cargo.toml
index 24844b7..7f6191e 100644
--- a/test-programs/Cargo.toml
+++ b/test-programs/Cargo.toml
@@ -6,9 +6,6 @@ license.workspace = true
rust-version.workspace = true
publish = false
-[dependencies]
-fslock = "0.2.1"
-
[dev-dependencies]
anyhow.workspace = true
test-log.workspace = true
diff --git a/test-programs/src/lib.rs b/test-programs/src/lib.rs
index e7d9289..86e6d9f 100644
--- a/test-programs/src/lib.rs
+++ b/test-programs/src/lib.rs
@@ -1,6 +1,6 @@
include!(concat!(env!("OUT_DIR"), "/gen.rs"));
-use fslock::LockFile;
+use std::fs::File;
use std::net::TcpStream;
use std::process::{Child, Command};
use std::thread::sleep;
@@ -9,7 +9,7 @@ use std::time::Duration;
/// Manages exclusive access to port 8081, and kills the process when dropped
pub struct WasmtimeServe {
#[expect(dead_code, reason = "exists to live for as long as wasmtime process")]
- lockfile: LockFile,
+ lockfile: File,
process: Child,
}
@@ -23,9 +23,9 @@ impl WasmtimeServe {
/// Kills the wasmtime process, and releases the lock, once dropped.
pub fn new(guest: &str) -> std::io::Result {
let mut lockfile = std::env::temp_dir();
- lockfile.push("TEST_PROGRAMS_WASMTIME_SERVE.pid");
- let mut lockfile = LockFile::open(&lockfile)?;
- lockfile.lock_with_pid()?;
+ lockfile.push("TEST_PROGRAMS_WASMTIME_SERVE.lock");
+ let lockfile = File::create(&lockfile)?;
+ lockfile.lock()?;
// Run wasmtime serve.
// Enable -Scli because we currently don't have a way to build with the
From 3bbd1f2df02da044fd5b77794beb8275476b3f1e Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Thu, 23 Oct 2025 10:58:47 -0700
Subject: [PATCH 11/11] clippy for new msrv
---
src/io/copy.rs | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/io/copy.rs b/src/io/copy.rs
index 6aa1aff..4fd178e 100644
--- a/src/io/copy.rs
+++ b/src/io/copy.rs
@@ -8,11 +8,11 @@ where
{
// Optimized path when we have an `AsyncInputStream` and an
// `AsyncOutputStream`.
- if let Some(reader) = reader.as_async_input_stream() {
- if let Some(writer) = writer.as_async_output_stream() {
- reader.copy_to(writer).await?;
- return Ok(());
- }
+ if let Some(reader) = reader.as_async_input_stream()
+ && let Some(writer) = writer.as_async_output_stream()
+ {
+ reader.copy_to(writer).await?;
+ return Ok(());
}
// Unoptimized case: read the input and then write it.