From 3b3b80bcb2b280f37719194dba34fa17f52b50a6 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 01:12:24 -0800 Subject: [PATCH 01/20] feat: expose externally serviced host-call continuations --- CHANGELOG.md | 5 + README.md | 18 + crates/tinywasm/src/func/host_execution.rs | 251 +++++++++++++ crates/tinywasm/src/func/mod.rs | 2 + crates/tinywasm/src/lib.rs | 3 +- crates/tinywasm/tests/host_execution.rs | 397 +++++++++++++++++++++ 6 files changed, 675 insertions(+), 1 deletion(-) create mode 100644 crates/tinywasm/src/func/host_execution.rs create mode 100644 crates/tinywasm/tests/host_execution.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d12a3..443a750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `Function::call_resumable_with_host_calls` and `HostExecution` for externally serviced host calls, + with checked results, rooted arguments, and access to the calling module's context. + ## [0.12.0-beta.0] - 2026-11-01 Preliminary x86-64 benchmarks show around a 20% performance improvement over 0.11.0 on nightly, depending on the workload. diff --git a/README.md b/README.md index 4a37004..be4f885 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,24 @@ assert_eq!(result, 3); See the [examples](./examples) directory and [documentation](https://docs.rs/tinywasm) for more information. +## External host-call scheduling + +`Function::call_resumable_with_host_calls` returns a `HostExecution` that stops before each +host callback. On `HostExecProgress::HostCall`, obtain `host_call()` and either `invoke()` the +registered synchronous callback or `complete(&results)` with externally produced values. +Dropping the pending-call view leaves the call waiting; resuming does not replay it. + +An external adapter can own futures, register wakeups, and resume Wasm only when work is ready. +It must cancel that work when its invocation is dropped. `PendingHostCall::with_context` gives +the adapter access to host state and the calling module's exported memory and functions, +including blocking calls to canonical ABI realloc and post-return functions. Arguments remain +rooted across collection, and completion checks result types and store ownership. + +Component parsing, canonical ABI lowering, resource tables, and task scheduling remain outside +the runtime. Each execution handle exclusively borrows its store; nested calls are synchronous. +This seam does not implement the WASI 0.3 component concurrency model. The existing synchronous +and fuel-only execution APIs use their existing drivers and dispatch paths. + ## Cargo Features - **`full`:** Enables `archive`, `debug`, `parallel-parser`, `parser`, `state`, and `validate`. Enabled by default. diff --git a/crates/tinywasm/src/func/host_execution.rs b/crates/tinywasm/src/func/host_execution.rs new file mode 100644 index 0000000..1a1ff6b --- /dev/null +++ b/crates/tinywasm/src/func/host_execution.rs @@ -0,0 +1,251 @@ +use alloc::{boxed::Box, vec, vec::Vec}; + +use super::{FuncContext, Function}; +use crate::interpreter::executor::{Budget, ExecOutcome, Executor, HostCall}; +use crate::interpreter::stack::CallFrame; +use crate::store::FuncValueTypes; +use crate::{Error, Result, Store, Trap, WasmValue, types::TypeAddr}; + +/// Progress of an invocation whose host calls are handled by the embedder. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HostExecProgress { + /// The invocation wrote its final results to the supplied result slice. + Completed, + /// Execution exhausted its fuel at an interpreter boundary. + Suspended, + /// A host call is waiting for [`HostExecution::host_call`]. + HostCall, +} + +/// An invocation that stops before invoking each host callback. +/// +/// The embedder can invoke a synchronous callback or supply results from an external future. +/// Futures, wakeups, and cancellation of external work belong to the embedder. Dropping this +/// handle cancels the Wasm continuation and releases its stack and pending argument roots. +/// Only one invocation can borrow a store at a time; this is not a component task scheduler. +pub struct HostExecution<'store> { + store: &'store mut Store, + results: &'store mut [WasmValue], + root_type: TypeAddr, + state: State, +} + +enum State { + Ready(CallFrame), + ReturnFromHost(HostCall), + Pending { function: Function, params: Vec, continuation: Option }, + Completed, + Finished, +} + +/// Access to the current host call, with its arguments rooted in the owning store. +/// +/// Dropping this view leaves the call pending. Clone arguments needed by external work, drop +/// the view before waiting, and obtain it again when the results are ready. The callback is +/// never replayed by [`HostExecution::resume_with_fuel`]. +pub struct PendingHostCall<'execution, 'store> { + execution: &'execution mut HostExecution<'store>, +} + +impl Function { + /// Starts an invocation that yields to the embedder before every host callback. + /// + /// Unlike [`Self::call_resumable`], this also defers a host function used as the root call. + /// Use [`PendingHostCall::invoke`] for synchronous callbacks and + /// [`PendingHostCall::complete`] for results produced outside the interpreter. + /// This API uses the same fuel boundaries as [`super::FuncExecution::resume_with_fuel`]. + pub fn call_resumable_with_host_calls<'store>( + &self, + store: &'store mut Store, + params: &[WasmValue], + results: &'store mut [WasmValue], + ) -> Result> { + let root_type = self.validate_call(store, params, results.len())?; + let state = { + let mut store = store.enter_execution()?; + store.call_stack.clear(); + store.value_stack.clear(); + (|| -> Result { + if store.code.funcs.is_host(self.addr()) { + return Ok(State::Pending { function: self.clone(), params: params.to_vec(), continuation: None }); + } + let wasm = store.code.funcs.wasm(self.addr()); + let (params_count, locals, max_stack) = (wasm.func.params, wasm.func.locals, wasm.func.max_stack); + store.push_wasm_values(params)?; + let base = store.value_stack.enter_locals(¶ms_count, &locals, &max_stack)?; + Ok(State::Ready(CallFrame::new(self.addr(), base, locals))) + })() + .inspect_err(|_| store.value_stack.clear())? + }; + Ok(HostExecution { store, results, root_type, state }) + } +} + +impl<'store> HostExecution<'store> { + /// Runs until completion, fuel exhaustion, or the next host call. + /// + /// A pending host call is returned again without executing guest code or invoking a callback. + /// Completing the host call commits its results; a later resume continues after that call. + /// A trap or panic makes the invocation terminal, as with ordinary resumable execution. + pub fn resume_with_fuel(&mut self, fuel: u32) -> Result { + match self.state { + State::Pending { .. } => return Ok(HostExecProgress::HostCall), + State::Completed => { + self.state = State::Finished; + return Ok(HostExecProgress::Completed); + } + State::Finished => return Err(Error::other("execution already completed")), + _ => {} + } + let state = core::mem::replace(&mut self.state, State::Finished); + let outcome = { + let mut store = self.store.enter_execution()?; + store.execution_fuel = fuel; + match state { + State::Ready(frame) => Executor::new(&mut store, frame, 0, Budget::Fuel).run(), + State::ReturnFromHost(call) => { + Executor::new(&mut store, call.caller, 0, Budget::Fuel).return_from_host() + } + _ => unreachable!(), + } + }; + self.accept_outcome(outcome).inspect_err(|_| self.finish()) + } + + fn accept_outcome(&mut self, outcome: Result) -> Result { + match outcome? { + ExecOutcome::Complete => { + self.store.pop_stack_values(self.root_type, self.results)?; + Ok(HostExecProgress::Completed) + } + ExecOutcome::Suspended(frame) => { + self.state = State::Ready(frame); + Ok(HostExecProgress::Suspended) + } + ExecOutcome::Host(call) => { + let type_addr = self.store.code.funcs.type_addr(call.func); + let base = + self.store.value_stack.base_before(self.store.get_func_type(call.func).params().iter().collect()); + let params = self.store.stack_value_iter(type_addr, FuncValueTypes::Params, base)?.collect(); + self.store.value_stack.truncate_to_base(base); + let function = + Function { item: crate::StoreItem::new(self.store.id(), call.func), module_id: call.module }; + self.state = State::Pending { function, params, continuation: Some(call) }; + Ok(HostExecProgress::HostCall) + } + ExecOutcome::Exception(reference) => Err(Error::Exception(self.store.root_exception(reference)?)), + } + } + + /// Returns the pending host call, if execution has reached one. + pub fn host_call(&mut self) -> Option> { + matches!(self.state, State::Pending { .. }).then_some(PendingHostCall { execution: self }) + } + + fn finish(&mut self) { + self.state = State::Finished; + self.store.call_stack.clear(); + self.store.value_stack.clear(); + } +} + +impl Drop for HostExecution<'_> { + fn drop(&mut self) { + self.finish(); + } +} + +impl PendingHostCall<'_, '_> { + /// The resolved host function, including the importing module's context. + pub fn function(&self) -> &Function { + let State::Pending { function, .. } = &self.execution.state else { unreachable!() }; + function + } + + /// Arguments in WebAssembly signature order. Managed references remain rooted while pending. + pub fn params(&self) -> &[WasmValue] { + let State::Pending { params, .. } = &self.execution.state else { unreachable!() }; + params + } + + /// Read-only access to the owning store, for inspecting types and store-checked handles. + pub fn store(&self) -> &Store { + self.execution.store + } + + /// Accesses host state, guest memory, and blocking nested calls while this call is pending. + /// + /// The context cannot outlive this callback. Use [`FuncContext::call`] for nested calls, + /// including canonical ABI realloc and post-return functions. Nested calls remain synchronous. + /// A panic in the callback terminates the invocation. + pub fn with_context(&mut self, callback: impl FnOnce(FuncContext<'_>) -> Result) -> Result { + let module_id = self.function().module_id; + let pending = core::mem::replace(&mut self.execution.state, State::Finished); + let result = { + let mut store = self.execution.store.enter_execution()?; + callback(FuncContext { store: &mut store, module_id }) + }; + self.execution.state = pending; + result + } + + /// Commits externally produced results without replaying the host callback. + /// + /// Invalid result counts, types, and foreign-store references leave the call pending so + /// the embedder can correct them. A failure to grow the result stack terminates execution. + pub fn complete(self, results: &[WasmValue]) -> Result<()> { + let function = self.function(); + let store = &*self.execution.store; + for value in results { + if matches!(value, WasmValue::Ref(_)) { + value.to_runtime(store)?; + } + } + let expected = function.ty(store)?; + if results.len() != expected.results().len() + || !results.iter().zip(expected.results()).all(|(value, &ty)| store.value_matches_type(value, ty)) + { + return Err(Error::InvalidHostFnReturn { expected: Box::new(expected.clone()), actual: results.to_vec() }); + } + let State::Pending { continuation, .. } = &self.execution.state else { unreachable!() }; + if let Some(call) = *continuation { + if let Err(error) = self.execution.store.push_wasm_values(results) { + self.execution.finish(); + return Err(error); + } + self.execution.state = if call.tail { State::ReturnFromHost(call) } else { State::Ready(call.caller) }; + } else { + self.execution.results.clone_from_slice(results); + self.execution.state = State::Completed; + } + Ok(()) + } + + /// Invokes the registered synchronous callback once, then commits its results. + /// + /// An error or panic terminates this invocation. Errors use the same host-trap wrapping as + /// synchronous calls from Wasm; root host calls return the callback's error directly. + pub fn invoke(self) -> Result<()> { + let State::Pending { function, continuation, .. } = &self.execution.state else { unreachable!() }; + let (addr, module_id, continuation) = (function.addr(), function.module_id, *continuation); + let type_addr = self.execution.store.code.funcs.type_addr(addr); + let callback = self.execution.store.code.funcs.host(addr).func.clone(); + let mut results = vec![WasmValue::I32(0); self.execution.store.get_func_type(addr).results().len()]; + // Keep owned arguments alive while a callback runs, even if it panics or collects. + let State::Pending { params, .. } = core::mem::replace(&mut self.execution.state, State::Finished) else { + unreachable!() + }; + let result = { + let mut store = self.execution.store.enter_execution()?; + callback.call_values(&mut store, module_id, type_addr, ¶ms, &mut results) + }; + if let Err(error) = result { + self.execution.finish(); + return Err(if continuation.is_some() { Error::Trap(Trap::HostFunction(Box::new(error))) } else { error }); + } + // Complete through the same checked result path used by external callbacks. + let function = Function { item: crate::StoreItem::new(self.execution.store.id(), addr), module_id }; + self.execution.state = State::Pending { function, params, continuation }; + self.complete(&results) + } +} diff --git a/crates/tinywasm/src/func/mod.rs b/crates/tinywasm/src/func/mod.rs index 869e2a7..14f6c16 100644 --- a/crates/tinywasm/src/func/mod.rs +++ b/crates/tinywasm/src/func/mod.rs @@ -9,12 +9,14 @@ use crate::{FuncRef, WasmValue}; mod context; mod host; +mod host_execution; mod resume; mod values; pub use context::FuncContext; pub use host::HostFunction; #[doc(hidden)] pub use host::HostFunctionCallback; +pub use host_execution::{HostExecProgress, HostExecution, PendingHostCall}; pub use resume::{ExecProgress, FuncExecution, FuncExecutionTyped}; pub use values::{FromWasmValues, IntoWasmValues, WasmTypes, WasmValueType}; diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 6aeb645..2843c86 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -123,7 +123,8 @@ mod error; pub use error::*; pub use func::{ ExecProgress, FromWasmValues, FuncContext, FuncExecution, FuncExecutionTyped, Function, FunctionTyped, - HostFunction, HostFunctionCallback, IntoWasmValues, WasmTypes, WasmValueType, + HostExecProgress, HostExecution, HostFunction, HostFunctionCallback, IntoWasmValues, PendingHostCall, WasmTypes, + WasmValueType, }; pub use imports::*; pub use instance::{ExternItem, ModuleInstance}; diff --git a/crates/tinywasm/tests/host_execution.rs b/crates/tinywasm/tests/host_execution.rs new file mode 100644 index 0000000..027a3e9 --- /dev/null +++ b/crates/tinywasm/tests/host_execution.rs @@ -0,0 +1,397 @@ +use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, +}; +use tinywasm::{ + Error, FuncContext, HostExecProgress, HostFunction, Imports, ModuleInstance, Result, Store, Trap, WasmValue, +}; + +fn instance(store: &mut Store, source: &str, calls: Arc) -> Result { + let mut imports = Imports::new(); + imports.define( + "env", + "wait", + HostFunction::from(move |_: FuncContext<'_>, value: i32| { + calls.fetch_add(1, Ordering::SeqCst); + Ok(value + 1) + }), + ); + let module = tinywasm::parse_bytes(&wat::parse_str(source).unwrap())?; + ModuleInstance::instantiate(store, &module, Some(&imports)) +} + +const SOURCE: &str = r#"(module + (type $i2i (func (param i32) (result i32))) + (import "env" "wait" (func $wait (type $i2i))) + (memory (export "memory") 1) + (table 1 funcref) + (elem (i32.const 0) func $wait) + (func (export "run") (result i32) i32.const 20 call $wait i32.const 2 i32.mul) + (func (export "tail") (result i32) i32.const 20 return_call $wait) + (func (export "indirect") (result i32) i32.const 20 i32.const 0 call_indirect (type $i2i)) + (func (export "tail-indirect") (result i32) i32.const 20 i32.const 0 return_call_indirect (type $i2i)) + (func (export "ref") (result i32) i32.const 20 ref.func $wait call_ref $i2i) + (func (export "tail-ref") (result i32) i32.const 20 ref.func $wait return_call_ref $i2i) + (export "host" (func $wait)))"#; + +#[test] +fn host_calls_wait_without_replay_and_validate_results() -> Result<()> { + let calls = Arc::new(AtomicUsize::new(0)); + let mut store = Store::default(); + let instance = instance(&mut store, SOURCE, calls.clone())?; + for (name, expected) in [ + ("run", 84), + ("tail", 42), + ("host", 42), + ("indirect", 42), + ("tail-indirect", 42), + ("ref", 42), + ("tail-ref", 42), + ] { + let function = instance.func_untyped(&store, name)?; + let params = if name == "host" { vec![WasmValue::I32(20)] } else { vec![] }; + let mut results = [WasmValue::I32(-1)]; + let mut execution = function.call_resumable_with_host_calls(&mut store, ¶ms, &mut results)?; + while execution.resume_with_fuel(100)? == HostExecProgress::Suspended {} + for _ in 0..3 { + assert_eq!(execution.resume_with_fuel(0)?, HostExecProgress::HostCall); + let call = execution.host_call().unwrap(); + assert_eq!(call.params(), &[WasmValue::I32(20)]); + } + assert!(matches!(execution.host_call().unwrap().complete(&[]), Err(Error::InvalidHostFnReturn { .. }))); + assert!(matches!( + execution.host_call().unwrap().complete(&[WasmValue::I64(42)]), + Err(Error::InvalidHostFnReturn { .. }) + )); + execution.host_call().unwrap().complete(&[WasmValue::I32(42)])?; + while execution.resume_with_fuel(1)? == HostExecProgress::Suspended {} + assert!(execution.resume_with_fuel(100).is_err()); + drop(execution); + assert_eq!(results, [WasmValue::I32(expected)]); + } + assert_eq!(calls.load(Ordering::SeqCst), 0); + Ok(()) +} + +#[test] +fn synchronous_callbacks_can_be_forwarded_once() -> Result<()> { + let calls = Arc::new(AtomicUsize::new(0)); + let mut store = Store::default(); + let instance = instance(&mut store, SOURCE, calls.clone())?; + for (name, expected) in [ + ("run", 42), + ("tail", 21), + ("host", 21), + ("indirect", 21), + ("tail-indirect", 21), + ("ref", 21), + ("tail-ref", 21), + ] { + let function = instance.func_untyped(&store, name)?; + let params = if name == "host" { vec![WasmValue::I32(20)] } else { vec![] }; + let mut results = [WasmValue::I32(-1)]; + let mut execution = function.call_resumable_with_host_calls(&mut store, ¶ms, &mut results)?; + loop { + match execution.resume_with_fuel(1)? { + HostExecProgress::HostCall => execution.host_call().unwrap().invoke()?, + HostExecProgress::Suspended => {} + HostExecProgress::Completed => break, + } + } + drop(execution); + assert_eq!(results, [WasmValue::I32(expected)]); + } + assert_eq!(calls.load(Ordering::SeqCst), 7); + Ok(()) +} + +#[test] +fn external_adapter_uses_callers_memory_and_canonical_abi_exports() -> Result<()> { + let calls = Arc::new(AtomicUsize::new(0)); + let mut store = Store::default(); + let leaf = instance( + &mut store, + r#"(module + (import "env" "wait" (func $wait (param i32) (result i32))) + (memory (export "memory") 1) + (global $post (mut i32) (i32.const 0)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 64) + (func (export "post-return") (param i32) i32.const 1 global.set $post) + (func (export "run") (result i32) + i32.const 20 call $wait i32.load global.get $post i32.add))"#, + calls, + )?; + let mut imports = Imports::new(); + imports.link_module("leaf", leaf)?; + let module = tinywasm::parse_bytes( + &wat::parse_str( + r#"(module + (import "leaf" "run" (func $run (result i32))) + (memory (export "memory") 1) + (func (export "run") (result i32) call $run))"#, + ) + .unwrap(), + )?; + let root = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + let function = root.func_untyped(&store, "run")?; + let mut results = [WasmValue::I32(0)]; + let mut execution = function.call_resumable_with_host_calls(&mut store, &[], &mut results)?; + assert_eq!(execution.resume_with_fuel(100)?, HostExecProgress::HostCall); + let mut call = execution.host_call().unwrap(); + let pointer = call.with_context(|mut context| { + let realloc = context.module().func::<(i32, i32, i32, i32), i32>(context.store(), "realloc")?; + let pointer = context.call(&realloc, (0, 0, 4, 4))?; + context.memory("memory")?.copy_from_slice(context.store_mut(), pointer as usize, &41_i32.to_le_bytes())?; + let post = context.module().func::(context.store(), "post-return")?; + context.call(&post, pointer)?; + context.store_mut().gc()?; + Ok(pointer) + })?; + call.complete(&[WasmValue::I32(pointer)])?; + assert_eq!(execution.resume_with_fuel(100)?, HostExecProgress::Completed); + drop(execution); + assert_eq!(results, [WasmValue::I32(42)]); + assert_eq!(root.memory("memory")?.read_vec(&store, 64, 4)?, [0; 4]); + Ok(()) +} + +#[test] +fn dropping_a_pending_call_releases_argument_roots() -> Result<()> { + struct Usage(Arc); + impl tinywasm::ResourceLimiter for Usage { + fn gc_growing(&self, current: usize, _: usize, _: Option) -> std::result::Result { + self.0.store(current, Ordering::SeqCst); + Ok(true) + } + } + let current = Arc::new(AtomicUsize::new(usize::MAX)); + let config = tinywasm::engine::Config::new() + .with_gc_collection_threshold(usize::MAX) + .with_resource_limiter(Usage(current.clone())); + let mut store = Store::new(tinywasm::Engine::new(config)); + let mut imports = Imports::new(); + imports.define( + "env", + "wait", + HostFunction::from(|_: FuncContext<'_>, value: Option| Ok(value)), + ); + let module = tinywasm::parse_bytes( + &wat::parse_str( + r#"(module + (type $s (struct (field i32))) + (import "env" "wait" (func $wait (param externref) (result externref))) + (func (export "make") (result externref) i32.const 7 struct.new $s extern.convert_any) + (func (export "read") (param externref) (result i32) + local.get 0 any.convert_extern ref.cast (ref $s) struct.get $s 0) + (func (export "run") (param externref) (result externref) local.get 0 return_call $wait))"#, + ) + .unwrap(), + )?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + let make = instance.func::<(), Option>(&store, "make")?; + let reference = make.call(&mut store, ())?.unwrap(); + let function = instance.func_untyped(&store, "run")?; + let params = [WasmValue::from(reference)]; + let mut results = [WasmValue::I32(0)]; + let mut execution = function.call_resumable_with_host_calls(&mut store, ¶ms, &mut results)?; + drop(params); + assert_eq!(execution.resume_with_fuel(100)?, HostExecProgress::HostCall); + let mut call = execution.host_call().unwrap(); + let param = call.params()[0].clone(); + call.with_context(move |mut context| { + context.store_mut().gc()?; + let read = context.module().func_untyped(context.store(), "read")?; + let mut result = [WasmValue::I32(0)]; + context.call_untyped(&read, &[param], &mut result)?; + assert_eq!(result, [WasmValue::I32(7)]); + Ok(()) + })?; + let mut foreign_store = Store::default(); + let foreign = tinywasm::ExternRef::try_new(&mut foreign_store, 0)?; + assert!(matches!( + execution.host_call().unwrap().complete(&[WasmValue::from(foreign)]), + Err(Error::Trap(Trap::InvalidStore)) + )); + drop(execution); + store.gc()?; + drop(make.call(&mut store, ())?); + assert_eq!(current.load(Ordering::SeqCst), 0, "pending arguments must not leak GC objects"); + assert_eq!( + instance + .func::, Option>(&store, "run")? + .call(&mut store, None)?, + None + ); + Ok(()) +} + +#[test] +fn callback_panics_and_guest_traps_are_terminal() -> Result<()> { + use std::panic::{AssertUnwindSafe, catch_unwind}; + let mut store = Store::default(); + let mut imports = Imports::new(); + imports.define( + "env", + "wait", + HostFunction::from(|_: FuncContext<'_>, (): ()| -> Result<()> { panic!("host panic") }), + ); + let module = tinywasm::parse_bytes( + &wat::parse_str( + r#"(module + (import "env" "wait" (func $wait)) + (func (export "run") call $wait) + (func (export "trap") unreachable) + (func (export "ok") (result i32) i32.const 42))"#, + ) + .unwrap(), + )?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + let function = instance.func_untyped(&store, "run")?; + let mut execution = function.call_resumable_with_host_calls(&mut store, &[], &mut [])?; + assert_eq!(execution.resume_with_fuel(100)?, HostExecProgress::HostCall); + assert!(catch_unwind(AssertUnwindSafe(|| execution.host_call().unwrap().invoke())).is_err()); + assert!(execution.resume_with_fuel(100).is_err()); + drop(execution); + let mut execution = function.call_resumable_with_host_calls(&mut store, &[], &mut [])?; + assert_eq!(execution.resume_with_fuel(100)?, HostExecProgress::HostCall); + assert!( + catch_unwind(AssertUnwindSafe(|| { + execution.host_call().unwrap().with_context(|_| -> Result<()> { panic!("context panic") }) + })) + .is_err() + ); + assert!(execution.resume_with_fuel(100).is_err()); + drop(execution); + let trap = instance.func_untyped(&store, "trap")?; + let mut execution = trap.call_resumable_with_host_calls(&mut store, &[], &mut [])?; + assert!(matches!(execution.resume_with_fuel(100), Err(Error::Trap(Trap::Unreachable)))); + assert!(execution.resume_with_fuel(100).is_err()); + drop(execution); + assert_eq!(instance.func::<(), i32>(&store, "ok")?.call(&mut store, ())?, 42); + Ok(()) +} + +#[test] +fn future_adapter_waits_for_a_wakeup_without_repolling_wasm() -> Result<()> { + use std::future::Future; + use std::pin::Pin; + use std::sync::Mutex; + use std::task::{Context, Poll, Wake, Waker}; + + #[derive(Default)] + struct Mailbox { + value: Option, + waker: Option, + } + struct Deferred { + mailbox: Arc>, + cancelled: Arc, + done: bool, + } + impl Drop for Deferred { + fn drop(&mut self) { + if !self.done { + self.cancelled.fetch_add(1, Ordering::SeqCst); + } + } + } + impl Future for Deferred { + type Output = i32; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let mut mailbox = this.mailbox.lock().unwrap(); + if let Some(value) = mailbox.value.take() { + this.done = true; + return Poll::Ready(value); + } + mailbox.waker = Some(cx.waker().clone()); + Poll::Pending + } + } + // All future and scheduler state lives in the external adapter. No runtime-private APIs. + struct Adapter<'a> { + execution: tinywasm::HostExecution<'a>, + mailbox: Arc>, + future: Option, + cancelled: Arc, + } + impl Future for Adapter<'_> { + type Output = Result<()>; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + loop { + if let Some(future) = &mut this.future { + let value = std::task::ready!(Pin::new(future).poll(cx)); + this.execution.host_call().unwrap().complete(&[WasmValue::I32(value)])?; + this.future = None; + } + match this.execution.resume_with_fuel(128)? { + HostExecProgress::Completed => return Poll::Ready(Ok(())), + HostExecProgress::Suspended => { + cx.waker().wake_by_ref(); + return Poll::Pending; + } + HostExecProgress::HostCall => { + assert_eq!(this.execution.host_call().unwrap().params(), &[WasmValue::I32(20)]); + this.future = Some(Deferred { + mailbox: this.mailbox.clone(), + cancelled: this.cancelled.clone(), + done: false, + }); + } + } + } + } + } + struct WakeCount(AtomicUsize); + impl Wake for WakeCount { + fn wake(self: Arc) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + let calls = Arc::new(AtomicUsize::new(0)); + let mut store = Store::default(); + let instance = instance(&mut store, SOURCE, calls.clone())?; + let function = instance.func_untyped(&store, "run")?; + let mut results = [WasmValue::I32(0)]; + let mailbox = Arc::new(Mutex::new(Mailbox::default())); + let cancelled = Arc::new(AtomicUsize::new(0)); + let mut adapter = Adapter { + cancelled: cancelled.clone(), + execution: function.call_resumable_with_host_calls(&mut store, &[], &mut results)?, + mailbox: mailbox.clone(), + future: None, + }; + let wakes = Arc::new(WakeCount(AtomicUsize::new(0))); + let waker = Waker::from(wakes.clone()); + let mut cx = Context::from_waker(&waker); + assert!(Pin::new(&mut adapter).poll(&mut cx).is_pending()); + assert!(Pin::new(&mut adapter).poll(&mut cx).is_pending()); + assert_eq!(wakes.0.load(Ordering::SeqCst), 0, "host wait must not self-wake"); + let wake = { + let mut mailbox = mailbox.lock().unwrap(); + mailbox.value = Some(42); + mailbox.waker.take().unwrap() + }; + wake.wake(); + assert_eq!(wakes.0.load(Ordering::SeqCst), 1); + assert!(matches!(Pin::new(&mut adapter).poll(&mut cx), Poll::Ready(Ok(())))); + drop(adapter); + assert_eq!(results, [WasmValue::I32(84)]); + assert_eq!(calls.load(Ordering::SeqCst), 0); + assert_eq!(cancelled.load(Ordering::SeqCst), 0); + let mut cancelled_results = [WasmValue::I32(-1)]; + let mut adapter = Adapter { + execution: function.call_resumable_with_host_calls(&mut store, &[], &mut cancelled_results)?, + mailbox: Arc::new(Mutex::new(Mailbox::default())), + future: None, + cancelled: cancelled.clone(), + }; + assert!(Pin::new(&mut adapter).poll(&mut cx).is_pending()); + drop(adapter); + assert_eq!(cancelled.load(Ordering::SeqCst), 1); + assert_eq!(cancelled_results, [WasmValue::I32(-1)]); + assert_eq!(instance.func::<(), i32>(&store, "run")?.call(&mut store, ())?, 42); + + Ok(()) +} From 0f3904c504e5d5421409e67da3aad0a2dbfb8190 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 10:56:51 -0800 Subject: [PATCH 02/20] feat: isolate optional host continuations and define shared C API requirements --- .github/workflows/test.yaml | 4 ++++ CHANGELOG.md | 3 ++- README.md | 3 +++ crates/c-api/README.md | 39 ++++++++++++++++++++++++++++++++- crates/tinywasm/Cargo.toml | 8 ++++++- crates/tinywasm/src/func/mod.rs | 2 ++ crates/tinywasm/src/lib.rs | 5 +++-- 7 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6be4ae5..3f3d66c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -114,3 +114,7 @@ jobs: - name: Run clippy run: cargo clippy --workspace --target ${{ matrix.target }} ${{ matrix.args }} if: matrix.target != 'armv7-unknown-linux-gnueabihf' + + - name: Run optional host-call continuation tests + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: cargo test -p tinywasm --target ${{ matrix.target }} --features external-host-calls --test host_execution diff --git a/CHANGELOG.md b/CHANGELOG.md index 443a750..ea3a293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `Function::call_resumable_with_host_calls` and `HostExecution` for externally serviced host calls, - with checked results, rooted arguments, and access to the calling module's context. + with checked results, rooted arguments, and access to the calling module's context, behind the + opt-in `external-host-calls` feature. ## [0.12.0-beta.0] - 2026-11-01 diff --git a/README.md b/README.md index be4f885..82248fb 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ See the [examples](./examples) directory and [documentation](https://docs.rs/tin ## External host-call scheduling +Enable the `external-host-calls` Cargo feature to include this API. It is excluded from both +`default` and `full`; disabled builds do not compile the continuation driver or its public types. + `Function::call_resumable_with_host_calls` returns a `HostExecution` that stops before each host callback. On `HostExecProgress::HostCall`, obtain `host_call()` and either `invoke()` the registered synchronous callback or `complete(&results)` with externally produced values. diff --git a/crates/c-api/README.md b/crates/c-api/README.md index c885ede..239003c 100644 --- a/crates/c-api/README.md +++ b/crates/c-api/README.md @@ -35,7 +35,44 @@ When building with Cargo directly, enable `custom-prefix` and set the `TINYWASM_C_API_PREFIX` environment variable. Include `tinywasm.h` before `wasm.h` so the symbol aliases take effect. -## Notes +## Runtime-neutral extension direction + +The integration target is the standard `wasm.h` plus a small, separately versioned common +extension header implemented by TinyWasm and WAMR. This is a proposed contract, not an +implemented C extension. The Rust `external-host-calls` API is backend machinery for it. +See the [WAMR roadmap discussion](https://github.com/wasm-micro-runtime/wasm-micro-runtime/discussions/5164#discussioncomment-19111943). + +Keep ordinary module linking, calls, memories, tables, and ownership on the standard API. +Test the same external component/WASI layer against both implementations. A common header +alone does not establish shared semantics, object compatibility, or binary plugin compatibility. +Objects must remain with their originating runtime and store. Symbol prefixes permit static +coexistence; any runtime dispatch table belongs in an optional adapter outside the core. + +The proposed extension needs an opaque invocation handle, explicit completed/pending/trap +outcomes, host-call suspension, checked result completion, and cancellation. Specify argument +and result ownership, reference rooting, caller memory/context access, reentrancy, error and +trap ownership, and cancellation/finalizer ordering before freezing declarations. Callback-style +async can use ordinary calls; stackful suspension support must be qualified per execution mode. +Do not expose interpreter frames, instruction pointers, or Rust futures across the C ABI. +Wakers, event loops, resource tables, canonical ABI, and WASI implementations stay external. + +Optional functionality must not add fields to ordinary stores/instances/call frames, branches +to synchronous dispatch, global registration roots, or allocation/initialization work in disabled +builds. Put implementations in separately selectable crates or translation units. Avoid forcing +whole-archive retention; shared-library export roots and callback tables need explicit auditing. +LTO cannot remove observable fields or reachable hooks merely because an application never +uses their feature. + +Acceptance requires final linked-artifact comparisons with identical compiler, linker, LTO, +whole-program, panic, allocator, and stripping settings: baseline versus extension-disabled; +enabled-but-unused; and enabled-and-used. Check text/rodata/data/bss, retained symbols and link +maps, store/frame layouts, heap/stack high-water marks, and execution cost. Use the same portable +C conformance fixtures for both runtimes, including shared memory/table imports and cancel/error +paths. On a Pico, measure static RAM and heap/stack peaks; RSS applies to hosted deployments. +The current TinyWasm C wrapper requires `std`; Pico C-wrapper support and zero-overhead linked +builds have not been demonstrated by the Rust seam tests. + +## Current API limitations - Follow the ownership annotations in `wasm.h` and use the matching delete functions. Keep a store and its objects on the same thread. diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index bca9371..44fac88 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -12,13 +12,17 @@ keywords.workspace = true categories.workspace = true [package.metadata.docs.rs] -features = ["std", "validate", "parser", "archive", "log", "canonicalize-nans", "state", "debug", "guest-debug"] +features = ["std", "validate", "parser", "archive", "log", "canonicalize-nans", "state", "debug", "guest-debug", "external-host-calls"] rustdoc-args = ["--cfg", "docsrs"] [lib] name = "tinywasm" path = "src/lib.rs" +[[test]] +name = "host_execution" +required-features = ["external-host-calls"] + [[test]] harness = false name = "test-wasm-1" @@ -95,6 +99,8 @@ wat.workspace = true autocfg = "1.5" [features] +# expose host-call continuations for external schedulers; excluded from default and full +external-host-calls = [] default = ["full", "nightly", "simd-intrinsics", "std"] full = ["archive", "debug", "parallel-parser", "parser", "state", "validate"] diff --git a/crates/tinywasm/src/func/mod.rs b/crates/tinywasm/src/func/mod.rs index 14f6c16..1ff9fb3 100644 --- a/crates/tinywasm/src/func/mod.rs +++ b/crates/tinywasm/src/func/mod.rs @@ -9,6 +9,7 @@ use crate::{FuncRef, WasmValue}; mod context; mod host; +#[cfg(feature = "external-host-calls")] mod host_execution; mod resume; mod values; @@ -16,6 +17,7 @@ pub use context::FuncContext; pub use host::HostFunction; #[doc(hidden)] pub use host::HostFunctionCallback; +#[cfg(feature = "external-host-calls")] pub use host_execution::{HostExecProgress, HostExecution, PendingHostCall}; pub use resume::{ExecProgress, FuncExecution, FuncExecutionTyped}; pub use values::{FromWasmValues, IntoWasmValues, WasmTypes, WasmValueType}; diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 2843c86..3d0fded 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -123,9 +123,10 @@ mod error; pub use error::*; pub use func::{ ExecProgress, FromWasmValues, FuncContext, FuncExecution, FuncExecutionTyped, Function, FunctionTyped, - HostExecProgress, HostExecution, HostFunction, HostFunctionCallback, IntoWasmValues, PendingHostCall, WasmTypes, - WasmValueType, + HostFunction, HostFunctionCallback, IntoWasmValues, WasmTypes, WasmValueType, }; +#[cfg(feature = "external-host-calls")] +pub use func::{HostExecProgress, HostExecution, PendingHostCall}; pub use imports::*; pub use instance::{ExternItem, ModuleInstance}; pub use reference::*; From 91b64623e2d13ece86e83cc0c606f86ad79020dc Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 12:18:26 -0800 Subject: [PATCH 03/20] feat: add shared numeric C continuations and conformance fixture --- .github/workflows/shared_continuations.yml | 14 + CHANGELOG.md | 2 + crates/c-api/Cargo.toml | 1 + crates/c-api/Makefile | 5 +- crates/c-api/README.md | 17 +- crates/c-api/include/tinywasm-prefix.h | 5 + crates/c-api/include/wasm_ext.h | 52 +++ crates/c-api/src/continuation.rs | 285 ++++++++++++ crates/c-api/src/function.rs | 2 + crates/c-api/src/lib.rs | 2 + crates/c-api/src/runtime.rs | 18 +- crates/c-api/tests/continuations/README.md | 59 +++ crates/c-api/tests/continuations/fixture.c | 426 ++++++++++++++++++ crates/c-api/tests/continuations/fixture.wasm | Bin 0 -> 258 bytes crates/c-api/tests/continuations/fixture.wat | 22 + crates/c-api/tests/continuations/measure.py | 62 +++ crates/c-api/tests/continuations/run.py | 44 ++ crates/c-api/tests/continuations/sections.py | 47 ++ crates/c-api/tests/continuations/wide.h | 10 + crates/c-api/tests/continuations/wide.wasm | Bin 0 -> 63 bytes crates/c-api/tests/continuations/wide.wat | 2 + crates/tinywasm/src/func/host_execution.rs | 29 +- crates/tinywasm/tests/host_execution.rs | 25 + 23 files changed, 1118 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/shared_continuations.yml create mode 100644 crates/c-api/include/wasm_ext.h create mode 100644 crates/c-api/src/continuation.rs create mode 100644 crates/c-api/tests/continuations/README.md create mode 100644 crates/c-api/tests/continuations/fixture.c create mode 100644 crates/c-api/tests/continuations/fixture.wasm create mode 100644 crates/c-api/tests/continuations/fixture.wat create mode 100644 crates/c-api/tests/continuations/measure.py create mode 100644 crates/c-api/tests/continuations/run.py create mode 100644 crates/c-api/tests/continuations/sections.py create mode 100644 crates/c-api/tests/continuations/wide.h create mode 100644 crates/c-api/tests/continuations/wide.wasm create mode 100644 crates/c-api/tests/continuations/wide.wat diff --git a/.github/workflows/shared_continuations.yml b/.github/workflows/shared_continuations.yml new file mode 100644 index 0000000..c88b22a --- /dev/null +++ b/.github/workflows/shared_continuations.yml @@ -0,0 +1,14 @@ +name: Shared C continuations +on: + workflow_dispatch: + pull_request: + paths: ["crates/tinywasm/**", "crates/c-api/**", ".github/workflows/shared_continuations.yml"] +jobs: + c-fixture: + runs-on: ubuntu-26.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f + with: + toolchain: stable + - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3a293..10243c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Function::call_resumable_with_host_calls` and `HostExecution` for externally serviced host calls, with checked results, rooted arguments, and access to the calling module's context, behind the opt-in `external-host-calls` feature. +- Experimental numeric C continuations in `wasm_ext.h`, with a shared WAMR fixture and + disabled-feature linked-artifact measurements. ## [0.12.0-beta.0] - 2026-11-01 diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml index 63a1c47..b02bc0c 100644 --- a/crates/c-api/Cargo.toml +++ b/crates/c-api/Cargo.toml @@ -16,6 +16,7 @@ name = "tinywasm" tinywasm = { workspace = true, features = ["archive", "parser", "std", "validate"] } [features] +external-host-calls = ["tinywasm/external-host-calls"] custom-prefix = [] [lints] diff --git a/crates/c-api/Makefile b/crates/c-api/Makefile index dfe2b60..77438b0 100644 --- a/crates/c-api/Makefile +++ b/crates/c-api/Makefile @@ -10,6 +10,9 @@ TARGET_DIR ?= $(if $(CARGO_TARGET_DIR),$(CARGO_TARGET_DIR),../../target) BUILD_LIBDIR := $(abspath $(TARGET_DIR)/$(if $(filter dev,$(PROFILE)),debug,$(PROFILE))) BUILDDIR := $(abspath $(TARGET_DIR)/c-api) CARGO_FLAGS := --profile $(PROFILE) +ifeq ($(CONTINUATIONS),1) +CARGO_FLAGS += --features external-host-calls +endif ifneq ($(TINYWASM_C_API_PREFIX),) export TINYWASM_C_API_PREFIX CARGO_FLAGS += --features custom-prefix @@ -44,7 +47,7 @@ test: example install: build install -d "$(DESTDIR)$(PREFIX)/include" "$(DESTDIR)$(LIBDIR)/pkgconfig" "$(DESTDIR)$(PREFIX)/share/licenses/tinywasm" - install -m644 include/wasm.h include/tinywasm.h include/tinywasm-prefix.h "$(DESTDIR)$(PREFIX)/include/" + install -m644 include/wasm.h include/wasm_ext.h include/tinywasm.h include/tinywasm-prefix.h "$(DESTDIR)$(PREFIX)/include/" install -m644 "$(BUILD_LIBDIR)/libtinywasm.a" "$(BUILD_LIBDIR)/libtinywasm.$(SHARED_EXT)" "$(DESTDIR)$(LIBDIR)/" install -m644 ../../LICENSE-APACHE ../../LICENSE-MIT "$(DESTDIR)$(PREFIX)/share/licenses/tinywasm/" sed -e 's|@PREFIX@|$(PREFIX)|g' -e 's|@LIBDIR@|$(LIBDIR)|g' -e 's|@VERSION@|$(VERSION)|g' -e 's|@NATIVE_LIBS@|$(NATIVE_LIBS)|g' -e 's|@PREFIX_FLAG@|$(if $(TINYWASM_C_API_PREFIX),-DTINYWASM_C_API_PREFIX=$(TINYWASM_C_API_PREFIX))|g' tinywasm.pc.in > "$(DESTDIR)$(LIBDIR)/pkgconfig/tinywasm.pc" diff --git a/crates/c-api/README.md b/crates/c-api/README.md index 239003c..19c945e 100644 --- a/crates/c-api/README.md +++ b/crates/c-api/README.md @@ -38,8 +38,9 @@ When building with Cargo directly, enable `custom-prefix` and set the ## Runtime-neutral extension direction The integration target is the standard `wasm.h` plus a small, separately versioned common -extension header implemented by TinyWasm and WAMR. This is a proposed contract, not an -implemented C extension. The Rust `external-host-calls` API is backend machinery for it. +extension header implemented by TinyWasm and WAMR. `wasm_ext.h` is an experimental numeric continuation implementation, not an approved standard. +Enable the C wrapper's `external-host-calls` feature (`CONTINUATIONS=1` with Make). +The Rust API is its backend machinery; see [the shared fixture](tests/continuations/README.md). See the [WAMR roadmap discussion](https://github.com/wasm-micro-runtime/wasm-micro-runtime/discussions/5164#discussioncomment-19111943). Keep ordinary module linking, calls, memories, tables, and ownership on the standard API. @@ -48,10 +49,10 @@ alone does not establish shared semantics, object compatibility, or binary plugi Objects must remain with their originating runtime and store. Symbol prefixes permit static coexistence; any runtime dispatch table belongs in an optional adapter outside the core. -The proposed extension needs an opaque invocation handle, explicit completed/pending/trap -outcomes, host-call suspension, checked result completion, and cancellation. Specify argument -and result ownership, reference rooting, caller memory/context access, reentrancy, error and -trap ownership, and cancellation/finalizer ordering before freezing declarations. Callback-style +The prototype provides opaque invocation handles, completed/pending/error outcomes, host-call +suspension, checked completion, and cancellation. Qualify reference rooting, caller memory/context +access, nested canonical ABI operations, error/trap ownership, and finalizer ordering before +freezing declarations. Callback-style async can use ordinary calls; stackful suspension support must be qualified per execution mode. Do not expose interpreter frames, instruction pointers, or Rust futures across the C ABI. Wakers, event loops, resource tables, canonical ABI, and WASI implementations stay external. @@ -69,8 +70,8 @@ enabled-but-unused; and enabled-and-used. Check text/rodata/data/bss, retained s maps, store/frame layouts, heap/stack high-water marks, and execution cost. Use the same portable C conformance fixtures for both runtimes, including shared memory/table imports and cancel/error paths. On a Pico, measure static RAM and heap/stack peaks; RSS applies to hosted deployments. -The current TinyWasm C wrapper requires `std`; Pico C-wrapper support and zero-overhead linked -builds have not been demonstrated by the Rust seam tests. +The current TinyWasm C wrapper requires `std`; Pico C-wrapper support is not qualified by hosted C fixture tests. Final linked-artifact +comparisons and process-memory measurements are recorded separately from Rust seam tests. ## Current API limitations diff --git a/crates/c-api/include/tinywasm-prefix.h b/crates/c-api/include/tinywasm-prefix.h index ba96772..43df532 100644 --- a/crates/c-api/include/tinywasm-prefix.h +++ b/crates/c-api/include/tinywasm-prefix.h @@ -305,4 +305,9 @@ #define wasm_valtype_vec_new TINYWASM_SYMBOL(wasm_valtype_vec_new) #define wasm_valtype_vec_new_empty TINYWASM_SYMBOL(wasm_valtype_vec_new_empty) #define wasm_valtype_vec_new_uninitialized TINYWASM_SYMBOL(wasm_valtype_vec_new_uninitialized) +#define wasm_ext_func_new TINYWASM_SYMBOL(wasm_ext_func_new) +#define wasm_ext_call_new TINYWASM_SYMBOL(wasm_ext_call_new) +#define wasm_ext_call_resume TINYWASM_SYMBOL(wasm_ext_call_resume) +#define wasm_ext_call_cancel TINYWASM_SYMBOL(wasm_ext_call_cancel) +#define wasm_ext_call_delete TINYWASM_SYMBOL(wasm_ext_call_delete) #endif diff --git a/crates/c-api/include/wasm_ext.h b/crates/c-api/include/wasm_ext.h new file mode 100644 index 0000000..ae16c94 --- /dev/null +++ b/crates/c-api/include/wasm_ext.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +/* Experimental shared continuation API. Include the runtime's wasm.h first. + * Not part of the standard WebAssembly C API. Thread-confined, numeric v1. + * Stores, functions, instances and callback environments outlive invocations. + * No nested calls, reentrant invocation APIs, unwinding/longjmp across + * callbacks, or deleting a store while an invocation is live. Input/completion + * vectors are borrowed only for the call. Results are written to caller-owned + * storage, never replaced. Invalid completion leaves pending. cancel/delete are + * terminal and notify the pending host exactly once. + */ +#ifndef WASM_EXT_H +#define WASM_EXT_H +#ifdef __cplusplus +extern "C" { +#endif +typedef struct wasm_ext_call_t wasm_ext_call_t; +typedef int32_t wasm_ext_status_t; +enum { + WASM_EXT_ERROR = -1, + WASM_EXT_DONE = 0, + WASM_EXT_PENDING = 1, + WASM_EXT_CANCELLED = 2 +}; +typedef wasm_ext_status_t (*wasm_ext_callback_t)(void *, const wasm_val_vec_t *, + wasm_val_vec_t *, + wasm_trap_t **); +typedef void (*wasm_ext_cancel_t)(void *); +/* Callback env stays caller-owned. Only numeric signatures, <=1 result in v1. + */ +WASM_API_EXTERN wasm_func_t * +wasm_ext_func_new(wasm_store_t *, const wasm_functype_t *, wasm_ext_callback_t, + void *, wasm_ext_cancel_t); +WASM_API_EXTERN wasm_ext_call_t * +wasm_ext_call_new(const wasm_func_t *, const wasm_val_vec_t *); +/* NULL completion starts execution; pending requires a correctly typed + * completion. out_trap receives an owned trap or NULL. ERROR may be recoverable + * while pending. DONE/ERROR/CANCELLED never replay guest instructions or + * callbacks. */ +WASM_API_EXTERN wasm_ext_status_t +wasm_ext_call_resume(wasm_ext_call_t *, const wasm_val_vec_t *completion, + wasm_val_vec_t *results, wasm_trap_t **out_trap); +WASM_API_EXTERN void +wasm_ext_call_cancel(wasm_ext_call_t *); +WASM_API_EXTERN void +wasm_ext_call_delete(wasm_ext_call_t *); +#ifdef __cplusplus +} +#endif +#endif diff --git a/crates/c-api/src/continuation.rs b/crates/c-api/src/continuation.rs new file mode 100644 index 0000000..a992599 --- /dev/null +++ b/crates/c-api/src/continuation.rs @@ -0,0 +1,285 @@ +//! Experimental numeric continuation adapter. All state is opt-in and thread-confined. +use crate::{function, objects::*, runtime::*, values::*, vectors::*}; +use std::{ + ffi::c_void, + ptr, + rc::{Rc, Weak}, +}; +use tinywasm::{HostExecProgress, HostExecution, WasmValue}; +type Callback = + unsafe extern "C" fn(*mut c_void, *const wasm_val_vec_t, *mut wasm_val_vec_t, *mut *mut wasm_trap_t) -> i32; +type Cancel = Option; +pub(crate) struct Host { + callback: Callback, + env: *mut c_void, + cancel: Cancel, + state: Weak, +} +pub struct wasm_ext_call_t { + // Drop the borrowed execution before its stable backing buffers and owning store. + execution: Option>, + values: Box<[WasmValue]>, + result_count: usize, + state: Rc, + pending: Option>, + status: i32, +} +fn numeric(ty: &tinywasm::types::FuncType) -> bool { + ty.results().len() <= 1 + && ty.params().iter().chain(ty.results()).all(|x| { + matches!( + x, + tinywasm::types::WasmType::I32 + | tinywasm::types::WasmType::I64 + | tinywasm::types::WasmType::F32 + | tinywasm::types::WasmType::F64 + ) + }) +} +unsafe extern "C" fn release_host(env: *mut c_void) { + unsafe { + drop(Rc::from_raw(env.cast::())); + } +} +unsafe extern "C" fn sync_host(env: *mut c_void, _: *const wasm_val_vec_t, _: *mut wasm_val_vec_t) -> *mut wasm_trap_t { + let host = unsafe { &*env.cast::() }; + host.state.upgrade().map_or(ptr::null_mut(), |state| trap(&state, "async host requires an extension invocation")) +} + +export! { pub unsafe extern "C" fn wasm_ext_func_new(store: *mut wasm_store_t, ty: *const crate::types::wasm_functype_t, cb: Option, env: *mut c_void, cancel: Cancel) -> *mut wasm_func_t { + if store.is_null() || ty.is_null() || cb.is_none() { return ptr::null_mut(); } + let state = unsafe { &(*store).0 }; + if state.ext_reserved.get() || !numeric(&unsafe { &*ty }.func()) { return ptr::null_mut(); } + let host = Rc::new(Host { callback: cb.unwrap(), env, cancel, state: Rc::downgrade(state) }); + let raw = Rc::into_raw(host.clone()).cast_mut().cast(); + let func = unsafe { function::wasm_func_new_with_env(store, ty, sync_host, raw, Some(release_host)) }; + if func.is_null() { return func; } + let func_ref = unsafe { &*func }; + let ObjectKind::Func(_, reference) = &func_ref.0.kind else { unreachable!() }; + state.ext_hosts.borrow_mut().insert(*reference, host); + func +}} +export! { pub unsafe extern "C" fn wasm_ext_call_new(func: *const wasm_func_t, args: *const wasm_val_vec_t) -> *mut wasm_ext_call_t { + if func.is_null() || args.is_null() { return ptr::null_mut(); } + if unsafe { (*args).size != 0 && (*args).data.is_null() } { return ptr::null_mut(); } + let object = unsafe { &(*func).0 }; + let Ok(state) = object.state() else { return ptr::null_mut(); }; + if !state.ext_available() { return ptr::null_mut(); } + let ObjectKind::Func(function, _) = &object.kind else { return ptr::null_mut(); }; + let prepared = state.access(|access| { + let ty = function.ty(access.store())?; + if !numeric(ty) { return Err(tinywasm::Error::Other("unsupported continuation signature".into())); } + let count = ty.results().len(); + if unsafe { (*args).as_slice() }.len()!=ty.params().len() || !unsafe { (*args).as_slice() }.iter().zip(ty.params()).all(|(v,t)|Some(v.kind)==crate::types::value_kind(*t)) { return Err(tinywasm::Error::Other("invalid arguments".into())); } + let args = unsafe { (*args).as_slice() }.iter().map(|v| unsafe { v.to_runtime(&state, access.store()) }).collect::>>()?; + Ok((args, count)) + }); + let Ok((args, count)) = prepared else { return ptr::null_mut(); }; + let mut call = Box::new(wasm_ext_call_t { execution: None, values: vec![WasmValue::I32(0);count].into_boxed_slice(), result_count: count, state, pending: None, status: 1 }); + call.state.ext_reserved.set(true); + // SAFETY: reservation excludes all idle C access; Rc pins the Store. Box pins results. + // The execution is dropped before either backing object and before releasing reservation. + let store: &'static mut tinywasm::Store = unsafe { &mut *call.state.store.get() }; + let values: &'static mut [WasmValue] = unsafe { &mut *(&mut *call.values as *mut [WasmValue]) }; + let execution = function.call_resumable_with_host_calls(store, &args, values); + match execution { + Ok(execution) => { call.execution = Some(execution); Box::into_raw(call) } + Err(_) => { call.state.ext_reserved.set(false); ptr::null_mut() } + } +}} +impl wasm_ext_call_t { + fn finish(&mut self, status: i32) { + self.execution.take(); + self.state.ext_reserved.set(false); + self.status = status; + } + fn cancel(&mut self) { + if self.execution.is_none() { + return; + } + self.status = 2; + if let Some(host) = self.pending.take() + && let Some(cancel) = host.cancel + { + unsafe { + cancel(host.env); + } + } + self.finish(2); + } +} +impl Drop for wasm_ext_call_t { + fn drop(&mut self) { + self.cancel(); + } +} +export! { pub unsafe extern "C" fn wasm_ext_call_cancel(call: *mut wasm_ext_call_t) { if let Some(call) = unsafe { call.as_mut() } { call.cancel(); } }} +export! { pub unsafe extern "C" fn wasm_ext_call_delete(call: *mut wasm_ext_call_t) { unsafe { crate::delete(call); } }} +export! { pub unsafe extern "C" fn wasm_ext_call_resume(call: *mut wasm_ext_call_t, completion: *const wasm_val_vec_t, results: *mut wasm_val_vec_t, out_trap: *mut *mut wasm_trap_t) -> i32 { + if out_trap.is_null() { return -1; } + unsafe { *out_trap = ptr::null_mut(); } + let Some(call) = (unsafe { call.as_mut() }) else { return -1; }; + if call.execution.is_none() { return if call.status == 2 { 2 } else { -1 }; } + if results.is_null() || unsafe { (*results).size } != call.result_count || unsafe { (*results).size != 0 && (*results).data.is_null() } { return -1; } + if call.pending.is_some() { + if completion.is_null() { return 1; } + if unsafe { (*completion).size != 0 && (*completion).data.is_null() } { return -1; } + let mut host_call = call.execution.as_mut().unwrap().host_call().unwrap(); + let state = call.state.clone(); + let ty=host_call.function().ty(host_call.store()).unwrap(); + if unsafe { (*completion).as_slice() }.len()!=ty.results().len() || !unsafe { (*completion).as_slice() }.iter().zip(ty.results()).all(|(v,t)|Some(v.kind)==crate::types::value_kind(*t)) { return -1; } + let input = host_call.with_context(|mut ctx| unsafe { (*completion).as_slice() }.iter().map(|v| unsafe { v.to_runtime(&state, ctx.store_mut()) }).collect::>>()); + let Ok(input) = input else { return -1; }; + if host_call.complete(&input).is_err() { return -1; } + call.pending = None; + } else if !completion.is_null() { return -1; } + let state = call.state.clone(); + let mut last = None; + loop { + let progress = call.execution.as_mut().unwrap().resume_with_fuel_selected(u32::MAX, |reference| { + if let Some((previous, selected)) = last && previous == reference { return selected; } + let selected = state.ext_hosts.borrow().contains_key(&reference); + last = Some((reference, selected)); + selected + }); + match progress { + Ok(HostExecProgress::Suspended) => continue, + Ok(HostExecProgress::Completed) => { + call.execution.take(); // End the exclusive result-buffer borrow before reading it. + // Numeric v1 results need no store lookup or reference allocation. + for (i,v) in call.values.iter().enumerate() { + let value = match v { WasmValue::I32(x)=>wasm_val_t{kind:0,of:wasm_val_union{i32:*x}}, WasmValue::I64(x)=>wasm_val_t{kind:1,of:wasm_val_union{i64:*x}}, WasmValue::F32(x)=>wasm_val_t{kind:2,of:wasm_val_union{f32:*x}}, WasmValue::F64(x)=>wasm_val_t{kind:3,of:wasm_val_union{f64:*x}}, _=>unreachable!() }; + unsafe { (*results).data.add(i).write(value); } + } + call.finish(0); return 0; + } + Err(error) => { unsafe { *out_trap = call.state.trap.borrow_mut().take().map_or_else(|| trap(&call.state,error.to_string()), |object| crate::boxed(wasm_ref_t(object))); } call.finish(-1); return -1; } + Ok(HostExecProgress::HostCall) => { + let mut request = call.execution.as_mut().unwrap().host_call().unwrap(); + let reference = request.function().as_func_ref(request.store()).unwrap(); + let host = call.state.ext_hosts.borrow().get(&reference).cloned(); + let Some(host) = host else { if let Err(error) = request.invoke() { unsafe { *out_trap = call.state.trap.borrow_mut().take().map_or_else(|| trap(&call.state,error.to_string()), |object| crate::boxed(wasm_ref_t(object))); } call.finish(-1); return -1; } continue; }; + let ty = request.function().ty(request.store()).unwrap().clone(); + let state=call.state.clone(); + let args=Vector::from_vec(request.params().iter().zip(ty.params()).map(|(v,t)| wasm_val_t::from_runtime(v,crate::types::value_kind(*t).unwrap(),&state,request.store()).unwrap()).collect()); + let mut result_values=ty.results().iter().map(|t|wasm_val_t{kind:crate::types::value_kind(*t).unwrap(),of:wasm_val_union{i64:0}}).collect::>(); + // Numeric-only borrowed output: the backing Vec frees storage without treating + // a malformed callback's union bits as an owned reference. + let mut out=std::mem::ManuallyDrop::new(Vector { size: result_values.len(), data: result_values.as_mut_ptr() }); + let data=out.data; let count=out.size; let mut trapped=ptr::null_mut(); + let status=request.with_context(|mut ctx| Ok(state.callback(&mut ctx,||unsafe{(host.callback)(host.env,&args,&mut *out,&mut trapped)}))).unwrap(); + if out.data!=data || out.size!=count { std::process::abort(); } + // Callback owns neither vector; modifications to ownership are C contract violations. + + if status==1 && trapped.is_null() { call.pending=Some(host); return 1; } + if status!=0 || !trapped.is_null() { unsafe{*out_trap=trapped;} call.finish(-1); return -1; } + if !unsafe { out.as_slice() }.iter().zip(ty.results()).all(|(v,t)|Some(v.kind)==crate::types::value_kind(*t)) { + unsafe { *out_trap = trap(&call.state,"host result type mismatch"); } + call.finish(-1); return -1; + } + let values=request.with_context(|mut ctx|unsafe{out.as_slice()}.iter().map(|v|unsafe{v.to_runtime(&state,ctx.store_mut())}).collect::>>()); + if values.and_then(|v|request.complete(&v)).is_err() { call.finish(-1); return -1; } + } + } + } +}} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::*; + use std::cell::Cell; + struct Counts { + calls: Cell, + cancels: Cell, + } + unsafe extern "C" fn pending( + env: *mut c_void, + _: *const wasm_val_vec_t, + _: *mut wasm_val_vec_t, + _: *mut *mut wasm_trap_t, + ) -> i32 { + let counts = unsafe { &*env.cast::() }; + counts.calls.set(counts.calls.get() + 1); + 1 + } + unsafe extern "C" fn cancel(env: *mut c_void) { + let counts = unsafe { &*env.cast::() }; + counts.cancels.set(counts.cancels.get() + 1); + } + #[test] + fn owned_execution_releases_borrows_before_results_and_cancel() { + let counts = Counts { calls: Cell::new(0), cancels: Cell::new(0) }; + unsafe { + let engine = wasm_engine_new(); + let store = wasm_store_new(engine); + let mut params = Vector::from_vec(vec![wasm_valtype_new(0)]); + let mut results = params.clone(); + let ty = wasm_functype_new(&mut params, &mut results); + let host = + wasm_ext_func_new(store, ty, Some(pending), (&counts as *const Counts).cast_mut().cast(), Some(cancel)); + assert!(!host.is_null()); + wasm_functype_delete(ty); + let args = Vector::from_vec(vec![wasm_val_t { kind: 0, of: wasm_val_union { i32: 20 } }]); + let mut out = Vector::from_vec(vec![wasm_val_t::default()]); + let completion = Vector::from_vec(vec![wasm_val_t { kind: 0, of: wasm_val_union { i32: 42 } }]); + let mut trap = ptr::null_mut(); + for _ in 0..3 { + let call = wasm_ext_call_new(host, &args); + assert!(!call.is_null()); + assert_eq!(wasm_ext_call_resume(call, ptr::null(), &mut out, &mut trap), 1); + assert_eq!(wasm_ext_call_resume(call, &completion, &mut out, &mut trap), 0); + assert_eq!(out.as_slice()[0].of.i32, 42); + assert!(trap.is_null()); + wasm_ext_call_delete(call); + } + let call = wasm_ext_call_new(host, &args); + assert_eq!(wasm_ext_call_resume(call, ptr::null(), &mut out, &mut trap), 1); + wasm_ext_call_cancel(call); + wasm_ext_call_cancel(call); + wasm_ext_call_delete(call); + assert_eq!(counts.calls.get(), 4); + assert_eq!(counts.cancels.get(), 1); + let binary = Vector::from_vec(include_bytes!("../tests/continuations/fixture.wasm").to_vec()); + let module = crate::module::wasm_module_new(store, &binary); + assert!(!module.is_null()); + unsafe extern "C" fn increment(args: *const wasm_val_vec_t, out: *mut wasm_val_vec_t) -> *mut wasm_trap_t { + unsafe { + (*out).data.write(wasm_val_t { kind: 0, of: wasm_val_union { i32: (*(*args).data).of.i32 + 1 } }); + } + ptr::null_mut() + } + let mut p = Vector::from_vec(vec![wasm_valtype_new(0)]); + let mut r = p.clone(); + let ty = wasm_functype_new(&mut p, &mut r); + let inc = crate::function::wasm_func_new(store, ty, increment); + wasm_functype_delete(ty); + let imports = Vector::from_vec(vec![crate::objects::wasm_func_copy(host), inc]); + let instance = crate::module::wasm_instance_new(store, module, &imports, &mut trap); + assert!(!instance.is_null()); + let mut exports = Vector::default(); + crate::module::wasm_instance_exports(instance, &mut exports); + // fixture.wat declares indirect, twice, trap, then run. + let function = crate::objects::wasm_extern_as_func(exports.as_slice()[3]); + let call = wasm_ext_call_new(function, &args); + assert!(!call.is_null()); + assert_eq!(wasm_ext_call_resume(call, ptr::null(), &mut out, &mut trap), 1); + assert_eq!(wasm_ext_call_resume(call, &completion, &mut out, &mut trap), 0); + assert_eq!(out.as_slice()[0].of.i32, 85); + wasm_ext_call_delete(call); + let three = Vector::from_vec(vec![wasm_val_t { kind: 0, of: wasm_val_union { i32: 3 } }]); + let function = crate::objects::wasm_extern_as_func(exports.as_slice()[5]); + let call = wasm_ext_call_new(function, &three); + assert_eq!(wasm_ext_call_resume(call, ptr::null(), &mut out, &mut trap), 0); + assert_eq!(out.as_slice()[0].of.i32, 9); + wasm_ext_call_delete(call); + crate::objects::wasm_instance_delete(instance); + crate::objects::wasm_module_delete(module); + drop(exports); + drop(imports); + wasm_func_delete(host); + wasm_store_delete(store); + wasm_engine_delete(engine); + } + } +} diff --git a/crates/c-api/src/function.rs b/crates/c-api/src/function.rs index e6cd58a..00b4fe4 100644 --- a/crates/c-api/src/function.rs +++ b/crates/c-api/src/function.rs @@ -130,6 +130,8 @@ export! { pub unsafe extern "C" fn wasm_func_call(value: *const wasm_func_t, arg Ok(state) => state, Err(error) => return failure(error), }; + #[cfg(feature = "external-host-calls")] + if state.ext_reserved.get() { return trap(&state, "store has a live continuation"); } let result = state.access(|access| { let ObjectKind::Func(function, _) = &object.kind else { return Err(tinywasm::Error::Other("expected function".into())); }; let ty = function.ty(access.store())?.clone(); diff --git a/crates/c-api/src/lib.rs b/crates/c-api/src/lib.rs index 484de1b..b036de4 100644 --- a/crates/c-api/src/lib.rs +++ b/crates/c-api/src/lib.rs @@ -27,6 +27,8 @@ #[macro_use] mod macros; +#[cfg(feature = "external-host-calls")] +mod continuation; mod externals; mod function; mod module; diff --git a/crates/c-api/src/runtime.rs b/crates/c-api/src/runtime.rs index 8d06405..ad3d656 100644 --- a/crates/c-api/src/runtime.rs +++ b/crates/c-api/src/runtime.rs @@ -15,7 +15,11 @@ pub struct wasm_store_t(pub(crate) Rc); /// Thread-confined store ownership and the dynamic callback borrow stack. pub(crate) struct StoreState { - store: UnsafeCell, + pub(crate) store: UnsafeCell, + #[cfg(feature = "external-host-calls")] + pub(crate) ext_reserved: Cell, + #[cfg(feature = "external-host-calls")] + pub(crate) ext_hosts: RefCell>>, busy: Cell, active: Cell<*mut FuncContext<'static>>, pub(crate) objects: RefCell>>, @@ -54,9 +58,17 @@ impl Access<'_> { } impl StoreState { + #[cfg(feature = "external-host-calls")] + pub(crate) fn ext_available(&self) -> bool { + !self.busy.get() && self.active.get().is_null() && !self.ext_reserved.get() + } fn new(engine: Engine) -> Self { Self { store: UnsafeCell::new(Store::new(engine)), + #[cfg(feature = "external-host-calls")] + ext_reserved: Cell::new(false), + #[cfg(feature = "external-host-calls")] + ext_hosts: RefCell::new(std::collections::HashMap::new()), busy: Cell::new(false), active: Cell::new(ptr::null_mut()), objects: RefCell::new(Vec::new()), @@ -66,6 +78,10 @@ impl StoreState { /// Acquires one exclusive borrow without aliasing an executing interpreter. pub(crate) fn access(&self, action: impl FnOnce(&mut Access<'_>) -> tinywasm::Result) -> tinywasm::Result { + #[cfg(feature = "external-host-calls")] + if self.ext_reserved.get() && self.active.get().is_null() { + return Err(tinywasm::Error::Other("store has a live continuation".into())); + } if self.busy.replace(true) { return Err(tinywasm::Error::Other("store is already borrowed".into())); } diff --git a/crates/c-api/tests/continuations/README.md b/crates/c-api/tests/continuations/README.md new file mode 100644 index 0000000..1439173 --- /dev/null +++ b/crates/c-api/tests/continuations/README.md @@ -0,0 +1,59 @@ +# Shared continuation C fixture + +The same `fixture.c`, Wasm inputs, and `wasm_ext.h` declarations are used for TinyWasm and WAMR. +The extension is experimental, numeric-only, thread-confined, and limited to one result. It is +not an implementation of the component model or the WASI 0.3 scheduler. Only explicit pending +host calls suspend; cancellation does not preempt executing guest code or a blocking callback. +Callbacks must return normally and must not reenter invocation APIs. Stores, instances, and +callback environments must outlive their invocations. C callers supply valid live handles and +initialized input buffers; guest-controlled counts/types are checked before completion is committed. + +Run from a compiler developer shell (Python 3 and the runtime's native toolchain are required): + +```sh +python path/to/run.py --runtime tinywasm --root /path/to/tinywasm --build /tmp/tiny-test --feature on +python path/to/run.py --runtime wamr --root /path/to/wamr --build /tmp/wamr-test --feature on +``` + +`--feature off` builds the ordinary C API benchmark without extension references. It uses release +LTO, linker dead stripping, and hidden visibility on Unix; MSVC uses `/GL`, `/LTCG`, `/OPT:REF`, +and `/OPT:ICF`. Use the same compiler/linker versions, flags, source, fixture, and executable name +when comparing an unmodified baseline with feature-off and feature-on-but-unused builds. The +TinyWasm Rust static library uses fat LTO and one codegen unit; final C link optimization does not +imply C/Rust cross-language inlining. Match panic strategy and allocator as well. + +The tests cover pending/resume/cancel/delete, invalid count/type completions, immediate completion, malformed callback results, host traps, no callback replay, +store invocation exclusion, repeated suspensions, indirect calls, guest traps, and mixed-width +numeric parameters/results. Each benchmark checks deterministic results and reports pure-Wasm +and native-import cost separately. Feature-on tests additionally measure native imports through +the opt-in driver. Peak process RSS is reported; Windows also reports peak committed bytes. +RSS is a noisy hosted process high-water mark, not a measurement of MCU SRAM or live heap. + +For WAMR, this prototype requires a single-module fast interpreter, software bounds checks, +no AOT/JIT, and no GC. The pending execution owns a dedicated 64 KiB interpreter stack. +Unsupported build combinations fail configuration. Numeric packing uses memcpy so mixed i32/i64 +sequences need not be naturally aligned. Async metadata is store-owned, independent of copied +C function handles. TinyWasm reserves its store while the Rust execution borrow is live and +releases that borrow before accessing the completed result buffer. + +The main fixture's standard wasm.h vectors are source-portable, not a promise of binary-plugin +compatibility: WAMR's public vector has additional fields. The fixture prints layout sizes. +A common binary plugin needs a normalized base ABI or an external adapter that translates it; +objects must never be passed between runtime implementations. The continuation header remains +a review prototype; its inherited wasm.h value/vector ABI must be settled before freezing it. + +`wide.wasm` is generated from `wide.wat`; `wide.h` contains those same bytes for the C test. +Rebuild with WABT's `wat2wasm`; the checked-in binaries let consumers run without WABT. +See [VALIDATION.md](VALIDATION.md) for measured results and limits, with raw timing/RSS and +section-size/hash samples. `measure.py --fixture fixture.wasm baseline=/path/to/baseline +disabled=/path/to/disabled enabled=/path/to/enabled` alternates seven rounds and emits CSV; +add `--cpu 2` on Windows/Linux for affinity. Keep the machine otherwise idle. + +## Component-parser experiment + +Reviewed TinyWasm's `exp/component-parser` at `2a942e64f26d3bb5727b0b64d05afb5d7a15c4eb`. +Its feature-gated `Component`/`ComponentDeclaration` metadata and core-module extraction provide +input for an external linker; they do not execute components or supply a scheduler. This fixture +therefore uses ordinary core modules and does not cherry-pick that branch's parser/type changes. +The experimental parser can evolve independently of execution handles and the default core build. +Imported-memory/table support and complete component/async ABI conformance remain separate gates. diff --git a/crates/c-api/tests/continuations/fixture.c b/crates/c-api/tests/continuations/fixture.c new file mode 100644 index 0000000..99e8c75 --- /dev/null +++ b/crates/c-api/tests/continuations/fixture.c @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "wasm.h" +#ifdef TEST_CONTINUATIONS +#include "wasm_ext.h" +#include "wide.h" +#endif +#include +#include +#include +#include +#include +#ifdef _WIN32 +// clang-format off: psapi.h requires Windows declarations first. +#include +#include +// clang-format on +#else +#include +#endif +#define CHECK(x) \ + do { \ + if (!(x)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \ + exit(1); \ + } \ + } while (0) +#ifdef TEST_CONTINUATIONS +static unsigned waits, cancels; +typedef struct HostState { + wasm_store_t *store; + unsigned mode; +} HostState; +#endif +static wasm_trap_t * +inc(const wasm_val_vec_t *a, wasm_val_vec_t *r) +{ + r->data[0].kind = WASM_I32; + r->data[0].of.i32 = a->data[0].of.i32 + 1; + return NULL; +} +#ifdef TEST_CONTINUATIONS +static wasm_ext_status_t +wait_host(void *env, const wasm_val_vec_t *a, wasm_val_vec_t *r, + wasm_trap_t **trap) +{ + HostState *host = env; + CHECK(a->size == 1); + CHECK(a->data[0].kind == WASM_I32); + waits++; + if (host->mode == 1) { + r->data[0].kind = WASM_I32; + r->data[0].of.i32 = a->data[0].of.i32 + 1; + return WASM_EXT_DONE; + } + if (host->mode == 2) { + r->data[0].kind = + WASM_EXTERNREF; /* Must reject before interpreting union bits. */ + r->data[0].of.i64 = 1; + return WASM_EXT_DONE; + } + if (host->mode == 3) { + wasm_message_t message; + wasm_name_new_from_string_nt(&message, "host failure"); + *trap = wasm_trap_new(host->store, &message); + wasm_byte_vec_delete(&message); + return WASM_EXT_ERROR; + } + return WASM_EXT_PENDING; +} +static wasm_ext_status_t +wide_host(void *env, const wasm_val_vec_t *a, wasm_val_vec_t *r, + wasm_trap_t **trap) +{ + (void)env; + (void)r; + (void)trap; + CHECK(a->size == 1 && a->data[0].kind == WASM_I64 + && a->data[0].of.i64 == INT64_C(0x100000004)); + return WASM_EXT_PENDING; +} +static void +cancel_host(void *env) +{ + (void)env; + cancels++; +} +#else +static wasm_trap_t * +wait_host(const wasm_val_vec_t *a, wasm_val_vec_t *r) +{ + return inc(a, r); +} +#endif +static wasm_func_t * +find(wasm_module_t *module, wasm_instance_t *instance, const char *name) +{ + wasm_exporttype_vec_t types; + wasm_extern_vec_t exports; + wasm_module_exports(module, &types); + wasm_instance_exports(instance, &exports); + wasm_func_t *found = NULL; + for (size_t i = 0; i < types.size; i++) { + const wasm_name_t *n = wasm_exporttype_name(types.data[i]); + if ((n->size == strlen(name) + || (n->size == strlen(name) + 1 && n->data[n->size - 1] == 0)) + && !memcmp(n->data, name, strlen(name))) + found = wasm_func_copy(wasm_extern_as_func(exports.data[i])); + } + wasm_exporttype_vec_delete(&types); + wasm_extern_vec_delete(&exports); + CHECK(found); + return found; +} +/* MSVC clock() measures elapsed time; use process CPU time for this fixture. */ +static double +cpu_seconds(void) +{ +#ifdef _WIN32 + FILETIME created, exited, kernel, user; + ULARGE_INTEGER k, u; + CHECK(GetProcessTimes(GetCurrentProcess(), &created, &exited, &kernel, + &user)); + k.LowPart = kernel.dwLowDateTime; + k.HighPart = kernel.dwHighDateTime; + u.LowPart = user.dwLowDateTime; + u.HighPart = user.dwHighDateTime; + return (double)(k.QuadPart + u.QuadPart) * 1e-7; +#else + return (double)clock() / CLOCKS_PER_SEC; +#endif +} +#ifdef _WIN32 +static uint64_t +thread_cycles(void) +{ + ULONG64 cycles; + CHECK(QueryThreadCycleTime(GetCurrentThread(), &cycles)); + return cycles; +} +#endif +static int32_t +invoke(wasm_func_t *f, int32_t n) +{ + wasm_val_t a[] = { WASM_I32_VAL(n) }, r[] = { WASM_I32_VAL(0) }; + wasm_val_vec_t av = WASM_ARRAY_VEC(a), rv = WASM_ARRAY_VEC(r); + wasm_trap_t *trap = wasm_func_call(f, &av, &rv); + CHECK(!trap); + return r[0].of.i32; +} +int +main(int argc, char **argv) +{ + wasm_engine_t *engine = wasm_engine_new(); + CHECK(engine); + wasm_store_t *store = wasm_store_new(engine); + CHECK(store); + printf("wasm_val_bytes %zu wasm_vec_bytes %zu\n", sizeof(wasm_val_t), + sizeof(wasm_val_vec_t)); + CHECK(argc >= 2); + FILE *file = fopen(argv[1], "rb"); + CHECK(file); + fseek(file, 0, SEEK_END); + long length = ftell(file); + rewind(file); + wasm_byte_vec_t bytes; + wasm_byte_vec_new_uninitialized(&bytes, (size_t)length); + CHECK(fread(bytes.data, 1, (size_t)length, file) == (size_t)length); + fclose(file); + + wasm_module_t *module = wasm_module_new(store, &bytes); + CHECK(module); + wasm_byte_vec_delete(&bytes); + wasm_functype_t *type = + wasm_functype_new_1_1(wasm_valtype_new_i32(), wasm_valtype_new_i32()); +#ifdef TEST_CONTINUATIONS + HostState host = { store, 0 }; + wasm_func_t *wait = + wasm_ext_func_new(store, type, wait_host, &host, cancel_host); +#else + wasm_func_t *wait = wasm_func_new(store, type, wait_host); +#endif + wasm_func_t *native = wasm_func_new(store, type, inc); + CHECK(wait && native); + wasm_functype_delete(type); + wasm_extern_t *items[] = { wasm_func_as_extern(wait), + wasm_func_as_extern(native) }; + wasm_extern_vec_t imports = WASM_ARRAY_VEC(items); + wasm_trap_t *trap = NULL; + wasm_instance_t *instance = + wasm_instance_new(store, module, &imports, &trap); + CHECK(instance && !trap); + wasm_func_t *run = find(module, instance, "run"), + *visits = find(module, instance, "visits"), + *compute = find(module, instance, "compute"), + *calls = find(module, instance, "native"); +#ifdef TEST_CONTINUATIONS + wasm_val_t a[] = { WASM_I32_VAL(20) }, r[] = { WASM_I32_VAL(-1) }, + v[] = { WASM_I32_VAL(42) }; + wasm_val_vec_t av = WASM_ARRAY_VEC(a), rv = WASM_ARRAY_VEC(r), + completion = WASM_ARRAY_VEC(v), empty = WASM_EMPTY_VEC; + a[0].kind = WASM_I64; + CHECK(!wasm_ext_call_new(run, &av)); + a[0].kind = WASM_I32; + wasm_ext_call_t *call = wasm_ext_call_new(run, &av); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_PENDING + && !trap); + CHECK(waits == 1 && r[0].of.i32 == -1); + trap = wasm_func_call(run, &av, &rv); + CHECK(trap); + wasm_trap_delete(trap); + trap = NULL; + CHECK(waits == 1); + CHECK(!wasm_ext_call_new(run, &av)); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_PENDING); + CHECK(waits == 1); + CHECK(wasm_ext_call_resume(call, &empty, &rv, &trap) == WASM_EXT_ERROR + && !trap); + v[0].kind = WASM_I64; + v[0].of.i64 = 42; + CHECK(wasm_ext_call_resume(call, &completion, &rv, &trap) == WASM_EXT_ERROR + && !trap); + CHECK(waits == 1); + v[0].kind = WASM_I32; + v[0].of.i32 = 42; + CHECK(wasm_ext_call_resume(call, &completion, &rv, &trap) == WASM_EXT_DONE + && !trap); + CHECK(r[0].of.i32 == 85 && waits == 1); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_ERROR); + wasm_ext_call_delete(call); + wasm_val_vec_t noargs = WASM_EMPTY_VEC; + CHECK(!wasm_func_call(visits, &noargs, &rv)); + CHECK(r[0].of.i32 == 1); + call = wasm_ext_call_new(run, &av); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_PENDING); + wasm_ext_call_cancel(call); + wasm_ext_call_cancel(call); + CHECK(cancels == 1); + CHECK(wasm_ext_call_resume(call, &completion, &rv, &trap) + == WASM_EXT_CANCELLED); + wasm_ext_call_delete(call); + CHECK(cancels == 1); + call = wasm_ext_call_new(run, &av); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_PENDING); + wasm_ext_call_delete(call); + CHECK(cancels == 2); + CHECK(!wasm_func_call(visits, &noargs, &rv)); + CHECK(r[0].of.i32 == 3); + unsigned before_waits = waits, before_cancels = cancels; + call = wasm_ext_call_new(run, &av); + CHECK(call); + wasm_ext_call_cancel(call); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_CANCELLED); + wasm_ext_call_delete(call); + CHECK(waits == before_waits && cancels == before_cancels); + wasm_func_t *twice = find(module, instance, "twice"), + *indirect = find(module, instance, "indirect"), + *bad = find(module, instance, "trap"); + for (int i = 0; i < 100; i++) { + unsigned old = waits; + call = wasm_ext_call_new(twice, &av); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_PENDING); + CHECK(wasm_ext_call_resume(call, &completion, &rv, &trap) + == WASM_EXT_PENDING); + CHECK(waits == old + 2); + CHECK(wasm_ext_call_resume(call, &completion, &rv, &trap) + == WASM_EXT_DONE); + CHECK(r[0].of.i32 == 42); + wasm_ext_call_delete(call); + } + call = wasm_ext_call_new(indirect, &av); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_PENDING); + CHECK(wasm_ext_call_resume(call, &completion, &rv, &trap) == WASM_EXT_DONE); + CHECK(r[0].of.i32 == 42); + wasm_ext_call_delete(call); + call = wasm_ext_call_new(bad, &noargs); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &empty, &trap) == WASM_EXT_ERROR + && trap); + wasm_trap_delete(trap); + trap = NULL; + wasm_ext_call_delete(call); + wasm_func_delete(twice); + wasm_func_delete(indirect); + wasm_func_delete(bad); + unsigned old_waits = waits; + trap = wasm_func_call(run, &av, &rv); + CHECK(trap && waits == old_waits); + wasm_trap_delete(trap); + trap = NULL; + for (host.mode = 1; host.mode <= 3; host.mode++) { + call = wasm_ext_call_new(run, &av); + CHECK(call); + if (host.mode == 1) { + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_DONE + && !trap); + CHECK(r[0].of.i32 == 43); + } + else { + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_ERROR + && trap); + wasm_trap_delete(trap); + trap = NULL; + } + CHECK(wasm_ext_call_resume(call, NULL, &rv, &trap) == WASM_EXT_ERROR + && !trap); + wasm_ext_call_delete(call); + } + host.mode = 0; + wasm_byte_vec_t wide_bytes; + wasm_byte_vec_new(&wide_bytes, sizeof(wide_wasm), (const char *)wide_wasm); + wasm_module_t *wide_module = wasm_module_new(store, &wide_bytes); + CHECK(wide_module); + wasm_byte_vec_delete(&wide_bytes); + wasm_functype_t *wide_type = + wasm_functype_new_1_1(wasm_valtype_new_i64(), wasm_valtype_new_i64()); + wasm_func_t *wide_cb = + wasm_ext_func_new(store, wide_type, wide_host, NULL, NULL); + CHECK(wide_cb); + wasm_functype_delete(wide_type); + wasm_extern_t *wide_import[] = { wasm_func_as_extern(wide_cb) }; + wasm_extern_vec_t wide_imports = WASM_ARRAY_VEC(wide_import); + wasm_instance_t *wide_instance = + wasm_instance_new(store, wide_module, &wide_imports, &trap); + if (trap) { + wasm_message_t m; + wasm_trap_message(trap, &m); + fprintf(stderr, "wide instantiate: %.*s\n", (int)m.size, m.data); + wasm_byte_vec_delete(&m); + } + CHECK(wide_instance && !trap); + wasm_func_t *wide_run = find(wide_module, wide_instance, "run"); + wasm_val_t wa[] = { WASM_I32_VAL(7), WASM_I64_VAL(INT64_C(0x100000004)) }, + wr[] = { WASM_I64_VAL(0) }, + wc[] = { WASM_I64_VAL(-INT64_C(0x100000009)) }; + wasm_val_vec_t wav = WASM_ARRAY_VEC(wa), wrv = WASM_ARRAY_VEC(wr), + wcv = WASM_ARRAY_VEC(wc); + call = wasm_ext_call_new(wide_run, &wav); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &wrv, &trap) == WASM_EXT_PENDING + && !trap); + CHECK(wasm_ext_call_resume(call, &wcv, &wrv, &trap) == WASM_EXT_DONE + && !trap); + CHECK(wr[0].of.i64 == -INT64_C(0x100000008)); + wasm_ext_call_delete(call); + wasm_func_delete(wide_run); + wasm_instance_delete(wide_instance); + wasm_module_delete(wide_module); + wasm_func_delete(wide_cb); + puts("pending/resume/cancel, repeat, indirect, guest trap, mixed i32/i64: " + "PASS"); +#endif + const int32_t n = getenv("WASM_EXT_TEST_ONLY") ? 100 : 100000; + for (int mode = 0; mode < 2; mode++) { + const unsigned loops = + getenv("WASM_EXT_TEST_ONLY") ? 1 : (mode ? 100 : 1000); + wasm_func_t *f = mode ? calls : compute; + /* Accumulate in 64 bits to avoid overflow before division. */ + int32_t expected = + (int32_t)((uint64_t)n * (n + 1u) / 2u + (mode ? (uint32_t)n : 0)); + for (unsigned i = 0; i < 3; i++) + CHECK(invoke(f, n) == expected); + double start = cpu_seconds(); +#ifdef _WIN32 + uint64_t cycle_start = thread_cycles(); +#endif + for (unsigned i = 0; i < loops; i++) + CHECK(invoke(f, n) == expected); + printf("%s_ns_per_iteration %.4f\n", mode ? "native" : "compute", + 1e9 * (cpu_seconds() - start) / loops / n); +#ifdef _WIN32 + printf("%s_cycles_per_iteration %.4f\n", mode ? "native" : "compute", + (double)(thread_cycles() - cycle_start) / loops / n); +#endif + } +#ifdef TEST_CONTINUATIONS + wasm_val_t native_arg[] = { WASM_I32_VAL(n) }, + native_out[] = { WASM_I32_VAL(0) }; + wasm_val_vec_t nav = WASM_ARRAY_VEC(native_arg), + nrv = WASM_ARRAY_VEC(native_out); + double native_start = cpu_seconds(); + for (unsigned i = 0; i < 20; i++) { + call = wasm_ext_call_new(calls, &nav); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &nrv, &trap) == WASM_EXT_DONE + && !trap); + CHECK(native_out[0].of.i32 + == (int32_t)((uint64_t)n * (n + 1u) / 2u + n)); + wasm_ext_call_delete(call); + } + printf("optin_native_ns_per_iteration %.4f\n", + 1e9 * (cpu_seconds() - native_start) / 20 / n); +#endif + wasm_func_delete(run); + wasm_func_delete(visits); + wasm_func_delete(compute); + wasm_func_delete(calls); + wasm_instance_delete(instance); + wasm_module_delete(module); + wasm_func_delete(wait); + wasm_func_delete(native); + wasm_store_delete(store); + wasm_engine_delete(engine); +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS memory; + CHECK(GetProcessMemoryInfo(GetCurrentProcess(), &memory, sizeof(memory))); + printf("peak_rss_bytes %zu peak_commit_bytes %zu\n", + (size_t)memory.PeakWorkingSetSize, (size_t)memory.PeakPagefileUsage); +#else + struct rusage memory; + CHECK(!getrusage(RUSAGE_SELF, &memory)); +#ifdef __APPLE__ + printf("peak_rss_bytes %ld\n", memory.ru_maxrss); +#else + printf("peak_rss_bytes %ld\n", memory.ru_maxrss * 1024); +#endif +#endif + return 0; +} diff --git a/crates/c-api/tests/continuations/fixture.wasm b/crates/c-api/tests/continuations/fixture.wasm new file mode 100644 index 0000000000000000000000000000000000000000..55b57a512c645c8a0ebf0799fd20b6b517fb5447 GIT binary patch literal 258 zcmZ9E%MOAt5Jm5_wJkO#xYM}7$8VU(pFohrqErGEmi)S-3m0YPpfL5}g zndSm0K(p8gb$bp`O{I^(m!%ICAQQ{mfMBbr18L27Ih;8g>%-Oebrm5Naq?9SsbXJt zP_lmoXg&JD#{ou%GWxNGHPy52Ua>}!7y^GGEQ3vuF|Pq', '', len(b), hashlib.sha256(b).hexdigest()]) + if b[:4] == b'\xcf\xfa\xed\xfe': + commands = struct.unpack_from('D$I3XB2_PK-6&0C@8XmH+?% literal 0 HcmV?d00001 diff --git a/crates/c-api/tests/continuations/wide.wat b/crates/c-api/tests/continuations/wide.wat new file mode 100644 index 0000000..35e7dd0 --- /dev/null +++ b/crates/c-api/tests/continuations/wide.wat @@ -0,0 +1,2 @@ +(module (import "host" "wide" (func $wide (param i64) (result i64))) + (func (export "run") (param i32 i64) (result i64) local.get 1 call $wide i64.const 1 i64.add)) diff --git a/crates/tinywasm/src/func/host_execution.rs b/crates/tinywasm/src/func/host_execution.rs index 1a1ff6b..bf3bb9b 100644 --- a/crates/tinywasm/src/func/host_execution.rs +++ b/crates/tinywasm/src/func/host_execution.rs @@ -88,6 +88,18 @@ impl<'store> HostExecution<'store> { /// Completing the host call commits its results; a later resume continues after that call. /// A trap or panic makes the invocation terminal, as with ordinary resumable execution. pub fn resume_with_fuel(&mut self, fuel: u32) -> Result { + self.resume_with_fuel_selected(fuel, |_| true) + } + + /// Stops only at selected host imports; forwards other imports through their normal stack API. + /// + /// Selection runs at host-call boundaries, before allocating owned argument vectors. It must + /// not call into the store. Root host calls and already pending calls remain explicitly pending. + pub fn resume_with_fuel_selected( + &mut self, + fuel: u32, + mut select: impl FnMut(crate::FuncRef) -> bool, + ) -> Result { match self.state { State::Pending { .. } => return Ok(HostExecProgress::HostCall), State::Completed => { @@ -98,7 +110,7 @@ impl<'store> HostExecution<'store> { _ => {} } let state = core::mem::replace(&mut self.state, State::Finished); - let outcome = { + let mut outcome = { let mut store = self.store.enter_execution()?; store.execution_fuel = fuel; match state { @@ -109,6 +121,21 @@ impl<'store> HostExecution<'store> { _ => unreachable!(), } }; + while let Ok(ExecOutcome::Host(call)) = outcome { + let reference = crate::FuncRef::new(self.store.id(), call.func); + if select(reference) { + break; + } + outcome = (|| { + let mut store = self.store.enter_execution()?; + let host = store.code.funcs.host(call.func); + let type_addr = host.type_addr; + let callback = host.func.clone(); + callback.call_stack(&mut store, call.module, type_addr)?; + let mut executor = Executor::new(&mut store, call.caller, 0, Budget::Fuel); + if call.tail { executor.return_from_host() } else { executor.run() } + })(); + } self.accept_outcome(outcome).inspect_err(|_| self.finish()) } diff --git a/crates/tinywasm/tests/host_execution.rs b/crates/tinywasm/tests/host_execution.rs index 027a3e9..60b9df5 100644 --- a/crates/tinywasm/tests/host_execution.rs +++ b/crates/tinywasm/tests/host_execution.rs @@ -395,3 +395,28 @@ fn future_adapter_waits_for_a_wakeup_without_repolling_wasm() -> Result<()> { Ok(()) } + +#[test] +fn unselected_imports_use_the_synchronous_stack_path() -> Result<()> { + let calls = Arc::new(AtomicUsize::new(0)); + let mut store = Store::default(); + let instance = instance(&mut store, SOURCE, calls.clone())?; + for (name, expected) in + [("run", 42), ("tail", 21), ("indirect", 21), ("tail-indirect", 21), ("ref", 21), ("tail-ref", 21)] + { + let function = instance.func_untyped(&store, name)?; + let mut results = [WasmValue::I32(0)]; + let mut execution = function.call_resumable_with_host_calls(&mut store, &[], &mut results)?; + loop { + match execution.resume_with_fuel_selected(1, |_| false)? { + HostExecProgress::Completed => break, + HostExecProgress::Suspended => {} + HostExecProgress::HostCall => panic!("unselected callback was exposed"), + } + } + drop(execution); + assert_eq!(results, [WasmValue::I32(expected)]); + } + assert_eq!(calls.load(Ordering::SeqCst), 6); + Ok(()) +} From e0642c98d17f768fea34d28f3d488858c40e6602 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 12:22:23 -0800 Subject: [PATCH 04/20] docs: record continuation size and performance measurements --- .../c-api/tests/continuations/VALIDATION.md | 159 ++++++++++++++++++ .../tests/continuations/macos_sections.csv | 91 ++++++++++ crates/c-api/tests/continuations/measure.py | 2 +- .../tests/continuations/measurements.csv | 85 ++++++++++ crates/c-api/tests/continuations/sections.py | 2 +- .../tests/continuations/windows_profile.csv | 109 ++++++++++++ .../tests/continuations/windows_sections.csv | 37 ++++ 7 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 crates/c-api/tests/continuations/VALIDATION.md create mode 100644 crates/c-api/tests/continuations/macos_sections.csv create mode 100644 crates/c-api/tests/continuations/measurements.csv create mode 100644 crates/c-api/tests/continuations/windows_profile.csv create mode 100644 crates/c-api/tests/continuations/windows_sections.csv diff --git a/crates/c-api/tests/continuations/VALIDATION.md b/crates/c-api/tests/continuations/VALIDATION.md new file mode 100644 index 0000000..eabc11b --- /dev/null +++ b/crates/c-api/tests/continuations/VALIDATION.md @@ -0,0 +1,159 @@ +# Shared C continuation validation + +Measurements taken 2026-11-02. These qualify a numeric, thread-confined core-module +prototype, not a complete WASI 0.3/component runtime or a Pico deployment. +The TinyWasm changes are independent of PR #81. + +## Builds and fixtures + +The unmodified snapshots were TinyWasm `ef5ded81f1fb0d01393d7ef55b8d258f71c5c216` +and WAMR `48381c35ebebc5349b52b362f464874672cd38bb`. Compare each runtime to its own +snapshot with the same source fixture, allocator, panic strategy, toolchain, and +link flags. `base` means unmodified; `off` is this change with the feature omitted; +`on` enables the feature but the benchmark client never references the extension. +The feature-used conformance executable is built separately. + +macOS arm64 used Clang/LLD 23.1.1, Rust nightly +`1.100.0-nightly (e457a7b0d 2026-08-27)`, release `panic=abort`, Rust fat LTO and +one codegen unit; WAMR used thin LTO, `-O3 -DNDEBUG -fvisibility=hidden`, function +and data sections. The C client used `-O3 -flto=thin -fuse-ld=lld`, hidden visibility, +`-Wl,-dead_strip`, then `llvm-strip -S -x`. All variants used the filename `probe`. +Use `-Wl,-object_path_lto,` with the tested macOS LLD for its +intermediate object. Final C LTO does not imply Rust/C cross-language inlining. + +Windows x64 used an Intel Core i7-8650U, Windows 11 Pro build 26300, +VS 2022 MSVC 19.44.35229.0 and Rust 1.98.1 (48a229cea 2026-09-01). +Runtime C compilation used CMake IPO; the client used `/O2 /GL /Gy /Gw`, +`/LTCG /OPT:REF /OPT:ICF /Brepro /INCREMENTAL:NO`. Rust used the same fat-LTO, +one-codegen-unit, abort strategy. No whole-archive retention was used. +WAMR used the fast interpreter with software bounds checks and no AOT/JIT, +SIMD, WASI libc, or builtin libc. MSVC and stable Rust use switch/match dispatch; +these numbers do not compare the fastest dispatch configuration of either runtime. + +`run.py` records reproducible build commands for either backend. WAMR's public +`wasm_c_api.h` was copied as `wasm.h` for the unchanged C client. Define +`LIBWASM_STATIC` and `WASM_API_EXTERN=` for static MSVC clients. `measure.py` +alternates the executable order for seven rounds and emits all samples. Use +`sections.py variant=/path/to/image` for Mach-O64/PE section hashes; use +`llvm-size -A`/`readelf -S` for ELF. CSV receipts accompany this report. +The shared C fixture SHA-256 is +`d38be8465b16cfbdf09e752637e5750738b461a8899795ad7f4c2667a6006cdd`. + +## Disabled-feature artifacts + +| Host / runtime | Baseline bytes | Disabled bytes | Enabled, unused bytes | +|---|---:|---:|---:| +| macos / wamr | 219328 | 219328 | 219328 | +| macos / tiny | 1633552 | 1633552 | 1633632 | +| windows / wamr | 355840 | 355840 | 360960 | +| windows / tiny | 1736704 | 1736704 | 1737728 | + +Baseline and disabled code section SHA-256 hashes match exactly on both hosts. +All loadable section sizes, including writable data, BSS and TLS, also match. +Windows `.data` hashes match; macOS TinyWasm has equal-sized relocation/constant +metadata with some different bytes, so complete file identity is not claimed. +Disabled static libraries retain no `wasm_ext_*`, `HostExecution`, or +`PendingHostCall` symbols. Opt-in store/environment fields preprocess out; ordinary +interpreter, executor and native callback dispatch retain no continuation hooks. +Identical code gives stronger evidence of no added instructions than noisy timing +ratios. This is a result for these static-link configurations; exported shared +libraries, other feature combinations and other toolchains need their own audit. + +| Host / variant | Compute ns / iteration | Native ns / import | Windows cycles / import | Peak RSS bytes, min–max | +|---|---:|---:|---:|---:| +| macos-arm64 / wamr-base | 4.6825 | 22.7907 | — | 2179072–2211840 | +| macos-arm64 / wamr-off | 4.6521 | 22.8135 | — | 2162688–2211840 | +| macos-arm64 / wamr-on | 4.6756 | 23.0202 | — | 2179072–2211840 | +| macos-arm64 / tiny-base | 5.4229 | 105.5251 | — | 6848512–6914048 | +| macos-arm64 / tiny-off | 5.3573 | 118.2820 | — | 6897664–6946816 | +| macos-arm64 / tiny-on | 5.3012 | 125.8884 | — | 6832128–6864896 | +| windows-x64 / wamr-base | 10.1562 | 92.1875 | 199.3785 | 4595712–4599808 | +| windows-x64 / wamr-off | 9.0625 | 62.5000 | 130.0800 | 4595712–4603904 | +| windows-x64 / wamr-on | 8.1250 | 67.1875 | 144.7130 | 4599808–4603904 | +| windows-x64 / tiny-base | 12.6562 | 525.0000 | 1111.8623 | 4276224–4276224 | +| windows-x64 / tiny-off | 11.5625 | 475.0000 | 1002.2335 | 4272128–4276224 | +| windows-x64 / tiny-on | 14.8438 | 453.1250 | 951.3873 | 4427776–4427776 | + +Each compute sample performs 100 million loop iterations; the native sample makes +10 million ordinary C API host imports. Results are checked deterministically. +Values above are medians of seven alternating rounds; all samples and peak process +RSS/Windows commit values are in `measurements.csv`. Windows uses process CPU time +(`GetProcessTimes`) and main-thread cycles (`QueryThreadCycleTime`), rather than +MSVC's elapsed-time `clock()`. Windows children were pinned to logical CPU 2; +macOS was unpinned. Frequency changes, allocation placement, and hosted-process +noise still affect the results. The timing variation does not establish tight percentage bounds on runtime overhead. +The matching disabled code hashes establish the absence of additional compiled instructions; +these hosted timings and RSS samples do not prove an exact zero difference in elapsed time or RAM. + +Process RSS and committed memory are high-water marks, not isolated store or live +heap measurements. Baseline/off static RAM and instructions match, while measured +process-memory values vary in pages. No claim of exactly equal runtime RSS is made. +The TinyWasm C wrapper currently requires std. No physical Pico SRAM, stack, heap, +or cycle qualification has been performed; cross-host figures are not MCU budgets. +The enabled WAMR invocation separately allocates a 64 KiB interpreter stack. + +## Safety and continuation coverage + +The identical C source passed on macOS arm64 and physical Windows x64, including +pending/resume/cancel/delete, cancellation before start, exactly-once notification, +no callback replay, repeated suspension, indirect calls, invalid initial/completion +types and counts, store-call exclusion, immediate completion, malformed host result +rejection, host and guest traps, and mixed i32/i64 arguments with high result bits. +The malformed-result case ensures a numeric adapter rejects reference-kind union +bits before attempting reference ownership or conversion. A prior synchronous trap +must not poison a later continuation. The feature-used fixture also exercises +ordinary native imports through the opt-in driver and prints that cost separately. + +TinyWasm's C adapter unit tests, seven Rust host-execution integration tests, +fallback dispatch tests, no-default-feature check, formatting and strict Clippy +passed. Miri passed the owned C adapter's pending/resume/cancel, actual guest +execution and ordinary native-import forwarding. The exclusive store reservation +keeps the owned execution borrow valid; it is dropped before inspecting result +storage. Pending numeric values are checked before committing the continuation. + +WAMR's new CTest suite reuses the shared C source and generates the main module +from WAT. AddressSanitizer found no address errors in the exercised paths. +UBSan alignment warnings occur in the baseline fast loader/interpreter/table +implementation as well; this is not a clean all-UBSan result. New extension packing +uses memcpy for potentially unaligned mixed-width arguments. Repository naming, +license and staged formatting checks passed. The full preset clang-tidy script +could not configure `all-on-fast-jit` on this host; that check remains unqualified. A targeted core-analyzer scan of the enabled C API +translation unit reproduces the baseline warning about `func_import` in import setup; +no new adapter diagnostics remain in that scan. This is not a clean full static-analysis claim. + +## Windows profiling and remaining work + +VTune 2024.2 hardware event-based sampling completed for both feature-off clients. +The workload included both compute and native imports. Summary and hotspot receipts +are preserved separately from unprofiled timing rounds. WAMR retired about 20.27 +billion instructions with sampled CPI 0.256; TinyWasm about 34.99 billion with CPI +0.331 in that collection. These are whole-fixture counts, not per-call attribution. +The optimized images lack internal PDB symbols: internal addresses are unsymbolized. +TinyWasm's `RtlFreeHeap` samples account for 8.7% of sampled CPU time, making native +callback allocation a useful next investigation, not a proven sole bottleneck. + +The ordinary native-import gap remains significant. Selected async imports must +pay for owned pending state; unselected synchronous imports use TinyWasm's existing +stack callback API before argument-vector materialization. Scheduling, wakeups, +futures and resources stay out of the core. Further optimization can change the +ordinary interpreter and native callback implementation independently of this API. + +`wasm_val_t` was 16 bytes in both builds, but `wasm_val_vec_t` was 16 bytes in +TinyWasm and 40 in WAMR. This fixture establishes shared C source behavior, not a +binary plugin ABI. Normalize or adapt the base vector ABI before freezing a common +header. C caller-context access, nested canonical realloc/post-return operations, +imported memory/table parity, reference signatures, full component task concurrency, +AOT/JIT and additional targets remain separate acceptance gates. Only pending host +boundaries suspend; cancellation cannot preempt guest loops or blocking callbacks. +The component-parser branch review is documented in the fixture README. + +Reproduce the Windows collection with VTune 2024.2: + +```text +vtune -collect hotspots -knob sampling-mode=hw -knob enable-stack-collection=false -result-dir profile -- fixture.exe fixture.wasm +vtune -report summary -r profile -format csv +vtune -report hotspots -r profile -format csv +``` + +`windows_profile.csv` preserves summary metrics and the collected per-function CPU samples; +internal addresses remain unsymbolized. Full VTune result directories remain on the test device. diff --git a/crates/c-api/tests/continuations/macos_sections.csv b/crates/c-api/tests/continuations/macos_sections.csv new file mode 100644 index 0000000..6b1cade --- /dev/null +++ b/crates/c-api/tests/continuations/macos_sections.csv @@ -0,0 +1,91 @@ +variant,section,virtual_bytes,file_bytes,sha256 +wamr-base,,,219328,3349a50f3a31658681bc353eda4b67b14ef712818d7de86dfe462f74dc185a6f +wamr-base,"__TEXT,__text",149664,149664,967ecdad26d44f3a3d013ac02a05ea35c6bc2627580e80a03cd080f997176ef0 +wamr-base,"__TEXT,__stubs",516,516,2b6840756a499fbc22cd54a22bb87a80250e745980edd5b932f3442142e41682 +wamr-base,"__TEXT,__literals",40,40,3bda21b945b6efcb209148fc61a7672f2d352883db8d8df3e14d0d2708002da7 +wamr-base,"__TEXT,__cstring",9565,9565,3808b42343813cbd23f3d95bb362594cb9e6282873d20f1b61989e5ede56e0e5 +wamr-base,"__TEXT,__const",673,673,4e695b0b72405cb3cd4bbe60a5d1cb68b3597689de2b3acb25f525127855ba32 +wamr-base,"__TEXT,__unwind_info",4200,4200,e29279af15d89540e6d3194067f9940ed8f867651871172f16588af22144cf2b +wamr-base,"__DATA_CONST,__got",360,360,d0e6f43cc7ddbc4398afb64dfac90fe2a55edbba10aca4bb49df345450dd55a3 +wamr-base,"__DATA_CONST,__const",40,40,934865706af2bed1bf71e2cfd494c484c6ddcc8f98723431a8ea860c1243f79f +wamr-base,"__DATA,__thread_vars",48,48,a4264c6f95e36347b4b6f9cd6797be30d568fab6e44dc382bbcc06b7bdd91384 +wamr-base,"__DATA,__data",2232,2232,5339f6a4980b88ec4c3628e8b381d3b026a6b76a731d7276d26dd4c31fd589e9 +wamr-base,"__DATA,__thread_bss",12,0, +wamr-base,"__DATA,__common",136,0, +wamr-base,"__DATA,__bss",8,0, +wamr-off,,,219328,ba6084b906af3a1dcee06354b273176e3af6e237d6fecd3f8434142f9e363ef0 +wamr-off,"__TEXT,__text",149664,149664,967ecdad26d44f3a3d013ac02a05ea35c6bc2627580e80a03cd080f997176ef0 +wamr-off,"__TEXT,__stubs",516,516,2b6840756a499fbc22cd54a22bb87a80250e745980edd5b932f3442142e41682 +wamr-off,"__TEXT,__literals",40,40,3bda21b945b6efcb209148fc61a7672f2d352883db8d8df3e14d0d2708002da7 +wamr-off,"__TEXT,__cstring",9565,9565,3808b42343813cbd23f3d95bb362594cb9e6282873d20f1b61989e5ede56e0e5 +wamr-off,"__TEXT,__const",673,673,4e695b0b72405cb3cd4bbe60a5d1cb68b3597689de2b3acb25f525127855ba32 +wamr-off,"__TEXT,__unwind_info",4200,4200,e29279af15d89540e6d3194067f9940ed8f867651871172f16588af22144cf2b +wamr-off,"__DATA_CONST,__got",360,360,d0e6f43cc7ddbc4398afb64dfac90fe2a55edbba10aca4bb49df345450dd55a3 +wamr-off,"__DATA_CONST,__const",40,40,934865706af2bed1bf71e2cfd494c484c6ddcc8f98723431a8ea860c1243f79f +wamr-off,"__DATA,__thread_vars",48,48,a4264c6f95e36347b4b6f9cd6797be30d568fab6e44dc382bbcc06b7bdd91384 +wamr-off,"__DATA,__data",2232,2232,5339f6a4980b88ec4c3628e8b381d3b026a6b76a731d7276d26dd4c31fd589e9 +wamr-off,"__DATA,__thread_bss",12,0, +wamr-off,"__DATA,__common",136,0, +wamr-off,"__DATA,__bss",8,0, +wamr-on,,,219328,62b3094c819b98cb4820beb8ad0a0a3009d80b2099ae2a2a88427204be52a810 +wamr-on,"__TEXT,__text",149920,149920,3ae8bf9efe8c31854c28a02d7106cb47892ffc240698bdc4744003cb32124b69 +wamr-on,"__TEXT,__stubs",516,516,1df9768488fbebfd132144ca67a5678bbafa80443918c65447eab26ee5cdcb1a +wamr-on,"__TEXT,__literals",40,40,3bda21b945b6efcb209148fc61a7672f2d352883db8d8df3e14d0d2708002da7 +wamr-on,"__TEXT,__cstring",9595,9595,a087993751dc07373d1dd9a0429918d02ed8aa349c1b27e2c351266d4af57131 +wamr-on,"__TEXT,__const",673,673,4e695b0b72405cb3cd4bbe60a5d1cb68b3597689de2b3acb25f525127855ba32 +wamr-on,"__TEXT,__unwind_info",4200,4200,ca96b167775ba269de26461c695333c149b81f0235d9c604bc303ac87464b047 +wamr-on,"__DATA_CONST,__got",360,360,d0e6f43cc7ddbc4398afb64dfac90fe2a55edbba10aca4bb49df345450dd55a3 +wamr-on,"__DATA_CONST,__const",40,40,c72d186763e0ee38ed3982792c73a3a512f520bacd31a8b7216fd60378b46999 +wamr-on,"__DATA,__thread_vars",48,48,a4264c6f95e36347b4b6f9cd6797be30d568fab6e44dc382bbcc06b7bdd91384 +wamr-on,"__DATA,__data",2232,2232,749d0cc8b79ed334d4d87f26f26654fb9a4f99b83520d94e053f076fb978fb73 +wamr-on,"__DATA,__thread_bss",12,0, +wamr-on,"__DATA,__common",136,0, +wamr-on,"__DATA,__bss",8,0, +tiny-base,,,1633552,c85be18881c16468877701dc56640f4ab12d083d54b0b47df22a9a9b49a5e4b9 +tiny-base,"__TEXT,__text",1422600,1422600,0906d6eb704d2b5aee8b0ab1fd1d8f636b7ba3b1a391f6bc6a55c44faa4a69aa +tiny-base,"__TEXT,__stubs",864,864,9a2fcbdef07366ff5a761c7e7fbf3b4e1315fbba800cf6d7490c9da3020963a0 +tiny-base,"__TEXT,__literals",4428,4428,c68fb0f93056d78be5a10681fa487a0f04871db61bfb2a23aad053acc549e55d +tiny-base,"__TEXT,__gcc_except_tab",3228,3228,a0b2254c77feff000cebc2526fc3052812e24f9bb67504283cb4fcbaa6411810 +tiny-base,"__TEXT,__const",68334,68334,4eb234463d717566ac5549d94cc470c58fcff71bd3c1540042010a8b4591e8fd +tiny-base,"__TEXT,__cstring",15307,15307,c83003b574e72507662de4fa99dcaf3541b4c749b2ef924c2ee4270bd38a0df4 +tiny-base,"__TEXT,__unwind_info",9420,9420,394c22013d61bc76b562c62980bc36b6e964b6e3594c717e8b971de7555b37e3 +tiny-base,"__TEXT,__eh_frame",20592,20592,5352b9cbf8e8330f6f765c74b5b94ced7f072c259325dff572157d6099b652c8 +tiny-base,"__DATA_CONST,__got",600,600,8a0109bba6484088e7940d88a02a05c78f6091d7d2dce1b2e51d17f23bbe7f1b +tiny-base,"__DATA_CONST,__const",27952,27952,237a967f4ae1e281a0e4918b1345a810ecdb309f6cc1cc9196c57474513c071f +tiny-base,"__DATA,__thread_vars",144,144,358954a42abdf234b59b3ddd636cacd579326ad1368d44cfa1d677501cf38b40 +tiny-base,"__DATA,__data",2576,2576,dd9721344c283347ee01ce1614bd908fddf2d834a0ba49d4f67d4c0e5aedd9c1 +tiny-base,"__DATA,__thread_data",72,72,ac5d32dc98f1a65e0b86688dd74d8c0dc15cd3c1ba0525aac9c6212bbbc94159 +tiny-base,"__DATA,__thread_bss",33,0, +tiny-base,"__DATA,__bss",96,0, +tiny-off,,,1633552,5096f57898531c3ad3202a2f75b2261cc2dcec63a62ad591c0b1a214aaed9429 +tiny-off,"__TEXT,__text",1422600,1422600,0906d6eb704d2b5aee8b0ab1fd1d8f636b7ba3b1a391f6bc6a55c44faa4a69aa +tiny-off,"__TEXT,__stubs",864,864,9a2fcbdef07366ff5a761c7e7fbf3b4e1315fbba800cf6d7490c9da3020963a0 +tiny-off,"__TEXT,__literals",4428,4428,c68fb0f93056d78be5a10681fa487a0f04871db61bfb2a23aad053acc549e55d +tiny-off,"__TEXT,__gcc_except_tab",3228,3228,a0b2254c77feff000cebc2526fc3052812e24f9bb67504283cb4fcbaa6411810 +tiny-off,"__TEXT,__const",68334,68334,4eb234463d717566ac5549d94cc470c58fcff71bd3c1540042010a8b4591e8fd +tiny-off,"__TEXT,__cstring",15307,15307,c83003b574e72507662de4fa99dcaf3541b4c749b2ef924c2ee4270bd38a0df4 +tiny-off,"__TEXT,__unwind_info",9420,9420,394c22013d61bc76b562c62980bc36b6e964b6e3594c717e8b971de7555b37e3 +tiny-off,"__TEXT,__eh_frame",20592,20592,5352b9cbf8e8330f6f765c74b5b94ced7f072c259325dff572157d6099b652c8 +tiny-off,"__DATA_CONST,__got",600,600,8a0109bba6484088e7940d88a02a05c78f6091d7d2dce1b2e51d17f23bbe7f1b +tiny-off,"__DATA_CONST,__const",27952,27952,a3e1143786b776009fdb316ae8670afba7fa982e190103efc6b4a7eaca97a11e +tiny-off,"__DATA,__thread_vars",144,144,358954a42abdf234b59b3ddd636cacd579326ad1368d44cfa1d677501cf38b40 +tiny-off,"__DATA,__data",2576,2576,dd9721344c283347ee01ce1614bd908fddf2d834a0ba49d4f67d4c0e5aedd9c1 +tiny-off,"__DATA,__thread_data",72,72,ac5d32dc98f1a65e0b86688dd74d8c0dc15cd3c1ba0525aac9c6212bbbc94159 +tiny-off,"__DATA,__thread_bss",33,0, +tiny-off,"__DATA,__bss",96,0, +tiny-on,,,1633632,08a930d1711850633eefab92237357f29be1f33ea8c7fff5625fc4faa86aacfb +tiny-on,"__TEXT,__text",1424456,1424456,50f97701cffb20998083a98f7472838ffda162d0232a59653e7807fceaa8cad8 +tiny-on,"__TEXT,__stubs",876,876,2d61045b8a95ac5657c9ddeae6cf1829430b583db7ae0e79905244da87c1ffd0 +tiny-on,"__TEXT,__literals",4436,4436,48dc24a80fb957f23185db2b13340fe7c4f4477e5c561c138168d83c45104173 +tiny-on,"__TEXT,__gcc_except_tab",3228,3228,e75cd6da042a736152a2f8d52356ed9a809bba5f885ac462aee724416447812b +tiny-on,"__TEXT,__const",68406,68406,44bc0a2d51d4eafe73610e20e3174f1b829435452d062cf3f7db4bfcccf139d9 +tiny-on,"__TEXT,__cstring",15399,15399,458978729404bcb852d49c654cc99f9ed213a0041f228f58706986a077695d01 +tiny-on,"__TEXT,__unwind_info",9420,9420,ebd82b2ebdff511685859e4feb8bd4a20d076d678b20623b7807b29a7dbf3c3a +tiny-on,"__TEXT,__eh_frame",20640,20640,f1ace780ecef04aabfb2daf184633d8c620462bff7e949b8f8de2b6f1e98727d +tiny-on,"__DATA_CONST,__got",608,608,6c68371eb3b24409877063c5cd8f266121868a796ec4b3a5d9488d975c14db45 +tiny-on,"__DATA_CONST,__const",28032,28032,351026599d86526b79446154e502033dcc74ded882dadc6afacf7175a3317578 +tiny-on,"__DATA,__thread_vars",168,168,d925146a9820ab4593a712fcb998ac4cc7e9de5ffc648e4a215d9593d90825a3 +tiny-on,"__DATA,__data",2576,2576,9dafb727861c7afef6028eaf993e4b8ed8b1e6bef74cfcdf07d297f0d93088d9 +tiny-on,"__DATA,__thread_data",72,72,ac5d32dc98f1a65e0b86688dd74d8c0dc15cd3c1ba0525aac9c6212bbbc94159 +tiny-on,"__DATA,__thread_bss",57,0, +tiny-on,"__DATA,__bss",96,0, diff --git a/crates/c-api/tests/continuations/measure.py b/crates/c-api/tests/continuations/measure.py index 105f5e8..4fd048c 100644 --- a/crates/c-api/tests/continuations/measure.py +++ b/crates/c-api/tests/continuations/measure.py @@ -27,7 +27,7 @@ k32.OpenProcess.restype = ctypes.c_void_p k32.SetProcessAffinityMask.argtypes = [ctypes.c_void_p, ctypes.c_size_t] k32.CloseHandle.argtypes = [ctypes.c_void_p] -writer = csv.writer(sys.stdout) +writer = csv.writer(sys.stdout, lineterminator="\n") writer.writerow(['variant', 'round', 'compute_ns', 'native_ns', 'peak_rss_bytes', 'peak_commit_bytes', 'compute_cycles', 'native_cycles', 'cpu']) for i in range(a.rounds): for name, path in (variants if i % 2 == 0 else reversed(variants)): diff --git a/crates/c-api/tests/continuations/measurements.csv b/crates/c-api/tests/continuations/measurements.csv new file mode 100644 index 0000000..f777eb0 --- /dev/null +++ b/crates/c-api/tests/continuations/measurements.csv @@ -0,0 +1,85 @@ +host,variant,round,compute_ns,native_ns,peak_rss_bytes,peak_commit_bytes,compute_cycles,native_cycles,cpu +macos-arm64,wamr-base,0,4.6897,22.8454,2195456,,,, +macos-arm64,wamr-off,0,4.7325,23.0183,2211840,,,, +macos-arm64,tiny-base,0,5.33,104.4893,6914048,,,, +macos-arm64,tiny-off,0,5.3249,118.282,6930432,,,, +macos-arm64,wamr-on,0,4.7614,23.0364,2195456,,,, +macos-arm64,tiny-on,0,5.3012,133.0726,6848512,,,, +macos-arm64,tiny-on,1,5.3005,125.1592,6864896,,,, +macos-arm64,wamr-on,1,4.6181,23.088,2195456,,,, +macos-arm64,tiny-off,1,5.3568,110.9622,6914048,,,, +macos-arm64,tiny-base,1,5.3582,109.9292,6848512,,,, +macos-arm64,wamr-off,1,4.6105,22.8027,2195456,,,, +macos-arm64,wamr-base,1,4.7231,22.7907,2179072,,,, +macos-arm64,wamr-base,2,4.6493,22.7687,2195456,,,, +macos-arm64,wamr-off,2,4.6521,22.7635,2179072,,,, +macos-arm64,tiny-base,2,5.3296,104.7998,6897664,,,, +macos-arm64,tiny-off,2,5.4175,118.727,6946816,,,, +macos-arm64,wamr-on,2,4.7041,23.0172,2211840,,,, +macos-arm64,tiny-on,2,5.6189,125.5273,6864896,,,, +macos-arm64,tiny-on,3,5.2835,135.3571,6832128,,,, +macos-arm64,wamr-on,3,4.6359,23.0202,2195456,,,, +macos-arm64,tiny-off,3,5.4147,110.92,6914048,,,, +macos-arm64,tiny-base,3,5.4584,105.5251,6881280,,,, +macos-arm64,wamr-off,3,4.6313,22.8135,2195456,,,, +macos-arm64,wamr-base,3,4.6937,22.7802,2211840,,,, +macos-arm64,wamr-base,4,4.6342,22.7423,2195456,,,, +macos-arm64,wamr-off,4,4.7112,22.7573,2162688,,,, +macos-arm64,tiny-base,4,5.4229,105.8439,6897664,,,, +macos-arm64,tiny-off,4,5.3573,119.2575,6914048,,,, +macos-arm64,wamr-on,4,4.6765,22.9839,2179072,,,, +macos-arm64,tiny-on,4,5.3395,126.0261,6832128,,,, +macos-arm64,tiny-on,5,5.3648,125.8884,6848512,,,, +macos-arm64,wamr-on,5,4.6756,22.9951,2195456,,,, +macos-arm64,tiny-off,5,5.3614,111.4458,6897664,,,, +macos-arm64,tiny-base,5,5.4255,105.1268,6864896,,,, +macos-arm64,wamr-off,5,4.6461,22.9443,2179072,,,, +macos-arm64,wamr-base,5,4.6825,22.8166,2195456,,,, +macos-arm64,wamr-base,6,4.661,23.1055,2195456,,,, +macos-arm64,wamr-off,6,4.7302,22.8203,2179072,,,, +macos-arm64,tiny-base,6,5.4711,110.9677,6881280,,,, +macos-arm64,tiny-off,6,5.3335,119.1099,6946816,,,, +macos-arm64,wamr-on,6,4.6502,23.0425,2195456,,,, +macos-arm64,tiny-on,6,5.2979,124.6378,6832128,,,, +windows-x64,wamr-base,0,24.5312,246.875,4599808,724992,51.8987,515.7158,2 +windows-x64,wamr-off,0,8.75,67.1875,4599808,720896,18.2686,143.7033,2 +windows-x64,tiny-base,0,28.9062,779.6875,4276224,1015808,60.8355,1643.4859,2 +windows-x64,tiny-off,0,10.9375,915.625,4276224,1015808,22.7407,1922.6742,2 +windows-x64,wamr-on,0,26.4062,67.1875,4603904,720896,56.0926,141.7581,2 +windows-x64,tiny-on,0,14.2188,453.125,4427776,1056768,29.74,951.3873,2 +windows-x64,tiny-on,1,10.4688,335.9375,4427776,1048576,22.2876,707.8526,2 +windows-x64,wamr-on,1,8.125,56.25,4603904,712704,16.867,120.8783,2 +windows-x64,tiny-off,1,9.5312,234.375,4276224,1007616,20.1251,493.079,2 +windows-x64,tiny-base,1,7.9688,259.375,4276224,1007616,17.0409,544.0264,2 +windows-x64,wamr-off,1,9.0625,201.5625,4599808,716800,18.9103,429.461,2 +windows-x64,wamr-base,1,8.9062,64.0625,4599808,720896,18.7641,133.279,2 +windows-x64,wamr-base,2,20.7812,53.125,4599808,720896,43.6562,112.5738,2 +windows-x64,wamr-off,2,12.8125,53.125,4599808,716800,27.015,113.9741,2 +windows-x64,tiny-base,2,12.6562,810.9375,4276224,1015808,26.8351,1706.1418,2 +windows-x64,tiny-off,2,10.625,303.125,4276224,1015808,22.7852,634.5342,2 +windows-x64,wamr-on,2,7.9688,210.9375,4603904,720896,16.9338,442.0621,2 +windows-x64,tiny-on,2,21.7188,473.4375,4427776,1052672,45.6056,994.9018,2 +windows-x64,tiny-on,3,14.8438,531.25,4427776,1060864,31.1137,1114.9535,2 +windows-x64,wamr-on,3,30,37.5,4603904,716800,63.2478,78.8525,2 +windows-x64,tiny-off,3,12.1875,450,4272128,1003520,25.4928,946.178,2 +windows-x64,tiny-base,3,8.125,271.875,4276224,1015808,17.2402,573.1668,2 +windows-x64,wamr-off,3,9.375,39.0625,4599808,716800,19.7032,82.5541,2 +windows-x64,wamr-base,3,11.0938,92.1875,4599808,720896,23.4004,199.3785,2 +windows-x64,wamr-base,4,8.75,182.8125,4599808,720896,18.5762,394.4424,2 +windows-x64,wamr-off,4,8.75,62.5,4595712,712704,18.5902,130.08,2 +windows-x64,tiny-base,4,15.3125,650,4276224,1003520,32.1245,1382.3206,2 +windows-x64,tiny-off,4,13.4375,475,4276224,1007616,28.2076,1002.2335,2 +windows-x64,wamr-on,4,13.2812,168.75,4599808,712704,28.321,357.5123,2 +windows-x64,tiny-on,4,16.0938,359.375,4427776,1056768,33.8766,749.3814,2 +windows-x64,tiny-on,5,8.125,270.3125,4427776,1048576,17.3509,567.9389,2 +windows-x64,wamr-on,5,8.125,67.1875,4603904,724992,16.8798,144.713,2 +windows-x64,tiny-off,5,11.5625,1193.75,4272128,1003520,24.4732,2516.4352,2 +windows-x64,tiny-base,5,13.2812,525,4276224,1007616,27.6993,1111.8623,2 +windows-x64,wamr-off,5,8.75,51.5625,4599808,716800,18.6569,107.4219,2 +windows-x64,wamr-base,5,10.1562,142.1875,4595712,712704,21.5423,311.7255,2 +windows-x64,wamr-base,6,8.5938,43.75,4599808,716800,18.3146,91.4795,2 +windows-x64,wamr-off,6,9.8438,62.5,4603904,737280,20.9578,131.6602,2 +windows-x64,tiny-base,6,8.125,479.6875,4276224,1011712,16.9527,1014.2841,2 +windows-x64,tiny-off,6,22.3438,626.5625,4276224,1011712,47.3412,1332.7018,2 +windows-x64,wamr-on,6,8.125,134.375,4603904,716800,17.0913,278.867,2 +windows-x64,tiny-on,6,20,825,4427776,1052672,42.0622,1744.6173,2 diff --git a/crates/c-api/tests/continuations/sections.py b/crates/c-api/tests/continuations/sections.py index 9244ed4..e83ad65 100644 --- a/crates/c-api/tests/continuations/sections.py +++ b/crates/c-api/tests/continuations/sections.py @@ -10,7 +10,7 @@ p = argparse.ArgumentParser() p.add_argument('executables', nargs='+', help='variant=path') a = p.parse_args() -w = csv.writer(sys.stdout) +w = csv.writer(sys.stdout, lineterminator="\n") w.writerow(['variant', 'section', 'virtual_bytes', 'file_bytes', 'sha256']) for item in a.executables: name, path = item.split('=', 1) diff --git a/crates/c-api/tests/continuations/windows_profile.csv b/crates/c-api/tests/continuations/windows_profile.csv new file mode 100644 index 0000000..3ce4b51 --- /dev/null +++ b/crates/c-api/tests/continuations/windows_profile.csv @@ -0,0 +1,109 @@ +runtime,category,name,module,value +wamr,image,.text SHA-256,wamr-off.exe,0cf728fc9a9df05827efd766d876864cfec5b09978a66893a06a183c9c33713c +wamr,summary,Elapsed Time,,2.200799 +wamr,summary,CPU Time,,1.961174 +wamr,summary,Instructions Retired,,20265400000 +wamr,summary,CPI Rate,,0.256177 +wamr,summary,Microarchitecture Usage,,96.6 +wamr,hotspot CPU seconds,func@0x140023460,wamr-off.exe,0.571259 +wamr,hotspot CPU seconds,func@0x140025ff4,wamr-off.exe,0.390436 +wamr,hotspot CPU seconds,func@0x14000f840,wamr-off.exe,0.232102 +wamr,hotspot CPU seconds,func@0x1400231f0,wamr-off.exe,0.150237 +wamr,hotspot CPU seconds,func@0x140025d7c,wamr-off.exe,0.139441 +wamr,hotspot CPU seconds,func@0x14002363c,wamr-off.exe,0.123248 +wamr,hotspot CPU seconds,[wamr-off.exe],wamr-off.exe,0.079167 +wamr,hotspot CPU seconds,func@0x1400246b0,wamr-off.exe,0.074669 +wamr,hotspot CPU seconds,func@0x1400246d0,wamr-off.exe,0.071970 +wamr,hotspot CPU seconds,func@0x140001310,wamr-off.exe,0.047680 +wamr,hotspot CPU seconds,func@0x140023980,wamr-off.exe,0.043182 +wamr,hotspot CPU seconds,func@0x140026920,wamr-off.exe,0.021591 +wamr,hotspot CPU seconds,func@0x14039ad44,ntoskrnl.exe,0.001799 +wamr,hotspot CPU seconds,RtlAcquireSRWLockExclusive,ntdll.dll,0.000900 +wamr,hotspot CPU seconds,func@0x140023180,wamr-off.exe,0.000900 +wamr,hotspot CPU seconds,func@0x140039d18,wamr-off.exe,0.000900 +wamr,hotspot CPU seconds,func@0x1401c0910,ntfs.sys,0.000900 +wamr,hotspot CPU seconds,func@0x140294a80,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x1402de580,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x14039be34,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x1406b3ad0,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x1406b9b4f,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x140865f40,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x140869e20,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x140a2eca0,ntoskrnl.exe,0.000900 +wamr,hotspot CPU seconds,func@0x180009635,ntdll.dll,0.000900 +wamr,hotspot CPU seconds,func@0x180032d05,ntdll.dll,0.000900 +wamr,hotspot CPU seconds,func@0x1800461c0,ntdll.dll,0.000900 +wamr,hotspot CPU seconds,func@0x1800f19dc,kernelbase.dll,0.000900 +wamr,hotspot CPU seconds,func@0x140216820,ntoskrnl.exe,0.0 +wamr,hotspot CPU seconds,func@0x14025f1ca,ntoskrnl.exe,0.0 +wamr,hotspot CPU seconds,func@0x1402789e0,ntoskrnl.exe,0.0 +wamr,hotspot CPU seconds,func@0x14027dc00,ntoskrnl.exe,0.0 +wamr,hotspot CPU seconds,func@0x14032da64,ntoskrnl.exe,0.0 +wamr,hotspot CPU seconds,func@0x140bc98d0,ntoskrnl.exe,0.0 +wamr,hotspot CPU seconds,func@0x18002c310,ntdll.dll,0.0 +wamr,hotspot CPU seconds,func@0x18011bfb0,ntdll.dll,0.0 +wamr,hotspot CPU seconds,func@0x180125fdc,ntdll.dll,0.0 +tiny,image,.text SHA-256,tiny-off.exe,314a14af0eb57dc51e70b288326e86350acaba349778e48282200913930fea79 +tiny,summary,Elapsed Time,,3.981305 +tiny,summary,CPU Time,,3.752320 +tiny,summary,Instructions Retired,,34990400000 +tiny,summary,CPI Rate,,0.331133 +tiny,summary,Microarchitecture Usage,,71.9 +tiny,hotspot CPU seconds,func@0x14001e7d0,tiny-off.exe,0.510985 +tiny,hotspot CPU seconds,func@0x14001cca0,tiny-off.exe,0.490294 +tiny,hotspot CPU seconds,func@0x140003530,tiny-off.exe,0.360748 +tiny,hotspot CPU seconds,func@0x140031832,tiny-off.exe,0.333759 +tiny,hotspot CPU seconds,RtlFreeHeap,ntdll.dll,0.325663 +tiny,hotspot CPU seconds,func@0x14002033f,tiny-off.exe,0.297775 +tiny,hotspot CPU seconds,func@0x180032d05,ntdll.dll,0.252794 +tiny,hotspot CPU seconds,func@0x180032750,ntdll.dll,0.195218 +tiny,hotspot CPU seconds,func@0x180032060,ntdll.dll,0.135843 +tiny,hotspot CPU seconds,func@0x140047590,tiny-off.exe,0.123248 +tiny,hotspot CPU seconds,func@0x1400401a7,tiny-off.exe,0.074669 +tiny,hotspot CPU seconds,RtlAllocateHeap,ntdll.dll,0.064773 +tiny,hotspot CPU seconds,func@0x180027491,ntdll.dll,0.057576 +tiny,hotspot CPU seconds,func@0x140032ffd,tiny-off.exe,0.048580 +tiny,hotspot CPU seconds,func@0x140047420,tiny-off.exe,0.045881 +tiny,hotspot CPU seconds,GetProcessHeap,kernel32.dll,0.041383 +tiny,hotspot CPU seconds,func@0x140045409,tiny-off.exe,0.041383 +tiny,hotspot CPU seconds,func@0x140162750,tiny-off.exe,0.040483 +tiny,hotspot CPU seconds,func@0x14001ba50,tiny-off.exe,0.036884 +tiny,hotspot CPU seconds,func@0x140005b80,tiny-off.exe,0.029687 +tiny,hotspot CPU seconds,func@0x140049b90,tiny-off.exe,0.023390 +tiny,hotspot CPU seconds,func@0x140047b80,tiny-off.exe,0.021591 +tiny,hotspot CPU seconds,func@0x140047f90,tiny-off.exe,0.021591 +tiny,hotspot CPU seconds,func@0x140047a60,tiny-off.exe,0.016193 +tiny,hotspot CPU seconds,func@0x140047a89,tiny-off.exe,0.016193 +tiny,hotspot CPU seconds,func@0x14014b360,tiny-off.exe,0.016193 +tiny,hotspot CPU seconds,func@0x140005cd0,tiny-off.exe,0.015294 +tiny,hotspot CPU seconds,func@0x140005d01,tiny-off.exe,0.015294 +tiny,hotspot CPU seconds,func@0x140035276,tiny-off.exe,0.015294 +tiny,hotspot CPU seconds,func@0x140045474,tiny-off.exe,0.015294 +tiny,hotspot CPU seconds,func@0x140047b9e,tiny-off.exe,0.015294 +tiny,hotspot CPU seconds,func@0x14014a350,tiny-off.exe,0.010795 +tiny,hotspot CPU seconds,func@0x18003223a,ntdll.dll,0.009896 +tiny,hotspot CPU seconds,func@0x140038b7c,tiny-off.exe,0.007197 +tiny,hotspot CPU seconds,HeapFree,kernel32.dll,0.006297 +tiny,hotspot CPU seconds,func@0x1406b3ad0,ntoskrnl.exe,0.004498 +tiny,hotspot CPU seconds,func@0x14039ad44,ntoskrnl.exe,0.002699 +tiny,hotspot CPU seconds,func@0x140217d30,ntoskrnl.exe,0.001799 +tiny,hotspot CPU seconds,func@0x1403e63e0,ntoskrnl.exe,0.001799 +tiny,hotspot CPU seconds,RtlTryEnterCriticalSection,ntdll.dll,0.000900 +tiny,hotspot CPU seconds,func@0x140287590,ntoskrnl.exe,0.000900 +tiny,hotspot CPU seconds,func@0x1402ba000,ntoskrnl.exe,0.000900 +tiny,hotspot CPU seconds,func@0x1402d820c,ntoskrnl.exe,0.000900 +tiny,hotspot CPU seconds,func@0x14033b98c,ntoskrnl.exe,0.000900 +tiny,hotspot CPU seconds,func@0x1406b8ee0,ntoskrnl.exe,0.000900 +tiny,hotspot CPU seconds,func@0x1406c7540,ntoskrnl.exe,0.000900 +tiny,hotspot CPU seconds,func@0x18002dfd0,cng.sys,0.000900 +tiny,hotspot CPU seconds,func@0x1800461c0,ntdll.dll,0.000900 +tiny,hotspot CPU seconds,ExReleaseResourceLite,ntoskrnl.exe,0.0 +tiny,hotspot CPU seconds,ExpInterlockedPushEntrySList,ntoskrnl.exe,0.0 +tiny,hotspot CPU seconds,RtlImageDirectoryEntryToData,ntdll.dll,0.0 +tiny,hotspot CPU seconds,func@0x140258620,ntoskrnl.exe,0.0 +tiny,hotspot CPU seconds,func@0x1402ba47d,ntoskrnl.exe,0.0 +tiny,hotspot CPU seconds,func@0x14033b530,ntoskrnl.exe,0.0 +tiny,hotspot CPU seconds,func@0x14039bb70,ntoskrnl.exe,0.0 +tiny,hotspot CPU seconds,func@0x180008640,ntdll.dll,0.0 +tiny,hotspot CPU seconds,func@0x18011bfb0,ntdll.dll,0.0 +tiny,hotspot CPU seconds,func@0x18012c4e0,ntdll.dll,0.0 diff --git a/crates/c-api/tests/continuations/windows_sections.csv b/crates/c-api/tests/continuations/windows_sections.csv new file mode 100644 index 0000000..7c679ca --- /dev/null +++ b/crates/c-api/tests/continuations/windows_sections.csv @@ -0,0 +1,37 @@ +variant,section,virtual_bytes,file_bytes,sha256 +wamr-base,,,355840,db47f7cfaf19ce32c10a431c9ee5ed3d83520b268a9a7125fed66c7ae681c8a4 +wamr-base,.text,258288,258560,0cf728fc9a9df05827efd766d876864cfec5b09978a66893a06a183c9c33713c +wamr-base,.rdata,78264,78336,2011f8f61166d516802e08349ca8085aa1ecdb500bb6f24239ddd45d3fd12d4b +wamr-base,.data,7600,3072,16772f917651dc396781991ff50b43245b951542d4c5e982cc86dd3fd80278a5 +wamr-base,.pdata,12672,12800,c917c23d01a09c58d29e5043b2c95a569831cf6b7d7d80b77c04e8e018cac043 +wamr-base,.reloc,1684,2048,68e13d2fdc895bdb450b84e76c259c275f98a6eebb7dc904799503fd074a0eb3 +wamr-off,,,355840,afc09397ad9388b0f296b41f96f49ba6847b0fac108e0acf161f280b87a02f80 +wamr-off,.text,258288,258560,0cf728fc9a9df05827efd766d876864cfec5b09978a66893a06a183c9c33713c +wamr-off,.rdata,78264,78336,99f4bf9e018609fa244a56ca68c39b6d9f7364b100e7d59e2b3918a098cb3f86 +wamr-off,.data,7600,3072,16772f917651dc396781991ff50b43245b951542d4c5e982cc86dd3fd80278a5 +wamr-off,.pdata,12672,12800,c917c23d01a09c58d29e5043b2c95a569831cf6b7d7d80b77c04e8e018cac043 +wamr-off,.reloc,1684,2048,68e13d2fdc895bdb450b84e76c259c275f98a6eebb7dc904799503fd074a0eb3 +wamr-on,,,360960,870c62d53917dac014104e8094d469fda9ad3a70e1766d7f7817fde49cbd5bb7 +wamr-on,.text,262624,262656,c0fb6f8bd30cf037e04cfa14d72d1527ab15f7a815c64bf99fdb583cf0ef3ee5 +wamr-on,.rdata,78792,78848,308aa12e740ada15f163b47a90f811432e625480c61c9b39dfe7680d13ce21e9 +wamr-on,.data,7600,3072,ccd230513730fa53c3c1583b0fbab146fb186f2a0148e87cec233c1b0b825652 +wamr-on,.pdata,12912,13312,7b4c0903574f7bbc84d6f0e09771d2503d53448d0db8400916e3dc42e3c08f20 +wamr-on,.reloc,1684,2048,24e8b632b49ddef228813883d97e26e2f1a3930acbc82348c8c1cc93311e3a69 +tiny-base,,,1736704,2f6185702fdb4d6472fa1d65cd0c0fcbe604d72b254084954b9a8ab64e37eb63 +tiny-base,.text,1504880,1505280,314a14af0eb57dc51e70b288326e86350acaba349778e48282200913930fea79 +tiny-base,.rdata,186614,186880,32744daf7e67495cd5d987fdef5a6039f523844139f050db56079423de8f7685 +tiny-base,.data,7504,3072,334e5075100c7dd7afd5a5623b275535f720f6e43d4eb7359858aa2130ccb8bb +tiny-base,.pdata,34140,34304,b901f337d99ec56f755ea6560ecb14ae23098033268c44a7b99f6dc8e74996be +tiny-base,.reloc,5792,6144,04d07efac048ac91576d108b53f72af88537c909e394072972296210044cfa1d +tiny-off,,,1736704,6f6c4f8d126065e5944a85e387d6ca0bdc135f82a51b9fe29dae015e14843ef6 +tiny-off,.text,1504880,1505280,314a14af0eb57dc51e70b288326e86350acaba349778e48282200913930fea79 +tiny-off,.rdata,186614,186880,095e0ad88045c9fadfe034a6f40e5b21f76344da9d41ef15d520d0216ccc7792 +tiny-off,.data,7504,3072,334e5075100c7dd7afd5a5623b275535f720f6e43d4eb7359858aa2130ccb8bb +tiny-off,.pdata,34140,34304,b901f337d99ec56f755ea6560ecb14ae23098033268c44a7b99f6dc8e74996be +tiny-off,.reloc,5792,6144,04d07efac048ac91576d108b53f72af88537c909e394072972296210044cfa1d +tiny-on,,,1737728,e23e066259ad7dd2b08bb277559369cbfbb9d009610c421827a803c2a5470817 +tiny-on,.text,1505920,1506304,ff1321b6294914759569ed861ff8860cac00aab03a63891d47488d04c7bd2263 +tiny-on,.rdata,186850,186880,2fe16dd7cfb7f73ef6e77b39dc2c965d50c6f4b714a49fad7333eccba7a37f17 +tiny-on,.data,7504,3072,ea0c23c0b1568ce17b9430eafce61826b222f73997f239d92f350034fe00e6e9 +tiny-on,.pdata,34224,34304,2a45da03a22729cdf12c73fc1959519c774299e2336c5412101e23c74889962c +tiny-on,.reloc,5788,6144,c64fc5794387fd3f1ac3a574d7fb2b90d302ad12cca039ae9bc9186283d3adc3 From 485de14db5ebe7c32db0b51929ada3611eae78f7 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 12:24:38 -0800 Subject: [PATCH 05/20] test: separate fixture diagnostics from backend LTO warnings --- crates/c-api/tests/continuations/VALIDATION.md | 4 +++- crates/c-api/tests/continuations/run.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/c-api/tests/continuations/VALIDATION.md b/crates/c-api/tests/continuations/VALIDATION.md index eabc11b..84f13de 100644 --- a/crates/c-api/tests/continuations/VALIDATION.md +++ b/crates/c-api/tests/continuations/VALIDATION.md @@ -30,7 +30,9 @@ WAMR used the fast interpreter with software bounds checks and no AOT/JIT, SIMD, WASI libc, or builtin libc. MSVC and stable Rust use switch/match dispatch; these numbers do not compare the fastest dispatch configuration of either runtime. -`run.py` records reproducible build commands for either backend. WAMR's public +`run.py` records reproducible build commands for either backend. It compiles the C fixture +with warnings as errors separately from the LTO link, where existing backend warnings +(such as GCC's WAMR table-type warning) remain visible without becoming client errors. WAMR's public `wasm_c_api.h` was copied as `wasm.h` for the unchanged C client. Define `LIBWASM_STATIC` and `WASM_API_EXTERN=` for static MSVC clients. `measure.py` alternates the executable order for seven rounds and emits all samples. Use diff --git a/crates/c-api/tests/continuations/run.py b/crates/c-api/tests/continuations/run.py index f8c5706..046a545 100644 --- a/crates/c-api/tests/continuations/run.py +++ b/crates/c-api/tests/continuations/run.py @@ -36,9 +36,14 @@ def run(cmd): if a.feature=='on':cmd+=['/DTEST_CONTINUATIONS'] cmd+=['/link','/LTCG','/OPT:REF','/OPT:ICF','/INCREMENTAL:NO','/Brepro','ws2_32.lib','userenv.lib','bcrypt.lib','ntdll.lib','advapi32.lib','kernel32.lib'] else: - cmd=[env.get('CC','cc'),'-std=c11','-O3','-Wall','-Wextra','-Werror','-flto','-fvisibility=hidden','-I'+str(inc),fixture/'fixture.c',lib,'-o',exe] + # Keep fixture warnings strict without applying -Werror to backend code at LTO link. + obj=build/'fixture.o' + cmd=[env.get('CC','cc'),'-std=c11','-O3','-Wall','-Wextra','-Werror','-flto','-fvisibility=hidden','-I'+str(inc),'-c',fixture/'fixture.c','-o',obj] if a.feature=='on':cmd+=['-DTEST_CONTINUATIONS'] if a.sanitize:cmd+=['-fsanitize=address,undefined','-fno-omit-frame-pointer'] + run(cmd) + cmd=[env.get('CC','cc'),'-O3','-flto','-fvisibility=hidden',obj,lib,'-o',exe] + if a.sanitize:cmd+=['-fsanitize=address,undefined','-fno-omit-frame-pointer'] if mac:cmd+=['-Wl,-dead_strip','-framework','Security','-framework','CoreFoundation','-liconv','-lSystem'] else:cmd+=['-Wl,--gc-sections','-lm','-lpthread','-ldl'] run(cmd);run([exe,fixture/'fixture.wasm']) From d71f9d746e622384d529c985fdbb7bafd354c95c Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 16:12:13 -0800 Subject: [PATCH 06/20] test: audit async readiness and expand native sanitizer coverage --- .github/workflows/shared_continuations.yml | 62 +++++- crates/c-api/include/wasm_ext.h | 6 + crates/c-api/src/continuation.rs | 69 +++++++ crates/c-api/tests/continuations/READINESS.md | 128 ++++++++++++ crates/c-api/tests/continuations/README.md | 16 ++ .../c-api/tests/continuations/VALIDATION.md | 5 + .../tests/continuations/audit_receipts.csv | 15 ++ .../tests/continuations/audit_results.md | 53 +++++ .../continuations/conformance_inventory.csv | 46 +++++ crates/c-api/tests/continuations/edges.c | 192 ++++++++++++++++++ crates/c-api/tests/continuations/edges.h | 17 ++ crates/c-api/tests/continuations/edges.wasm | Bin 0 -> 146 bytes crates/c-api/tests/continuations/edges.wat | 12 ++ crates/c-api/tests/continuations/run.py | 182 +++++++++++++---- .../tests/continuations/wasi_p3_inventory.csv | 56 +++++ 15 files changed, 814 insertions(+), 45 deletions(-) create mode 100644 crates/c-api/tests/continuations/READINESS.md create mode 100644 crates/c-api/tests/continuations/audit_receipts.csv create mode 100644 crates/c-api/tests/continuations/audit_results.md create mode 100644 crates/c-api/tests/continuations/conformance_inventory.csv create mode 100644 crates/c-api/tests/continuations/edges.c create mode 100644 crates/c-api/tests/continuations/edges.h create mode 100644 crates/c-api/tests/continuations/edges.wasm create mode 100644 crates/c-api/tests/continuations/edges.wat create mode 100644 crates/c-api/tests/continuations/wasi_p3_inventory.csv diff --git a/.github/workflows/shared_continuations.yml b/.github/workflows/shared_continuations.yml index c88b22a..456e8cb 100644 --- a/.github/workflows/shared_continuations.yml +++ b/.github/workflows/shared_continuations.yml @@ -3,12 +3,68 @@ on: workflow_dispatch: pull_request: paths: ["crates/tinywasm/**", "crates/c-api/**", ".github/workflows/shared_continuations.yml"] +permissions: + contents: read jobs: c-fixture: - runs-on: ubuntu-26.04 + name: C fixture (${{ matrix.os }}, ${{ matrix.rust }}) + strategy: + matrix: + include: + - os: ubuntu-24.04 + rust: stable + extra: "" + core_args: "--features external-host-calls" + capi_args: "--features external-host-calls" + - os: windows-2025 + rust: stable + extra: "" + core_args: "--features external-host-calls" + capi_args: "--features external-host-calls" + - os: windows-2025 + rust: nightly-2026-08-28 + extra: "--tail-calls" + core_args: "--no-default-features --features full,std,nightly-tail-calls,external-host-calls" + capi_args: "--features external-host-calls,tinywasm/nightly-tail-calls" + runs-on: ${{ matrix.os }} + env: + WASM_EXT_TEST_ONLY: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f with: - toolchain: stable - - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on + toolchain: ${{ matrix.rust }} + - if: runner.os != 'Windows' + run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on ${{ matrix.extra }} + - name: Windows x64 release and C-client ASan (Rust uninstrumented) + if: runner.os == 'Windows' + shell: cmd + run: | + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do call "%%i\Common7\Tools\VsDevCmd.bat" -arch=amd64 + python crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on ${{ matrix.extra }} + if errorlevel 1 exit /b 1 + python crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-client-asan --client-asan ${{ matrix.extra }} + - run: cargo test --locked -p tinywasm ${{ matrix.core_args }} --test host_execution + - run: cargo test --locked -p tinywasm-c-api ${{ matrix.capi_args }} --lib + rust-and-c-asan: + name: Linux x64 Rust, std and C ASan + runs-on: ubuntu-24.04 + env: + WASM_EXT_TEST_ONLY: "1" + CC: clang + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f + with: + toolchain: nightly + components: rust-src + - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-full-asan --sanitize + miri: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f + with: + toolchain: nightly + components: miri + - run: cargo miri test --locked -p tinywasm-c-api --features external-host-calls --lib diff --git a/crates/c-api/include/wasm_ext.h b/crates/c-api/include/wasm_ext.h index ae16c94..3b3d4e8 100644 --- a/crates/c-api/include/wasm_ext.h +++ b/crates/c-api/include/wasm_ext.h @@ -10,6 +10,12 @@ * vectors are borrowed only for the call. Results are written to caller-owned * storage, never replaced. Invalid completion leaves pending. cancel/delete are * terminal and notify the pending host exactly once. + * cancel/delete discard the core continuation; they do not run guest cleanup + * and are not Component Model cooperative task cancellation. PENDING is a host + * boundary only, not a canonical WAIT/YIELD event or a fairness guarantee. + * Notification does not establish external I/O quiescence. The adapter must + * fence late completions and retain buffer/store exclusion until external work + * no longer accesses guest memory; it must never use a deleted call pointer. */ #ifndef WASM_EXT_H #define WASM_EXT_H diff --git a/crates/c-api/src/continuation.rs b/crates/c-api/src/continuation.rs index a992599..9eeb388 100644 --- a/crates/c-api/src/continuation.rs +++ b/crates/c-api/src/continuation.rs @@ -282,4 +282,73 @@ mod tests { wasm_engine_delete(engine); } } + #[test] + fn callback_memory_access_survives_pending_and_growth() { + struct Env { + memory: Cell<*mut wasm_memory_t>, + calls: Cell, + } + unsafe extern "C" fn pause( + env: *mut c_void, + args: *const wasm_val_vec_t, + _: *mut wasm_val_vec_t, + _: *mut *mut wasm_trap_t, + ) -> i32 { + unsafe { + let env = &*env.cast::(); + let data = crate::externals::wasm_memory_data(env.memory.get()); + let size = crate::externals::wasm_memory_data_size(env.memory.get()); + assert!(!data.is_null()); + assert!(size >= 65536); + assert_eq!(data.cast::().read_unaligned(), 17); + if (*(*args).data).of.i32 == 1 { + assert!(size >= 131072); + assert_eq!(data.add(65536).cast::().read_unaligned(), 18); + } + env.calls.set(env.calls.get() + 1); + 1 + } + } + unsafe { + let env = Env { memory: Cell::new(ptr::null_mut()), calls: Cell::new(0) }; + let engine = wasm_engine_new(); + let store = wasm_store_new(engine); + let mut p = Vector::from_vec(vec![wasm_valtype_new(0)]); + let mut r = Vector::default(); + let ty = wasm_functype_new(&mut p, &mut r); + let host = wasm_ext_func_new(store, ty, Some(pause), (&env as *const Env).cast_mut().cast(), None); + assert!(!host.is_null()); + wasm_functype_delete(ty); + let bytes = Vector::from_vec(include_bytes!("../tests/continuations/edges.wasm").to_vec()); + let module = crate::module::wasm_module_new(store, &bytes); + assert!(!module.is_null()); + let imports = Vector::from_vec(vec![crate::objects::wasm_func_copy(host)]); + let mut trap = ptr::null_mut(); + let instance = crate::module::wasm_instance_new(store, module, &imports, &mut trap); + assert!(!instance.is_null() && trap.is_null()); + let mut exports = Vector::default(); + crate::module::wasm_instance_exports(instance, &mut exports); + env.memory.set(crate::objects::wasm_extern_as_memory(exports.as_slice()[0])); + let function = crate::objects::wasm_extern_as_func(exports.as_slice()[1]); + let args = Vector::from_vec(vec![wasm_val_t { kind: 0, of: wasm_val_union { i32: 17 } }]); + let completion = Vector::default(); + let mut out = Vector::from_vec(vec![wasm_val_t::default()]); + let call = wasm_ext_call_new(function, &args); + assert!(!call.is_null()); + assert_eq!(wasm_ext_call_resume(call, ptr::null(), &mut out, &mut trap), 1); + assert_eq!(wasm_ext_call_resume(call, &completion, &mut out, &mut trap), 1); + assert_eq!(wasm_ext_call_resume(call, &completion, &mut out, &mut trap), 0); + assert!(trap.is_null()); + assert_eq!(env.calls.get(), 2); + assert_eq!(out.as_slice()[0].of.i32, 35); + wasm_ext_call_delete(call); + drop(exports); + drop(imports); + wasm_func_delete(host); + crate::objects::wasm_instance_delete(instance); + crate::objects::wasm_module_delete(module); + wasm_store_delete(store); + wasm_engine_delete(engine); + } + } } diff --git a/crates/c-api/tests/continuations/READINESS.md b/crates/c-api/tests/continuations/READINESS.md new file mode 100644 index 0000000..472f0ee --- /dev/null +++ b/crates/c-api/tests/continuations/READINESS.md @@ -0,0 +1,128 @@ +# Async/component readiness audit + +Status: core-continuation prototype only. Do not propose the current header as a +WASIp3-capable API or freeze it on the strength of the numeric C fixture. + +## What was and was not run + +The original evidence in VALIDATION.md is core C fixture testing and hosted +measurements. It did not include WASIp3 conformance, streams, Canonical ABI futures, +or Windows sanitizers. Miri and macOS WAMR sanitizers were separate checks. + +This audit pins the Component Model reference suite at +`fed10b0b823b82553dd8363f16a640816f5eac65`. All 45 `test/async/*.wast` files are +inventoried with content hashes in `conformance_inventory.csv`. Every runtime +result is explicitly NOT_RUN: neither draft has a component-WAST driver or an +external Canonical ABI async implementation. Some upstream cases also cover +features beyond a released WASI subset; `upstream_nyi` records the suite's own +exclusions. Inventory and reading the tests are not executing them. No subset of +these component assertions has been passed by either draft. + +The suite's [test instructions](https://github.com/WebAssembly/component-model/blob/fed10b0b823b82553dd8363f16a640816f5eac65/test/README.md) +use Wasmtime as a runner. Running that implementation alone would not qualify +TinyWasm or WAMR. Extracting only a core module and stubbing the canonical imports +would remove exactly the assertions that need verification, so it is not counted +as conformance either. + +The distinct official [WASI testsuite](https://github.com/WebAssembly/wasi-testsuite/tree/de7fbe267210b52f63893270403177239b4f4a9b) +is pinned at `de7fbe267210b52f63893270403177239b4f4a9b`. Its P3 Rust source programs +are separately inventoried by Git blob ID in `wasi_p3_inventory.csv`, all NOT_RUN. +These are WASI interface/component tests, whereas the preceding inventory tests +Component Model async semantics. The official suite's WAMR adapter advertises only `wasm32-wasip1`; a successful default +run through that adapter would not establish P3 coverage. The draft fixture imports `host.wait`/`host.inc`, +not WASI 0.3 interfaces. No working external P3 adapter currently connects either +runtime to the official suite, so there is no defensible passed P3 subset here. + +## Shape that remains useful + +A small runtime-neutral C surface, opaque core execution handles, and external +resource tables, futures, streams and scheduling remain sensible boundaries. +Canonical async values can cross core boundaries as numeric handles; the runtime +need not acquire language-specific futures or a mandatory event loop. Ordinary +native calls must keep their existing path. Compiled-out state/dispatch overhead +continues to be an independent acceptance gate. + +This does not establish that the current handle contract is sufficient. A +stackless callback adapter can use ordinary core calls and own task lifetime +outside each completed core invocation. A stackful adapter needs multiple parked +execution contexts to make progress in the same component/store, with precise +rooting and reentry rules. The current store-wide reservation cannot establish +that capability; cross-store interleaving is a much weaker property. + +## Concrete semantic gaps + +| Requirement | Current primitive / missing proof | Pinned reference examples | +|---|---|---| +| Distinct ready-to-run yield and wait-for-event states | PENDING is one host-call state; C resume loops through fuel suspension, with no budget/yield outcome | `switch-to-ready-callback.wast`, `empty-wait.wast` | +| Same-instance scheduling and exclusive-lock eligibility | A pending core call reserves its entire store; it cannot establish required task progress | `during-sync-scheduling-candidates.wast`, `same-component-stream-future.wast` | +| Task returns a stream but its producer keeps running | Core invocation completion is not component task completion; external task/end ownership is missing | `partial-stream-copies.wast`, `idle-drop.wast` | +| Cooperative cancellation and completion winning a race | Current cancel discards frames and notifies a host; it neither delivers a guest event nor permits guest cleanup | `cancel-delivery.wast`, `cancel-subtask.wast`, `future-completion-order.wast` | +| Partial transfer, backpressure, zero-length operations, closed ends | No stream/future endpoint or waitable-set implementation exists | `partial-stream-copies.wast`, `zero-length.wast`, `closed-stream.wast`, `futures-must-write.wast` | +| Borrow/resource lifetime, trap poisoning, post-return and reentry | Numeric core tests do not check these canonical rules | `drop-cross-task-borrow.wast`, `passing-resources.wast`, `builtin-trap-poisons-instance.wast`, `reentrance.wast` | + +These are requirements from the upstream [concurrency semantics](https://github.com/WebAssembly/component-model/blob/fed10b0b823b82553dd8363f16a640816f5eac65/design/mvp/Concurrency.md) +and test assertions, not additional built-ins proposed for the core. They should +primarily live in an external component adapter, but its runtime seams must permit +the necessary scheduling, memory access, synchronous realloc/post-return, and +safe ownership of parked stacks. Merely naming a destructive operation cancel +must not imply canonical cancellation compatibility. + +## Additional core regression tests + +`edges.c` covers empty completions versus NULL re-polls, zero-result roots, +repeated polls without callback replay, invalid completion while pending, +destructive cancellation before start and at each host boundary, completion after +cancellation on a still-live handle, memory growth with callback pointers resolved +again, distinct-store interleaving, a trap after resume and subsequent store reuse. +It repeats the cancellation boundary matrix 32 times. Its same-store exclusion +assertion records an architectural limitation, not a passing component concurrency +test. No C test calls a deleted invocation pointer; a late wakeup requires adapter +ownership/generation checks before touching such a handle. + +These tests cover additional primitive cases. They do not claim all cooperative +scheduling interleavings, stale asynchronous completion safety in a real scheduler, +all numeric layouts, component memory/table imports, or allocation-failure paths. +Destructive cancellation also does not certify native I/O quiescence: an adapter +using guest buffers must keep them stable and fence late completion before it +permits memory growth, releases memory, or frees callback state. + +## Sanitizer coverage and Windows + +`--sanitize` instruments WAMR and its C clients on Windows/Unix. For TinyWasm on +supported Unix targets it uses nightly Rust, `-Zsanitizer=address`, +`-Zexternal-clangrt`, and `-Zbuild-std`, then links C clients with the same external +ASan runtime. This includes Rust dependencies and rebuilt std, rather than merely +sanitizing a C harness. Sanitizer builds are separate from LTO size measurements. + +Native Rust ASan does not currently list Windows MSVC as a supported target. +`--sanitize --runtime tinywasm` therefore rejects that configuration. Windows +`--client-asan` explicitly instruments only C clients with an uninstrumented Rust +library; passing it is not a whole-runtime sanitizer result. MSVC runtime +interception does not close that instrumentation gap. See the official +[Rust sanitizer target list](https://doc.rust-lang.org/unstable-book/compiler-flags/sanitizer.html#addresssanitizer) +and [MSVC ASan documentation](https://learn.microsoft.com/en-us/cpp/sanitizers/asan?view=msvc-170). + +Every sanitizer mode first runs a separate intentional heap-overflow canary and +requires ASan to detect it. The canary's expected failure is distinct from fixture +results. Windows CI explicitly runs release/C ABI tests, C-client ASan, optional +Rust host-execution tests, and Rust C API unit tests. Linux CI additionally runs +Rust+std+C ASan and Miri. WAMR CI runs C runtime/client ASan on Windows and Linux. +Current receipts, including the physical machine's exact source provenance, are +in `audit_results.md`; a configured CI job is not a passing receipt. + +## Gates before an upstream API proposal + +1. Choose the supported stackless/stackful scope and model task/stream lifetime + separately from core invocation lifetime. Define cancellation request/delivery + separately from destructive execution teardown. +2. Implement an out-of-tree adapter with futures, streams, waitable sets and + eligible-task scheduling. Prove same-instance progress, buffer/resource + ownership, memory growth, reentry and late-completion teardown ordering. +3. Run named, pinned upstream assertions through that adapter on both runtimes. + Report pass/fail/unsupported and the exact supported subset; no silent skips. +4. Run the native Windows matrix plus full supported-host instrumentation, fuzz + legal and adversarial operation sequences, and repeat disabled-feature artifact + checks whenever production layouts, allocation or dispatch change. + +Until those gates are met, these fork drafts are suitable for prototype review, +not evidence that a common WASIp3 runtime ABI is ready. diff --git a/crates/c-api/tests/continuations/README.md b/crates/c-api/tests/continuations/README.md index 1439173..75c9b5c 100644 --- a/crates/c-api/tests/continuations/README.md +++ b/crates/c-api/tests/continuations/README.md @@ -1,6 +1,10 @@ # Shared continuation C fixture The same `fixture.c`, Wasm inputs, and `wasm_ext.h` declarations are used for TinyWasm and WAMR. +Read [READINESS.md](READINESS.md) before proposing or freezing this API: no WASIp3 +conformance subset has been executed, and the current cancellation/store contract is insufficient +to establish full component concurrency. + The extension is experimental, numeric-only, thread-confined, and limited to one result. It is not an implementation of the component model or the WASI 0.3 scheduler. Only explicit pending host calls suspend; cancellation does not preempt executing guest code or a blocking callback. @@ -57,3 +61,15 @@ input for an external linker; they do not execute components or supply a schedul therefore uses ordinary core modules and does not cherry-pick that branch's parser/type changes. The experimental parser can evolve independently of execution handles and the default core build. Imported-memory/table support and complete component/async ABI conformance remain separate gates. + +## Instrumented validation + +`--sanitize` enables runtime/client ASan (nightly Rust with rust-src for TinyWasm +on supported Unix targets). `--client-asan` on Windows TinyWasm is explicitly +C-client-only coverage; native Rust remains uninstrumented. Both modes require a +working ASan canary and run the shared fixtures plus TinyWasm's standard C API tests. +`--ubsan` additionally enables Unix WAMR UBSan; baseline alignment warnings remain +a separate limitation. Use a fresh build directory for each mode. + +`edges.wasm` and `edges.h` contain the same module generated from `edges.wat`. +The additional edge fixture does not change the measured `fixture.c` benchmark. diff --git a/crates/c-api/tests/continuations/VALIDATION.md b/crates/c-api/tests/continuations/VALIDATION.md index 84f13de..fe3f3aa 100644 --- a/crates/c-api/tests/continuations/VALIDATION.md +++ b/crates/c-api/tests/continuations/VALIDATION.md @@ -1,5 +1,10 @@ # Shared C continuation validation +For the later async-semantics audit, explicit conformance gaps, Windows CI and +instrumentation coverage, read [READINESS.md](READINESS.md) and +[audit_results.md](audit_results.md). The measurements below are the original +primitive validation, not a WASIp3 conformance result. + Measurements taken 2026-11-02. These qualify a numeric, thread-confined core-module prototype, not a complete WASI 0.3/component runtime or a Pico deployment. The TinyWasm changes are independent of PR #81. diff --git a/crates/c-api/tests/continuations/audit_receipts.csv b/crates/c-api/tests/continuations/audit_receipts.csv new file mode 100644 index 0000000..0709884 --- /dev/null +++ b/crates/c-api/tests/continuations/audit_receipts.csv @@ -0,0 +1,15 @@ +platform,runtime,instrumentation,case,outcome,raw_log_sha256 +macos-arm64,tiny,runtime+client ASan; Rust std rebuilt for tiny,main+edges,PASS,ffd1e7743c9a32ede3542c2d0fde486067c73438995dab9e356315f594c9674a +macos-arm64,tiny,ASan instrumentation canary,expected heap overflow detected,PASS,961419e00d60d2057981d048d4b5bf5ac65fb9000631d5ee473077219dfca4ea +macos-arm64,wamr,runtime+client ASan; Rust std rebuilt for tiny,main+edges,PASS,ecc0dc8af550e98079fdbbd9586dd52a226364250b8c2f261a0b7a253fbcd443 +macos-arm64,wamr,ASan instrumentation canary,expected heap overflow detected,PASS,7c242f72d3914fc02956519392de42efa6e66b503ac5de9143b2b58ef44950cd +macos-arm64,tiny,Rust+std+C ASan,standard C API,PASS,ffd1e7743c9a32ede3542c2d0fde486067c73438995dab9e356315f594c9674a +macos-arm64,tiny,Miri,six C API tests including callback memory growth,PASS,33db1d73ce65d657d3c1f4e406e9ba0e351847fb84660e2f98637878b207a754 +macos-arm64,tiny,nightly tail ABI enabled,main+edges+standard C API,PASS,0debc7a9d9e6e6152964896c6875dd667c421d98d3a1277487d66ecd854096c1 +windows-x64-physical,tiny,C-client-only ASan; Rust UNINSTRUMENTED,main+edges+standard C API,PASS,9b7da3c6a0b78cddba9534cb0c2b704aeb680606b87fc93afc461c928dcd2697 +windows-x64-physical,wamr,C/C++ runtime+client ASan,main+edges,PASS,1a77dca9883a5784af01815c7233aa68c3eb920d87b58187cab93c2b88d92dbf +windows-x64-physical,tiny,ASan instrumentation canary,expected heap overflow detected,PASS,bd52c69f7d64ac554fd1e069f64fed79bc82936182474f0fe5217cd025f1058f +windows-x64-physical,wamr,ASan instrumentation canary,expected heap overflow detected,PASS,5a57f7d4391157bb212a6672efa8f14bb6cc6b3561a672082cedf3dc2ce920d1 +windows-x64-physical,tiny,native stable Rust; unsanitized,seven host-execution tests,PASS,7e2dc3bd82ef5f17063b57c0e9f48557a47cf24c186b85817af3bfae829d420c +windows-x64-physical,tiny,native stable Rust; unsanitized,six C API unit tests,PASS,caf7114496154b17d4e9fa42b6691944c0034d093d41861a769d1c4f94f0e904 +macos-arm64,wamr,CTest,both shared fixtures,PASS,158857594e8003b0645312271225910b7e2de5e9b2b35f557e51443356f9e409 diff --git a/crates/c-api/tests/continuations/audit_results.md b/crates/c-api/tests/continuations/audit_results.md new file mode 100644 index 0000000..7094c02 --- /dev/null +++ b/crates/c-api/tests/continuations/audit_results.md @@ -0,0 +1,53 @@ +# Additional audit receipts (2026-11-02) + +These results strengthen the core primitive. They do not qualify WASIp3 or all +cooperative scheduling corner cases. No WASI P3 or Component Model conformance +assertions have been executed against either draft; see READINESS.md and the two +explicit NOT_RUN inventories. + +| Platform | Runtime / check | Actual coverage | Result | +|---|---|---|---| +| macOS arm64 | TinyWasm ASan | Rust runtime, dependencies, rebuilt std and all three C clients | Pass | +| macOS arm64 | TinyWasm Miri | All six Rust C API tests, including callback memory reads around suspension/growth | Pass | +| macOS arm64 | TinyWasm nightly tail ABI | Verified tail cfg, main fixture, edges, standard C API | Pass | +| macOS arm64 | WAMR ASan | C runtime and main/edge C clients | Pass | +| macOS arm64 | WAMR CTest | Main and edge suites | 2/2 pass | +| Physical Windows x64 | TinyWasm ASan | C clients only; Rust runtime remains uninstrumented | Pass, partial coverage | +| Physical Windows x64 | TinyWasm native stable Rust | Host execution and C API unit tests | 7/7 and 6/6 pass | +| Physical Windows x64 | WAMR ASan | C/C++ runtime and main/edge C clients | Pass | + +Each ASan mode first detected a deliberate heap-buffer-overflow in its isolated +canary executable. The expected canary failure is not a fixture finding. The +runtime tests then passed without address-error reports. WAMR's Windows C and C++ +release flags were `/O1 /Zi /fsanitize=address /DNDEBUG`; sanitizer and LTO +measurement builds use distinct directories. These ASan-only passes do not erase +the previously documented baseline UBSan alignment findings. + +The Windows audit used fresh checkouts of published fork revisions: + +- TinyWasm: `485de14db5ebe7c32db0b51929ada3611eae78f7` +- WAMR: `4968dde873cd2ac9ef3713041c8b4acc790ed0f2` + +Only the explicit new fixture/runner/header test overlay was then copied in; the +TinyWasm C API source additionally received the new cfg(test) memory regression. +Production runtime behavior is unchanged by this audit. The measured main C source +remains byte-for-byte unchanged. Windows checked its inputs against these hashes: + +| Input | SHA-256 | +|---|---| +| fixture.c | `d38be8465b16cfbdf09e752637e5750738b461a8899795ad7f4c2667a6006cdd` | +| edges.c | `126bca9f3b5977c5a2b24ae6eb033f388524d273df5e71a150871ad754be557c` | +| run.py | `bb8aed39f39332eac04652e40c1b4693c09fe38a4317f7ae0516c82c7e360549` | +| continuation.rs, including new test | `b50af175229130b8c189d3c10f65a4ca75cfc1a98928c1ace517f49b70a427f7` | + +Earlier copied-source scratch runs are not used as these audit receipts. Native +ASan does not instrument Windows Rust: the client-only row must not be promoted +to an end-to-end Rust sanitizer claim. Supported-host Rust+std ASan and Miri +provide separate evidence, not a substitute Windows pass. + +`audit_receipts.csv` records the cases, coverage and hashes of preserved raw logs. +The raw receipts remain in the local/physical-device task directories. CI now +explicitly exercises Windows stable and nightly sysv64 tail calling conventions, +Windows client-only ASan, Linux Rust+std+C ASan, Miri, and WAMR Windows/Linux +runtime ASan. Job definitions are not pass receipts; inspect the draft checks for +hosted outcomes. The nightly job verifies the requested tail ABI actually enabled. diff --git a/crates/c-api/tests/continuations/conformance_inventory.csv b/crates/c-api/tests/continuations/conformance_inventory.csv new file mode 100644 index 0000000..c068988 --- /dev/null +++ b/crates/c-api/tests/continuations/conformance_inventory.csv @@ -0,0 +1,46 @@ +upstream_revision,path,sha256,upstream_nyi,tinywasm_status,wamr_status,reason +fed10b0b823b82553dd8363f16a640816f5eac65,async/async-calls-sync.wast,bacf1fab5f97e7dfcceca89c19a6b84bf4d89ebaedb69ccb448928f9af0fc879,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/big-interleaving-test.wast,94d776825d50d7b74505653caf4e6428a0ea4650813bcb42ab171f846c9fbb81,yes,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/builtin-trap-poisons-instance.wast,d513bcff29bb9ca0356f31806cfaed2b15ad3ac1593c1fa1eb913fd1010c0c77,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cancel-and-exclusive-lock.wast,a6bef2fe693216c65239ba9bfde3eb2636280282cefef58808a8c58d98be9f27,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cancel-delivery.wast,e4ae0061ad2ce2bfe95e05fa31d8bac3d367725701a9df061f423c4d418194cc,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cancel-not-delivered.wast,746524e3306b0f218f9816949d0449d20a160f76f04856bf708f447a6eb56774,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cancel-resumed-callback-switch.wast,35112c3305f0fc553648567d406d95da2e0c6affc7cef52bfc83f496632b984d,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cancel-stream.wast,bd3bb6babaebf6f03e41e6eb7cb30c5dafa2adc276dc1356e2f2c5d4743fbebf,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cancel-subtask.wast,b72065c29f656e56c0f12e589ec1300005a9ae48061a945cdaca0bf21f1c4497,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cancel-targeted-resume.wast,8cfa1d2e0782b06c0072a789a4dd29f81090e251198df28292e70b8b577b944e,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/closed-stream.wast,5fddd95e1f7920f6a453e4f72bd4abc9d8a1615f8937f33ae06e0df31ca4d2a0,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cross-abi-calls.wast,32a9b35490c7f3ca8191ddd9d812810d9d0b70d67b50b97e10f75a72bfd8b85e,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/cross-task-future.wast,d962876ba33012ce0ff068a0d056b2d8b27f5a86749fc410ca1051bd26a50a14,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/deadlock.wast,9bdd951733f5219bba0b3e226a15d85429f054d67faa6d784c42b50588213e19,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/dont-block-start.wast,69fd1debc9898c4bdf71f9a2bbc3f9994d1f3ef143ebbf4df6eacbdc47affc9d,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/drop-cross-task-borrow.wast,1e8f60537f6f0a6f78a693de01b8175181c1e15fbd8a78be27a3764576d1e1e3,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/drop-stream.wast,ff226f805c84cd6a3e445905a312a7abd94e7439359c1e912ce055d774a386ad,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/drop-subtask.wast,2b41e426796f0b49b25ce8ce8ae5bb1ed1b1d9279e59d2d0bdc1dadaf0f67b97,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/drop-waitable-set.wast,8c4deb26d062ab421c99d8c178e59916473bc0e3016efea6b68779b0338ee59f,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/during-sync-call-exclusive-resume.wast,2276977b966fe04706758ef9d95b9194bc11dde8c96d2f74c59ba93ec1f37aa0,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/during-sync-call-may-block-if-other-ready-threads.wast,d949b721f1890b1f6e67f14ad1edb548e8357d3b336b793d1135e95ad91f982a,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/during-sync-call-no-sibling-resume.wast,0c64d9224cb25a94cda2840c650f2317d633a93ceb5e60658a8e2fbec19d3dcb,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/during-sync-scheduling-candidates.wast,282a97133650c6e1be560f40be6e721bd93ae90b477e78f608b2828b2a776810,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/empty-wait.wast,cadb124978a329d9fa2912d109901ad862357d7969f072325069511646b892c9,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/forward.wast,c9c8557a21e64fea87680f1a72b5cc1220cff6de6d0c3037dafaf8c05e7bfbfc,yes,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/future-completion-order.wast,a4312e5a4e2fb354f9b213c6623a30f4913c4008779f36e021e3000f8b4972c0,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/idle-drop.wast,96ba6a39c7556e4678013f3b5066363decb5cbf9abbcea4cabfcce30b6b68c11,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/partial-stream-copies.wast,74bdc4f5f643c3e7c8449d676133f19fa396003c53d52dac88986bc1938a395f,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/passing-resources.wast,5fcee86ff0e2581af82095e56ed6661a651f80f6c6060a428f00ec7911bddf23,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/reentrance.wast,4683f1edbbbf5f095a6ec2dd529a876d1a62113a7d8f68369f41334cca5dd173,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/self-switch-traps.wast,eb4a4a38963ee151f085abad80459fbdc86bd8ec3cdcfc23fe95882be4aede84,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/switch-to-ready-callback.wast,5eef918faedae04b9ffdd8698f649bb41f8fb58637fe7e1423b354616718509a,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/sync-barges-in.wast,11c3e1046c8b01259fd2e6b1057677d9add26701857fdbb5e4d304f57eb94509,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/sync-streams.wast,bcf37448f4495e986adbd17915060ed6b7cce7e5c0fa11b8c3a4cce2b08919e1,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/trap-if-block-and-sync.wast,0f53f570bbb8e8d8c6d47cb0ad71ce702d691cae945c56b4c4fcb0c779945c4d,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/trap-if-done.wast,fdafc2fc7f90698f6a4cc5e5fca67a247963f964787879d9deb645d95e878a82,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/trap-if-sync-and-waitable-set.wast,c86d28eb001b4aecabab9a054e0d3b9a275d3c08ef982d7c85d389dde9cbd399,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/trap-if-transfer-in-waitable-set.wast,71cb99c7f7b6dd6533bc3fc744392f495c78814eae27e7d72ee9431c8e299460,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/validate-no-async-abi-for-sync-type.wast,ac454e9b7685b2b22b29be1f3b8b5d4c5d38e3378ebe1b49a88d0bdad0ebb55b,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/validate-no-stream-char.wast,5767951700e4e823da091bc82bdaa3cf99ded92b279b0ddd0352dddf4c545bec,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/validate-task-return.wast,9f998564a78c1bf67249ffa7e7b876629d987238bf4cb82fccdeb1954ce6857a,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/wait-during-callback.wast,8231ba4aac41de37aed764c6fff78b610b05d1fbfcf7c0da3ccc01cc8d926079,no,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them +fed10b0b823b82553dd8363f16a640816f5eac65,async/zero-length.wast,ddc4afd566c70bd255d29cdbda9372e9db4af3c582cdf1078d249e30cc2c9595,yes,NOT_RUN,NOT_RUN,Requires component WAST driver and Canonical ABI async adapter; neither draft implements them diff --git a/crates/c-api/tests/continuations/edges.c b/crates/c-api/tests/continuations/edges.c new file mode 100644 index 0000000..ec64547 --- /dev/null +++ b/crates/c-api/tests/continuations/edges.c @@ -0,0 +1,192 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#include "wasm.h" +#include "wasm_ext.h" +#include "edges.h" +#include +#include +#include +#define CHECK(x) \ + do { \ + if (!(x)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \ + exit(1); \ + } \ + } while (0) +typedef struct Guest { + wasm_store_t *store; + wasm_module_t *module; + wasm_instance_t *instance; + wasm_func_t *host, *run, *empty, *bad; + wasm_memory_t *memory; + unsigned calls, cancels; + int32_t seed; +} Guest; +static wasm_ext_status_t +pause_host(void *env, const wasm_val_vec_t *a, wasm_val_vec_t *r, + wasm_trap_t **trap) +{ + Guest *g = env; + (void)trap; + CHECK(a->size == 1 && a->data[0].kind == WASM_I32 && r->size == 0); + int32_t stage = a->data[0].of.i32; + if (stage == 0 || stage == 1) { + /* Resolve the pointer on each callback: memory.grow may relocate it. */ + const unsigned char *data = + (const unsigned char *)wasm_memory_data(g->memory); + size_t size = wasm_memory_data_size(g->memory); + int32_t first, last; + CHECK(data && size >= 65536); + memcpy(&first, data, 4); + CHECK(first == g->seed); + if (stage == 1) { + CHECK(size >= 131072); + memcpy(&last, data + 65536, 4); + CHECK(last == g->seed + 1); + } + } + else + CHECK(stage == 8 || stage == 9); + g->calls++; + return WASM_EXT_PENDING; +} +static void +cancelled(void *env) +{ + ((Guest *)env)->cancels++; +} +static void +guest_new(Guest *g, wasm_engine_t *engine, int32_t seed) +{ + memset(g, 0, sizeof(*g)); + g->seed = seed; + g->store = wasm_store_new(engine); + CHECK(g->store); + wasm_byte_vec_t bytes; + wasm_byte_vec_new(&bytes, sizeof(edges_wasm), (const char *)edges_wasm); + g->module = wasm_module_new(g->store, &bytes); + wasm_byte_vec_delete(&bytes); + CHECK(g->module); + wasm_functype_t *ty = wasm_functype_new_1_0(wasm_valtype_new_i32()); + g->host = wasm_ext_func_new(g->store, ty, pause_host, g, cancelled); + wasm_functype_delete(ty); + CHECK(g->host); + wasm_extern_t *items[] = { wasm_func_as_extern(g->host) }; + wasm_extern_vec_t imports = WASM_ARRAY_VEC(items); + wasm_trap_t *trap = NULL; + g->instance = wasm_instance_new(g->store, g->module, &imports, &trap); + CHECK(g->instance && !trap); + wasm_extern_vec_t exports; + wasm_instance_exports(g->instance, &exports); + CHECK(exports.size == 4); + g->memory = wasm_memory_copy(wasm_extern_as_memory(exports.data[0])); + g->run = wasm_func_copy(wasm_extern_as_func(exports.data[1])); + g->empty = wasm_func_copy(wasm_extern_as_func(exports.data[2])); + g->bad = wasm_func_copy(wasm_extern_as_func(exports.data[3])); + wasm_extern_vec_delete(&exports); + CHECK(g->memory && g->run && g->empty && g->bad); +} +static void +guest_delete(Guest *g) +{ + wasm_func_delete(g->run); + wasm_func_delete(g->empty); + wasm_func_delete(g->bad); + wasm_memory_delete(g->memory); + wasm_instance_delete(g->instance); + wasm_module_delete(g->module); + wasm_func_delete(g->host); + wasm_store_delete(g->store); +} +static wasm_ext_status_t +resume(wasm_ext_call_t *call, const wasm_val_vec_t *completion, + wasm_val_vec_t *out) +{ + wasm_trap_t *trap = NULL; + wasm_ext_status_t status = + wasm_ext_call_resume(call, completion, out, &trap); + CHECK(!trap); + return status; +} +int +main(void) +{ + wasm_engine_t *engine = wasm_engine_new(); + CHECK(engine); + wasm_val_vec_t empty = WASM_EMPTY_VEC; + wasm_val_t argument[] = { WASM_I32_VAL(17) }, + result[] = { WASM_I32_VAL(-1) }; + wasm_val_vec_t args = WASM_ARRAY_VEC(argument), + out = WASM_ARRAY_VEC(result); + /* Destructive teardown at every boundary, including late completion. */ + for (unsigned repeat = 0; repeat < 32; repeat++) + for (unsigned boundary = 0; boundary < 3; boundary++) { + Guest g; + guest_new(&g, engine, 17); + wasm_ext_call_t *call = wasm_ext_call_new(g.run, &args); + CHECK(call); + if (boundary) { + CHECK(resume(call, NULL, &out) == WASM_EXT_PENDING); + for (unsigned i = 0; i < 100; i++) + CHECK(resume(call, NULL, &out) == WASM_EXT_PENDING); + CHECK(g.calls == 1); + CHECK(resume(call, &args, &out) + == WASM_EXT_ERROR); /* void completion */ + CHECK(g.calls == 1); + CHECK(!wasm_ext_call_new( + g.run, &args)); /* Known store-wide exclusion. */ + if (boundary == 2) + CHECK(resume(call, &empty, &out) == WASM_EXT_PENDING); + } + wasm_ext_call_cancel(call); + wasm_ext_call_cancel(call); + CHECK(g.cancels == (boundary ? 1u : 0u)); + CHECK(resume(call, &empty, &out) == WASM_EXT_CANCELLED); + wasm_ext_call_delete(call); + CHECK(result[0].of.i32 == -1); + guest_delete(&g); + } + /* Distinct stores can make progress while another core call is suspended. + */ + Guest a, b; + guest_new(&a, engine, 17); + guest_new(&b, engine, 99); + wasm_ext_call_t *ca = wasm_ext_call_new(a.run, &args); + CHECK(ca); + CHECK(resume(ca, NULL, &out) == WASM_EXT_PENDING); + argument[0].of.i32 = 99; + wasm_ext_call_t *cb = wasm_ext_call_new(b.run, &args); + CHECK(cb); + CHECK(resume(cb, NULL, &out) == WASM_EXT_PENDING); + CHECK(resume(ca, &empty, &out) == WASM_EXT_PENDING); + CHECK(resume(cb, &empty, &out) == WASM_EXT_PENDING); + CHECK(resume(ca, &empty, &out) == WASM_EXT_DONE && result[0].of.i32 == 35); + CHECK(resume(cb, &empty, &out) == WASM_EXT_DONE && result[0].of.i32 == 199); + wasm_ext_call_cancel(ca); + wasm_ext_call_cancel(cb); + CHECK(!a.cancels && !b.cancels); + CHECK(resume(ca, &empty, &out) == WASM_EXT_ERROR); + wasm_ext_call_delete(ca); + wasm_ext_call_delete(cb); + /* Zero-result root and trap after resumption, followed by store reuse. */ + ca = wasm_ext_call_new(a.bad, &empty); + CHECK(ca); + CHECK(resume(ca, NULL, &empty) == WASM_EXT_PENDING); + wasm_trap_t *trap = NULL; + CHECK(wasm_ext_call_resume(ca, &empty, &empty, &trap) == WASM_EXT_ERROR + && trap); + wasm_trap_delete(trap); + wasm_ext_call_delete(ca); + ca = wasm_ext_call_new(a.empty, &empty); + CHECK(ca); + CHECK(resume(ca, NULL, &empty) == WASM_EXT_PENDING); + CHECK(resume(ca, &empty, &empty) == WASM_EXT_DONE); + wasm_ext_call_delete(ca); + CHECK(!a.cancels); + guest_delete(&a); + guest_delete(&b); + wasm_engine_delete(engine); + puts("void yields, cancellation boundaries, late completion, memory " + "growth, cross-store interleaving, post-resume trap: PASS"); + return 0; +} diff --git a/crates/c-api/tests/continuations/edges.h b/crates/c-api/tests/continuations/edges.h new file mode 100644 index 0000000..808b8f2 --- /dev/null +++ b/crates/c-api/tests/continuations/edges.h @@ -0,0 +1,17 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +static const unsigned char edges_wasm[] = { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0d, 0x03, 0x60, + 0x01, 0x7f, 0x00, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x60, 0x00, 0x00, 0x02, + 0x0b, 0x01, 0x01, 0x68, 0x05, 0x70, 0x61, 0x75, 0x73, 0x65, 0x00, 0x00, + 0x03, 0x04, 0x03, 0x01, 0x02, 0x02, 0x05, 0x04, 0x01, 0x01, 0x01, 0x03, + 0x07, 0x1e, 0x04, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, + 0x03, 0x72, 0x75, 0x6e, 0x00, 0x01, 0x04, 0x76, 0x6f, 0x69, 0x64, 0x00, + 0x02, 0x04, 0x74, 0x72, 0x61, 0x70, 0x00, 0x03, 0x0a, 0x40, 0x03, 0x2f, + 0x00, 0x41, 0x00, 0x20, 0x00, 0x36, 0x02, 0x00, 0x41, 0x00, 0x10, 0x00, + 0x41, 0x01, 0x40, 0x00, 0x1a, 0x41, 0x80, 0x80, 0x04, 0x20, 0x00, 0x41, + 0x01, 0x6a, 0x36, 0x02, 0x00, 0x41, 0x01, 0x10, 0x00, 0x41, 0x00, 0x28, + 0x02, 0x00, 0x41, 0x80, 0x80, 0x04, 0x28, 0x02, 0x00, 0x6a, 0x0b, 0x06, + 0x00, 0x41, 0x09, 0x10, 0x00, 0x0b, 0x07, 0x00, 0x41, 0x08, 0x10, 0x00, + 0x00, 0x0b +}; diff --git a/crates/c-api/tests/continuations/edges.wasm b/crates/c-api/tests/continuations/edges.wasm new file mode 100644 index 0000000000000000000000000000000000000000..afa477c2d31beeb3a9281e9b09df5b6e1b1056fe GIT binary patch literal 146 zcmWlNu?~Vj5Cz}6T|m8Zw`mP0p;`RgEgd4c*FpLaNY=QKeMja(5?\nint main(int argc,char **argv) { (void)argv; volatile char *p=malloc(8); p[argc+8]=1; free((void *)p); return 0; }\n') + probe = build/('asan_canary.exe' if win else 'asan_canary') + client(canary, probe, build) + result = subprocess.run([str(probe)], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + (build/'asan_canary.log').write_text(result.stdout) + if result.returncode == 0 or 'AddressSanitizer' not in result.stdout or 'heap-buffer-overflow' not in result.stdout: + raise RuntimeError('ASan canary did not detect the expected fault; see asan_canary.log') + print('ASan canary detected the expected heap-buffer-overflow', flush=True) + +if a.runtime == 'tinywasm': + env.update(CARGO_PROFILE_RELEASE_LTO='off' if san else 'fat', CARGO_PROFILE_RELEASE_CODEGEN_UNITS='1', CARGO_PROFILE_RELEASE_PANIC='abort') + target = None + cmd = ['cargo', 'rustc' if a.sanitize else 'build', '--locked', '--release', '--manifest-path', root/'Cargo.toml', '--target-dir', build/'runtime', '-p', 'tinywasm-c-api'] + features = ['external-host-calls'] if a.feature == 'on' else [] + if a.tail_calls: + features += ['tinywasm/nightly-tail-calls'] + if features: + cmd += ['--features', ','.join(features)] + if a.sanitize: + info = subprocess.check_output(['rustc', '-vV'], env=env, text=True) + target = next(line.split(': ', 1)[1] for line in info.splitlines() if line.startswith('host: ')) + env['RUSTFLAGS'] = '-Zsanitizer=address -Zexternal-clangrt -Cforce-frame-pointers=yes' + env['CARGO_PROFILE_RELEASE_DEBUG'] = '1' + cmd += ['-Zbuild-std', '--target', target, '--crate-type', 'staticlib'] + print('Rust instrumentation: '+env['RUSTFLAGS']+'; target='+target+'; -Zbuild-std', flush=True) + run(cmd) + if a.tail_calls: + outputs = [path for name in ('output', 'stdout') for path in (build/'runtime').rglob(name) + if any(part == 'tinywasm' or part.startswith('tinywasm-') for part in path.parts)] + flags = '\n'.join(path.read_text() for path in outputs) + expected = 'cargo::rustc-cfg=tw_tail_abi="sysv64-unwind"' if win else 'cargo::rustc-cfg=tw_tail_calls' + if expected not in flags.splitlines(): + raise RuntimeError('requested nightly tail ABI was not enabled: '+expected) + print('Verified '+expected, flush=True) + inc = root/'crates/c-api/include' + lib = build/'runtime' + if target: + lib /= target + lib /= 'release/tinywasm.lib' if win else 'release/libtinywasm.a' else: - target='windows' if win else 'darwin' if mac else 'linux' - cmd=['cmake','-S',root/'product-mini/platforms'/target,'-B',build/'runtime','-DCMAKE_BUILD_TYPE=Release','-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON','-DWAMR_BUILD_INTERP=1','-DWAMR_BUILD_FAST_INTERP=1','-DWAMR_BUILD_AOT=0','-DWAMR_BUILD_JIT=0','-DWAMR_BUILD_FAST_JIT=0','-DWAMR_BUILD_LIBC_WASI=0','-DWAMR_BUILD_LIBC_BUILTIN=0','-DWAMR_BUILD_SIMD=0','-DWAMR_DISABLE_HW_BOUND_CHECK=1','-DWAMR_BUILD_C_API_CONTINUATIONS='+('1' if a.feature=='on' else '0')] - if a.sanitize:cmd+=['-DCMAKE_C_FLAGS=-fsanitize=address,undefined -fno-omit-frame-pointer','-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address,undefined'] - run(cmd);run(['cmake','--build',build/'runtime','--config','Release','-j','4']) - inc=build/'include';inc.mkdir(exist_ok=True);shutil.copy2(root/'core/iwasm/include/wasm_c_api.h',inc/'wasm.h'); - if a.feature=='on':shutil.copy2(root/'core/iwasm/include/wasm_ext.h',inc/'wasm_ext.h') - candidates=list((build/'runtime').rglob('libiwasm.lib' if win else 'libiwasm.a'));assert len(candidates)==1,candidates - lib=candidates[0] -exe=build/('fixture.exe' if win else 'fixture') -if win: - cmd=['cl','/nologo','/std:c17','/O2','/GL','/Gy','/Gw','/DLIBWASM_STATIC','/DWASM_API_EXTERN=','/I'+str(inc),fixture/'fixture.c',lib,'/Fe:'+str(exe)] - if a.feature=='on':cmd+=['/DTEST_CONTINUATIONS'] - cmd+=['/link','/LTCG','/OPT:REF','/OPT:ICF','/INCREMENTAL:NO','/Brepro','ws2_32.lib','userenv.lib','bcrypt.lib','ntdll.lib','advapi32.lib','kernel32.lib'] -else: - # Keep fixture warnings strict without applying -Werror to backend code at LTO link. - obj=build/'fixture.o' - cmd=[env.get('CC','cc'),'-std=c11','-O3','-Wall','-Wextra','-Werror','-flto','-fvisibility=hidden','-I'+str(inc),'-c',fixture/'fixture.c','-o',obj] - if a.feature=='on':cmd+=['-DTEST_CONTINUATIONS'] - if a.sanitize:cmd+=['-fsanitize=address,undefined','-fno-omit-frame-pointer'] - run(cmd) - cmd=[env.get('CC','cc'),'-O3','-flto','-fvisibility=hidden',obj,lib,'-o',exe] - if a.sanitize:cmd+=['-fsanitize=address,undefined','-fno-omit-frame-pointer'] - if mac:cmd+=['-Wl,-dead_strip','-framework','Security','-framework','CoreFoundation','-liconv','-lSystem'] - else:cmd+=['-Wl,--gc-sections','-lm','-lpthread','-ldl'] -run(cmd);run([exe,fixture/'fixture.wasm']) + target = 'windows' if win else 'darwin' if mac else 'linux' + cmd = ['cmake', '-S', root/'product-mini/platforms'/target, '-B', build/'runtime', '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_INTERPROCEDURAL_OPTIMIZATION='+('OFF' if san else 'ON'), '-DWAMR_BUILD_INTERP=1', '-DWAMR_BUILD_FAST_INTERP=1', '-DWAMR_BUILD_AOT=0', '-DWAMR_BUILD_JIT=0', '-DWAMR_BUILD_FAST_JIT=0', '-DWAMR_BUILD_LIBC_WASI=0', '-DWAMR_BUILD_LIBC_BUILTIN=0', '-DWAMR_BUILD_SIMD=0', '-DWAMR_DISABLE_HW_BOUND_CHECK=1', '-DWAMR_BUILD_C_API_CONTINUATIONS='+('1' if a.feature=='on' else '0')] + if san: + flags = '/O1 /Zi /fsanitize=address /DNDEBUG' if win else '-O1 -g -DNDEBUG '+' '.join(sanitize_flags) + cmd += ['-DCMAKE_C_FLAGS_RELEASE='+flags, '-DCMAKE_CXX_FLAGS_RELEASE='+flags] + if not win: + cmd += ['-DCMAKE_EXE_LINKER_FLAGS='+' '.join(sanitize_flags)] + run(cmd) + run(['cmake', '--build', build/'runtime', '--config', 'Release', '-j', '4']) + inc = build/'include' + inc.mkdir(exist_ok=True) + shutil.copy2(root/'core/iwasm/include/wasm_c_api.h', inc/'wasm.h') + if a.feature == 'on': + shutil.copy2(root/'core/iwasm/include/wasm_ext.h', inc/'wasm_ext.h') + libraries = list((build/'runtime').rglob('libiwasm.lib' if win else 'libiwasm.a')) + assert len(libraries) == 1, libraries + lib = libraries[0] +for name in ['fixture'] + (['edges'] if a.feature == 'on' else []): + exe = build/(name+('.exe' if win else '')) + client(fixture/(name+'.c'), exe, inc, lib, ['TEST_CONTINUATIONS'] if a.feature=='on' else []) + run([exe, fixture/'fixture.wasm'] if name=='fixture' else [exe]) + +if a.runtime == 'tinywasm': + exe = build/('api.exe' if win else 'api') + client(root/'crates/c-api/tests/api.c', exe, inc, lib) + run([exe, root/'crates/c-api/tests/fixtures']) diff --git a/crates/c-api/tests/continuations/wasi_p3_inventory.csv b/crates/c-api/tests/continuations/wasi_p3_inventory.csv new file mode 100644 index 0000000..4fb0ac8 --- /dev/null +++ b/crates/c-api/tests/continuations/wasi_p3_inventory.csv @@ -0,0 +1,56 @@ +upstream_revision,path,git_blob_oid,tinywasm_status,wamr_status,reason +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/cli-env.rs,ebacec3fae69940b02b023fa41c8f75a39cbf5b5,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/cli-exit.rs,49daf67fa2090cb6a6979dd5acef5146c34433b9,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/cli-stdio-roundtrip.rs,57d0607d298c9448c7524629e367fb66ce88ea45,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/cli-stdio.rs,5309b0ec2188866601ccf39a1fd64d465e4725e3,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/cli-stdout-flush.rs,b79b6920599984ed440fb0bbff49f5237d682602,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/cli-terminal.rs,fca6827cd925af43e0be1df313e0d193d5bd24a4,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-advise.rs,1e9410353c4ea41ce45c17c8298fd3648f093668,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-dotdot.rs,ad77fd8e731ee0dcc37a1edba216185471f66c7f,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-flags-and-type.rs,52feda6fc53da72b93cabd81f9b1cc7c6dc7ba3e,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-hard-links.rs,6cc27b84bc72e150b272c3aca21ef12d5b5d818f,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-io.rs,fcad8a5dedf63c42fd42b9b3aeb3cb8c93c1fd72,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-is-same-object.rs,8518fe70ed3efc9c1eae9f0912a82bd29ee9fd95,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-metadata-hash.rs,636767162284486f92da8b104d18307876939aed,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-mkdir-rmdir.rs,e91eb74666698576ede3f754a328d95a01b506d6,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-open-errors.rs,7a31c3d7e01e9baee1a354866c46ba58737c1b46,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-read-directory.rs,0072e06b1349e0bed1ec41617d3c5d7010650bf4,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-rename.rs,d53096ba1baa90713e030d9be62a8f293e4d8e43,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-set-size.rs,893978e3a1500a92b07b75e434ba621a29aac8d5,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-stat.rs,2a966414ac391837f5bea9e2c471ddd5ddcb4937,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/filesystem-unlink-errors.rs,98c0b3bca5897fb93c01d7bb4096bd53af90dbef,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-chunked.rs,629b76ea94cef67549faf7b0faab178f59872795,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-headers.rs,c52b860730f91a9c0350505df83813140543256e,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-method.rs,a70a99ffc1ef00dd2961ae03c7493b067b37f407,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-path-none.rs,a254bcb0aa0818eaee95bb6a40a97a32b33703fe,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-path.rs,1b679cf2915ce3186e91782d89c52ea513f1b307,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-send-errors.rs,7310a590da61dbc30900ca975be04af5500496d0,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-sent.rs,5f55b05378ddb8b61afbebc7a9ff6ae6a0c4578f,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-status.rs,644510e5e47051ac481b51d6f1b4488c4a716b45,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-trailer-error.rs,71bf5c8a2a83a8bd0bcfe8fc15cfc8147ddc5592,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client-trailers.rs,650e98a727293657c6a06d768f5c2a408ebab096,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-client.rs,cd854cab48d856bee536b2674ccaf8dbd900c8b5,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-fields.rs,2adb93dd6866c4ec80abf4550f53b83c4ec58cbe,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-request-options.rs,199a1ea3d73dc0c81faf8d2622d0876f9a55fc4f,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-request.rs,b74986d0748e6998872b413774b1d2582c102931,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-response.rs,2bf90747013d30668d5bc0e794f410ed26775f67,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-service-echo.rs,1e551446b6ea42c2b533a1c0eff31dd00a4672db,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-service-uri.rs,6dd142ca46b55b09f82e0062c9da7ec1040cafbe,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/http-service.rs,318632f85c93c973c4bdb358c94585a94da448a4,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/monotonic-clock.rs,0f738ec320246d361d444ab4a0ddc44f24629504,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/multi-clock-wait.rs,35314235f65509e96a6c2fd0c596ed1d24fac4e8,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/random.rs,701788f09acce6ff8d4af32738c7d1ba6695f390,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/run-with-err.rs,45718b02af141da9cf679eb760168d1491b62ebb,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-echo.rs,0da790c87f835ce231607f551ac037de3e6cd674,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-tcp-bind.rs,1f1e5d8ed8ae5fbe095eb23226116b208bb2d15b,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-tcp-connect.rs,b705edb72d0913d1a16cfdf92f8d351e5018ec44,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-tcp-listen.rs,5705f04e63cd0e5fda4d04ca50e8d2303c7bf676,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-tcp-properties.rs,62ff9c7ed3ecca7713f99e20b5b63b3748a11a92,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-tcp-receive.rs,be6c0eea1797cc1ef28579095d3f6d6411124d34,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-tcp-send.rs,3bcd64bf0154573792b4a0dd4f60f4f109abddf8,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-udp-bind.rs,8296e1e4af02fc677eba22560b06c01ce6554648,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-udp-connect.rs,18aa12d0ae33d790c29285d6d7eafb1ae195aa06,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-udp-properties.rs,0a9f639072b864e5310553769207c829512d72fc,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-udp-receive.rs,5840526318bdd1860bc072b742c02e86fad842c4,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/sockets-udp-send.rs,5b5c2de65e0763dab11fbab19b831b3c4a9d3693,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts +de7fbe267210b52f63893270403177239b4f4a9b,tests/rust/wasm32-wasip3/src/bin/wall-clock.rs,ea9446df231f4141db7273c609c4ff003296b40f,NOT_RUN,NOT_RUN,No WASIp3 component adapter or WASI 0.3 host interfaces implemented by these drafts From 6059423eae8211fbbe6ea040e5ef3900f89422eb Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 16:19:04 -0800 Subject: [PATCH 07/20] test: match Windows client and runtime CRT selection --- crates/c-api/tests/continuations/VALIDATION.md | 5 ++++- crates/c-api/tests/continuations/audit_results.md | 4 ++-- crates/c-api/tests/continuations/run.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/c-api/tests/continuations/VALIDATION.md b/crates/c-api/tests/continuations/VALIDATION.md index fe3f3aa..dbb7585 100644 --- a/crates/c-api/tests/continuations/VALIDATION.md +++ b/crates/c-api/tests/continuations/VALIDATION.md @@ -30,7 +30,10 @@ Windows x64 used an Intel Core i7-8650U, Windows 11 Pro build 26300, VS 2022 MSVC 19.44.35229.0 and Rust 1.98.1 (48a229cea 2026-09-01). Runtime C compilation used CMake IPO; the client used `/O2 /GL /Gy /Gw`, `/LTCG /OPT:REF /OPT:ICF /Brepro /INCREMENTAL:NO`. Rust used the same fat-LTO, -one-codegen-unit, abort strategy. No whole-archive retention was used. +one-codegen-unit, abort strategy. No whole-archive retention was used. These original images used the compiler-default +CRT selection. The later test runner explicitly uses `/MD` consistently for Windows +clients and sanitizer runtime builds; the table is not a measurement of that new +CRT configuration. Recheck final artifacts for the chosen upstream configuration. WAMR used the fast interpreter with software bounds checks and no AOT/JIT, SIMD, WASI libc, or builtin libc. MSVC and stable Rust use switch/match dispatch; these numbers do not compare the fastest dispatch configuration of either runtime. diff --git a/crates/c-api/tests/continuations/audit_results.md b/crates/c-api/tests/continuations/audit_results.md index 7094c02..e82b74b 100644 --- a/crates/c-api/tests/continuations/audit_results.md +++ b/crates/c-api/tests/continuations/audit_results.md @@ -19,7 +19,7 @@ explicit NOT_RUN inventories. Each ASan mode first detected a deliberate heap-buffer-overflow in its isolated canary executable. The expected canary failure is not a fixture finding. The runtime tests then passed without address-error reports. WAMR's Windows C and C++ -release flags were `/O1 /Zi /fsanitize=address /DNDEBUG`; sanitizer and LTO +release flags were `/MD /O1 /Zi /fsanitize=address /DNDEBUG`; sanitizer and LTO measurement builds use distinct directories. These ASan-only passes do not erase the previously documented baseline UBSan alignment findings. @@ -37,7 +37,7 @@ remains byte-for-byte unchanged. Windows checked its inputs against these hashes |---|---| | fixture.c | `d38be8465b16cfbdf09e752637e5750738b461a8899795ad7f4c2667a6006cdd` | | edges.c | `126bca9f3b5977c5a2b24ae6eb033f388524d273df5e71a150871ad754be557c` | -| run.py | `bb8aed39f39332eac04652e40c1b4693c09fe38a4317f7ae0516c82c7e360549` | +| run.py | `d8e0dda609af249242065e5e747054230e39826d4db26583115c3d15ee3c2d2e` | | continuation.rs, including new test | `b50af175229130b8c189d3c10f65a4ca75cfc1a98928c1ace517f49b70a427f7` | Earlier copied-source scratch runs are not used as these audit receipts. Native diff --git a/crates/c-api/tests/continuations/run.py b/crates/c-api/tests/continuations/run.py index 569004b..f0b02bf 100644 --- a/crates/c-api/tests/continuations/run.py +++ b/crates/c-api/tests/continuations/run.py @@ -51,7 +51,7 @@ def run(cmd): cc = env.get('CC', 'cl' if win else 'cc') def client(source, exe, inc, lib=None, defines=()): if win: - cmd = [cc, '/nologo', '/std:c17', '/O1' if san else '/O2', '/DLIBWASM_STATIC', '/DWASM_API_EXTERN=', '/I'+str(inc), source, '/Fe:'+str(exe), '/Fo:'+str(exe.with_suffix('.obj')), '/Fd:'+str(exe.with_suffix('.compile.pdb'))] + cmd = [cc, '/nologo', '/std:c17', '/MD', '/O1' if san else '/O2', '/DLIBWASM_STATIC', '/DWASM_API_EXTERN=', '/I'+str(inc), source, '/Fe:'+str(exe), '/Fo:'+str(exe.with_suffix('.obj')), '/Fd:'+str(exe.with_suffix('.compile.pdb'))] cmd += ['/D'+x for x in defines] cmd += ['/fsanitize=address', '/Zi'] if san else ['/GL', '/Gy', '/Gw'] if lib: @@ -122,7 +122,7 @@ def client(source, exe, inc, lib=None, defines=()): target = 'windows' if win else 'darwin' if mac else 'linux' cmd = ['cmake', '-S', root/'product-mini/platforms'/target, '-B', build/'runtime', '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_INTERPROCEDURAL_OPTIMIZATION='+('OFF' if san else 'ON'), '-DWAMR_BUILD_INTERP=1', '-DWAMR_BUILD_FAST_INTERP=1', '-DWAMR_BUILD_AOT=0', '-DWAMR_BUILD_JIT=0', '-DWAMR_BUILD_FAST_JIT=0', '-DWAMR_BUILD_LIBC_WASI=0', '-DWAMR_BUILD_LIBC_BUILTIN=0', '-DWAMR_BUILD_SIMD=0', '-DWAMR_DISABLE_HW_BOUND_CHECK=1', '-DWAMR_BUILD_C_API_CONTINUATIONS='+('1' if a.feature=='on' else '0')] if san: - flags = '/O1 /Zi /fsanitize=address /DNDEBUG' if win else '-O1 -g -DNDEBUG '+' '.join(sanitize_flags) + flags = '/MD /O1 /Zi /fsanitize=address /DNDEBUG' if win else '-O1 -g -DNDEBUG '+' '.join(sanitize_flags) cmd += ['-DCMAKE_C_FLAGS_RELEASE='+flags, '-DCMAKE_CXX_FLAGS_RELEASE='+flags] if not win: cmd += ['-DCMAKE_EXE_LINKER_FLAGS='+' '.join(sanitize_flags)] From f9205d275fbc1ca1cc005471f2c6171d3c332d66 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 16:20:57 -0800 Subject: [PATCH 08/20] docs: refresh pinned Windows sanitizer receipts --- crates/c-api/tests/continuations/audit_receipts.csv | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/c-api/tests/continuations/audit_receipts.csv b/crates/c-api/tests/continuations/audit_receipts.csv index 0709884..336f2bd 100644 --- a/crates/c-api/tests/continuations/audit_receipts.csv +++ b/crates/c-api/tests/continuations/audit_receipts.csv @@ -6,10 +6,10 @@ macos-arm64,wamr,ASan instrumentation canary,expected heap overflow detected,PAS macos-arm64,tiny,Rust+std+C ASan,standard C API,PASS,ffd1e7743c9a32ede3542c2d0fde486067c73438995dab9e356315f594c9674a macos-arm64,tiny,Miri,six C API tests including callback memory growth,PASS,33db1d73ce65d657d3c1f4e406e9ba0e351847fb84660e2f98637878b207a754 macos-arm64,tiny,nightly tail ABI enabled,main+edges+standard C API,PASS,0debc7a9d9e6e6152964896c6875dd667c421d98d3a1277487d66ecd854096c1 -windows-x64-physical,tiny,C-client-only ASan; Rust UNINSTRUMENTED,main+edges+standard C API,PASS,9b7da3c6a0b78cddba9534cb0c2b704aeb680606b87fc93afc461c928dcd2697 -windows-x64-physical,wamr,C/C++ runtime+client ASan,main+edges,PASS,1a77dca9883a5784af01815c7233aa68c3eb920d87b58187cab93c2b88d92dbf -windows-x64-physical,tiny,ASan instrumentation canary,expected heap overflow detected,PASS,bd52c69f7d64ac554fd1e069f64fed79bc82936182474f0fe5217cd025f1058f -windows-x64-physical,wamr,ASan instrumentation canary,expected heap overflow detected,PASS,5a57f7d4391157bb212a6672efa8f14bb6cc6b3561a672082cedf3dc2ce920d1 -windows-x64-physical,tiny,native stable Rust; unsanitized,seven host-execution tests,PASS,7e2dc3bd82ef5f17063b57c0e9f48557a47cf24c186b85817af3bfae829d420c -windows-x64-physical,tiny,native stable Rust; unsanitized,six C API unit tests,PASS,caf7114496154b17d4e9fa42b6691944c0034d093d41861a769d1c4f94f0e904 +windows-x64-physical,tiny,C-client-only ASan; Rust UNINSTRUMENTED,main+edges+standard C API,PASS,6927ca63d5c93a08d24ac72f97858c03b4c4c808fc4baa21dc015da75e941846 +windows-x64-physical,wamr,C/C++ runtime+client ASan,main+edges,PASS,702c32c4d0afff64ca46144ba70379ce2afe03c2c7dd82ade4ccfd1db6333ebd +windows-x64-physical,tiny,ASan instrumentation canary,expected heap overflow detected,PASS,6fc154a7ba43abc72e7fdba5d83b492407e8eda4dd4a69908f1878d295626b87 +windows-x64-physical,wamr,ASan instrumentation canary,expected heap overflow detected,PASS,4d32771f4d683c4c15fccc38930701e920e24a0be05c28ce87a76917c59bca3e +windows-x64-physical,tiny,native stable Rust; unsanitized,seven host-execution tests,PASS,901bf07d6167bb5d95403b09c9ce7e81a45919832a3d98fc2c3353b178659f8a +windows-x64-physical,tiny,native stable Rust; unsanitized,six C API unit tests,PASS,613a863f7f05ea3157b886cb6470feaa0f4c925e90ec4a576f6b6414aff973bc macos-arm64,wamr,CTest,both shared fixtures,PASS,158857594e8003b0645312271225910b7e2de5e9b2b35f557e51443356f9e409 From dbf1d6ef4e5768ad8a1a5aa2dfa2f82a7351ba25 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 17:40:45 -0800 Subject: [PATCH 09/20] feat: demonstrate bounded stackless HTTP components --- .github/workflows/stackless_http.yml | 46 ++ .../examples/stackless-http/CMakeLists.txt | 48 ++ .../c-api/examples/stackless-http/README.md | 147 ++++++ .../c-api/examples/stackless-http/RESULTS.md | 68 +++ .../examples/stackless-http/build_guest.py | 68 +++ crates/c-api/examples/stackless-http/check.c | 50 ++ .../examples/stackless-http/component.py | 247 ++++++++++ .../examples/stackless-http/conformance.csv | 22 + .../stackless-http/embedded-wamr.cmake | 21 + crates/c-api/examples/stackless-http/guest.c | 210 +++++++++ .../stackless-http/handler.component.wasm | Bin 0 -> 3651 bytes .../stackless-http/handler.linked.wasm | Bin 0 -> 2844 bytes .../c-api/examples/stackless-http/handler.wit | 7 + crates/c-api/examples/stackless-http/http.c | 141 ++++++ crates/c-api/examples/stackless-http/http.h | 38 ++ crates/c-api/examples/stackless-http/p3.c | 430 +++++++++++++++++ crates/c-api/examples/stackless-http/p3.h | 82 ++++ .../stackless-http/pico/CMakeLists.txt | 31 ++ .../examples/stackless-http/pico/lwipopts.h | 34 ++ .../stackless-http/pico/pico_server.c | 323 +++++++++++++ .../stackless-http/pico/platform_internal.h | 40 ++ .../stackless-http/pico/wamr-platform.c | 163 +++++++ .../stackless-http/pico/wamr-platform.cmake | 4 + .../stackless-http/qemu/CMakeLists.txt | 24 + .../c-api/examples/stackless-http/qemu/m33.ld | 21 + .../c-api/examples/stackless-http/qemu/run.py | 109 +++++ .../c-api/examples/stackless-http/qemu/sim.c | 183 ++++++++ .../examples/stackless-http/qemu/startup.c | 62 +++ .../examples/stackless-http/receipts.json | 442 ++++++++++++++++++ crates/c-api/examples/stackless-http/run.py | 122 +++++ crates/c-api/examples/stackless-http/server.c | 301 ++++++++++++ crates/c-api/examples/stackless-http/task.c | 95 ++++ .../examples/stackless-http/test_server.py | 118 +++++ .../examples/stackless-http/tools/Cargo.lock | 191 ++++++++ .../examples/stackless-http/tools/Cargo.toml | 11 + .../examples/stackless-http/tools/src/main.rs | 13 + crates/c-api/examples/stackless-http/vm.c | 354 ++++++++++++++ crates/c-api/examples/stackless-http/vm.h | 77 +++ crates/c-api/tests/continuations/READINESS.md | 5 + 39 files changed, 4348 insertions(+) create mode 100644 .github/workflows/stackless_http.yml create mode 100644 crates/c-api/examples/stackless-http/CMakeLists.txt create mode 100644 crates/c-api/examples/stackless-http/README.md create mode 100644 crates/c-api/examples/stackless-http/RESULTS.md create mode 100644 crates/c-api/examples/stackless-http/build_guest.py create mode 100644 crates/c-api/examples/stackless-http/check.c create mode 100644 crates/c-api/examples/stackless-http/component.py create mode 100644 crates/c-api/examples/stackless-http/conformance.csv create mode 100644 crates/c-api/examples/stackless-http/embedded-wamr.cmake create mode 100644 crates/c-api/examples/stackless-http/guest.c create mode 100644 crates/c-api/examples/stackless-http/handler.component.wasm create mode 100644 crates/c-api/examples/stackless-http/handler.linked.wasm create mode 100644 crates/c-api/examples/stackless-http/handler.wit create mode 100644 crates/c-api/examples/stackless-http/http.c create mode 100644 crates/c-api/examples/stackless-http/http.h create mode 100644 crates/c-api/examples/stackless-http/p3.c create mode 100644 crates/c-api/examples/stackless-http/p3.h create mode 100644 crates/c-api/examples/stackless-http/pico/CMakeLists.txt create mode 100644 crates/c-api/examples/stackless-http/pico/lwipopts.h create mode 100644 crates/c-api/examples/stackless-http/pico/pico_server.c create mode 100644 crates/c-api/examples/stackless-http/pico/platform_internal.h create mode 100644 crates/c-api/examples/stackless-http/pico/wamr-platform.c create mode 100644 crates/c-api/examples/stackless-http/pico/wamr-platform.cmake create mode 100644 crates/c-api/examples/stackless-http/qemu/CMakeLists.txt create mode 100644 crates/c-api/examples/stackless-http/qemu/m33.ld create mode 100644 crates/c-api/examples/stackless-http/qemu/run.py create mode 100644 crates/c-api/examples/stackless-http/qemu/sim.c create mode 100644 crates/c-api/examples/stackless-http/qemu/startup.c create mode 100644 crates/c-api/examples/stackless-http/receipts.json create mode 100644 crates/c-api/examples/stackless-http/run.py create mode 100644 crates/c-api/examples/stackless-http/server.c create mode 100644 crates/c-api/examples/stackless-http/task.c create mode 100644 crates/c-api/examples/stackless-http/test_server.py create mode 100644 crates/c-api/examples/stackless-http/tools/Cargo.lock create mode 100644 crates/c-api/examples/stackless-http/tools/Cargo.toml create mode 100644 crates/c-api/examples/stackless-http/tools/src/main.rs create mode 100644 crates/c-api/examples/stackless-http/vm.c create mode 100644 crates/c-api/examples/stackless-http/vm.h diff --git a/.github/workflows/stackless_http.yml b/.github/workflows/stackless_http.yml new file mode 100644 index 0000000..9043838 --- /dev/null +++ b/.github/workflows/stackless_http.yml @@ -0,0 +1,46 @@ +name: Stackless HTTP component +on: + workflow_dispatch: + pull_request: + paths: ["crates/tinywasm/**", "crates/c-api/**", "crates/c-api/examples/stackless-http/**", ".github/workflows/stackless_http.yml"] +permissions: + contents: read +jobs: + hosted: + name: Component assertions and TCP (${{ matrix.os }}) + strategy: + matrix: + os: [ubuntu-24.04, windows-2025] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: WebAssembly/component-model + ref: fed10b0b823b82553dd8363f16a640816f5eac65 + path: .stackless-component-tests + - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f + with: + toolchain: stable + rustflags: "" + - name: Linux release and canonical assertions + if: runner.os != 'Windows' + run: python3 crates/c-api/examples/stackless-http/run.py --runtime tinywasm --root . --build build/stackless --component-tests .stackless-component-tests + - name: Windows x64 release and C-client ASan (Rust uninstrumented) + if: runner.os == 'Windows' + shell: cmd + run: | + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do call "%%i\Common7\Tools\VsDevCmd.bat" -arch=amd64 + if errorlevel 1 exit /b 1 + python crates/c-api/examples/stackless-http/run.py --runtime tinywasm --root . --build build/stackless --component-tests .stackless-component-tests + if errorlevel 1 exit /b 1 + python crates/c-api/examples/stackless-http/run.py --runtime tinywasm --root . --build build/stackless-asan --component-tests .stackless-component-tests --client-asan + - name: Install Rust std sources for full instrumentation + if: runner.os != 'Windows' && true + run: rustup toolchain install nightly --profile minimal --component rust-src + - name: Linux runtime/client ASan + if: runner.os != 'Windows' + env: + CC: clang + RUSTUP_TOOLCHAIN: nightly + run: python3 crates/c-api/examples/stackless-http/run.py --runtime tinywasm --root . --build build/stackless-asan --component-tests .stackless-component-tests --sanitize diff --git a/crates/c-api/examples/stackless-http/CMakeLists.txt b/crates/c-api/examples/stackless-http/CMakeLists.txt new file mode 100644 index 0000000..b9f3d17 --- /dev/null +++ b/crates/c-api/examples/stackless-http/CMakeLists.txt @@ -0,0 +1,48 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +cmake_minimum_required(VERSION 3.20) +project(stackless_http C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +if(NOT P3_RUNTIME_LIBRARY OR NOT P3_RUNTIME_INCLUDE OR NOT P3_HANDLER_INCLUDE) + message(FATAL_ERROR "Set P3_RUNTIME_LIBRARY, P3_RUNTIME_INCLUDE and P3_HANDLER_INCLUDE") +endif() + +add_executable(stackless_http p3.c vm.c task.c http.c server.c) +add_executable(component_check p3.c vm.c check.c) +target_compile_definitions(component_check PRIVATE P3_HANDLES=128) +foreach(target stackless_http component_check) + target_include_directories(${target} PRIVATE ${P3_RUNTIME_INCLUDE} ${P3_HANDLER_INCLUDE}) + target_link_libraries(${target} PRIVATE ${P3_RUNTIME_LIBRARY}) + if(P3_WAMR) + target_compile_definitions(${target} PRIVATE P3_WAMR) + endif() + if(MSVC) + target_compile_definitions(${target} PRIVATE LIBWASM_STATIC WASM_API_EXTERN=) + target_compile_options(${target} PRIVATE /W3) + target_link_libraries(${target} PRIVATE ws2_32 psapi userenv bcrypt ntdll advapi32) + target_link_options(${target} PRIVATE /INCREMENTAL:NO /OPT:REF /OPT:ICF) + if(P3_ASAN) + target_compile_options(${target} PRIVATE /fsanitize=address /Zi) + endif() + else() + target_compile_options(${target} PRIVATE -Wall -Wextra -Werror -fvisibility=hidden) + if(P3_ASAN) + target_compile_options(${target} PRIVATE -fsanitize=address -fno-omit-frame-pointer -g) + target_link_options(${target} PRIVATE -fsanitize=address) + endif() + if(APPLE) + if(NOT P3_WAMR) + target_link_libraries(${target} PRIVATE "-framework Security" "-framework CoreFoundation" iconv) + endif() + target_link_options(${target} PRIVATE -Wl,-dead_strip) + else() + target_link_libraries(${target} PRIVATE m pthread dl) + target_link_options(${target} PRIVATE -Wl,--gc-sections) + endif() + endif() + if(NOT P3_ASAN) + set_property(TARGET ${target} PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) + endif() +endforeach() diff --git a/crates/c-api/examples/stackless-http/README.md b/crates/c-api/examples/stackless-http/README.md new file mode 100644 index 0000000..189ef22 --- /dev/null +++ b/crates/c-api/examples/stackless-http/README.md @@ -0,0 +1,147 @@ +# Stackless HTTP component on a bounded host + +This example serves an actual HTTP response from a validated component using +Canonical ABI streams, futures and stackless callbacks. The same C adapter and +guest are linked against ordinary `wasm.h` calls on WAMR and TinyWasm, with the +experimental stackful continuation feature **off**. The runtime cores, ordinary +import dispatch and disabled-feature layouts are unchanged. + +The application world is [handler.wit](handler.wit). This is a deliberately +small Component Model async profile, **not** the `wasi:http@0.3.0` host world. +`handler.component.wasm` is the validated component; `handler.linked.wasm` is +its statically linked core image. The offline linker resolves the canonical +bindings, fuses a memory-only provider into its single consumer, and calls the +core start function after the C API has installed callback environments and +exported memory. There is no component parser, graph linker or file loader on +the board. Both artifacts can be regenerated from `guest.c`. + +## Design and scope + +The native transport accepts up to four connections. A guest callback receives +bytes in a bounded header buffer and resolves the lifted call with a response +`stream` and completion `future`. The producer keeps running after +`task.return`, driven by send capacity and waitable events. `/` serves a short +body and `/large` streams 64 KiB in 1 KiB chunks; the host does not allocate a +whole-body buffer. A request whose header exceeds the 2 KiB limit is rejected. +The demo serves one request per connection, uses plain HTTP, and has no request +body API, TLS, authentication, or general-purpose component loading. + +- No interpreter stack is parked across I/O. The adapter distinguishes + `WAIT`, which requires an event, from `YIELD`, which is ready to run again. + Its ready lists coalesce notifications and use constant-time enqueue/dequeue. +- A disconnect before `task.return` delivers cooperative task cancellation and + runs guest cleanup. After return, the native host closes the stream; the + producer observes the drop and resolves its completion future. The adapter + validates endpoint kind/ownership, transfer extent and event alignment and + resolves guest offsets again when copying. Native buffers are only borrowed + during a transfer; callbacks do not retain guest pointers. +- The host accepts only this built-in guest. A trap poisons the adapter + instance. Supporting arbitrary untrusted components additionally requires + instruction budgets, a general type/resource linker, task ownership rules + and integration fuzzing. The profile does not implement stackful calls, + cross-component linking, generic Canonical ABI payloads or resource lifting. +- The small WAMR binding accounts for its extra C vector fields and chooses + an 8 KiB execution stack and no extra app heap. The shared source does not + promise a binary ABI across the two `wasm.h` implementations. + +See [RESULTS.md](RESULTS.md) for the hosted and emulator receipts and exact +limits of the qualification. + +The byte transfer path is useful for networking and control messages. It is +not the required bulk path for CapsHost: retain native GPU buffers and command +batches, coalesce frame/fence notifications for GPU/XR, keep PCM in host-owned +rings and never enter Wasm from the audio render callback. Bluetooth events +need bounded copied payloads, ordered admission and explicit backpressure. +The component adapter should carry handles and readiness, with optional buffer +leases for consumers that can prove lifetime and alignment. Asynchronous GPU +and Bluetooth callbacks need owner-thread delivery with generation-checked +completion tokens and teardown fencing before pool slots can be reused. The +server's single-owner socket/PCB lifetime rules are not a general cross-thread +completion mechanism. No webgpu-caps runtime or interface was changed. + +## Hosted validation + +With CMake, Ninja, Python 3 and Rust available: + +```sh +python3 run.py --runtime wamr --root /path/to/wamr --build /tmp/http-wamr \ + --component-tests /path/to/component-model +python3 run.py --runtime tinywasm --root /path/to/tinywasm --build /tmp/http-tiny \ + --component-tests /path/to/component-model +``` + +Use the pinned Component Model revision `fed10b0b823b82553dd8363f16a640816f5eac65`. +The runner validates the original supported component definitions, executes +their assertions through both cores, and records unsupported assertions. +It also runs real TCP tests on loopback, idle measurement, and seven samples +of a checked 10-million-import `context.get` loop. That metric includes the +guest loop and canonical callback, and is separate from the ordinary-import +control in `ordinary-native.log`. `--sanitize` instruments WAMR runtime and +client or TinyWasm Rust/std/C on a supported Unix host. `--client-asan` is +explicitly client-only for TinyWasm on Windows. Use a new build directory for +instrumented runs. With MSVC, run from the x64 developer prompt. + +The executed upstream assertions are in `future-completion-order.wast` (2), +`same-component-stream-future.wast` (12), and `futures-must-write.wast` (5 of 7; +2 require cross-component linking). These are **Component Model async** tests. +The WASI testsuite's `http-service`, `http-service-echo` and `http-service-uri` +target the standard HTTP resource world. The stdio roundtrip targets WASI CLI +streams and futures. None can execute against this application's world; no +official WASI 0.3 interface conformance results are claimed. See the earlier +WASI inventory next to the continuation fixture. + +To regenerate the artifacts, use a Clang with the `wasm32` backend and `wasm-ld`, +the `wasm-tools print` CLI, and the pinned local validator: + +```sh +cargo build --locked --release --manifest-path tools/Cargo.toml --target-dir /tmp/stackless-tools +python3 build_guest.py /tmp/stackless-guest \ + --validator /tmp/stackless-tools/release/stackless-component-tool +``` + +## Cortex-M33 simulation and Pico 2 W firmware + +The `qemu` and `pico` projects build WAMR with the same fast interpreter profile, +the same statically linked guest, and the same C adapter. TinyWasm's current C +library needs Rust `std`; its tests are hosted, not bare metal. + +Set `HANDLER_INCLUDE` to the directory containing `handler.h` from either +`run.py` or `build_guest.py`. With an Arm embedded GCC toolchain: + +```sh +cmake -G Ninja -S qemu -B /tmp/http-m33 -DCMAKE_BUILD_TYPE=Release \ + -DWAMR_ROOT=/path/to/wamr -DHANDLER_INCLUDE=/tmp/stackless-guest +cmake --build /tmp/http-m33 +python3 qemu/run.py /tmp/http-m33/stackless_m33.elf --output /tmp/http-m33-results +``` + +QEMU executes the app with a 520 KiB linker RAM region and a 64 KiB allowance +for network buffers. It drives the same HTTP adapter with simulated input and +send capacity, verifies all bytes of the streamed response, exercises +cancellation and resource reuse, and waits for a real emulated timer interrupt +using `WFI`. Its trace counts executed Cortex-M33 instructions. QEMU AN505 is +not an RP2350/CYW43 Wi-Fi emulation; the hosted tests cover real TCP and the +Pico link covers the actual board networking configuration. + +For Pico SDK 2.2.0 with `lib/lwip`, `lib/cyw43-driver` and `lib/tinyusb` +initialized: + +```sh +cmake -G Ninja -S pico -B /tmp/http-pico -DCMAKE_BUILD_TYPE=Release \ + -DPICO_SDK_PATH=/path/to/pico-sdk -DWAMR_ROOT=/path/to/wamr \ + -DHANDLER_INCLUDE=/tmp/stackless-guest +cmake --build /tmp/http-pico +python3 qemu/run.py /tmp/http-m33/stackless_m33.elf --output /tmp/http-m33-results \ + --pico-elf /tmp/http-pico/stackless_http.elf +``` + +The Pico firmware uses lwIP RAW callbacks with the SDK's background IRQ mode. +They queue network work; only the foreground owner enters the runtime. The +owner sleeps until a network event or deadline. Wi-Fi credentials are read +at boot from the serial console and are not compiled into the image. The +server listens on port 8080. There is a 10-second connection deadline, a fixed +128 KiB WAMR pool including linear memory, fixed lwIP pools, a capped 16 KiB +libc heap and a 4 KiB native stack. With no picotool, `-DPICO_NO_PICOTOOL=1` +builds an ELF for a debug probe. The serial counters report allocator high +water, foreground service time, I/O wakes and import-loop wall time on a board. +Foreground time excludes sleeping time and is not total CPU or power. diff --git a/crates/c-api/examples/stackless-http/RESULTS.md b/crates/c-api/examples/stackless-http/RESULTS.md new file mode 100644 index 0000000..5255b28 --- /dev/null +++ b/crates/c-api/examples/stackless-http/RESULTS.md @@ -0,0 +1,68 @@ +# Stackless component validation (2026-11-02) + +The Cortex-M33 simulation completed 34 HTTP responses, one cancellation before +return, and one disconnect after return. It streamed and verified every byte of +one 64 KiB response while three other tasks were live, then recycled the pool +through 32 additional requests. No handles remained live. This executes the +real guest and adapter using simulated network readiness, without a Pico Wi-Fi +peripheral model. Both hosted backends additionally ran real TCP loopback. + +| ARM build | SRAM reserved | Detail | +|---|---:|---| +| Pico 2 W firmware with Wi-Fi | 220,264 bytes (215.1 KiB) | Includes 128 KiB WAMR pool, lwIP/driver/USB data, bounded 16 KiB libc heap and 4 KiB stack | +| Cortex-M33 AN505 with 520 KiB linker cap | 229,312 bytes (223.9 KiB) | Includes 64 KiB placeholder for networking, the same WAMR pool and stack/heap bounds | +| WAMR allocation high water in simulator | 95,024 bytes | Already inside the reserved pool; includes the 64 KiB guest memory | + +The Pico link uses 41.4% of its SRAM budget. Reservations +are not a measured physical high water. No Pico board was available. The +simulator ran with QEMU 11.1.3 `mps2-an505`, Arm embedded GCC 9.2.1 and LTO; +Pico SDK 2.2.0 is `a1438dff1d38bd9c65dbd693f0e5db4b9ae91779`. Both runs +used the same component and core image. The ELF section counts and hashes +are in [receipts.json](receipts.json). + +With one instruction per QEMU translation block, the traced loop executed +464,982 guest instructions for 1,000 +`context.get` calls, including the Wasm interpreter loop and call setup. +The `WFI` interval, including its timer wake and interrupt service, executed +22 instructions and had one wake. These are +emulated instruction counts; QEMU TCG is not a cycle-accurate CPU or power model. + +| Hosted release | Runtime | Peak process RSS (bytes) | Idle process CPU / interval | Canonical import median ns (range) | +|---|---|---:|---:|---:| +| macOS arm64 | wamr | 2,244,608 | 12 µs / 3.001 s | 13.81 (13.62–15.78) | +| macOS arm64 | tinywasm | 7,241,728 | 12 µs / 3.001 s | 53.55 (50.34–56.34) | +| Windows x64 | wamr | 4,812,800 | 0 µs / 3.031 s | 50.00 (45.31–134.38) | +| Windows x64 | tinywasm | 5,844,992 | 0 µs / 3.016 s | 207.81 (143.75–598.44) | + +Each hosted idle run waited once and received no I/O wakeups. Windows CPU +values of zero mean below the resolution of the process CPU clock. RSS includes +host libraries and OS accounting; it is not MCU SRAM. Native-call samples are +seven runs of ten million checked `context.get` imports per runtime. Windows +timings vary substantially; these are measurements of this workload and +configuration, not limits on relative performance. The disabled-feature +controls are separate in the earlier continuation validation. No core +production code changed in this follow-up. + +On macOS arm64 and physical Windows x64, both releases completed 25 verified +TCP responses, cancelled three unfinished requests, handled one disconnect +while streaming, and reaped all handles. All eight release/instrumented +combinations in [receipts.json](receipts.json) passed the same 19 pinned +Component Model assertions and the TCP workload. WAMR was instrumented with +runtime and client ASan on both hosts. TinyWasm used Rust/dependencies/rebuilt +std and C ASan on macOS; on Windows only its C clients were instrumented. +The Mac instrumented libraries were reused from the earlier sanitizer audit; +the hosted release and Windows builds used the feature-off runtime. + +[conformance.csv](conformance.csv) records the original assertion, pinned +source hash, expected value or trap and results. Two assertions in the +selected files require cross-component linking and were explicitly unsupported. +These are Component Model async results. The official WASI HTTP and CLI tests +remain unexecuted against either draft: the example has no `wasi:http` resource +bindings or WASI CLI world. No WASI 0.3 conformance claim follows from this run. + +The physical Windows build used MSVC 19.44.35229.0 on a Core i7-8650U with +Windows 11 build 26300 and stable Rust 1.98.1. The base runtime snapshots on the +machine were WAMR `82ef5a7daec8936e454436858154f9585a49bd2f` and TinyWasm +`f9205d275fbc1ca1cc005471f2c6171d3c332d66`, with the adapter compiled separately. +The macOS release link used Clang/LLD 23.1.1. Full raw reports and emulator +trace are retained separately from these portable summaries. diff --git a/crates/c-api/examples/stackless-http/build_guest.py b/crates/c-api/examples/stackless-http/build_guest.py new file mode 100644 index 0000000..d65e299 --- /dev/null +++ b/crates/c-api/examples/stackless-http/build_guest.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Compile the freestanding HTTP handler, validate its component and link it.""" +import argparse +from pathlib import Path +import subprocess +from component import emit, lower, parse + +p = argparse.ArgumentParser(description=__doc__) +p.add_argument('output', type=Path) +p.add_argument('--clang', default='clang') +p.add_argument('--wasm-tools', default='wasm-tools') +p.add_argument('--validator', required=True) +a = p.parse_args() +here = Path(__file__).resolve().parent +a.output.mkdir(parents=True, exist_ok=True) +core = a.output / 'guest.wasm' +subprocess.run([a.clang, '--target=wasm32', '-O2', '-fno-builtin', '-nostdlib', + '-Wl,--no-entry,--import-memory,--initial-memory=65536,--max-memory=65536,-z,stack-size=4096,--strip-all', + here / 'guest.c', '-o', core], check=True) +module = parse(subprocess.check_output([a.wasm_tools, 'print', core], text=True))[0] +fields = [['core', 'module', '$Memory', ['memory', ['export', '"mem"'], '1', '1']], + ['core', 'instance', '$mem', ['instantiate', '$Memory']], + ['core', 'module', '$Guest', *module[1:]], + ['type', '$Stream', ['stream', 'u8']], ['type', '$Future', ['future', 'u8']], + ['type', '$Result', ['tuple', '$Stream', '$Future']]] +imports = [] +for decl in module[1:]: + if not isinstance(decl, list) or decl[0] != 'import': + continue + assert decl[1] == '"env"' + name = decl[2][1:-1] + if decl[3][0] == 'memory': + imports.append(['export', decl[2], ['memory', '$mem', '"mem"']]) + continue + ident = '$' + name + canon = ['canon', name] + if name.startswith(('stream.', 'future.')): + canon.append('$Stream' if name.startswith('stream.') else '$Future') + if name.endswith(('.read', '.write', '.cancel-read', '.cancel-write')): + canon.append('async') + if name.endswith(('.read', '.write')): + canon.append(['memory', ['core', 'memory', '$mem', '"mem"']]) + if name == 'task.return': + canon.append(['result', '$Result']) + if name.startswith('context.'): + canon.extend(['i32', '0']) + canon.append(['core', 'func', ident]) + fields.append(canon) + imports.append(['export', decl[2], ['func', ident]]) +fields.extend([['core', 'instance', '$h', ['instantiate', '$Guest', ['with', '"env"', ['instance', *imports]]]], + ['func', ['export', '"handle"'], 'async', ['param', '"slot"', 'u32'], + ['param', '"request"', '$Stream'], ['result', '$Result'], + ['canon', 'lift', ['core', 'func', '$h', '"handle"'], 'async', + ['callback', ['core', 'func', '$h', '"handle-event"']]]]]) +component = ['component', *fields] +wat = a.output / 'handler.component.wat' +wat.write_text(emit(component)) +subprocess.run([a.validator, 'parse', wat, '-o', a.output / 'handler.component.wasm'], check=True) +core, _ = lower(component) +wat = a.output / 'handler.linked.wat' +wat.write_text(emit(core)) +binary = a.output / 'handler.linked.wasm' +subprocess.run([a.validator, 'parse', wat, '-o', binary], check=True) +data = binary.read_bytes() +(a.output / 'handler.h').write_text('static const unsigned char handler_wasm[] = {\n' + + '\n'.join(' ' + ', '.join(f'0x{v:02x}' for v in data[i:i+16]) + ',' for i in range(0, len(data), 16)) + '\n};\n') diff --git a/crates/c-api/examples/stackless-http/check.c b/crates/c-api/examples/stackless-http/check.c new file mode 100644 index 0000000..dd4b3ab --- /dev/null +++ b/crates/c-api/examples/stackless-http/check.c @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "vm.h" +#include +#include +#include +int +main(int argc, char **argv) +{ + if (argc < 4 || argc > 8) + return 2; + FILE *file = fopen(argv[1], "rb"); + if (!file || fseek(file, 0, SEEK_END)) + return 2; + long length = ftell(file); + if (length < 0 || length > 1024 * 1024 || fseek(file, 0, SEEK_SET)) + return 2; + uint8_t *bytes = malloc((size_t)length); + if (!bytes || fread(bytes, 1, (size_t)length, file) != (size_t)length) + return 2; + fclose(file); + p3_vm vm; + if (!p3_vm_open(&vm, bytes, (size_t)length)) { + fprintf(stderr, "instantiate: %s\n", vm.error); + return 1; + } + free(bytes); + int32_t value = 0; + bool result = false; + int32_t args[4]; + for (int i = 4; i < argc; ++i) + args[i - 4] = (int32_t)strtol(argv[i], NULL, 0); + bool ok = + p3_vm_call(&vm, argv[2], args, (size_t)(argc - 4), &value, &result); + bool expected_trap = !strncmp(argv[3], "trap:", 5); + if (expected_trap) + ok = !ok && strstr(vm.error, argv[3] + 5) != NULL; + else + ok = ok + && ((!strcmp(argv[3], "void") && !result) + || (result && value == (int32_t)strtol(argv[3], NULL, 0))); + printf("%s %s result=%d live=%u peak=%u adapter_bytes=%zu transferred=%llu " + "error=%s\n", + ok ? "PASS" : "FAIL", argv[2], value, vm.async.live, vm.async.peak, + sizeof(vm.async), (unsigned long long)vm.async.bytes, vm.error); + p3_vm_close(&vm); + return ok ? 0 : 1; +} diff --git a/crates/c-api/examples/stackless-http/component.py b/crates/c-api/examples/stackless-http/component.py new file mode 100644 index 0000000..6475377 --- /dev/null +++ b/crates/c-api/examples/stackless-http/component.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Offline linker for the deliberately restricted single-memory numeric profile. + +Resolve canonical imports from the component graph, rather than naming stub +imports in an extracted module. The memory-only provider is statically fused +with its sole consumer; memory identity and all canonical operations remain. +Unsupported graphs/types fail closed. This is not a general component loader. +""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess + + +class Unsupported(ValueError): + pass + + +def parse(text): + tokens = [] + i = 0 + while i < len(text): + if text[i].isspace(): + i += 1 + elif text.startswith(';;', i): + i = text.find('\n', i) + if i == -1: + break + elif text.startswith('(;', i): + depth = 1 + i += 2 + while depth: + if i == len(text): + raise ValueError('unterminated comment') + if text.startswith('(;', i): + depth += 1 + i += 2 + elif text.startswith(';)', i): + depth -= 1 + i += 2 + else: + i += 1 + elif text[i] in '()': + tokens.append(text[i]) + i += 1 + elif text[i] == '"': + start = i + i += 1 + while i < len(text) and text[i] != '"': + i += 2 if text[i] == '\\' else 1 + if i == len(text): + raise ValueError('unterminated string') + i += 1 + tokens.append(text[start:i]) + else: + m = re.match(r'[^\s()]+', text[i:]) + tokens.append(m[0]) + i += len(m[0]) + stack = [[]] + for token in tokens: + if token == '(': + node = [] + stack[-1].append(node) + stack.append(node) + elif token == ')': + if len(stack) == 1: + raise ValueError('unexpected closing parenthesis') + stack.pop() + else: + stack[-1].append(token) + if len(stack) != 1: + raise ValueError('unterminated expression') + return stack[0] + + +def emit(node): + return '(' + ' '.join(map(emit, node)) + ')' if isinstance(node, list) else node + + +def children(node, prefix): + return [n for n in node if isinstance(n, list) and n[:len(prefix)] == prefix] + + +def one(nodes, description): + if len(nodes) != 1: + raise Unsupported('requires one ' + description) + return nodes[0] + + +OPS = {'stream.new', 'stream.read', 'stream.write', 'stream.cancel-read', + 'stream.cancel-write', 'stream.drop-readable', 'stream.drop-writable', + 'future.new', 'future.read', 'future.write', 'future.cancel-read', + 'future.cancel-write', 'future.drop-readable', 'future.drop-writable', + 'waitable-set.new', 'waitable-set.poll', 'waitable-set.wait', 'waitable-set.drop', 'waitable.join', + 'task.return', 'task.cancel', 'context.get', 'context.set'} + + +def lower(component): + allowed = {'type', 'canon', 'func'} + for item in component: + if isinstance(item, list) and item[0] not in allowed and item[:2] not in (['core', 'module'], ['core', 'instance']): + raise Unsupported('unsupported component field: ' + item[0]) + modules = children(component, ['core', 'module']) + providers = [m for m in modules if len(children(m, ['memory'])) == 1 + and not children(m, ['func'])] + provider = one(providers, 'memory provider') + if len(modules) != 2: + raise Unsupported('profile requires a memory provider and one core consumer') + memory = one(children(provider, ['memory']), 'provider memory') + if any(isinstance(n, list) and n[0] not in {'memory', 'export'} for n in provider[3:]): + raise Unsupported('memory provider must have no code or initialization') + if children(provider, ['export']): + raise Unsupported('use an inline provider memory export') + memory_export = one(children(memory, ['export']), 'provider memory export')[1] + consumers = [m for m in modules if m is not provider] + core = consumers[0] + instances = children(component, ['core', 'instance']) + if len(instances) != 2: + raise Unsupported('profile requires exactly two core instantiations') + provider_inst = one([n for n in instances if children(n, ['instantiate']) + and children(n, ['instantiate'])[0][1] == provider[2]], 'memory instance') + inst = one([n for n in instances if children(n, ['instantiate']) + and children(n, ['instantiate'])[0][1] == core[2]], 'consumer instance') + bindings = one(children(one(children(inst, ['instantiate']), 'instantiate'), ['with']), 'import namespace') + binding_instance = one(children(bindings, ['instance']), 'import instance') + aliases = {n[1]: n[2] for n in children(binding_instance, ['export'])} + types = {n[1]: n[2] for n in children(component, ['type'])} + canons = {} + for canon in children(component, ['canon']): + if canon[1] not in OPS: + raise Unsupported('unsupported canonical operation: ' + canon[1]) + name = one(children(canon, ['core', 'func']), 'canonical function name')[2] + if canon[1].startswith('context.') and canon[2:4] != ['i32', '0']: + raise Unsupported('only i32 context slot zero is supported') + element = 'unit' + if canon[1].startswith(('stream.', 'future.')): + ty = types.get(canon[2]) + if not ty or ty[0] != canon[1].split('.')[0] or ty[1:] not in ([], ['u8'], ['bool'], ['char']): + raise Unsupported('stream/future element type outside profile') + element = ty[1] if ty[1:] else 'unit' + for option in children(canon, ['memory']): + if option[1] != ['core', 'memory', provider_inst[2], memory_export]: + raise Unsupported('canonical operation uses a different memory') + canons[name] = canon[1] + ':' + element + ':' + str(int('async' in canon)) + output = ['module'] + imported_memory = [] + for item in children(core, ['import']): + if item[1] != bindings[1] or item[2] not in aliases: + raise Unsupported('unresolved core import ' + emit(item)) + target = aliases[item[2]] + desc = item[3] + if desc[0] == 'memory': + if target != ['memory', provider_inst[2], memory_export]: + raise Unsupported('memory import identity mismatch') + imported_memory.append(desc) + elif desc[0] == 'func' and target[0] == 'func' and target[1] in canons: + output.append(['import', '"p3"', json.dumps(canons[target[1]]), desc]) + else: + raise Unsupported('unresolved canonical function ' + emit(item)) + imported = one(imported_memory, 'imported memory') + limits = [n for n in memory[1:] if not isinstance(n, list) and not n.startswith('$')] + import_limits = [n for n in imported[1:] if not n.startswith('$')] + if import_limits != limits: + raise Unsupported('memory limits must match for static fusion') + output.append(['memory', ['export', '"__p3_memory"'], *limits]) + for item in core[3:]: + if isinstance(item, list) and item[0] != 'import': + if item[0] == 'start': + output.append(['export', '"__p3_start"', ['func', item[1]]]) + continue + if item[0] == 'memory': + raise Unsupported('additional memory/table outside profile') + output.append(item) + exports = {} + for fun in children(component, ['func']): + exp = children(fun, ['export']) + lift = children(fun, ['canon', 'lift']) + if len(exp) != 1 or len(lift) != 1 or lift[0][2][:3] != ['core', 'func', inst[2]]: + raise Unsupported('requires direct numeric canonical lifts') + exports[exp[0][1]] = lift[0][2][3] + return output, exports + + +def compile_script(source, out, wasm_tools): + out.mkdir(parents=True, exist_ok=True) + definitions, cases = {}, [] + validated = set() + current = None + asserted = False + for node in parse(source.read_text()): + if node[:2] == ['component', 'definition']: + definitions[node[2]] = node + elif node[:2] == ['component', 'instance']: + current = definitions[node[3]] + asserted = False + elif node[0] == 'component': + current = node + asserted = False + elif node[0] in {'assert_return', 'assert_trap'}: + if current is None: + raise Unsupported('assertion has no component instance') + try: + if asserted: + raise Unsupported('successive assertions on one live instance outside profile') + core, exports = lower(current) + except Unsupported as error: + cases.append({'status': 'UNSUPPORTED', 'assertion': emit(node), 'reason': str(error)}) + asserted = True + continue + asserted = True + if id(current) not in validated: + component = ['component', *current[3:]] if current[:2] == ['component', 'definition'] else current + original = out / f'component-{len(validated)}.wat' + original.write_text(emit(component)) + subprocess.run([wasm_tools, 'parse', original, '-o', original.with_suffix('.wasm')], check=True) + validated.add(id(current)) + name = 'case-' + str(len(cases)) + wat = out / (name + '.wat') + wat.write_text(emit(core)) + binary = out / (name + '.wasm') + subprocess.run([wasm_tools, 'parse', str(wat), '-o', str(binary)], check=True) + invoke = node[1] + if invoke[0] != 'invoke': + raise Unsupported('only direct component export invocations') + expected = ('trap:' + json.loads(node[2]) if node[0] == 'assert_trap' + else str(int(node[2][1], 0)) if len(node) > 2 else 'void') + cases.append({'module': binary.name, 'export': json.loads(exports[invoke[1]]), + 'expected': expected, 'args': [1 if x == ['bool.const', 'true'] else 0 if x == ['bool.const', 'false'] else int(x[1], 0) for x in invoke[2:]]}) + else: + raise Unsupported('unsupported script directive ' + node[0]) + manifest = {'source': str(source), 'sha256': hashlib.sha256(source.read_bytes()).hexdigest(), + 'profile': 'single-memory-static-numeric-u8-unit', 'cases': cases} + (out / 'cases.json').write_text(json.dumps(manifest, indent=2) + '\n') + return manifest + + +if __name__ == '__main__': + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('source', type=Path) + p.add_argument('output', type=Path) + p.add_argument('--validator', required=True) + a = p.parse_args() + compile_script(a.source, a.output, a.validator) diff --git a/crates/c-api/examples/stackless-http/conformance.csv b/crates/c-api/examples/stackless-http/conformance.csv new file mode 100644 index 0000000..5dd8921 --- /dev/null +++ b/crates/c-api/examples/stackless-http/conformance.csv @@ -0,0 +1,22 @@ +component_model_revision,file,source_sha256,assertion,export,args,expected,scope,mac-wamr-release,win-wamr-release,mac-wamr-asan,win-wamr-asan,mac-tinywasm-release,win-tinywasm-release,mac-tinywasm-asan,win-tinywasm-asan +fed10b0b823b82553dd8363f16a640816f5eac65,future-completion-order.wast,a4312e5a4e2fb354f9b213c6623a30f4913c4008779f36e021e3000f8b4972c0,1,writer-completes-inline,[],42,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,future-completion-order.wast,a4312e5a4e2fb354f9b213c6623a30f4913c4008779f36e021e3000f8b4972c0,2,reader-completes-inline,[],42,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,1,test-empty,[],42,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,2,test-bytes,[],43,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,3,test-no-read-char,[],trap:cannot read from and write to intra-component future,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,4,test-no-write-char,[],trap:cannot read from and write to intra-component future,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,5,test-no-copy-bool,"[1, 0, 0]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,6,test-no-copy-bool,"[1, 0, 1]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,7,test-no-copy-bool,"[1, 1, 0]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,8,test-no-copy-bool,"[1, 1, 1]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,9,test-no-copy-bool,"[0, 0, 0]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,10,test-no-copy-bool,"[0, 0, 1]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,11,test-no-copy-bool,"[0, 1, 0]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,same-component-stream-future.wast,cdfe74da64e77a5b348d7b5161c5991a3b78933a69e29652c97c07706127bfc2,12,test-no-copy-bool,"[0, 1, 1]",trap:cannot read from and write to intra-component future/stream with non-numeric payload,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,1,"(assert_return (invoke ""drop-readable-future-before-read"") (u32.const 42))",[],,unsupported component field: component,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED +fed10b0b823b82553dd8363f16a640816f5eac65,futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,2,"(assert_trap (invoke ""drop-writable-future-before-write"") ""cannot drop future write end without first writing a value"")",[],,successive assertions on one live instance outside profile,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED +fed10b0b823b82553dd8363f16a640816f5eac65,futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,3,cancel-write-after-completion,[],42,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,4,cancel-read-after-completion,[],42,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,5,cancel-write-with-nothing-written,[],42,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,6,drop-writable-after-cancelled-write,[],trap:cannot drop future write end without first writing a value,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS +fed10b0b823b82553dd8363f16a640816f5eac65,futures-must-write.wast,f9574e013de0db6da1ce2fbbb295526da87f51bc5214c60354ce61700b18f5c3,7,cancel-write-after-readable-dropped,[],42,static single-memory component,PASS,PASS,PASS,PASS,PASS,PASS,PASS,PASS diff --git a/crates/c-api/examples/stackless-http/embedded-wamr.cmake b/crates/c-api/examples/stackless-http/embedded-wamr.cmake new file mode 100644 index 0000000..44a44e3 --- /dev/null +++ b/crates/c-api/examples/stackless-http/embedded-wamr.cmake @@ -0,0 +1,21 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +set(WAMR_BUILD_PLATFORM embedded) +set(WAMR_BUILD_TARGET THUMBV8M.MAIN) +set(WAMR_BUILD_INTERP 1) +set(WAMR_BUILD_FAST_INTERP 1) +set(WAMR_BUILD_AOT 0) +set(WAMR_BUILD_JIT 0) +set(WAMR_BUILD_FAST_JIT 0) +set(WAMR_BUILD_LIBC_WASI 0) +set(WAMR_BUILD_LIBC_BUILTIN 0) +set(WAMR_BUILD_LIB_PTHREAD 0) +set(WAMR_BUILD_THREAD_MGR 0) +set(WAMR_BUILD_SIMD 0) +set(WAMR_BUILD_REF_TYPES 0) +set(WAMR_BUILD_SHRUNK_MEMORY 0) +set(WAMR_BUILD_C_API_CONTINUATIONS 0) +set(WAMR_DISABLE_HW_BOUND_CHECK 1) +set(WAMR_BUILD_MEMORY_PROFILING 1) +set(SHARED_PLATFORM_CONFIG ${CMAKE_CURRENT_LIST_DIR}/pico/wamr-platform.cmake) +include(${WAMR_ROOT}/build-scripts/runtime_lib.cmake) diff --git a/crates/c-api/examples/stackless-http/guest.c b/crates/c-api/examples/stackless-http/guest.c new file mode 100644 index 0000000..75de8aa --- /dev/null +++ b/crates/c-api/examples/stackless-http/guest.c @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +/* Freestanding wasm32 guest. The build wraps these imports/exports in a + * validated Component Model component with canonical stackless lifting. */ +#include +#include +#define IMPORT(name) __attribute__((import_module("env"), import_name(name))) +#define EXPORT(name) __attribute__((export_name(name))) +#define BLOCKED UINT32_MAX +#define CHUNK 1024U +#define CONNECTIONS 4U + +IMPORT("stream.new") uint64_t stream_new(void); +IMPORT("stream.read") uint32_t stream_read(uint32_t, uint32_t, uint32_t); +IMPORT("stream.write") uint32_t stream_write(uint32_t, uint32_t, uint32_t); +IMPORT("stream.cancel-read") uint32_t cancel_read(uint32_t); +IMPORT("stream.drop-readable") void drop_read(uint32_t); +IMPORT("stream.drop-writable") void drop_write(uint32_t); +IMPORT("future.new") uint64_t future_new(void); +IMPORT("future.write") uint32_t future_write(uint32_t, uint32_t); +IMPORT("future.drop-writable") void drop_future(uint32_t); +IMPORT("waitable-set.new") uint32_t set_new(void); +IMPORT("waitable-set.drop") void set_drop(uint32_t); +IMPORT("waitable.join") void join(uint32_t, uint32_t); +IMPORT("task.return") void task_return(uint32_t, uint32_t); +IMPORT("task.cancel") void task_cancel(void); +IMPORT("context.get") uint32_t context_get(void); +IMPORT("context.set") void context_set(uint32_t); + +enum { HEADERS = 1, RESPONSE_HEADER, RESPONSE_BODY, FINISH }; +typedef struct { + uint32_t set, rx, tx, done, phase, count, length, offset; + uint32_t header_length; + const char *header; + uint8_t result; + uint8_t input[2 * CHUNK], output[CHUNK]; +} Request; +static Request requests[CONNECTIONS]; +static const char small_header[] = + "HTTP/1.1 200 OK\r\nContent-Length: 15\r\nConnection: close\r\n\r\n"; +static const char large_header[] = + "HTTP/1.1 200 OK\r\nContent-Length: 65536\r\nConnection: close\r\n\r\n"; +static const char bad_header[] = "HTTP/1.1 400 Bad Request\r\nContent-Length: " + "0\r\nConnection: close\r\n\r\n"; +static const char long_header[] = + "HTTP/1.1 431 Request Header Fields Too Large\r\nContent-Length: " + "0\r\nConnection: close\r\n\r\n"; +static const char small_body[] = "stackless wasm\n"; + +static uint32_t +ptr(const void *p) +{ + return (uint32_t)(uintptr_t)p; +} +static uint32_t +wait_for(Request *r) +{ + return 2U | (r->set << 4); +} +static uint32_t +exit_request(Request *r) +{ + if (r->done) + drop_future(r->done); + set_drop(r->set); + r->set = r->phase = r->rx = r->tx = r->done = 0; + return 0; +} +static uint32_t +finish(Request *r, uint8_t error) +{ + drop_write(r->tx); + r->tx = 0; + r->result = error; + r->phase = FINISH; + if (future_write(r->done, ptr(&r->result)) == BLOCKED) + return wait_for(r); + return exit_request(r); +} +static uint32_t +emit_response(Request *r, uint32_t previous) +{ + /* At most one bounded chunk per callback. This native adapter only takes + * bytes when it has socket capacity, so a fresh write parks at WAIT. */ + if ((previous & 15U) == 1) + return finish(r, 1); + r->offset += previous >> 4; + if (r->phase == RESPONSE_HEADER && r->offset == r->header_length) { + r->phase = RESPONSE_BODY; + r->offset = 0; + } + uint32_t status; + if (r->phase == RESPONSE_HEADER) + status = stream_write(r->tx, ptr(r->header + r->offset), + r->header_length - r->offset); + else if (r->offset == r->length) + return finish(r, 0); + else { + uint32_t n = r->length - r->offset; + if (n > CHUNK) + n = CHUNK; + if (r->length == sizeof(small_body) - 1) + status = stream_write(r->tx, ptr(small_body + r->offset), n); + else { + for (uint32_t i = 0; i < n; ++i) + r->output[i] = (uint8_t)('a' + ((r->offset + i) % 26)); + status = stream_write(r->tx, ptr(r->output), n); + } + } + if (status == BLOCKED) + return wait_for(r); + if ((status & 15U) == 1) + return finish(r, 1); + /* Immediate completion is legal with another adapter. Yield rather than + * monopolizing a host turn on an arbitrarily large body. */ + r->offset += status >> 4; + return 1U | (r->set << 4); +} +static uint32_t +respond(Request *r, const char *header, uint32_t header_len, uint32_t length) +{ + drop_read(r->rx); + r->rx = 0; + uint64_t body = stream_new(), done = future_new(); + r->tx = (uint32_t)(body >> 32); + r->done = (uint32_t)(done >> 32); + join(r->tx, r->set); + join(r->done, r->set); + r->header = header; + r->header_length = header_len; + r->length = length; + r->offset = 0; + r->phase = RESPONSE_HEADER; + /* The lifted call resolves before either returned endpoint completes. */ + task_return((uint32_t)body, (uint32_t)done); + return emit_response(r, 0); +} +static uint32_t +receive_headers(Request *r, uint32_t status) +{ + if ((status & 15U) == 1) + return respond(r, bad_header, sizeof(bad_header) - 1, 0); + uint32_t before = r->count; + r->count += status >> 4; + for (uint32_t i = before > 3 ? before - 3 : 0; i + 3 < r->count; ++i) + if (r->input[i] == '\r' && r->input[i + 1] == '\n' + && r->input[i + 2] == '\r' && r->input[i + 3] == '\n') { + const uint8_t *a = r->input; + if (i < 6 || a[0] != 'G' || a[1] != 'E' || a[2] != 'T' + || a[3] != ' ' || a[4] != '/') + return respond(r, bad_header, sizeof(bad_header) - 1, 0); + if (i >= 11 && a[5] == 'l' && a[6] == 'a' && a[7] == 'r' + && a[8] == 'g' && a[9] == 'e' && a[10] == ' ') + return respond(r, large_header, sizeof(large_header) - 1, + 65536); + return respond(r, small_header, sizeof(small_header) - 1, + sizeof(small_body) - 1); + } + if (r->count == sizeof(r->input)) + return respond(r, long_header, sizeof(long_header) - 1, 0); + status = stream_read(r->rx, ptr(r->input + r->count), + sizeof(r->input) - r->count); + if (status != BLOCKED) + __builtin_trap(); /* This profile supplies input only after WAIT. */ + return wait_for(r); +} +EXPORT("handle") uint32_t handle(uint32_t slot, uint32_t input) +{ + if (slot >= CONNECTIONS || requests[slot].phase) + __builtin_trap(); + Request *r = &requests[slot]; + context_set(slot); + r->phase = HEADERS; + r->count = 0; + r->set = set_new(); + r->rx = input; + join(input, r->set); + return receive_headers(r, 0); +} +EXPORT("handle-event") +uint32_t handle_event(uint32_t code, uint32_t endpoint, uint32_t payload) +{ + /* Task-specific context is supplied by the canonical context slot. */ + uint32_t slot = context_get(); + if (slot >= CONNECTIONS) + __builtin_trap(); + Request *r = &requests[slot]; + if (code == 6 && r->phase == HEADERS) { + cancel_read(r->rx); + drop_read(r->rx); + task_cancel(); + return exit_request(r); + } + if (code == 2 && endpoint == r->rx && r->phase == HEADERS) + return receive_headers(r, payload); + if ((code == 3 && endpoint == r->tx) || (code == 0 && r->tx)) + return emit_response(r, payload); + if (code == 5 && endpoint == r->done && r->phase == FINISH) + return exit_request(r); + __builtin_trap(); +} +EXPORT("native-loop") uint32_t native_loop(uint32_t iterations) +{ + uint32_t total = 0; + for (uint32_t i = 0; i < iterations; ++i) + total += context_get(); + return total; +} diff --git a/crates/c-api/examples/stackless-http/handler.component.wasm b/crates/c-api/examples/stackless-http/handler.component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..9cbd93271636f83a8600586f8335179aca09d378 GIT binary patch literal 3651 zcmc&%&2Jk;6n}4Kzv8vy%}1e1ix?ZErXNn5exy<)P^MK|sur~>{R3i>Z5zk2Q+wOc zL+gkWN3PtFDiDWWxm8FAPN+x-i4*DpapA-fA;fQHca4{%DhR1KQs(WO_cd>R@9kS^ z_BSfT#0Llz6OrpMe!kCgBi;~1<4&i!5tHw|9dC4d_k?4F_?tbsJRcof<^e4e50=Hi z5)YQ;U@*Wln}0-x55fTxtN2k&$hdRYgMDKV-ppgT((NSid&yisPKXRA+G?&P&3D^z z1FX4DyhT>9lC$Sp-L(!0BRbtrdU11OZnf9lY=BxdYN?e{wq^9CkuBZ0licaW)}UpC zNwdE`*Nef;%!Qebu|!TGx|@j3hB2ARtTa0-aoaj2*NR$mDVsbSUFOk7x7AAzl}0y_ zAI6^12&DYNfr={S%$niiUW|is7E8)++2H;7w6BMnFBR!(sqa5GG@7)@)_8{sNi`2Da6LcwNyMIxY6%5#A#u z9xC}T@~r={ICoohcR@^xB~8lF_gi&_2j|khx}#-FxmPMAmk=s8>)ZnK zV5>o-D<)`1AF>an^x^R6&XM7c%^hp+qB&-R4(hMSp147W^p`n#Sbvw3N3_cJ8~W&Z zWY`4z;*x{2vJSIg=kzamSV#5KoIIv~&B=NFb50)DKjq|!>$=Z!2b;q-)D$;QhP2U%%O3`ZVT57=gM7=d_Az& z0DNgD(V|xc)PHc<-vQhps^DzANV8brGr-~ib}*VkRL{1e@tx+f{W_nsXH7Fe5ip%u zF&M^^S-{G1yFWuMkiFkv%i;@;A}z9j6)+5%HJLgx#`a^82q{c`?o9=N$61j^PcI;P z1hrn5dOJ|&-^G3Q`l~=V;>5KhG^zQ#tt-A=bJ0=&5s51Mqk6mM86hQVzC>0PDlyQX zMlEQ}Yyz0AMIsgGPr!kY^7Twd&$QQD`IfImtL8%*i&P#mC{XA~SEyXm#E>4VYt>tS z*|eZE_i1*Ws9NK)$a%A9Gr}sY+l{nP1BCsko(Y?D^R5?P=zRYVfGjF9baOA>v1d?@e!5E|HvL|iE3!qGflmQ^!*1+6RPL}Fhre<5eu)Br!$H5r zHQ^0o-X}F_V3GbQ8ux|Ejw{Ixl{9$A1v-XuqOqBDl~D;P2di8sQ&kk9s_0& z{l415po_y-(z(`iFPOCXp}fKOW4_-N#6jeT1A6tfDH^9eJVt`NF(#zmjKsl|UnSv5 zDh+6Y*xkqV>B7FxFEjReRLAGY=!g-X_*e*@$cJEA9DjEieg8i87AC~u$SX(3U1xOE zbqiyzR~T{qp?5Rh2D^>6W2t&J)|RGs9cjZ|#@TWq(BUXsDUPw@#Sykj=|{mN`_p_@ zIQ@1vx##y%h!axziXcLkv!NDbVXO^sU6>C&e?o?3rc4Yx`c{mubT>A;9aACZ!-n67 zrHFXhsjCBxP>| zNjF`47&UwS{w8~~;&g^-caSn&`Ml;IW%fyEnSK0>{TZ;}4hJL_#iC(W-mEe#Ur+Z! z(GX9=D^-pIhG}L5bmEXL@ykMMUKfTLz91A9u^>Pjp~16EPX^z3LOc?%X}GOzS$>U- zHiXiW4nRpO($`)nm&~W{iU~3TYA=MvGECtxBWn#3<+S}4ng#}T&4PH#uqAd`Ok)f0 z+K4?P_jJkmKVCz?q&5{Q1fU+qp|>EW#G++J*-zq-!-KkcDpXd+#=Bi*auK0I81fj* zGjWqKTcx0tJ?l1=Y;*2;$@#Iw)sEZJ4N9}g&e-pn%ecwT+V2YToc*yN&s&qrn|Agp zDonw?zTzNPZi777mi?^=Yt}w1$T|CaLALF$1$n{#Qjqg`3ihe7@7Ud4rKhOs_OrU} z*qYmL%j3Gc02CSCf-KEChkwuRYAON$Z;1FQN5s?mD-l6St0Mo#9ZiO(6 z9W7uLxILYt7RWwL*mC%SBW*<`ZSxE=0Z+aoxvhEYg%vL4ig`xdP+S5JP#Ku2YZqZ8}iW zeUen^TAfdbg4e*95l&&<39?2lBkadvewfP5UwH8cCg1-9Acu+^-Fy)2U?}s^p;?0o zjzR2=2eER4xI`$&Jbl3+rg?AZid@s>;4=W!2 zCv{1%$o>sG>I;u6PcknslJJfTR75F#WF}jsuAt;#O@xfPifMEew$xQJ>nfRbm0Pl| z;!gKkRnqFLg2H<>iH-xOe5EQTWB_21U9?qpOo5{79XIbka#N;(4%CtT<3V%$v{n%c zz{LYv_4kG8g>VOp)c zM;~oT+PoDd_tOU*Gv`=Iw34p%la5*G^@mZlQmwE9e`9+WVDI+L!)6vPU7BBbxv5v` zw=cb18Ig?jFV44%T+DTR$VCHlbuH?x4$a+u-`wgB?j!ntpCi6*R@QsbaA) -> tuple, future>; +} diff --git a/crates/c-api/examples/stackless-http/http.c b/crates/c-api/examples/stackless-http/http.c new file mode 100644 index 0000000..4192c04 --- /dev/null +++ b/crates/c-api/examples/stackless-http/http.c @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "http.h" +#include + +bool +http_open(http_server *server, unsigned slot) +{ + if (slot >= HTTP_CONNECTIONS) + return false; + http_connection *c = &server->connections[slot]; + if (c->live) + return false; + memset(c, 0, sizeof(*c)); + uint64_t ends; + if (!p3_call(&server->vm.async, P3_NEW, false, 1, true, 0, 0, 0, &ends)) + return false; + c->input = (uint32_t)(ends >> 32); + if (!p3_host_end(&server->vm.async, c->input)) + return false; + int32_t args[] = { (int32_t)slot, (int32_t)ends }; + p3_task_init(&c->task, &server->vm, "handle-event"); + c->live = true; + return p3_task_start(&c->task, "handle", args, 2); +} +bool +http_input(http_server *server, unsigned slot, uint8_t *data, uint32_t size) +{ + uint32_t result; + return p3_native_transfer(&server->vm.async, + server->connections[slot].input, data, size, + &result) + && result == (size << 4); +} +static bool +close_end(p3_context *cx, uint32_t *end) +{ + if (*end && !p3_native_drop(cx, *end)) + return false; + *end = 0; + return true; +} +bool +http_end_input(http_server *server, unsigned slot) +{ + return close_end(&server->vm.async, &server->connections[slot].input); +} +bool +http_disconnect(http_server *server, unsigned slot) +{ + http_connection *c = &server->connections[slot]; + if (c->disconnected) + return true; + c->disconnected = true; + c->sent = c->length = 0; + if (!c->task.returned && !c->task.exited) + (void)p3_task_request_cancel(&c->task); + return http_end_input(server, slot) + && close_end(&server->vm.async, &c->task.body); +} +bool +http_progress(http_server *server, unsigned slot) +{ + p3_context *cx = &server->vm.async; + http_connection *c = &server->connections[slot]; + /* Bound the callbacks per connection per turn. Ready connections stay + * eligible while the transport services other sockets. */ + for (unsigned i = 0; i < 4 && c->live; ++i) { + bool progress = false; + if (p3_task_ready(&c->task)) { + if (!p3_task_step(&c->task)) + return false; + ++server->callbacks; + progress = true; + } + if (c->task.body && c->sent == c->length && !c->disconnected + && p3_native_capacity(cx, c->task.body)) { + uint32_t result; + if (!p3_native_transfer(cx, c->task.body, c->output, + sizeof(c->output), &result) + || (result & 15U)) + return false; + c->sent = 0; + c->length = result >> 4; + progress = true; + } + if (c->task.completion && p3_native_capacity(cx, c->task.completion)) { + uint32_t result; + if (!p3_native_transfer(cx, c->task.completion, &c->outcome, 1, + &result) + || result || !close_end(cx, &c->task.completion)) + return false; + c->done = true; + progress = true; + } + if (!progress) + break; + } + return true; +} +bool +http_finished(const http_connection *c) +{ + return c->live && c->task.exited && c->sent == c->length; +} +bool +http_reap(http_server *server, unsigned slot) +{ + http_connection *c = &server->connections[slot]; + if (!http_finished(c) || c->task.failed || (c->task.returned && !c->done)) + return false; + if (!http_end_input(server, slot) + || !close_end(&server->vm.async, &c->task.body)) + return false; + if (c->task.cancelled) + ++server->cancelled; + else if (c->disconnected) + ++server->disconnected; + else if (c->outcome == 0) + ++server->completed; + else + return false; + c->live = false; + return true; +} +bool +http_native_loop(http_server *server, int32_t iterations) +{ + if (server->vm.current || iterations <= 0) + return false; + p3_task probe = { .vm = &server->vm, .context = 1 }; + int32_t result; + bool has_result; + server->vm.current = &probe; + bool ok = p3_vm_call(&server->vm, "native-loop", &iterations, 1, &result, + &has_result); + server->vm.current = NULL; + return ok && has_result && result == iterations; +} diff --git a/crates/c-api/examples/stackless-http/http.h b/crates/c-api/examples/stackless-http/http.h new file mode 100644 index 0000000..6dabf7f --- /dev/null +++ b/crates/c-api/examples/stackless-http/http.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_HTTP_H +#define STACKLESS_HTTP_H +#include "vm.h" +#define HTTP_CONNECTIONS 4 +#define HTTP_CHUNK 1024 +typedef struct http_connection { + p3_task task; + uint32_t input; + size_t sent, length; + bool live, disconnected, done; + uint8_t outcome, output[HTTP_CHUNK]; +} http_connection; +typedef struct http_server { + p3_vm vm; + http_connection connections[HTTP_CONNECTIONS]; + uint32_t completed, cancelled, disconnected, callbacks; +} http_server; +bool +http_open(http_server *server, unsigned slot); +bool +http_input(http_server *server, unsigned slot, uint8_t *data, uint32_t size); +bool +http_end_input(http_server *server, unsigned slot); +bool +http_disconnect(http_server *server, unsigned slot); +bool +http_progress(http_server *server, unsigned slot); +bool +http_finished(const http_connection *connection); +bool +http_reap(http_server *server, unsigned slot); +bool +http_native_loop(http_server *server, int32_t iterations); +#endif diff --git a/crates/c-api/examples/stackless-http/p3.c b/crates/c-api/examples/stackless-http/p3.c new file mode 100644 index 0000000..dcc8282 --- /dev/null +++ b/crates/c-api/examples/stackless-http/p3.c @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "p3.h" +#include + +enum { FREE, STREAM_RX, STREAM_TX, FUTURE_RX, FUTURE_TX, SET }; +enum { IDLE, COPYING, DONE }; +_Static_assert(P3_HANDLES > 2 && P3_HANDLES <= 255, "handle index width"); +static bool +fail(p3_context *cx, const char *message) +{ + cx->error = message; + return false; +} +static uint32_t +index_of(uint32_t h) +{ + return h - 1U; +} +static uint32_t +handle(p3_context *cx, p3_slot *s) +{ + return (uint32_t)(s - cx->slots) + 1U; +} +static p3_slot * +get(p3_context *cx, uint32_t h) +{ + uint32_t i = index_of(h); + if (i >= P3_HANDLES || cx->slots[i].kind == FREE) { + fail(cx, "invalid or stale async handle"); + return NULL; + } + return &cx->slots[i]; +} +static p3_slot * +allocate(p3_context *cx, unsigned kind) +{ + if (cx->free_count || cx->allocated < P3_HANDLES) { + unsigned i = + cx->free_count ? cx->free_slots[--cx->free_count] : cx->allocated++; + p3_slot *s = &cx->slots[i]; + memset(s, 0, sizeof(*s)); + s->kind = (uint8_t)kind; + if (++cx->live > cx->peak) + cx->peak = cx->live; + return s; + } + fail(cx, "async handle capacity exhausted"); + return NULL; +} +static void +release(p3_context *cx, p3_slot *s) +{ + s->kind = FREE; + cx->free_slots[cx->free_count++] = (uint16_t)(s - cx->slots); + --cx->live; +} +static void +unqueue(p3_context *cx, p3_slot *s) +{ + if (!s->queued) + return; + p3_slot *set = get(cx, s->set); + if (s->prev) + get(cx, s->prev)->next = s->next; + else + set->head = s->next; + if (s->next) + get(cx, s->next)->prev = s->prev; + else + set->tail = s->prev; + s->prev = s->next = 0; + s->queued = 0; +} +static void +enqueue(p3_context *cx, p3_slot *s) +{ + if (!s->set || !s->pending || s->queued) + return; + p3_slot *set = get(cx, s->set); + bool was_empty = set->head == 0; + s->prev = set->tail; + if (set->tail) + get(cx, set->tail)->next = handle(cx, s); + else + set->head = handle(cx, s); + set->tail = handle(cx, s); + s->queued = 1; + if (was_empty && cx->wake) + cx->wake(cx->wake_env, s->set); +} +static void +notify(p3_context *cx, p3_slot *s) +{ + s->pending = 1; + enqueue(cx, s); +} +static uint32_t +consume(p3_context *cx, p3_slot *s) +{ + unsigned result; + unqueue(cx, s); + s->pending = s->buffer = 0; + if (s->kind >= FUTURE_RX && s->progress == 1) { + result = + 0; /* Completion wins over a later peer drop or cancellation. */ + s->state = DONE; + } + else if (!s->peer) { + result = 1; + s->state = DONE; + } + else { + result = s->cancelling ? 2 : 0; + s->state = IDLE; + } + s->cancelling = 0; + return s->kind >= FUTURE_RX ? result : result | (s->progress << 4); +} +static uint8_t * +range(p3_context *cx, uint32_t ptr, uint32_t length) +{ + size_t size = 0; + uint8_t *data = cx->memory ? cx->memory(cx->memory_env, &size) : NULL; + if (!data || ptr > size || length > size - ptr) { + fail(cx, "canonical buffer out of bounds"); + return NULL; + } + return data + ptr; +} +void +p3_init(p3_context *cx) +{ + memset(cx, 0, sizeof(*cx)); +} +bool +p3_set_ready(p3_context *cx, uint32_t h) +{ + p3_slot *s = get(cx, h); + return s && s->kind == SET && s->head != 0; +} +bool +p3_waiter(p3_context *cx, uint32_t h, bool add) +{ + p3_slot *s = get(cx, h); + if (!s || s->kind != SET || (add ? s->waiters == UINT16_MAX : !s->waiters)) + return fail(cx, "invalid waitable set waiter"); + if (add) + ++s->waiters; + else + --s->waiters; + return true; +} +bool +p3_take_event(p3_context *cx, uint32_t h, p3_event *event) +{ + p3_slot *set = get(cx, h); + if (!set || set->kind != SET) + return fail(cx, "invalid waitable set"); + memset(event, 0, sizeof(*event)); + if (set->head) { + p3_slot *s = get(cx, set->head); + event->index = handle(cx, s); + event->code = s->event; + event->payload = consume(cx, s); + } + return true; +} +bool +p3_host_end(p3_context *cx, uint32_t h) +{ + p3_slot *s = get(cx, h); + if (!s || s->kind == SET) + return false; + s->native = 1; + return true; +} +bool +p3_lift_pair(p3_context *cx, uint32_t stream, uint32_t future) +{ + p3_slot *body = get(cx, stream), *done = get(cx, future); + if (!body || !done || body->kind != STREAM_RX || done->kind != FUTURE_RX + || body->native || done->native || body->set || done->set + || body->state != IDLE || done->state != IDLE || body->element != 1 + || done->element != 1) + return fail(cx, "cannot lift response stream/future endpoints"); + body->native = done->native = 1; + return true; +} +bool +p3_native_drop(p3_context *cx, uint32_t h) +{ + p3_slot *s = get(cx, h); + if (!s || !s->native) + return false; + s->native = 0; + uint64_t ignored; + enum p3_op op = s->kind == STREAM_TX || s->kind == FUTURE_TX ? P3_DROP_WRITE + : P3_DROP_READ; + return p3_call(cx, op, s->kind >= FUTURE_RX, s->element, true, h, 0, 0, + &ignored); +} +uint32_t +p3_native_capacity(p3_context *cx, uint32_t h) +{ + p3_slot *s = get(cx, h), *peer; + if (!s || !s->native || !s->peer) + return 0; + peer = get(cx, s->peer); + return peer && peer->buffer && !peer->pending + ? peer->length - peer->progress + : 0; +} +bool +p3_native_transfer(p3_context *cx, uint32_t h, uint8_t *bytes, uint32_t length, + uint32_t *result) +{ + p3_slot *s = get(cx, h), *peer; + if (!s || !s->native || s->state != IDLE || s->element != 1) + return fail(cx, "invalid native endpoint"); + if (s->kind >= FUTURE_RX && length != 1) + return fail(cx, "native future requires one byte"); + *result = P3_BLOCKED; + if (!s->peer) { + *result = 1; + s->state = DONE; + return true; + } + peer = get(cx, s->peer); + if (!peer->buffer || peer->pending) + return true; + uint32_t n = peer->length - peer->progress; + if (n > length) + n = length; + uint8_t *guest = range(cx, peer->ptr, n); + if (!guest || (!bytes && n)) + return false; + bool write = s->kind == STREAM_TX || s->kind == FUTURE_TX; + if (n) + memcpy(write ? guest : bytes, write ? bytes : guest, n); + peer->ptr += n; + peer->progress += n; + peer->buffer = 0; + cx->bytes += n; + ++cx->transfers; + notify(cx, peer); + if (s->kind >= FUTURE_RX) { + *result = 0; + s->state = DONE; + } + else + *result = n << 4; + return true; +} +bool +p3_call(p3_context *cx, enum p3_op op, bool future, uint8_t element, + bool async_, uint32_t a, uint32_t b, uint32_t c, uint64_t *out) +{ + p3_slot *s, *other; + unsigned kind = future ? FUTURE_RX : STREAM_RX; + cx->error = NULL; + *out = 0; + if (element != 0 && element != 1 && element != 2 && element != 4) + return fail(cx, "unsupported canonical element type"); + if (op == P3_NEW || op == P3_SET_NEW) { + s = allocate(cx, op == P3_SET_NEW ? SET : kind); + if (!s) + return false; + if (op == P3_SET_NEW) { + *out = handle(cx, s); + return true; + } + other = allocate(cx, kind + 1); + if (!other) { + release(cx, s); + return false; + } + s->element = other->element = element; + s->peer = handle(cx, other); + other->peer = handle(cx, s); + s->event = future ? 4 : 2; + other->event = future ? 5 : 3; + *out = handle(cx, s) | ((uint64_t)handle(cx, other) << 32); + return true; + } + s = get(cx, a); + if (!s) + return false; + if (s->native) + return fail(cx, "native endpoint is not guest-owned"); + if (op == P3_JOIN) { + if (s->kind == SET) + return fail(cx, "waitable.join requires an endpoint"); + other = b ? get(cx, b) : NULL; + if (b && (!other || other->kind != SET)) + return fail(cx, "invalid waitable set"); + unqueue(cx, s); + s->set = b; + enqueue(cx, s); + return true; + } + if (op == P3_SET_DROP || op == P3_SET_POLL || op == P3_SET_WAIT) { + if (s->kind != SET) + return fail(cx, "invalid waitable set"); + if (op == P3_SET_DROP) { + if (s->waiters) + return fail(cx, "cannot drop waitable set with a waiting task"); + for (unsigned i = 0; i < P3_HANDLES; ++i) + if (cx->slots[i].kind && cx->slots[i].set == a) { + unqueue(cx, &cx->slots[i]); + cx->slots[i].set = 0; + } + release(cx, s); + return true; + } + if (op == P3_SET_WAIT && !s->head) + return fail(cx, "wait would block outside a stackless callback"); + if (b & 3U) + return fail(cx, "unaligned canonical event pointer"); + uint8_t *dst = range(cx, b, 8); + if (!dst) + return false; + uint32_t event[2] = { 0, 0 }; + if (s->head) { + other = get(cx, s->head); + event[0] = handle(cx, other); + *out = other->event; + event[1] = consume(cx, other); + } + /* Canonical memory is little endian on every native architecture. */ + for (unsigned i = 0; i < 8; ++i) + dst[i] = (uint8_t)(event[i / 4] >> ((i % 4) * 8)); + return true; + } + bool write = op == P3_WRITE || op == P3_CANCEL_WRITE || op == P3_DROP_WRITE; + if (s->kind != kind + (unsigned)write || s->element != element) + return fail(cx, "canonical endpoint type mismatch"); + if (op == P3_DROP_READ || op == P3_DROP_WRITE) { + if (s->state == COPYING) + return fail(cx, "cannot drop a transferring endpoint"); + if (s->kind == FUTURE_TX && s->state != DONE) + return fail( + cx, + "cannot drop future write end without first writing a value"); + unqueue(cx, s); + if (s->peer) { + other = get(cx, s->peer); + other->peer = 0; + if (other->state != DONE && !other->pending) + notify(cx, other); + } + release(cx, s); + return true; + } + if (!async_ && s->set) + return fail(cx, "synchronous transfer in a waitable set"); + if (op == P3_CANCEL_READ || op == P3_CANCEL_WRITE) { + if (s->state != COPYING) + return fail(cx, "cancel requires a transferring endpoint"); + s->cancelling = 1; + if (!s->pending) + notify(cx, s); + *out = consume(cx, s); + return true; + } + if (s->state != IDLE) + return fail(cx, "endpoint is not idle"); + if (future) + c = 1; + if (c > P3_MAX_TRANSFER) + return fail(cx, "canonical transfer exceeds maximum length"); + unsigned width = element == 4 ? 4 : element != 0; + if (width && (b & (width - 1))) + return fail(cx, "unaligned canonical buffer"); + if (width && !range(cx, b, c * width)) + return false; + s->ptr = b; + s->length = c; + s->progress = 0; + s->state = COPYING; + if (!s->peer) { + notify(cx, s); + } + else { + other = get(cx, s->peer); + if (!other->buffer) + s->buffer = 1; + else if (element > 1) + return fail(cx, "cannot read from and write to intra-component " + "future/stream with non-numeric payload"); + else if (c && other->length > other->progress) { + uint32_t n = c < other->length - other->progress + ? c + : other->length - other->progress; + if (element) { + uint8_t *mine = range(cx, b, n); + uint8_t *theirs = range(cx, other->ptr, n); + if (!mine || !theirs) + return false; + memmove(write ? theirs : mine, write ? mine : theirs, n); + } + s->progress = n; + other->progress += n; + other->ptr += n * element; + cx->bytes += (uint64_t)n * element; + ++cx->transfers; + notify(cx, s); + notify(cx, other); + if (other->length == other->progress) + other->buffer = 0; + } + else if (c || (!write && other->length == other->progress)) { + notify(cx, other); + other->buffer = 0; + s->buffer = 1; + } + else + notify(cx, s); + } + if (s->pending) + *out = consume(cx, s); + else if (async_) + *out = P3_BLOCKED; + else + return fail(cx, + "blocking canonical transfer requires stackful execution"); + return true; +} diff --git a/crates/c-api/examples/stackless-http/p3.h b/crates/c-api/examples/stackless-http/p3.h new file mode 100644 index 0000000..bc9fc78 --- /dev/null +++ b/crates/c-api/examples/stackless-http/p3.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_P3_H +#define STACKLESS_P3_H +#include +#include +#include + +/* Consumer-owned, bounded storage. No runtime-private types or scheduler. */ +#ifndef P3_HANDLES +#define P3_HANDLES 64 +#endif +#define P3_BLOCKED UINT32_MAX +#define P3_MAX_TRANSFER 0x0fffffffU +enum p3_op { + P3_NEW, + P3_READ, + P3_WRITE, + P3_CANCEL_READ, + P3_CANCEL_WRITE, + P3_DROP_READ, + P3_DROP_WRITE, + P3_SET_NEW, + P3_SET_POLL, + P3_SET_DROP, + P3_JOIN, + P3_SET_WAIT, + P3_TASK_RETURN, + P3_TASK_CANCEL, + P3_CONTEXT_GET, + P3_CONTEXT_SET +}; +typedef struct p3_slot { + uint32_t peer, set, prev, next, head, tail; + uint32_t ptr, length, progress; + uint8_t kind, state, element, event, queued, pending, buffer, cancelling; + uint8_t native; + uint16_t waiters; +} p3_slot; +typedef struct p3_context { + p3_slot slots[P3_HANDLES]; + uint8_t *(*memory)(void *env, size_t *size); + void (*wake)(void *env, uint32_t set); + void *memory_env, *wake_env; + const char *error; + uint32_t live, peak, transfers; + uint16_t free_slots[P3_HANDLES], free_count, allocated; + uint64_t bytes; +} p3_context; +void +p3_init(p3_context *cx); +/* Element 0 is unit and 1 is u8. Tags 2/4 identify bool/char, for which an + * intra-instance transfer traps. Future selects single-value semantics. + * All pointers are guest offsets, resolved afresh at actual transfer time. + * Returns false on a canonical trap; no guest-supplied C pointers are used. */ +bool +p3_call(p3_context *cx, enum p3_op op, bool future, uint8_t element, + bool async_, uint32_t a, uint32_t b, uint32_t c, uint64_t *result); +bool +p3_set_ready(p3_context *cx, uint32_t set); +bool +p3_waiter(p3_context *cx, uint32_t set, bool add); +typedef struct p3_event { + uint32_t code, index, payload; +} p3_event; +bool +p3_take_event(p3_context *cx, uint32_t set, p3_event *event); +bool +p3_host_end(p3_context *cx, uint32_t end); +bool +p3_lift_pair(p3_context *cx, uint32_t stream, uint32_t future); +/* Native buffers are borrowed only for this call and never retained. */ +bool +p3_native_transfer(p3_context *cx, uint32_t end, uint8_t *bytes, + uint32_t length, uint32_t *result); +uint32_t +p3_native_capacity(p3_context *cx, uint32_t end); +bool +p3_native_drop(p3_context *cx, uint32_t end); +#endif diff --git a/crates/c-api/examples/stackless-http/pico/CMakeLists.txt b/crates/c-api/examples/stackless-http/pico/CMakeLists.txt new file mode 100644 index 0000000..cf60a5a --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/CMakeLists.txt @@ -0,0 +1,31 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +cmake_minimum_required(VERSION 3.20) +set(PICO_BOARD pico2_w CACHE STRING "Board") +if(NOT PICO_SDK_PATH) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) +endif() +include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) +project(stackless_http_pico C CXX ASM) +set(CMAKE_C_STANDARD 11) +pico_sdk_init() + +if(NOT WAMR_ROOT OR NOT HANDLER_INCLUDE) + message(FATAL_ERROR "Set WAMR_ROOT and HANDLER_INCLUDE (generated handler.h directory)") +endif() +include(../embedded-wamr.cmake) + +add_executable(stackless_http ${WAMR_RUNTIME_LIB_SOURCE} ../p3.c ../vm.c ../task.c ../http.c pico_server.c) +target_include_directories(stackless_http PRIVATE . .. ${HANDLER_INCLUDE} + ${WAMR_ROOT}/core/iwasm/include) +target_compile_definitions(stackless_http PRIVATE P3_WAMR PICO_STACK_SIZE=4096 PICO_HEAP_SIZE=16384) +target_link_libraries(stackless_http PRIVATE pico_stdlib pico_cyw43_arch_lwip_threadsafe_background) +set_property(TARGET stackless_http PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +# GCC's LTO pass cannot see the SDK's ld --wrap references. Keep the wrappers +# referenced by the firmware visible to the link plugin. +foreach(symbol printf vprintf puts putchar getchar snprintf vsnprintf ceil floor trunc) + target_link_options(stackless_http PRIVATE "LINKER:-u,__wrap_${symbol}") +endforeach() +pico_enable_stdio_usb(stackless_http 1) +pico_enable_stdio_uart(stackless_http 1) +pico_add_extra_outputs(stackless_http) diff --git a/crates/c-api/examples/stackless-http/pico/lwipopts.h b/crates/c-api/examples/stackless-http/pico/lwipopts.h new file mode 100644 index 0000000..7efccb4 --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/lwipopts.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_LWIPOPTS_H +#define STACKLESS_LWIPOPTS_H +#define NO_SYS 1 +#define LWIP_SOCKET 0 +#define LWIP_NETCONN 0 +#define LWIP_IPV4 1 +#define LWIP_IPV6 0 +#define LWIP_TCP 1 +#define LWIP_UDP 1 +#define LWIP_DHCP 1 +#define LWIP_DNS 0 +#define LWIP_NETIF_HOSTNAME 1 +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_LINK_CALLBACK 1 +#define LWIP_SINGLE_NETIF 1 +#define MEM_ALIGNMENT 4 +#define MEM_SIZE (16 * 1024) +#define MEMP_NUM_TCP_PCB 6 +#define MEMP_NUM_TCP_PCB_LISTEN 1 +#define MEMP_NUM_TCP_SEG 24 +#define PBUF_POOL_SIZE 16 +#define PBUF_POOL_BUFSIZE 1536 +#define TCP_MSS 1460 +#define TCP_WND (4 * TCP_MSS) +#define TCP_SND_BUF (4 * TCP_MSS) +#define TCP_SND_QUEUELEN 16 +#define LWIP_STATS 1 +#define LWIP_STATS_DISPLAY 0 +#define LWIP_TIMEVAL_PRIVATE 0 +#endif diff --git a/crates/c-api/examples/stackless-http/pico/pico_server.c b/crates/c-api/examples/stackless-http/pico/pico_server.c new file mode 100644 index 0000000..a474db2 --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/pico_server.c @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "http.h" +#include "handler.h" +#include "pico/cyw43_arch.h" +#include "pico/stdlib.h" +#include "pico/sem.h" +#include "lwip/tcp.h" +#include "lwip/stats.h" +#include "wasm_export.h" +#include +#include +#define REQUEST_US UINT64_C(10000000) +int +pico_libc_peak(void); + +/* lwIP callbacks own only network slots. No guest execution or guest memory + * access occurs in IRQ context, under the lwIP lock, or on core 1. */ +typedef struct network_slot { + struct tcp_pcb *pcb; + struct pbuf *input; + uint16_t received; + uint32_t unacked; + uint64_t deadline; + bool reserved, closed; +} network_slot; +static network_slot network[HTTP_CONNECTIONS]; +static http_server server; +static semaphore_t wake; +static _Alignas(16) uint8_t runtime_pool[128 * 1024]; + +static void +signal_work(void) +{ + sem_release(&wake); +} +static void +link_changed(struct netif *n) +{ + (void)n; + signal_work(); +} +static void +network_error(void *env, err_t error) +{ + (void)error; + network_slot *s = env; + s->pcb = NULL; + s->closed = true; + signal_work(); +} +static err_t +network_sent(void *env, struct tcp_pcb *pcb, u16_t bytes) +{ + (void)pcb; + network_slot *s = env; + if (bytes > s->unacked) + panic("TCP ACK accounting"); + s->unacked -= bytes; + signal_work(); + return ERR_OK; +} +static err_t +network_received(void *env, struct tcp_pcb *pcb, struct pbuf *data, err_t error) +{ + (void)pcb; + network_slot *s = env; + if (!data || error != ERR_OK) { + if (data) + pbuf_free(data); + s->closed = true; + } + else if (s->input) + return ERR_MEM; /* lwIP retains it and applies backpressure. */ + else { + s->input = data; + s->received = 0; + } + signal_work(); + return ERR_OK; +} +static err_t +network_accept(void *env, struct tcp_pcb *pcb, err_t error) +{ + (void)env; + if (error != ERR_OK || !pcb) + return ERR_VAL; + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) { + network_slot *s = &network[i]; + if (s->reserved) + continue; + *s = (network_slot){ .pcb = pcb, + .reserved = true, + .deadline = time_us_64() + REQUEST_US }; + tcp_arg(pcb, s); + tcp_err(pcb, network_error); + tcp_recv(pcb, network_received); + tcp_sent(pcb, network_sent); + signal_work(); + return ERR_OK; + } + tcp_abort(pcb); + return ERR_ABRT; +} +static void +clear_input(network_slot *s) +{ + if (s->input) + pbuf_free(s->input); + s->input = NULL; +} +static void +abort_connection(network_slot *s) +{ + if (s->pcb) { + tcp_arg(s->pcb, NULL); + tcp_err(s->pcb, NULL); + tcp_recv(s->pcb, NULL); + tcp_sent(s->pcb, NULL); + tcp_abort(s->pcb); + } + s->pcb = NULL; + clear_input(s); +} +static bool +pump(unsigned i) +{ + network_slot *s = &network[i]; + http_connection *c = &server.connections[i]; + uint8_t data[HTTP_CHUNK]; + cyw43_arch_lwip_begin(); + bool reserved = s->reserved; + cyw43_arch_lwip_end(); + if (!reserved) + return false; + bool progress = false; + if (!c->live) { + if (!http_open(&server, i)) + panic("component start: %s", server.vm.error); + progress = true; + } + uint32_t limit = + c->input ? p3_native_capacity(&server.vm.async, c->input) : 0; + if (limit > sizeof(data)) + limit = sizeof(data); + cyw43_arch_lwip_begin(); + bool closed = s->closed || time_us_64() >= s->deadline; + uint16_t count = 0; + if (!closed && limit && s->input) { + count = pbuf_copy_partial(s->input, data, (u16_t)limit, s->received); + s->received += count; + tcp_recved(s->pcb, count); + if (s->received == s->input->tot_len) + clear_input(s); + } + if (closed) + abort_connection(s); + cyw43_arch_lwip_end(); + if (closed && !c->disconnected) { + if (!http_disconnect(&server, i)) + panic("component disconnect"); + progress = true; + } + if (count) { + if (!http_input(&server, i, data, count)) + panic("component input"); + progress = true; + } + uint32_t callbacks = server.callbacks; + if (!http_progress(&server, i)) + panic("component callback: %s", server.vm.error); + progress |= callbacks != server.callbacks; + cyw43_arch_lwip_begin(); + if (s->pcb && c->sent < c->length) { + size_t count = c->length - c->sent; + if (count > tcp_sndbuf(s->pcb)) + count = tcp_sndbuf(s->pcb); + if (count + && tcp_write(s->pcb, c->output + c->sent, (u16_t)count, + TCP_WRITE_FLAG_COPY) + == ERR_OK) { + c->sent += count; + s->unacked += count; + tcp_output(s->pcb); + progress = true; + } + } + bool finished = http_finished(c) && (!s->pcb || !s->unacked); + if (finished) { + if (s->pcb) { + /* Reject further request bytes after the bounded single request. */ + if (s->input) + tcp_recved(s->pcb, s->input->tot_len - s->received); + tcp_arg(s->pcb, NULL); + tcp_err(s->pcb, NULL); + tcp_recv(s->pcb, NULL); + tcp_sent(s->pcb, NULL); + if (tcp_close(s->pcb) != ERR_OK) + tcp_abort(s->pcb); + } + clear_input(s); + memset(s, 0, sizeof(*s)); + } + cyw43_arch_lwip_end(); + if (finished && !http_reap(&server, i)) + panic("component reap"); + return progress || finished || p3_task_ready(&c->task); +} +static void +read_line(char *text, size_t capacity) +{ + size_t length = 0; + bool overflow = false; + for (;;) { + int c = getchar(); + if (c == '\n' || c == '\r') { + if (length || overflow) + break; + continue; + } + if (c < 0) + continue; + if (length + 1 < capacity) + text[length++] = (char)c; + else + overflow = true; + } + if (overflow) + panic("Wi-Fi credential too long"); + text[length] = 0; +} +int +main(void) +{ + stdio_init_all(); + sem_init(&wake, 0, 1); + if (cyw43_arch_init()) + panic("Wi-Fi init"); + cyw43_arch_enable_sta_mode(); + char ssid[33], password[64]; + printf("Wi-Fi SSID (then password, input is not echoed):\n"); + read_line(ssid, sizeof(ssid)); + read_line(password, sizeof(password)); + cyw43_arch_lwip_begin(); + netif_set_status_callback(netif_default, link_changed); + cyw43_arch_lwip_end(); + if (cyw43_arch_wifi_connect_async(ssid, password, CYW43_AUTH_WPA2_AES_PSK)) + panic("Wi-Fi join"); + absolute_time_t deadline = make_timeout_time_ms(30000); + ip4_addr_t ip; + for (;;) { + cyw43_arch_lwip_begin(); + ip = *netif_ip4_addr(netif_default); + cyw43_arch_lwip_end(); + if (!ip4_addr_isany_val(ip)) + break; + if (!sem_acquire_block_until(&wake, deadline)) + panic("Wi-Fi DHCP timeout"); + } + MemAllocOption memory = { 0 }; + memory.pool.heap_buf = runtime_pool; + memory.pool.heap_size = sizeof(runtime_pool); + if (!p3_vm_open_with_engine( + &server.vm, wasm_engine_new_with_args(Alloc_With_Pool, &memory), + handler_wasm, sizeof(handler_wasm))) + panic("component load: %s", server.vm.error); + uint64_t benchmark_start = time_us_64(); + if (!http_native_loop(&server, 100000)) + panic("native call benchmark"); + printf("NATIVE imports=100000 elapsed_us=%llu\n", + time_us_64() - benchmark_start); + cyw43_arch_lwip_begin(); + struct tcp_pcb *listener = tcp_new_ip_type(IPADDR_TYPE_V4); + if (!listener || tcp_bind(listener, IP_ANY_TYPE, 8080) != ERR_OK) + panic("TCP bind"); + listener = tcp_listen(listener); + if (!listener) + panic("TCP listen"); + tcp_accept(listener, network_accept); + cyw43_arch_lwip_end(); + printf("LISTEN http://%s:8080\n", ip4addr_ntoa(&ip)); + uint64_t active = 0; + uint32_t wakes = 0; + uint64_t window = time_us_64(); + for (;;) { + (void)sem_try_acquire(&wake); + uint64_t start = time_us_64(); + bool progress = false; + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) + progress |= pump(i); + active += time_us_64() - start; + if (time_us_64() - window >= 10000000) { + mem_alloc_info_t usage = { 0 }; + (void)wasm_runtime_get_mem_alloc_info(&usage); + cyw43_arch_lwip_begin(); + unsigned net_peak = lwip_stats.mem.max; + cyw43_arch_lwip_end(); + printf("RESULT elapsed_us=%llu owner_active_us=%llu io_wakes=%u " + "wamr_pool_peak=%lu " + "lwip_heap_peak=%u libc_heap_peak=%d completed=%lu " + "cancelled=%lu live=%lu peak_handles=%lu\n", + time_us_64() - window, active, wakes, + (unsigned long)usage.highmark_size, net_peak, + pico_libc_peak(), (unsigned long)server.completed, + (unsigned long)server.cancelled, + (unsigned long)server.vm.async.live, + (unsigned long)server.vm.async.peak); + window = time_us_64(); + active = wakes = 0; + } + uint64_t until = window + 10000000; + cyw43_arch_lwip_begin(); + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) + if (network[i].pcb && network[i].deadline < until) + until = network[i].deadline; + cyw43_arch_lwip_end(); + if (!progress + && sem_acquire_block_until(&wake, from_us_since_boot(until))) + ++wakes; + } +} diff --git a/crates/c-api/examples/stackless-http/pico/platform_internal.h b/crates/c-api/examples/stackless-http/pico/platform_internal.h new file mode 100644 index 0000000..220a44a --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/platform_internal.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_PICO_PLATFORM_H +#define STACKLESS_PICO_PLATFORM_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +/* Only the foreground owner executes WAMR; IRQs only service lwIP. */ +typedef unsigned korp_tid; +typedef unsigned korp_mutex; +typedef unsigned korp_cond; +typedef unsigned korp_sem; +typedef unsigned korp_rwlock; +typedef int os_file_handle; +typedef int os_raw_file_handle; +typedef int os_poll_file_handle; +typedef unsigned os_nfds_t; +typedef void *os_dir_stream; +typedef struct timespec os_timespec; +os_file_handle +os_get_invalid_handle(void); +#define os_printf printf +#define os_vprintf vprintf +static inline unsigned +os_getpagesize(void) +{ + return 4096; +} +#endif diff --git a/crates/c-api/examples/stackless-http/pico/wamr-platform.c b/crates/c-api/examples/stackless-http/pico/wamr-platform.c new file mode 100644 index 0000000..320a4af --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/wamr-platform.c @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "platform_api_vmcore.h" +#include "platform_api_extension.h" +#ifndef P3_QEMU +#include "pico/stdlib.h" +#endif +#include "wasm_export.h" + +/* Override the SDK's weak newlib hook so incidental libc allocations cannot + * grow beyond the heap reservation included in the ELF's RAM budget. */ +static int libc_used, libc_peak; +void * +_sbrk(int increment) +{ + extern char __end__; + if (increment > PICO_HEAP_SIZE - libc_used || increment < -libc_used) { + errno = ENOMEM; + return (void *)-1; + } + char *previous = &__end__ + libc_used; + libc_used += increment; + if (libc_used > libc_peak) + libc_peak = libc_used; + return previous; +} +int +pico_libc_peak(void) +{ + return libc_peak; +} + +int +bh_platform_init(void) +{ + return 0; +} +void +bh_platform_destroy(void) +{ +} +void * +os_malloc(unsigned size) +{ + (void)size; + return NULL; +} +void * +os_realloc(void *p, unsigned size) +{ + (void)p; + (void)size; + return NULL; +} +void +os_free(void *p) +{ + (void)p; +} +uint64 +os_time_get_boot_us(void) +{ +#ifdef P3_QEMU + return 0; /* Timing is obtained from the emulator instruction trace. */ +#else + return time_us_64(); +#endif +} +uint64 +os_time_thread_cputime_us(void) +{ + return 0; +} /* No thread CPU clock. */ +korp_tid +os_self_thread(void) +{ + return 1; +} +uint8 * +os_thread_get_stack_boundary(void) +{ + extern uint8 __StackBottom; + return &__StackBottom; +} +int +os_mutex_init(korp_mutex *m) +{ + *m = 0; + return 0; +} +int +os_mutex_destroy(korp_mutex *m) +{ + (void)m; + return 0; +} +int +os_mutex_lock(korp_mutex *m) +{ + (void)m; + return 0; +} +int +os_mutex_unlock(korp_mutex *m) +{ + (void)m; + return 0; +} +int +os_thread_env_init(void) +{ + return 0; +} +void +os_thread_env_destroy(void) +{ +} +bool +os_thread_env_inited(void) +{ + return true; +} +void * +os_mmap(void *hint, size_t size, int prot, int flags, os_file_handle file) +{ + (void)hint; + (void)prot; + (void)flags; + (void)file; + /* Charge committed linear memory to the same fixed pool as metadata. */ + if (size > UINT32_MAX) + return NULL; + void *p = wasm_runtime_malloc((uint32)size); + if (p) + memset(p, 0, size); + return p; +} +void +os_munmap(void *p, size_t n) +{ + (void)n; + wasm_runtime_free(p); +} +void * +os_mremap(void *p, size_t old_n, size_t n) +{ + return os_mremap_slow(p, old_n, n); +} +int +os_mprotect(void *p, size_t n, int prot) +{ + (void)p; + (void)n; + (void)prot; + return 0; +} +os_file_handle +os_get_invalid_handle(void) +{ + return -1; +} diff --git a/crates/c-api/examples/stackless-http/pico/wamr-platform.cmake b/crates/c-api/examples/stackless-http/pico/wamr-platform.cmake new file mode 100644 index 0000000..ad3f853 --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/wamr-platform.cmake @@ -0,0 +1,4 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +include_directories(${CMAKE_CURRENT_LIST_DIR} ${WAMR_ROOT}/core/shared/platform/include) +set(PLATFORM_SHARED_SOURCE ${CMAKE_CURRENT_LIST_DIR}/wamr-platform.c) diff --git a/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt b/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt new file mode 100644 index 0000000..09a60f2 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +cmake_minimum_required(VERSION 3.20) +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR arm) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(CMAKE_C_COMPILER arm-none-eabi-gcc) +set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) +project(stackless_http_m33 C ASM) +set(CMAKE_C_STANDARD 11) +if(NOT WAMR_ROOT OR NOT HANDLER_INCLUDE) + message(FATAL_ERROR "Set WAMR_ROOT and HANDLER_INCLUDE") +endif() +include(../embedded-wamr.cmake) +add_executable(stackless_m33 ${WAMR_RUNTIME_LIB_SOURCE} ../p3.c ../vm.c ../task.c ../http.c + startup.c sim.c) +target_include_directories(stackless_m33 PRIVATE .. ${HANDLER_INCLUDE} ${WAMR_ROOT}/core/iwasm/include) +target_compile_definitions(stackless_m33 PRIVATE P3_WAMR P3_QEMU PICO_HEAP_SIZE=16384) +target_compile_options(stackless_m33 PRIVATE -mcpu=cortex-m33 -mthumb -mfloat-abi=soft -ffunction-sections -fdata-sections) +target_link_options(stackless_m33 PRIVATE -mcpu=cortex-m33 -mthumb -mfloat-abi=soft -nostartfiles --specs=nosys.specs + -T${CMAKE_CURRENT_LIST_DIR}/m33.ld -Wl,--gc-sections,-Map=stackless_m33.map) +target_link_libraries(stackless_m33 PRIVATE m) +set_target_properties(stackless_m33 PROPERTIES SUFFIX .elf INTERPROCEDURAL_OPTIMIZATION TRUE) +set_property(TARGET stackless_m33 APPEND PROPERTY LINK_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/m33.ld) diff --git a/crates/c-api/examples/stackless-http/qemu/m33.ld b/crates/c-api/examples/stackless-http/qemu/m33.ld new file mode 100644 index 0000000..e3f3eb6 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/m33.ld @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +/* AN505 secure aliases; cap the application region to the Pico 2 W budget. */ +MEMORY { + XIP (rx) : ORIGIN = 0x10000000, LENGTH = 1024K + RAM (rwx) : ORIGIN = 0x38000000, LENGTH = 520K +} +ENTRY(reset) +SECTIONS { + .text : { KEEP(*(.vectors)) *(.text*) *(.rodata*) . = ALIGN(4); } > XIP + .ARM.exidx : { *(.ARM.exidx*) } > XIP + .data : { __data_start__ = .; *(.data*) . = ALIGN(4); __data_end__ = .; } > RAM AT > XIP + __data_load__ = LOADADDR(.data); + .bss (NOLOAD) : { __bss_start__ = .; *(.bss*) *(COMMON) . = ALIGN(8); __bss_end__ = .; } > RAM + .net_reserve (NOLOAD) : { . += 64K; } > RAM + .heap (NOLOAD) : { __end__ = .; . += 16K; } > RAM + .stack (NOLOAD) : { __StackBottom = .; . += 4K; __StackTop = .; } > RAM + ASSERT(__StackTop <= ORIGIN(RAM) + LENGTH(RAM), "Pico RAM budget exceeded") +} diff --git a/crates/c-api/examples/stackless-http/qemu/run.py b/crates/c-api/examples/stackless-http/qemu/run.py new file mode 100644 index 0000000..aa5d0d4 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/run.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Run the bounded M33 workload and count executed guest instructions. + +QEMU single-instruction translation blocks provide instruction counts, not +hardware cycles. The timed interrupt advances virtual time while WFI sleeps. +""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess + + +def run(*args): + return subprocess.run(list(map(str, args)), check=True, capture_output=True, + text=True, timeout=60) + + +def ram(elf): + sections = {} + for line in run('arm-none-eabi-size', '-A', elf).stdout.splitlines(): + words = line.split() + if len(words) != 3 or not words[0].startswith('.'): + continue + name, size, address = words[0], int(words[1]), int(words[2]) + if size and (0x20000000 <= address < 0x20082000 or 0x38000000 <= address < 0x38082000): + sections[name] = size + used = sum(sections.values()) + if not 131072 < used < 520 * 1024: + raise RuntimeError(f'bad Cortex-M33 SRAM accounting: {sections}') + return {'sram_reserved_bytes': used, 'sram_limit_bytes': 520 * 1024, 'sram_sections': sections} + + +def result(run_result): + output = run_result.stdout + run_result.stderr + row = next(line for line in output.splitlines() if line.startswith('M33 PASS ')) + print(row) + return {key: int(value) for key, value in (item.split('=') for item in row.split()[2:])} + + +def instructions(elf, trace): + names = {'native_begin', 'native_end', 'idle_begin', 'idle_end'} + markers = {} + for line in run('arm-none-eabi-nm', '-n', elf).stdout.splitlines(): + words = line.split() + if len(words) == 3 and words[2] in names: + markers[int(words[0], 16)] = words[2] + if set(markers.values()) != names: + raise RuntimeError('missing instruction count markers') + state = None + counts = {} + line_pattern = re.compile(r'^Trace \d+: .*?\[[0-9a-f]+/([0-9a-f]+)/', re.I) + with trace.open() as file: + for line in file: + match = line_pattern.search(line) + if not match: + continue + marker = markers.get(int(match[1], 16)) + if marker and marker.endswith('_begin'): + if state or marker in counts: + raise RuntimeError('unexpected repeated trace marker') + state = marker.split('_')[0] + counts[state] = 0 + elif marker and marker.endswith('_end'): + if state != marker.split('_')[0]: + raise RuntimeError('unpaired trace marker') + state = None + elif state: + counts[state] += 1 + if state or set(counts) != {'idle', 'native'} or not 0 < counts['idle'] < 100: + raise RuntimeError(f'bad instruction trace: {counts}') + return counts + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('elf', type=Path) + p.add_argument('--pico-elf', type=Path) + p.add_argument('--qemu', default='qemu-system-arm') + p.add_argument('--output', type=Path, required=True) + a = p.parse_args() + a.output.mkdir(parents=True, exist_ok=True) + command = [a.qemu, '-M', 'mps2-an505', '-nographic', '-monitor', 'none', '-serial', 'null', + '-net', 'none', '-icount', 'shift=0,sleep=off', '-kernel', a.elf.resolve()] + test = result(run(*command, '-semihosting-config', 'enable=on,target=native')) + if (test['completed'], test['cancelled'], test['disconnected'], test['live']) != (34, 1, 1, 0): + raise RuntimeError(f'incomplete simulated HTTP workload: {test}') + trace = a.output / 'instructions.log' + count = result(run(*command, '-semihosting-config', 'enable=on,target=native,arg=count', + '-accel', 'tcg,one-insn-per-tb=on', '-d', 'exec,nochain', '-D', trace)) + if count['completed'] or count['idle_wakes'] != 1: + raise RuntimeError('instruction-only mode did not run') + ops = instructions(a.elf, trace) + report = {'qemu': run(a.qemu, '--version').stdout.splitlines()[0], 'machine': 'mps2-an505', + 'elf_sha256': hashlib.sha256(a.elf.read_bytes()).hexdigest(), 'simulated_http': test, + 'm33_link': ram(a.elf), 'instruction_counts': ops, + 'instructions_per_context_get_import': ops['native'] / count['native_imports']} + if a.pico_elf: + report['pico_link'] = ram(a.pico_elf) + report['pico_link']['elf_sha256'] = hashlib.sha256(a.pico_elf.read_bytes()).hexdigest() + (a.output / 'results.json').write_text(json.dumps(report, indent=2) + '\n') + print(json.dumps(report, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/crates/c-api/examples/stackless-http/qemu/sim.c b/crates/c-api/examples/stackless-http/qemu/sim.c new file mode 100644 index 0000000..22e03c4 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/sim.c @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "http.h" +#include "handler.h" +#include "wasm_export.h" +#include +#include +#include + +int +semi(int operation, uintptr_t argument); +int +pico_libc_peak(void); +static http_server server; +static _Alignas(16) uint8_t pool[128 * 1024]; +static volatile unsigned wakes; +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + printf("FAIL %d: %s: %s\n", __LINE__, #condition, \ + server.vm.error); \ + fflush(stdout); \ + exit(1); \ + } \ + } while (0) +#define SYST_CSR (*(volatile uint32_t *)0xe000e010) +#define SYST_RVR (*(volatile uint32_t *)0xe000e014) +#define SYST_CVR (*(volatile uint32_t *)0xe000e018) + +void +systick(void) +{ + SYST_CSR = 0; + ++wakes; +} +__attribute__((noipa, used)) void +native_begin(void) +{ + __asm__ volatile("nop" ::: "memory"); +} +__attribute__((noipa, used)) void +native_end(void) +{ + __asm__ volatile("yield" ::: "memory"); +} +__attribute__((noipa, used)) void +idle_begin(void) +{ + __asm__ volatile("nop; nop" ::: "memory"); +} +__attribute__((noipa, used)) void +idle_end(void) +{ + __asm__ volatile("yield; nop" ::: "memory"); +} + +static void +wait_for_irq(void) +{ + SYST_RVR = 20000; + SYST_CVR = 0; + SYST_CSR = 7; + idle_begin(); + for (;;) { + __asm__ volatile("cpsid i" ::: "memory"); + if (wakes) { + __asm__ volatile("cpsie i" ::: "memory"); + break; + } + __asm__ volatile("wfi; cpsie i" ::: "memory"); + } + idle_end(); +} +static void +send_request(unsigned slot, const char *request) +{ + CHECK(http_input(&server, slot, (uint8_t *)request, + (uint32_t)strlen(request))); + CHECK(http_progress(&server, slot)); +} +static void +drain(unsigned slot, bool large) +{ + const char *header = large ? "HTTP/1.1 200 OK\r\nContent-Length: " + "65536\r\nConnection: close\r\n\r\n" + : "HTTP/1.1 200 OK\r\nContent-Length: " + "15\r\nConnection: close\r\n\r\n"; + const char *small = "stackless wasm\n"; + size_t count = 0, header_length = strlen(header); + http_connection *c = &server.connections[slot]; + for (unsigned turn = 0; turn < 6000 && !http_finished(c); ++turn) { + CHECK(http_progress(&server, slot)); + size_t chunk = c->length - c->sent; + if (chunk > 37) + chunk = 37; /* Simulated small transport window. */ + for (size_t j = 0; j < chunk; ++j, ++count) { + CHECK(count < header_length + (large ? 65536 : strlen(small))); + uint8_t expected = + (uint8_t)(count < header_length ? header[count] + : large ? 'a' + ((count - header_length) % 26) + : small[count - header_length]); + CHECK(c->output[c->sent + j] == expected); + } + c->sent += chunk; + } + CHECK(count == header_length + (large ? 65536 : strlen(small))); + CHECK(http_reap(&server, slot)); +} +static void +disconnect(unsigned slot) +{ + CHECK(http_disconnect(&server, slot)); + CHECK(http_progress(&server, slot)); + CHECK(http_reap(&server, slot)); +} +static void +run_http(void) +{ + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) + CHECK(http_open(&server, i)); + send_request(0, "GET / HTTP/1.1\r\nHost:"); + send_request(1, "GET /large HTTP/1.1\r\n\r\n"); + CHECK(server.connections[1].task.returned + && !server.connections[1].task.exited); + uint32_t callbacks = server.callbacks; + CHECK(http_progress(&server, 1)); + CHECK(server.callbacks + == callbacks); /* No room in the native output buffer. */ + drain(1, true); + send_request(0, " localhost\r\n\r\n"); + drain(0, false); + disconnect(2); + send_request(3, "GET /large HTTP/1.1\r\n\r\n"); + CHECK(server.connections[3].task.returned); + disconnect(3); + for (unsigned i = 0; i < 32; ++i) { + CHECK(http_open(&server, 0)); + send_request(0, "GET / HTTP/1.1\r\n\r\n"); + drain(0, false); + CHECK(server.vm.async.live == 0); + } + CHECK(server.completed == 34 && server.cancelled == 1 + && server.disconnected == 1); +} +int +main(void) +{ + MemAllocOption mem = { 0 }; + mem.pool.heap_buf = pool; + mem.pool.heap_size = sizeof(pool); + CHECK(p3_vm_open_with_engine( + &server.vm, wasm_engine_new_with_args(Alloc_With_Pool, &mem), + handler_wasm, sizeof(handler_wasm))); + char command[64] = { 0 }; + uintptr_t cmd[] = { (uintptr_t)command, sizeof(command) }; + semi(0x15, (uintptr_t)cmd); + bool count = strstr(command, "count") != NULL; + if (!count) + run_http(); + native_begin(); + bool ok = http_native_loop(&server, 1000); + native_end(); + CHECK(ok); + wait_for_irq(); + CHECK(wakes == 1 && server.vm.async.live == 0); + mem_alloc_info_t usage = { 0 }; + CHECK(wasm_runtime_get_mem_alloc_info(&usage)); + CHECK(usage.highmark_size >= 65536 && usage.highmark_size <= sizeof(pool)); + printf( + "M33 PASS completed=%lu cancelled=%lu disconnected=%lu live=%lu " + "peak_handles=%lu " + "pool_peak=%lu pool_reserved=%zu libc_peak=%d idle_wakes=%u " + "native_imports=1000\n", + (unsigned long)server.completed, (unsigned long)server.cancelled, + (unsigned long)server.disconnected, (unsigned long)server.vm.async.live, + (unsigned long)server.vm.async.peak, (unsigned long)usage.highmark_size, + sizeof(pool), pico_libc_peak(), wakes); + p3_vm_close(&server.vm); + fflush(stdout); + return 0; +} diff --git a/crates/c-api/examples/stackless-http/qemu/startup.c b/crates/c-api/examples/stackless-http/qemu/startup.c new file mode 100644 index 0000000..6095d6a --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/startup.c @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include +#include +#include + +int +main(void); +void +systick(void); +int +semi(int operation, uintptr_t argument) +{ + register int r0 __asm__("r0") = operation; + register uintptr_t r1 __asm__("r1") = argument; + __asm__ volatile("bkpt 0xab" : "+r"(r0) : "r"(r1) : "memory"); + return r0; +} +void __attribute__((noreturn)) +_exit(int status) +{ + uint32_t params[2] = { 0x20026, (uint32_t)status }; + semi(0x20, (uintptr_t)params); + for (;;) + __asm__ volatile("wfi"); +} +int +_write(int fd, char *buf, int len) +{ + (void)fd; + for (int i = 0; i < len; ++i) + semi(3, (uintptr_t)&buf[i]); + return len; +} +static void +fault(void) +{ + semi(4, (uintptr_t)"FAULT Cortex-M33\n"); + _exit(90); +} +void +reset(void) +{ + extern char __data_start__, __data_end__, __data_load__, __bss_start__, + __bss_end__; + memcpy(&__data_start__, &__data_load__, + (uintptr_t)&__data_end__ - (uintptr_t)&__data_start__); + memset(&__bss_start__, 0, + (uintptr_t)&__bss_end__ - (uintptr_t)&__bss_start__); + _exit(main()); +} +extern uint32_t __StackTop; +__attribute__((section(".vectors"), used)) const uintptr_t vectors[16] = { + (uintptr_t)&__StackTop, (uintptr_t)reset, (uintptr_t)fault, + (uintptr_t)fault, (uintptr_t)fault, (uintptr_t)fault, + (uintptr_t)fault, (uintptr_t)fault, (uintptr_t)fault, + (uintptr_t)fault, (uintptr_t)fault, (uintptr_t)fault, + (uintptr_t)fault, (uintptr_t)fault, (uintptr_t)fault, + (uintptr_t)systick +}; diff --git a/crates/c-api/examples/stackless-http/receipts.json b/crates/c-api/examples/stackless-http/receipts.json new file mode 100644 index 0000000..3fa7a35 --- /dev/null +++ b/crates/c-api/examples/stackless-http/receipts.json @@ -0,0 +1,442 @@ +{ + "hosted": { + "mac-wamr-release": { + "runtime": "wamr", + "host": "macOS-27.2-arm64-arm-64bit-Mach-O", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "none", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9368, + "rss_bytes": 2244608, + "cpu_s": 1.2e-05, + "elapsed_s": 3.001023, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 176, + "transfers": 172, + "bytes": 73053, + "live": 0, + "peak_handles": 15, + "server_bytes": 9368, + "rss_bytes": 2211840, + "cpu_s": 0.00276, + "elapsed_s": 6.00066, + "waits": 175, + "wakes": 174 + } + }, + "native_context_get": { + "imports": 10000000, + "ns_per_import": [ + 15.74, + 15.465, + 15.782, + 13.81, + 13.743, + 13.667, + 13.618 + ], + "median_ns_per_import": 13.81 + }, + "raw_report_sha256": "d957b2e4685b74db5888b04c1b68fe54b9cbc2e35f57916cf97f1e539478576f" + }, + "win-wamr-release": { + "runtime": "wamr", + "host": "Windows-11-10.0.26300-SP0", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "none", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9368, + "rss_bytes": 4763648, + "cpu_s": 0.0, + "elapsed_s": 3.031, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 174, + "transfers": 170, + "bytes": 71005, + "live": 0, + "peak_handles": 15, + "server_bytes": 9368, + "rss_bytes": 4812800, + "cpu_s": 0.015625, + "elapsed_s": 6.0, + "waits": 176, + "wakes": 175 + } + }, + "native_context_get": { + "imports": 10000000, + "ns_per_import": [ + 134.375, + 121.875, + 45.312, + 50.0, + 48.438, + 50.0, + 56.25 + ], + "median_ns_per_import": 50.0 + }, + "raw_report_sha256": "db1b567ef28220bbc3208f71d4acf84d35b08fdbbb479e6b768b95ac63447ce1" + }, + "mac-wamr-asan": { + "runtime": "wamr", + "host": "macOS-27.2-arm64-arm-64bit-Mach-O", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "runtime-and-client", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9368, + "rss_bytes": 12484608, + "cpu_s": 2e-05, + "elapsed_s": 3.001037, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 175, + "transfers": 171, + "bytes": 72029, + "live": 0, + "peak_handles": 15, + "server_bytes": 9368, + "rss_bytes": 14286848, + "cpu_s": 0.00335, + "elapsed_s": 6.001864, + "waits": 174, + "wakes": 173 + } + }, + "native_context_get": null, + "raw_report_sha256": "3329a1f93b97e7c0179fbd09ee1223e9f51c5e9d0bdb24597e34d2b84de4d29f" + }, + "win-wamr-asan": { + "runtime": "wamr", + "host": "Windows-11-10.0.26300-SP0", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "runtime-and-client", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9368, + "rss_bytes": 14340096, + "cpu_s": 0.0, + "elapsed_s": 3.0, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 174, + "transfers": 170, + "bytes": 71005, + "live": 0, + "peak_handles": 15, + "server_bytes": 9368, + "rss_bytes": 13897728, + "cpu_s": 0.03125, + "elapsed_s": 6.016, + "waits": 175, + "wakes": 174 + } + }, + "native_context_get": null, + "raw_report_sha256": "003b9d831ae167e4e717481c0b850c37eee8a5553ac5691432ed2b686430bdec" + }, + "mac-tinywasm-release": { + "runtime": "tinywasm", + "host": "macOS-27.2-arm64-arm-64bit-Mach-O", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "none", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9320, + "rss_bytes": 7159808, + "cpu_s": 1.2e-05, + "elapsed_s": 3.001021, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 176, + "transfers": 172, + "bytes": 73053, + "live": 0, + "peak_handles": 15, + "server_bytes": 9320, + "rss_bytes": 7241728, + "cpu_s": 0.002474, + "elapsed_s": 6.001585, + "waits": 175, + "wakes": 174 + } + }, + "native_context_get": { + "imports": 10000000, + "ns_per_import": [ + 53.552, + 56.345, + 50.452, + 53.877, + 52.095, + 50.343, + 55.118 + ], + "median_ns_per_import": 53.552 + }, + "raw_report_sha256": "a351c9e87831d56f68f64015f951326ddf19a21bb6ea2161bc042900dfb5739e" + }, + "win-tinywasm-release": { + "runtime": "tinywasm", + "host": "Windows-11-10.0.26300-SP0", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "none", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9320, + "rss_bytes": 5844992, + "cpu_s": 0.0, + "elapsed_s": 3.016, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 176, + "transfers": 172, + "bytes": 73053, + "live": 0, + "peak_handles": 15, + "server_bytes": 9320, + "rss_bytes": 5476352, + "cpu_s": 0.015625, + "elapsed_s": 6.016, + "waits": 178, + "wakes": 177 + } + }, + "native_context_get": { + "imports": 10000000, + "ns_per_import": [ + 598.438, + 207.812, + 298.438, + 471.875, + 171.875, + 187.5, + 143.75 + ], + "median_ns_per_import": 207.812 + }, + "raw_report_sha256": "4aea83bc14b8be58d79faa7f22d369172586bb4f8d5611d211ae5cea842366ec" + }, + "mac-tinywasm-asan": { + "runtime": "tinywasm", + "host": "macOS-27.2-arm64-arm-64bit-Mach-O", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "runtime-and-client", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9320, + "rss_bytes": 23609344, + "cpu_s": 3e-05, + "elapsed_s": 3.001056, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 174, + "transfers": 170, + "bytes": 71005, + "live": 0, + "peak_handles": 15, + "server_bytes": 9320, + "rss_bytes": 28852224, + "cpu_s": 0.021441, + "elapsed_s": 6.001681, + "waits": 172, + "wakes": 171 + } + }, + "native_context_get": null, + "raw_report_sha256": "260dd344201998859f5871bb6d3f1697c6c690ad09f231ec1fb0cea083aeb830" + }, + "win-tinywasm-asan": { + "runtime": "tinywasm", + "host": "Windows-11-10.0.26300-SP0", + "component_sha256": "f9e95efae6f82c0401043c73cdde58c937617e5a6e4dc08883770c1759d88fc4", + "linked_sha256": "482fe37d904e6d1d2f4d98bc3301c8f080a8656da42fb0d7a6740d4f6a18a5e3", + "sanitizer": "client-only", + "http": { + "idle": { + "completed": 0, + "cancelled": 0, + "disconnected": 0, + "callbacks": 0, + "transfers": 0, + "bytes": 0, + "live": 0, + "peak_handles": 0, + "server_bytes": 9320, + "rss_bytes": 15917056, + "cpu_s": 0.0, + "elapsed_s": 3.0, + "waits": 1, + "wakes": 0 + }, + "load": { + "completed": 25, + "cancelled": 3, + "disconnected": 1, + "callbacks": 174, + "transfers": 170, + "bytes": 71005, + "live": 0, + "peak_handles": 15, + "server_bytes": 9320, + "rss_bytes": 15740928, + "cpu_s": 0.03125, + "elapsed_s": 6.0, + "waits": 174, + "wakes": 173 + } + }, + "native_context_get": null, + "raw_report_sha256": "84c591ea58d059981d96a88b1dae666e406d15e480e0cbf119ba47194802fc70" + } + }, + "cortex_m33": { + "qemu": "QEMU emulator version 11.1.3", + "machine": "mps2-an505", + "elf_sha256": "9e2697fc643b16a1aadd24302f9128b2adc01d62c2b4215690bfcde9ed47412a", + "simulated_http": { + "completed": 34, + "cancelled": 1, + "disconnected": 1, + "live": 0, + "peak_handles": 15, + "pool_peak": 95024, + "pool_reserved": 131072, + "libc_peak": 0, + "idle_wakes": 1, + "native_imports": 1000 + }, + "m33_link": { + "sram_reserved_bytes": 229312, + "sram_limit_bytes": 532480, + "sram_sections": { + ".data": 3528, + ".bss": 139768, + ".net_reserve": 65536, + ".heap": 16384, + ".stack": 4096 + } + }, + "instruction_counts": { + "native": 464982, + "idle": 22 + }, + "instructions_per_context_get_import": 464.982, + "pico_link": { + "sram_reserved_bytes": 220264, + "sram_limit_bytes": 532480, + "sram_sections": { + ".ram_vector_table": 272, + ".uninitialized_data": 16, + ".data": 9328, + ".bss": 190168, + ".heap": 16384, + ".stack_dummy": 4096 + }, + "elf_sha256": "b47c22ecbb17ce16c341fc0fdcd6d3890ed5640856aee6f81ed3975d2f3afdf0" + } + } +} diff --git a/crates/c-api/examples/stackless-http/run.py b/crates/c-api/examples/stackless-http/run.py new file mode 100644 index 0000000..f6796b9 --- /dev/null +++ b/crates/c-api/examples/stackless-http/run.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Build the external adapter against feature-off runtimes and run its tests.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import statistics +import subprocess +from component import compile_script +from test_server import run as test_server + +HERE = Path(__file__).resolve().parent +PIN = 'fed10b0b823b82553dd8363f16a640816f5eac65' +CASES = {'future-completion-order': (2, 0), 'same-component-stream-future': (12, 0), + 'futures-must-write': (5, 2)} + + +def run(*cmd, **kw): + print(' '.join(map(str, cmd)), flush=True) + return subprocess.run(list(map(str, cmd)), check=True, timeout=900, **kw) + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--runtime', choices=('tinywasm', 'wamr'), required=True) + p.add_argument('--root', type=Path, required=True) + p.add_argument('--build', type=Path, required=True) + p.add_argument('--component-tests', type=Path, required=True) + p.add_argument('--library', type=Path, help='Reuse an already built static runtime archive') + p.add_argument('--validator', type=Path, help='Reuse the pinned component tool') + p.add_argument('--sanitize', action='store_true', help='WAMR runtime/client or supported-host Rust/std/C ASan') + p.add_argument('--client-asan', action='store_true', help='TinyWasm Windows C clients only') + a = p.parse_args() + root, build = a.root.resolve(), a.build.resolve() + win = os.name == 'nt' + suffix = '.exe' if win else '' + san = a.sanitize or a.client_asan + wamr = a.runtime == 'wamr' + build.mkdir(parents=True, exist_ok=True) + library = a.library + if not library: + fixture = root / ('samples/shared-continuations' if wamr else 'crates/c-api/tests/continuations') + flags = ['--sanitize'] if a.sanitize else ['--client-asan'] if a.client_asan else [] + with (build / 'ordinary-native.log').open('w') as f: + run('python3' if not win else 'python', fixture / 'run.py', '--runtime', a.runtime, + '--root', root, '--build', build / 'runtime', '--feature', 'off', *flags, + stdout=f, stderr=subprocess.STDOUT) + names = ('libiwasm.lib' if win else 'libiwasm.a') if wamr else ('tinywasm.lib' if win else 'libtinywasm.a') + matches = [path for path in (build / 'runtime' / 'runtime').rglob(names) + if path.parent.name != 'deps'] + if len(matches) != 1: + raise RuntimeError(f'expected exactly one runtime archive: {matches}') + library = matches[0] + include = root / ('core/iwasm/include' if wamr else 'crates/c-api/include') + validator = a.validator + if not validator: + run('cargo', 'build', '--locked', '--release', '--manifest-path', HERE / 'tools/Cargo.toml', + '--target-dir', build / 'tools') + validator = build / 'tools' / 'release' / ('stackless-component-tool' + suffix) + component_tree = a.component_tests.resolve() + revision = subprocess.check_output(['git', '-C', component_tree, 'rev-parse', 'HEAD'], text=True).strip() + if revision != PIN: + raise RuntimeError(f'expected Component Model tests at {PIN}; got {revision}') + artifact = HERE / 'handler.linked.wasm' + data = artifact.read_bytes() + (build / 'handler.h').write_text('static const unsigned char handler_wasm[] = {\n' + + '\n'.join(' ' + ', '.join(f'0x{v:02x}' for v in data[i:i+16]) + ',' for i in range(0, len(data), 16)) + '\n};\n') + env = os.environ.copy() + if san: + env['ASAN_OPTIONS'] = 'halt_on_error=1' + ('' if win else ':detect_stack_use_after_return=1') + if win and env.get('VSINSTALLDIR'): + tools = Path(env['VSINSTALLDIR']) / 'Common7/IDE/CommonExtensions/Microsoft/CMake' + env['PATH'] = os.pathsep.join([str(tools / 'CMake/bin'), str(tools / 'Ninja'), env['PATH']]) + output = build / 'adapter' + run('cmake', '-G', 'Ninja', '-S', HERE, '-B', output, '-DCMAKE_BUILD_TYPE=Release', + '-DP3_WAMR=' + str(int(wamr)), '-DP3_ASAN=' + str(int(san)), + '-DP3_RUNTIME_LIBRARY=' + str(library.resolve()), '-DP3_RUNTIME_INCLUDE=' + str(include), + '-DP3_HANDLER_INCLUDE=' + str(build), env=env) + run('cmake', '--build', output, '-j', '4', env=env) + receipts = [] + for name, (passed, unsupported) in CASES.items(): + rel = f'test/async/{name}.wast' + source = component_tree / rel + pinned = subprocess.check_output(['git', '-C', component_tree, 'show', f'{PIN}:{rel}']) + if source.read_bytes() != pinned: + raise RuntimeError('dirty upstream test input: ' + rel) + work = build / 'cases' / name + manifest = compile_script(source, work, str(validator)) + actual = [c for c in manifest['cases'] if 'module' in c] + skips = [c for c in manifest['cases'] if 'module' not in c] + if (len(actual), len(skips)) != (passed, unsupported): + raise RuntimeError(f'{name}: unexpected profile coverage') + for case in actual: + receipt = run(output / ('component_check' + suffix), work / case['module'], case['export'], + case['expected'], *case['args'], env=env, capture_output=True, text=True) + print(receipt.stdout, end='') + case.update(status='PASS', receipt=receipt.stdout.strip()) + receipts.append(manifest) + executable = output / ('stackless_http' + suffix) + report = {'runtime': a.runtime, 'host': platform.platform(), 'component_model_revision': PIN, + 'component_sha256': hashlib.sha256((HERE / 'handler.component.wasm').read_bytes()).hexdigest(), + 'linked_sha256': hashlib.sha256(data).hexdigest(), 'component_tests': receipts, + 'sanitizer': 'client-only' if a.client_asan else 'runtime-and-client' if a.sanitize else 'none', + 'http': test_server(str(executable), 3, env)} + if not san: + samples = [] + for _ in range(7): + out = run(executable, '--native', capture_output=True, text=True).stdout + line = next(line for line in out.splitlines() if line.startswith('NATIVE ')) + samples.append(float(line.split('cpu_ns_per_import=')[1])) + report['native_context_get'] = {'imports': 10000000, 'ns_per_import': samples, + 'median_ns_per_import': statistics.median(samples)} + print(report['native_context_get'], flush=True) + (build / 'results.json').write_text(json.dumps(report, indent=2) + '\n') + + +if __name__ == '__main__': + main() diff --git a/crates/c-api/examples/stackless-http/server.c b/crates/c-api/examples/stackless-http/server.c new file mode 100644 index 0000000..0d7a98e --- /dev/null +++ b/crates/c-api/examples/stackless-http/server.c @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef _WIN32 +#define _POSIX_C_SOURCE 200809L +#define _DARWIN_C_SOURCE +#endif +#include "http.h" +#include "handler.h" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#include +typedef SOCKET socket_t; +typedef WSAPOLLFD pollfd_t; +#define BAD_SOCKET INVALID_SOCKET +#define poll_sockets WSAPoll +#define CLOSE closesocket +static bool +nonblock(socket_t fd) +{ + u_long mode = 1; + return ioctlsocket(fd, FIONBIO, &mode) == 0; +} +static bool +again(void) +{ + int e = WSAGetLastError(); + return e == WSAEWOULDBLOCK || e == WSAEINTR; +} +static double +wall_time(void) +{ + return GetTickCount64() / 1000.0; +} +static double +cpu_time(void) +{ + FILETIME create, exit, kernel, user; + GetProcessTimes(GetCurrentProcess(), &create, &exit, &kernel, &user); + ULARGE_INTEGER k, u; + k.LowPart = kernel.dwLowDateTime; + k.HighPart = kernel.dwHighDateTime; + u.LowPart = user.dwLowDateTime; + u.HighPart = user.dwHighDateTime; + return (double)(k.QuadPart + u.QuadPart) / 10000000.0; +} +static size_t +rss(void) +{ + PROCESS_MEMORY_COUNTERS m; + return GetProcessMemoryInfo(GetCurrentProcess(), &m, sizeof(m)) + ? m.PeakWorkingSetSize + : 0; +} +#else +#include +#include +#include +#include +#include +#include +#include +#include +typedef int socket_t; +typedef struct pollfd pollfd_t; +#define BAD_SOCKET (-1) +#define CLOSE close +#define poll_sockets poll +static bool +nonblock(socket_t fd) +{ + return fcntl(fd, F_SETFL, O_NONBLOCK) == 0; +} +static bool +again(void) +{ + return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR; +} +static double +wall_time(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} +static double +cpu_time(void) +{ + struct timespec t; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} +static size_t +rss(void) +{ + struct rusage r; + if (getrusage(RUSAGE_SELF, &r)) + return 0; +#ifdef __APPLE__ + return (size_t)r.ru_maxrss; +#else + return (size_t)r.ru_maxrss * 1024; +#endif +} +#endif +static http_server server; + +int +main(int argc, char **argv) +{ + bool bench = argc > 1 && !strcmp(argv[1], "--native"); + int port = argc > 1 ? atoi(argv[1]) : 0; + double seconds = argc > 2 ? atof(argv[2]) : 5; + if (port < 0 || port > 65535 || seconds < 1 || seconds > 300) + return 2; +#ifdef _WIN32 + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa)) + return 2; +#else + signal(SIGPIPE, SIG_IGN); +#endif + if (!p3_vm_open(&server.vm, handler_wasm, sizeof(handler_wasm))) + goto fail; + if (bench) { + const int32_t iterations = 10000000; + if (!http_native_loop(&server, 10000)) + goto fail; + double start = cpu_time(); + if (!http_native_loop(&server, iterations)) + goto fail; + double ns = (cpu_time() - start) * 1e9 / iterations; + printf("NATIVE imports=%d cpu_ns_per_import=%.3f\n", iterations, ns); + p3_vm_close(&server.vm); + return 0; + } + socket_t listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listener == BAD_SOCKET || !nonblock(listener)) + goto fail; + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons((uint16_t)port); + if (bind(listener, (struct sockaddr *)&addr, sizeof(addr)) + || listen(listener, HTTP_CONNECTIONS)) + goto fail; +#ifdef _WIN32 + int addrlen = sizeof(addr); +#else + socklen_t addrlen = sizeof(addr); +#endif + if (getsockname(listener, (struct sockaddr *)&addr, &addrlen)) + goto fail; + socket_t sockets[HTTP_CONNECTIONS]; + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) + sockets[i] = BAD_SOCKET; + printf("LISTEN %u\n", ntohs(addr.sin_port)); + fflush(stdout); + double wall_start = wall_time(), cpu_start = cpu_time(); + unsigned wakes = 0, waits = 0; + uint8_t input[HTTP_CHUNK]; + while (wall_time() - wall_start < seconds) { + pollfd_t fds[HTTP_CONNECTIONS + 1]; + unsigned owners[HTTP_CONNECTIONS + 1], count = 1; + fds[0] = (pollfd_t){ listener, POLLIN, 0 }; + bool ready = false; + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) { + http_connection *c = &server.connections[i]; + if (!c->live) + continue; + if (!http_progress(&server, i)) + goto fail; + if (http_finished(c)) { + if (sockets[i] != BAD_SOCKET) + CLOSE(sockets[i]); + sockets[i] = BAD_SOCKET; + if (!http_reap(&server, i)) + goto fail; + continue; + } + ready |= p3_task_ready(&c->task); + if (sockets[i] == BAD_SOCKET) + continue; + short events = 0; + if (c->input && p3_native_capacity(&server.vm.async, c->input)) + events |= POLLIN; + if (c->sent < c->length) + events |= POLLOUT; + fds[count] = (pollfd_t){ sockets[i], events, 0 }; + owners[count++] = i; + } + double left = seconds - (wall_time() - wall_start); + if (left <= 0) + break; + int timeout = ready ? 0 : (int)(left * 1000) + 1; + ++waits; + int events = poll_sockets(fds, count, timeout); + if (events < 0) { + if (again()) + continue; + goto fail; + } + if (!events) + continue; + ++wakes; + if (fds[0].revents & POLLIN) { + socket_t fd = accept(listener, NULL, NULL); + if (fd != BAD_SOCKET) { + unsigned i = 0; + while (i < HTTP_CONNECTIONS && server.connections[i].live) + ++i; + if (i == HTTP_CONNECTIONS || !nonblock(fd)) + CLOSE(fd); + else { + int cap = 4096; + setsockopt(fd, SOL_SOCKET, SO_SNDBUF, (const char *)&cap, + sizeof(cap)); + sockets[i] = fd; + if (!http_open(&server, i)) + goto fail; + } + } + else if (!again()) + goto fail; + } + for (unsigned j = 1; j < count; ++j) { + unsigned i = owners[j]; + http_connection *c = &server.connections[i]; + bool broken = + (fds[j].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0; + if (!broken && (fds[j].revents & POLLIN)) { + uint32_t cap = p3_native_capacity(&server.vm.async, c->input); + if (cap > sizeof(input)) + cap = sizeof(input); + int n = (int)recv(sockets[i], (char *)input, (int)cap, 0); + if (n > 0) { + if (!http_input(&server, i, input, (uint32_t)n)) + goto fail; + } + else if (!n) { + /* A closed connection before a complete request cancels + * the lifted task and runs its guest cleanup callback. */ + broken = true; + } + else if (!again()) + broken = true; + } + if (!broken && (fds[j].revents & POLLOUT)) { + int n = (int)send(sockets[i], (const char *)c->output + c->sent, + (int)(c->length - c->sent), 0); + if (n > 0) + c->sent += (size_t)n; + else if (n < 0 && !again()) + broken = true; + } + if (broken) { + CLOSE(sockets[i]); + sockets[i] = BAD_SOCKET; + if (!http_disconnect(&server, i)) + goto fail; + } + } + } + double cpu = cpu_time() - cpu_start, elapsed = wall_time() - wall_start; + CLOSE(listener); + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) { + if (sockets[i] != BAD_SOCKET) + CLOSE(sockets[i]); + if (!server.connections[i].live) + continue; + if (!http_disconnect(&server, i) || !http_progress(&server, i) + || !http_reap(&server, i)) + goto fail; + } + printf("RESULT completed=%u cancelled=%u disconnected=%u callbacks=%u " + "transfers=%u bytes=%llu " + "live=%u peak_handles=%u server_bytes=%zu rss_bytes=%zu cpu_s=%.9f " + "elapsed_s=%.6f waits=%u wakes=%u\n", + server.completed, server.cancelled, server.disconnected, + server.callbacks, server.vm.async.transfers, + (unsigned long long)server.vm.async.bytes, server.vm.async.live, + server.vm.async.peak, sizeof(server), rss(), cpu, elapsed, waits, + wakes); + p3_vm_close(&server.vm); +#ifdef _WIN32 + WSACleanup(); +#endif + return server.vm.async.live ? 1 : 0; +fail: + fprintf(stderr, "HTTP failed: %s %s\n", server.vm.error, + server.vm.async.error ? server.vm.async.error : ""); + return 1; +} diff --git a/crates/c-api/examples/stackless-http/task.c b/crates/c-api/examples/stackless-http/task.c new file mode 100644 index 0000000..460b1f2 --- /dev/null +++ b/crates/c-api/examples/stackless-http/task.c @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "vm.h" +#include +#include + +void +p3_task_init(p3_task *task, p3_vm *vm, const char *callback) +{ + memset(task, 0, sizeof(*task)); + task->vm = vm; + task->callback = p3_vm_export(vm, callback); +} +static bool +task_call(p3_task *task, const p3_export *entry, const int32_t *args, + size_t argc) +{ + int32_t packed; + bool result; + if (task->vm->current) + return false; + task->vm->current = task; + bool ok = p3_vm_invoke(task->vm, entry, args, argc, &packed, &result); + task->vm->current = NULL; + if (!ok || !result || ((uint32_t)packed & 15U) > 2) + goto fail; + unsigned code = (uint32_t)packed & 15U; + task->set = code == 2 ? (uint32_t)packed >> 4 : 0; + task->yielded = code == 1; + task->exited = code == 0; + if (task->exited && !task->returned && !task->cancelled) + goto fail; + if (code == 2 && !p3_waiter(&task->vm->async, task->set, true)) + goto fail; + return true; +fail: + if (ok) + snprintf(task->vm->error, sizeof(task->vm->error), + "invalid stackless callback outcome"); + task->failed = task->exited = true; + return false; +} +bool +p3_task_start(p3_task *task, const char *start, const int32_t *args, + size_t argc) +{ + if (task->started || task->exited) + return false; + task->started = true; + if (task->cancel_requested) { + task->cancelled = task->exited = true; + return true; + } + return task_call(task, p3_vm_export(task->vm, start), args, argc); +} +bool +p3_task_ready(p3_task *task) +{ + return task->started && !task->exited + && (task->yielded + || (task->cancel_requested && !task->cancel_delivered) + || (task->set && p3_set_ready(&task->vm->async, task->set))); +} +bool +p3_task_step(p3_task *task) +{ + if (!p3_task_ready(task)) + return false; + if (task->set && !p3_waiter(&task->vm->async, task->set, false)) + return false; + p3_event event = { 0, 0, 0 }; + if (task->cancel_requested && !task->cancel_delivered) { + event.code = 6; + task->cancel_delivered = true; + } + else if (task->set && p3_set_ready(&task->vm->async, task->set)) { + if (!p3_take_event(&task->vm->async, task->set, &event)) + return false; + } + int32_t args[] = { (int32_t)event.code, (int32_t)event.index, + (int32_t)event.payload }; + ++task->callbacks; + return task_call(task, task->callback, args, 3); +} +bool +p3_task_request_cancel(p3_task *task) +{ + if (task->exited || task->returned || task->cancelled + || task->cancel_requested) + return false; + task->cancel_requested = true; + return true; +} diff --git a/crates/c-api/examples/stackless-http/test_server.py b/crates/c-api/examples/stackless-http/test_server.py new file mode 100644 index 0000000..613f03f --- /dev/null +++ b/crates/c-api/examples/stackless-http/test_server.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +import argparse +import json +import os +from pathlib import Path +import socket +import struct +import subprocess +import time + + +def stats(output): + row = next(line for line in output.splitlines() if line.startswith('RESULT ')) + return {k: float(v) if '.' in v else int(v) + for k, v in (pair.split('=') for pair in row.split()[1:])} + + +def start(executable, seconds, env): + p = subprocess.Popen([executable, '0', str(seconds)], text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, env=env) + for line in p.stdout: + if line.startswith('LISTEN '): + return p, int(line.split()[1]) + print(line, end='') + raise RuntimeError(f'server failed to start: {p.wait()}') + + +def collect(p): + output, _ = p.communicate(timeout=20) + print(output, end='') + if p.returncode: + raise RuntimeError(f'server exit {p.returncode}') + return stats(output) + + +def connect(port): + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2048) + s.settimeout(3) + s.connect(('127.0.0.1', port)) + return s + + +def response(s): + chunks = [] + while chunk := s.recv(4096): + chunks.append(chunk) + header, body = b''.join(chunks).split(b'\r\n\r\n', 1) + assert len(body) == int(next(line.split(b': ')[1] for line in header.splitlines() + if line.startswith(b'Content-Length: '))) + return header.split()[1], body + + +def request(port, data): + with connect(port) as s: + s.sendall(data) + return response(s) + + +def run(executable, idle_seconds, env=None): + p, _ = start(executable, idle_seconds, env) + idle = collect(p) + assert idle['wakes'] == 0 and idle['waits'] <= 2 and idle['callbacks'] == 0 + assert idle['elapsed_s'] >= idle_seconds * .95 + p, port = start(executable, 6, env) + try: + # A task with an incomplete request must not block another in the same store. + with connect(port) as slow: + slow.sendall(b'GET / HTTP/1.1\r\nHost:') + assert request(port, b'GET / HTTP/1.1\r\n\r\n') == (b'200', b'stackless wasm\n') + slow.sendall(b' localhost\r\n\r\n') + assert response(slow) == (b'200', b'stackless wasm\n') + # Leave three more tasks waiting while a fourth streams a bounded response. + waiting = [connect(port) for _ in range(3)] + try: + for s in waiting: + s.sendall(b'GET /') + with connect(port) as slow: + slow.sendall(b'GET /large HTTP/1.1\r\n\r\n') + time.sleep(.15) + status, data = response(slow) + assert status == b'200' and data == bytes(97 + i % 26 for i in range(65536)) + finally: + for s in waiting: + s.close() + # A disconnect after task.return closes the stream, not the resolved task. + with connect(port) as s: + s.sendall(b'GET /large HTTP/1.1\r\n\r\n') + assert s.recv(128).startswith(b'HTTP/1.1 200') + linger = struct.pack('hh' if os.name == 'nt' else 'ii', 1, 0) + s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, linger) + assert request(port, b'GET /' + b'x' * 2043) == (b'431', b'') + assert request(port, b'NO / HTTP/1.1\r\n\r\n') == (b'400', b'') + # Repeated opens prove resource pools are reused after cancellation and EOF. + for _ in range(20): + assert request(port, b'GET / HTTP/1.1\r\n\r\n') == (b'200', b'stackless wasm\n') + load = collect(p) + assert load['completed'] == 25 and load['cancelled'] == 3 + assert load['disconnected'] == 1 and load['live'] == 0 + assert load['peak_handles'] <= 4 * 7 + finally: + if p.poll() is None: + p.kill() + p.communicate() + return {'idle': idle, 'load': load} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Exercise the real TCP/component server and measure hosted idle CPU') + parser.add_argument('executable', type=Path) + parser.add_argument('--idle-seconds', type=float, default=3) + parser.add_argument('--json', type=Path) + args = parser.parse_args() + report = run(str(args.executable.resolve()), args.idle_seconds) + if args.json: + args.json.write_text(json.dumps(report, indent=2) + '\n') diff --git a/crates/c-api/examples/stackless-http/tools/Cargo.lock b/crates/c-api/examples/stackless-http/tools/Cargo.lock new file mode 100644 index 0000000..d95d43b --- /dev/null +++ b/crates/c-api/examples/stackless-http/tools/Cargo.lock @@ -0,0 +1,191 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "stackless-component-tool" +version = "0.1.0" +dependencies = [ + "wasmparser", + "wat", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "wasm-encoder" +version = "0.263.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b883a26aea02eaabe2d2c3fba7eb039ef5cdcb5d5201ae5551e73d5e251a81f" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.263.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fa0a054ae153606df820eb0a84213e28cda41bbe04ba2628e71b6ba100fcb" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wast" +version = "263.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d90bcf581b501aa1c358bc5dc0faa979cd375796908860ebcb6d1d70b907c7" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.263.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc9c5aca783b3f1838be89f3f9937c528b90ec5faa6fb62d809726c16a7d8f7" +dependencies = [ + "wast", +] diff --git a/crates/c-api/examples/stackless-http/tools/Cargo.toml b/crates/c-api/examples/stackless-http/tools/Cargo.toml new file mode 100644 index 0000000..a046578 --- /dev/null +++ b/crates/c-api/examples/stackless-http/tools/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "stackless-component-tool" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] + +[dependencies] +wat = "=1.263.0" +wasmparser = "=0.263.0" diff --git a/crates/c-api/examples/stackless-http/tools/src/main.rs b/crates/c-api/examples/stackless-http/tools/src/main.rs new file mode 100644 index 0000000..0694601 --- /dev/null +++ b/crates/c-api/examples/stackless-http/tools/src/main.rs @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Matt Hargett. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +fn main() -> Result<(), Box> { + let args: Vec<_> = std::env::args_os().skip(1).collect(); + if args.len() != 4 || args[0] != "parse" || args[2] != "-o" { + return Err("usage: stackless-component-tool parse input.wat -o output.wasm".into()); + } + let bytes = wat::parse_file(&args[1])?; + wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all()) + .validate_all(&bytes)?; + std::fs::write(&args[3], bytes)?; + Ok(()) +} diff --git a/crates/c-api/examples/stackless-http/vm.c b/crates/c-api/examples/stackless-http/vm.c new file mode 100644 index 0000000..1be2a3c --- /dev/null +++ b/crates/c-api/examples/stackless-http/vm.c @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "vm.h" +#include +#include + +static bool +name_is(const wasm_name_t *n, const char *s) +{ + size_t length = strlen(s); + return (n->size == length || (n->size == length + 1 && !n->data[length])) + && !memcmp(n->data, s, length); +} +static uint8_t * +memory(void *env, size_t *size) +{ + p3_vm *vm = env; + if (!vm->memory) { + *size = 0; + return NULL; + } + *size = wasm_memory_data_size(vm->memory); + return (uint8_t *)wasm_memory_data(vm->memory); +} +static wasm_trap_t * +trap(p3_vm *vm, const char *message) +{ + vm->poisoned = true; + snprintf(vm->error, sizeof(vm->error), "%s", message); + wasm_message_t text; + wasm_byte_vec_new(&text, strlen(message), message); + wasm_trap_t *t = wasm_trap_new(vm->store, &text); + wasm_byte_vec_delete(&text); + return t; +} +static void +trap_message(p3_vm *vm, wasm_trap_t *t) +{ + wasm_message_t message; + wasm_trap_message(t, &message); + size_t n = message.size < sizeof(vm->error) - 1 ? message.size + : sizeof(vm->error) - 1; + memcpy(vm->error, message.data, n); + vm->error[n] = 0; + wasm_byte_vec_delete(&message); + wasm_trap_delete(t); +} +static wasm_trap_t * +canonical(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results) +{ + p3_binding *b = env; + if (b->op == P3_TASK_RETURN || b->op == P3_TASK_CANCEL) { + p3_task *task = b->vm->current; + if (!task || results->size || task->returned || task->cancelled) + return trap(b->vm, "invalid task resolution"); + if (b->op == P3_TASK_CANCEL) { + if (args->size || !task->cancel_delivered) + return trap(b->vm, "cancellation was not delivered"); + task->cancelled = true; + } + else { + if (args->size == 2 && args->data[0].kind == WASM_I32 + && args->data[1].kind == WASM_I32) { + uint32_t body = (uint32_t)args->data[0].of.i32, + done = (uint32_t)args->data[1].of.i32; + if (!p3_lift_pair(&b->vm->async, body, done)) + return trap(b->vm, b->vm->async.error); + task->body = body; + task->completion = done; + } + else if (args->size) + return trap(b->vm, "unsupported task result type"); + task->returned = true; + } + return NULL; + } + if (b->op == P3_CONTEXT_GET || b->op == P3_CONTEXT_SET) { + p3_task *task = b->vm->current; + if (!task) + return trap(b->vm, "no current stackless task"); + if (b->op == P3_CONTEXT_GET && !args->size && results->size == 1) { + results->data[0].kind = WASM_I32; + results->data[0].of.i32 = (int32_t)task->context; + } + else if (b->op == P3_CONTEXT_SET && args->size == 1 && !results->size + && args->data[0].kind == WASM_I32) + task->context = (uint32_t)args->data[0].of.i32; + else + return trap(b->vm, "invalid context access"); + return NULL; + } + uint32_t a[3] = { 0, 0, 0 }; + if (args->size > 3 || results->size > 1) + return trap(b->vm, "invalid canonical signature"); + for (size_t i = 0; i < args->size; ++i) { + if (args->data[i].kind != WASM_I32) + return trap(b->vm, "invalid canonical argument"); + a[i] = (uint32_t)args->data[i].of.i32; + } + uint64_t value; + if (!p3_call(&b->vm->async, b->op, b->future != 0, b->element, + b->async_ != 0, a[0], a[1], a[2], &value)) + return trap(b->vm, b->vm->async.error); + if (results->size) { + if (b->op == P3_NEW) { + results->data[0].kind = WASM_I64; + results->data[0].of.i64 = (int64_t)value; + } + else { + results->data[0].kind = WASM_I32; + results->data[0].of.i32 = (int32_t)value; + } + } + return NULL; +} +static bool +binding(p3_binding *b, const wasm_name_t *name) +{ + static const struct { + const char *name; + enum p3_op op; + } names[] = { { "new", P3_NEW }, + { "read", P3_READ }, + { "write", P3_WRITE }, + { "cancel-read", P3_CANCEL_READ }, + { "cancel-write", P3_CANCEL_WRITE }, + { "drop-readable", P3_DROP_READ }, + { "drop-writable", P3_DROP_WRITE }, + { "waitable-set.new", P3_SET_NEW }, + { "waitable-set.poll", P3_SET_POLL }, + { "waitable-set.wait", P3_SET_WAIT }, + { "waitable-set.drop", P3_SET_DROP }, + { "waitable.join", P3_JOIN }, + { "task.return", P3_TASK_RETURN }, + { "task.cancel", P3_TASK_CANCEL }, + { "context.get", P3_CONTEXT_GET }, + { "context.set", P3_CONTEXT_SET } }; + char text[96]; + if (name->size >= sizeof(text)) + return false; + memcpy(text, name->data, name->size); + text[name->size] = 0; + char *element = strchr(text, ':'); + if (!element) + return false; + *element++ = 0; + char *async_ = strchr(element, ':'); + if (!async_) + return false; + *async_++ = 0; + if (strcmp(element, "u8") && strcmp(element, "unit") + && strcmp(element, "bool") && strcmp(element, "char")) + return false; + if (strcmp(async_, "0") && strcmp(async_, "1")) + return false; + b->element = !strcmp(element, "char") ? 4 + : !strcmp(element, "bool") ? 2 + : (uint8_t)!strcmp(element, "u8"); + b->async_ = (uint8_t)!strcmp(async_, "1"); + const char *operation = text; + if (!strncmp(operation, "future.", 7)) { + b->future = 1; + operation += 7; + } + else if (!strncmp(operation, "stream.", 7)) + operation += 7; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); ++i) + if (!strcmp(operation, names[i].name)) { + b->op = names[i].op; + return true; + } + return false; +} +bool +p3_vm_open(p3_vm *vm, const uint8_t *bytes, size_t size) +{ + return p3_vm_open_with_engine(vm, wasm_engine_new(), bytes, size); +} +bool +p3_vm_open_with_engine(p3_vm *vm, wasm_engine_t *engine, const uint8_t *bytes, + size_t size) +{ + memset(vm, 0, sizeof(*vm)); + p3_init(&vm->async); + vm->async.memory = memory; + vm->async.memory_env = vm; + vm->engine = engine; + if (!vm->engine) + goto fail; + vm->store = wasm_store_new(vm->engine); + if (!vm->store) + goto fail; + wasm_byte_vec_t binary; + wasm_byte_vec_new(&binary, size, (const char *)bytes); + vm->module = wasm_module_new(vm->store, &binary); + wasm_byte_vec_delete(&binary); + if (!vm->module) + goto fail; + wasm_importtype_vec_t types; + wasm_module_imports(vm->module, &types); + wasm_extern_t *imports[P3_IMPORTS] = { 0 }; + bool ok = types.size <= P3_IMPORTS; + size_t created = 0; + for (size_t i = 0; ok && i < types.size; ++i) { + p3_binding *b = &vm->bindings[i]; + b->vm = vm; + const wasm_functype_t *type = wasm_externtype_as_functype_const( + wasm_importtype_type(types.data[i])); + ok = type && name_is(wasm_importtype_module(types.data[i]), "p3") + && binding(b, wasm_importtype_name(types.data[i])); + if (!ok) + break; + wasm_func_t *fn = + wasm_func_new_with_env(vm->store, type, canonical, b, NULL); + if (!fn) { + ok = false; + break; + } + imports[created++] = wasm_func_as_extern(fn); + } + if (ok) { + wasm_extern_vec_t in = WASM_EMPTY_VEC; + in.size = created; + in.data = imports; +#ifdef P3_WAMR + in.num_elems = created; +#endif + wasm_trap_t *t = NULL; +#ifdef P3_WAMR + vm->instance = wasm_instance_new_with_args(vm->store, vm->module, &in, + &t, 8192, 0); +#else + vm->instance = wasm_instance_new(vm->store, vm->module, &in, &t); +#endif + if (t) + trap_message(vm, t); + ok = vm->instance != NULL; + } + for (size_t i = 0; i < created; ++i) + wasm_extern_delete(imports[i]); + wasm_importtype_vec_delete(&types); + if (!ok) + goto fail; + wasm_instance_exports(vm->instance, &vm->exports); + wasm_module_exports(vm->module, &vm->export_types); + if (vm->export_types.size > P3_EXPORTS + || vm->exports.size != vm->export_types.size) + goto fail; + for (size_t i = 0; i < vm->export_types.size; ++i) { + if (name_is(wasm_exporttype_name(vm->export_types.data[i]), + "__p3_memory")) + vm->memory = wasm_extern_as_memory(vm->exports.data[i]); + if (wasm_extern_kind(vm->exports.data[i]) != WASM_EXTERN_FUNC) + continue; + wasm_func_t *fn = wasm_extern_as_func(vm->exports.data[i]); + if (!fn) + continue; + wasm_functype_t *type = wasm_func_type(fn); + if (!type) + goto fail; + const wasm_valtype_vec_t *params = wasm_functype_params(type); + const wasm_valtype_vec_t *results = wasm_functype_results(type); + bool numeric = params->size <= 4 && results->size <= 1; + for (size_t j = 0; numeric && j < params->size; ++j) + numeric = wasm_valtype_kind(params->data[j]) == WASM_I32; + if (numeric && results->size) + numeric = wasm_valtype_kind(results->data[0]) == WASM_I32; + if (numeric) + vm->functions[i] = (p3_export){ fn, (uint8_t)params->size, + (uint8_t)results->size }; + wasm_functype_delete(type); + } + if (!vm->memory) + goto fail; + for (size_t i = 0; i < vm->export_types.size; ++i) + if (name_is(wasm_exporttype_name(vm->export_types.data[i]), + "__p3_start")) { + int32_t value; + bool result; + if (!p3_vm_call(vm, "__p3_start", NULL, 0, &value, &result)) + goto fail; + } + return true; +fail: + if (!vm->error[0]) + snprintf(vm->error, sizeof(vm->error), + "component profile instantiation failed"); + return false; +} +void +p3_vm_close(p3_vm *vm) +{ + wasm_extern_vec_delete(&vm->exports); + wasm_exporttype_vec_delete(&vm->export_types); + wasm_instance_delete(vm->instance); + wasm_module_delete(vm->module); + wasm_store_delete(vm->store); + wasm_engine_delete(vm->engine); +} +bool +p3_vm_call(p3_vm *vm, const char *name, const int32_t *args, size_t argc, + int32_t *result, bool *has_result) +{ + return p3_vm_invoke(vm, p3_vm_export(vm, name), args, argc, result, + has_result); +} +const p3_export * +p3_vm_export(p3_vm *vm, const char *name) +{ + for (size_t i = 0; i < vm->export_types.size; ++i) + if (name_is(wasm_exporttype_name(vm->export_types.data[i]), name) + && vm->functions[i].func) + return &vm->functions[i]; + return NULL; +} +bool +p3_vm_invoke(p3_vm *vm, const p3_export *entry, const int32_t *args, + size_t argc, int32_t *result, bool *has_result) +{ + if (vm->poisoned) + return false; + if (!entry || argc != entry->argc || argc > 4) { + snprintf(vm->error, sizeof(vm->error), + "unknown or incompatible core export"); + return false; + } + wasm_val_t av[4], rv = WASM_I32_VAL(0); + for (size_t j = 0; j < argc; ++j) { + av[j].kind = WASM_I32; + av[j].of.i32 = args[j]; + } + wasm_val_vec_t a = WASM_EMPTY_VEC, r = WASM_EMPTY_VEC; + a.size = argc; + a.data = av; + r.size = entry->result; + r.data = &rv; +#ifdef P3_WAMR + a.num_elems = argc; + r.num_elems = r.size; +#endif + wasm_trap_t *t = wasm_func_call(entry->func, &a, &r); + if (t) { + vm->poisoned = true; + trap_message(vm, t); + return false; + } + if (vm->poisoned) + return false; + *result = rv.of.i32; + *has_result = entry->result != 0; + return true; +} diff --git a/crates/c-api/examples/stackless-http/vm.h b/crates/c-api/examples/stackless-http/vm.h new file mode 100644 index 0000000..8cb05ae --- /dev/null +++ b/crates/c-api/examples/stackless-http/vm.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_VM_H +#define STACKLESS_VM_H +#include "p3.h" +#ifdef P3_WAMR +#include "wasm_c_api.h" +#else +#include "wasm.h" +#endif +#define P3_IMPORTS 48 +#define P3_EXPORTS 32 +typedef struct p3_vm p3_vm; +typedef struct p3_task p3_task; +typedef struct p3_binding { + p3_vm *vm; + enum p3_op op; + uint8_t future, element, async_; +} p3_binding; +typedef struct p3_export { + wasm_func_t *func; + uint8_t argc, result; +} p3_export; +struct p3_vm { + p3_context async; + wasm_engine_t *engine; + wasm_store_t *store; + wasm_module_t *module; + wasm_instance_t *instance; + wasm_extern_vec_t exports; + wasm_exporttype_vec_t export_types; + wasm_memory_t *memory; + p3_binding bindings[P3_IMPORTS]; + p3_export functions[P3_EXPORTS]; + char error[256]; + p3_task *current; + bool poisoned; +}; +struct p3_task { + p3_vm *vm; + const p3_export *callback; + uint32_t set, context, body, completion; + bool started, returned, cancel_requested, cancel_delivered, cancelled; + bool exited, yielded, failed; + uint32_t callbacks; +}; +bool +p3_vm_open(p3_vm *vm, const uint8_t *bytes, size_t size); +bool +p3_vm_open_with_engine(p3_vm *vm, wasm_engine_t *engine, const uint8_t *bytes, + size_t size); +void +p3_vm_close(p3_vm *vm); +/* Synchronous core calls only. Task state, streams and futures outlive calls. + */ +bool +p3_vm_call(p3_vm *vm, const char *name, const int32_t *args, size_t argc, + int32_t *result, bool *has_result); +const p3_export * +p3_vm_export(p3_vm *vm, const char *name); +bool +p3_vm_invoke(p3_vm *vm, const p3_export *entry, const int32_t *args, + size_t argc, int32_t *result, bool *has_result); +void +p3_task_init(p3_task *task, p3_vm *vm, const char *callback); +bool +p3_task_start(p3_task *task, const char *start, const int32_t *args, + size_t argc); +bool +p3_task_ready(p3_task *task); +bool +p3_task_step(p3_task *task); +bool +p3_task_request_cancel(p3_task *task); +#endif diff --git a/crates/c-api/tests/continuations/READINESS.md b/crates/c-api/tests/continuations/READINESS.md index 472f0ee..e142848 100644 --- a/crates/c-api/tests/continuations/READINESS.md +++ b/crates/c-api/tests/continuations/READINESS.md @@ -1,5 +1,10 @@ # Async/component readiness audit +The later [stackless HTTP example](../../examples/stackless-http/README.md) runs a validated component and a +subset of upstream Component Model async assertions with ordinary C API calls. +This document remains the earlier audit of the stackful continuation primitive. +The WASI interface suite remains unqualified. + Status: core-continuation prototype only. Do not propose the current header as a WASIp3-capable API or freeze it on the strength of the numeric C fixture. From ac22bbc727540e796b6f149c18e1959eb053e543 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 17:48:46 -0800 Subject: [PATCH 10/20] test: make stackless receipts portable across hosts --- .github/workflows/stackless_http.yml | 1 + crates/c-api/examples/stackless-http/qemu/run.py | 7 ++++--- crates/c-api/examples/stackless-http/qemu/sim.c | 4 ++-- crates/c-api/examples/stackless-http/receipts.json | 2 +- crates/c-api/examples/stackless-http/run.py | 7 ++++--- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/stackless_http.yml b/.github/workflows/stackless_http.yml index 9043838..5701c4e 100644 --- a/.github/workflows/stackless_http.yml +++ b/.github/workflows/stackless_http.yml @@ -9,6 +9,7 @@ jobs: hosted: name: Component assertions and TCP (${{ matrix.os }}) strategy: + fail-fast: false matrix: os: [ubuntu-24.04, windows-2025] runs-on: ${{ matrix.os }} diff --git a/crates/c-api/examples/stackless-http/qemu/run.py b/crates/c-api/examples/stackless-http/qemu/run.py index aa5d0d4..9d22007 100644 --- a/crates/c-api/examples/stackless-http/qemu/run.py +++ b/crates/c-api/examples/stackless-http/qemu/run.py @@ -29,7 +29,8 @@ def ram(elf): if size and (0x20000000 <= address < 0x20082000 or 0x38000000 <= address < 0x38082000): sections[name] = size used = sum(sections.values()) - if not 131072 < used < 520 * 1024: + if (not 131072 < used < 520 * 1024 or sections.get('.heap') != 16384 + or sections.get('.stack', sections.get('.stack_dummy')) != 4096): raise RuntimeError(f'bad Cortex-M33 SRAM accounting: {sections}') return {'sram_reserved_bytes': used, 'sram_limit_bytes': 520 * 1024, 'sram_sections': sections} @@ -60,7 +61,7 @@ def instructions(elf, trace): continue marker = markers.get(int(match[1], 16)) if marker and marker.endswith('_begin'): - if state or marker in counts: + if state or marker.split('_')[0] in counts: raise RuntimeError('unexpected repeated trace marker') state = marker.split('_')[0] counts[state] = 0 @@ -70,7 +71,7 @@ def instructions(elf, trace): state = None elif state: counts[state] += 1 - if state or set(counts) != {'idle', 'native'} or not 0 < counts['idle'] < 100: + if state or set(counts) != {'idle', 'native'} or not 0 < counts['idle'] < 100 or not counts['native']: raise RuntimeError(f'bad instruction trace: {counts}') return counts diff --git a/crates/c-api/examples/stackless-http/qemu/sim.c b/crates/c-api/examples/stackless-http/qemu/sim.c index 22e03c4..e6b84ed 100644 --- a/crates/c-api/examples/stackless-http/qemu/sim.c +++ b/crates/c-api/examples/stackless-http/qemu/sim.c @@ -171,12 +171,12 @@ main(void) printf( "M33 PASS completed=%lu cancelled=%lu disconnected=%lu live=%lu " "peak_handles=%lu " - "pool_peak=%lu pool_reserved=%zu libc_peak=%d idle_wakes=%u " + "pool_peak=%lu pool_reserved=%lu libc_peak=%d idle_wakes=%u " "native_imports=1000\n", (unsigned long)server.completed, (unsigned long)server.cancelled, (unsigned long)server.disconnected, (unsigned long)server.vm.async.live, (unsigned long)server.vm.async.peak, (unsigned long)usage.highmark_size, - sizeof(pool), pico_libc_peak(), wakes); + (unsigned long)sizeof(pool), pico_libc_peak(), wakes); p3_vm_close(&server.vm); fflush(stdout); return 0; diff --git a/crates/c-api/examples/stackless-http/receipts.json b/crates/c-api/examples/stackless-http/receipts.json index 3fa7a35..ebe5300 100644 --- a/crates/c-api/examples/stackless-http/receipts.json +++ b/crates/c-api/examples/stackless-http/receipts.json @@ -396,7 +396,7 @@ "cortex_m33": { "qemu": "QEMU emulator version 11.1.3", "machine": "mps2-an505", - "elf_sha256": "9e2697fc643b16a1aadd24302f9128b2adc01d62c2b4215690bfcde9ed47412a", + "elf_sha256": "b436dc0623b894e36349597af3f2355f065c215b31e572caf63fe291805726e0", "simulated_http": { "completed": 34, "cancelled": 1, diff --git a/crates/c-api/examples/stackless-http/run.py b/crates/c-api/examples/stackless-http/run.py index f6796b9..ef65139 100644 --- a/crates/c-api/examples/stackless-http/run.py +++ b/crates/c-api/examples/stackless-http/run.py @@ -84,11 +84,12 @@ def main(): receipts = [] for name, (passed, unsupported) in CASES.items(): rel = f'test/async/{name}.wast' - source = component_tree / rel pinned = subprocess.check_output(['git', '-C', component_tree, 'show', f'{PIN}:{rel}']) - if source.read_bytes() != pinned: - raise RuntimeError('dirty upstream test input: ' + rel) work = build / 'cases' / name + work.mkdir(parents=True, exist_ok=True) + # Read the pinned blob, independent of checkout filters such as CRLF. + source = work / (name + '.wast') + source.write_bytes(pinned) manifest = compile_script(source, work, str(validator)) actual = [c for c in manifest['cases'] if 'module' in c] skips = [c for c in manifest['cases'] if 'module' not in c] From 2826c6d70d0ac8030fab4addc29df7bd242ce21c Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 22:02:24 -0800 Subject: [PATCH 11/20] Add a bounded WASI 0.3 HTTP service adapter --- .../examples/stackless-http/CMakeLists.txt | 11 +- .../c-api/examples/stackless-http/README.md | 3 + .../c-api/examples/stackless-http/RESULTS.md | 6 +- .../examples/stackless-http/WASI_HTTP.md | 150 +++ .../stackless-http/build_wasi_http_guest.py | 56 ++ .../examples/stackless-http/component.py | 5 +- crates/c-api/examples/stackless-http/http.c | 7 + crates/c-api/examples/stackless-http/http.h | 20 +- crates/c-api/examples/stackless-http/p3.c | 68 +- crates/c-api/examples/stackless-http/p3.h | 15 + .../stackless-http/pico/CMakeLists.txt | 31 +- .../stackless-http/pico/pico_server.c | 10 +- .../stackless-http/qemu/CMakeLists.txt | 11 +- .../c-api/examples/stackless-http/qemu/m33.ld | 3 +- .../c-api/examples/stackless-http/qemu/run.py | 23 +- .../c-api/examples/stackless-http/qemu/sim.c | 17 + .../stackless-http/qemu/wasi_http_sim.h | 155 +++ crates/c-api/examples/stackless-http/server.c | 65 +- .../stackless-http/test_wasi_http_load.py | 146 +++ crates/c-api/examples/stackless-http/vm.c | 58 +- crates/c-api/examples/stackless-http/vm.h | 14 + .../stackless-http/wasi-http-receipts.json | 303 ++++++ .../c-api/examples/stackless-http/wasi_http.c | 893 ++++++++++++++++++ .../c-api/examples/stackless-http/wasi_http.h | 85 ++ .../examples/stackless-http/wasi_http_check.c | 50 + .../examples/stackless-http/wasi_http_guest.c | 324 +++++++ .../examples/stackless-http/wasi_http_link.py | 224 +++++ .../stackless-http/wasi_http_server.c | 484 ++++++++++ .../stackless-http/wasi_http_tests.py | 113 +++ 29 files changed, 3320 insertions(+), 30 deletions(-) create mode 100644 crates/c-api/examples/stackless-http/WASI_HTTP.md create mode 100644 crates/c-api/examples/stackless-http/build_wasi_http_guest.py create mode 100644 crates/c-api/examples/stackless-http/qemu/wasi_http_sim.h create mode 100644 crates/c-api/examples/stackless-http/test_wasi_http_load.py create mode 100644 crates/c-api/examples/stackless-http/wasi-http-receipts.json create mode 100644 crates/c-api/examples/stackless-http/wasi_http.c create mode 100644 crates/c-api/examples/stackless-http/wasi_http.h create mode 100644 crates/c-api/examples/stackless-http/wasi_http_check.c create mode 100644 crates/c-api/examples/stackless-http/wasi_http_guest.c create mode 100644 crates/c-api/examples/stackless-http/wasi_http_link.py create mode 100644 crates/c-api/examples/stackless-http/wasi_http_server.c create mode 100644 crates/c-api/examples/stackless-http/wasi_http_tests.py diff --git a/crates/c-api/examples/stackless-http/CMakeLists.txt b/crates/c-api/examples/stackless-http/CMakeLists.txt index b9f3d17..66beab3 100644 --- a/crates/c-api/examples/stackless-http/CMakeLists.txt +++ b/crates/c-api/examples/stackless-http/CMakeLists.txt @@ -11,8 +11,17 @@ endif() add_executable(stackless_http p3.c vm.c task.c http.c server.c) add_executable(component_check p3.c vm.c check.c) +add_executable(wasi_http_check p3.c vm.c task.c wasi_http.c wasi_http_check.c) +target_compile_definitions(wasi_http_check PRIVATE P3_WASI_HTTP P3_WASI_HTTP_HOSTED P3_IMPORTS=128 HTTP_FIELD_BYTES=4096) target_compile_definitions(component_check PRIVATE P3_HANDLES=128) -foreach(target stackless_http component_check) +set(targets stackless_http component_check wasi_http_check) +if(P3_WASI_HANDLER_INCLUDE) + add_executable(wasi_http_server p3.c vm.c task.c wasi_http.c wasi_http_server.c server.c) + target_include_directories(wasi_http_server PRIVATE ${P3_WASI_HANDLER_INCLUDE}) + target_compile_definitions(wasi_http_server PRIVATE P3_WASI_HTTP P3_WASI_HTTP_HOSTED P3_IMPORTS=128) + list(APPEND targets wasi_http_server wasi_http_check) +endif() +foreach(target IN LISTS targets) target_include_directories(${target} PRIVATE ${P3_RUNTIME_INCLUDE} ${P3_HANDLER_INCLUDE}) target_link_libraries(${target} PRIVATE ${P3_RUNTIME_LIBRARY}) if(P3_WAMR) diff --git a/crates/c-api/examples/stackless-http/README.md b/crates/c-api/examples/stackless-http/README.md index 189ef22..a294f14 100644 --- a/crates/c-api/examples/stackless-http/README.md +++ b/crates/c-api/examples/stackless-http/README.md @@ -145,3 +145,6 @@ libc heap and a 4 KiB native stack. With no picotool, `-DPICO_NO_PICOTOOL=1` builds an ELF for a debug probe. The serial counters report allocator high water, foreground service time, I/O wakes and import-loop wall time on a board. Foreground time excludes sleeping time and is not total CPU or power. + +See [WASI_HTTP.md](WASI_HTTP.md) for the optional standard WASI 0.3 service +adapter, pinned suite runner, and concurrent HTTP workload. diff --git a/crates/c-api/examples/stackless-http/RESULTS.md b/crates/c-api/examples/stackless-http/RESULTS.md index 5255b28..4d50aa7 100644 --- a/crates/c-api/examples/stackless-http/RESULTS.md +++ b/crates/c-api/examples/stackless-http/RESULTS.md @@ -56,9 +56,9 @@ the hosted release and Windows builds used the feature-off runtime. [conformance.csv](conformance.csv) records the original assertion, pinned source hash, expected value or trap and results. Two assertions in the selected files require cross-component linking and were explicitly unsupported. -These are Component Model async results. The official WASI HTTP and CLI tests -remain unexecuted against either draft: the example has no `wasi:http` resource -bindings or WASI CLI world. No WASI 0.3 conformance claim follows from this run. +These are Component Model async results. The original fixture had no standard HTTP or CLI resource bindings. +The separate standard service adapter and its suite coverage are described in +[WASI_HTTP.md](WASI_HTTP.md). The physical Windows build used MSVC 19.44.35229.0 on a Core i7-8650U with Windows 11 build 26300 and stable Rust 1.98.1. The base runtime snapshots on the diff --git a/crates/c-api/examples/stackless-http/WASI_HTTP.md b/crates/c-api/examples/stackless-http/WASI_HTTP.md new file mode 100644 index 0000000..8ad6e95 --- /dev/null +++ b/crates/c-api/examples/stackless-http/WASI_HTTP.md @@ -0,0 +1,150 @@ +# WASI 0.3 HTTP service fixture + +The optional adapter hosts a component exporting +`wasi:http/handler@0.3.2`. It keeps the HTTP resources, scheduling, +socket readiness, and memory budget in the embedder. The runtime uses the +same core Wasm C API for both backends. Each connection has its own task +context, input stream, response stream, and completion futures. The firmware +uses one Wasm instance and a fixed table of four connections. + +The offline linker validates the input component and resolves its canonical +lifts, lowers, and wit-component shim table. The two runtimes receive a core +image whose native imports are resolved once at instantiation. Existing +canonical imports keep their callback path. HTTP methods use a separate +callback with signatures checked during binding. The linker accepts the +single-memory, single-instance graph used by these guests. + +Resources use checked type, ownership, and generation. Host headers and +URI data live in bounded native storage. The adapter checks guest-memory +ranges, reacquires memory after guest allocation, and rejects HTTP imports +reentered by `cabi_realloc`. Native body transfers borrow buffers only for the +duration of a copy. The guest returns HTTP response metadata independently +of body production and trailer completion. The demo echoes a POST in +bounded chunks and returns an `x-upload-check` trailer. Closing a transport +releases the body endpoints and wakes the guest's pending work. + +The HTTP/1.1 transport serves one request per TCP connection. It bounds +headers to 2 KiB and accepts up to 64 KiB of content-length-framed request +body. It rejects duplicate content lengths and transfer-encoded requests. +Responses use chunked framing for streaming bodies. Extra connections are +closed on admission when four slots are in use. The firmware also uses the +existing lwIP connection deadline. The host example binds to loopback. +A guest trap terminates this fixture's shared instance. Outbound +`wasi:http/client.send`, inbound chunked uploads, and HTTP/2 and HTTP/3 +multiplexing are outside this profile. + +## Reproduce + +The test suite and WIT are pinned to artifact commit +`9d84631e427729802085c3d172d0a63cd360a1fa` from +[WebAssembly/wasi-testsuite](https://github.com/WebAssembly/wasi-testsuite/tree/9d84631e427729802085c3d172d0a63cd360a1fa). +The artifact commit references source commit +`de7fbe267210b52f63893270403177239b4f4a9b`. Use Python 3.10 or newer, a Clang +toolchain with wasm32 and wasm-ld, and wasm-tools 1.247.0. + +From this sample directory, set `SUITE` to the suite checkout and +`GUEST` to an absolute build-output directory. + +```sh +git -C "$SUITE" fetch origin prod/testsuite-base +git -C "$SUITE" checkout --detach 9d84631e427729802085c3d172d0a63cd360a1fa +python3 build_wasi_http_guest.py "$SUITE" "$GUEST" +``` + +Use the adapter build directory from the hosted instructions in +[README.md](README.md) as `ADAPTER`. Run the same steps with each runtime, +including its instrumented build if available. + +```sh +cmake -S . -B "$ADAPTER" -DP3_WASI_HANDLER_INCLUDE="$GUEST" +cmake --build "$ADAPTER" --target wasi_http_server wasi_http_check +python3 wasi_http_tests.py "$SUITE" "$ADAPTER" +python3 test_wasi_http_load.py "$ADAPTER/wasi_http_server" +"$ADAPTER/wasi_http_server" 0 3 +"$ADAPTER/wasi_http_server" --native +``` + +The suite runner validates the original components, exercises the upstream +service JSON assertions and CLI resource guests, and lists outbound cases +as unsupported when the linker finds the client import. The official +guests start with 17 Wasm memory pages. The freestanding workload uses +the same upstream service world and one page. The load test validates exact +4,096-byte JSON and PNG downloads and 16,384-byte PNG uploads with echo. +It runs one through four concurrent connections for each request mix, then +holds four connections open and probes overload in a burst of four. It also +checks recovery after disconnect and rejects ambiguous framing, excessive +body/header size, and invalid UTF-8 paths. + +For WAMR on Cortex-M33, use the same generated `GUEST` directory and set +`WAMR_ROOT` and `PICO_SDK_PATH` to absolute paths. The QEMU run uses the +AN505 memory map, a 520 KiB application region, and scripted transport +readiness. The separate firmware link includes the Pico SDK, Wi-Fi and lwIP. + +```sh +cmake -G Ninja -S qemu -B build/m33 -DCMAKE_BUILD_TYPE=Release \ + -DWAMR_ROOT="$WAMR_ROOT" -DHANDLER_INCLUDE="$GUEST" -DP3_WASI_HTTP=ON +cmake --build build/m33 +cmake -G Ninja -S pico -B build/pico -DCMAKE_BUILD_TYPE=Release \ + -DWAMR_ROOT="$WAMR_ROOT" -DHANDLER_INCLUDE="$GUEST" -DP3_WASI_HTTP=ON \ + -DPICO_SDK_PATH="$PICO_SDK_PATH" -DPICO_NO_PICOTOOL=1 +cmake --build build/pico +python3 qemu/run.py build/m33/stackless_m33.elf \ + --pico-elf build/pico/stackless_http.elf --wasi-http --output build/m33-report +``` + +The regular adapter targets and core runtime libraries can be built without +the HTTP feature. Neither runtime core needs an HTTP dependency. Firmware +without `P3_WASI_HTTP` continues to use the original fixture. + +## Validation + +| Configuration | Official service and resource cases | Outbound cases | +|---|---:|---:| +| WAMR macOS arm64 release and runtime/client ASan | 7 passed in each build | 11 unsupported | +| TinyWasm macOS arm64 release and Rust/std/C ASan | 7 passed in each build | 11 unsupported | + +The three service cases exercise the standard handler over TCP. The four +resource guests exercise fields, request, response, and request-options. +An additional run closed a partial upload while the official echo handler +was waiting. Both runtimes cancelled the task, then served a fresh request +and shut down with zero live resources and endpoints. + +The separate load test held four connections at a time. It verified GET +bodies, POST echo bodies, and recovery after overload and disconnects. +Each release completed 36 responses including five expected protocol +rejections, released 13 disconnected sockets and rejected 12 overflow +connections. Overflow probes observed TCP establishment followed by EOF +or reset. Peak endpoint occupancy was 49 of 64 slots. The suite runner +and workload finished with zero live handles and resources in all four +hosted builds. + +| Memory accounting | Bytes | Scope | +|---|---:|---| +| Cortex-M33 QEMU application reservations | 300,128 | Includes a 64 KiB networking allowance, 128 KiB runtime pool, 16 KiB libc heap, and 8 KiB stack | +| Pico 2 W Wi-Fi firmware reservations | 291,072 | Includes the linked lwIP and Wi-Fi build, with the 8 KiB stack in main SRAM | +| WAMR pool high-water mark in QEMU | 111,712 | Within the 128 KiB pool, including one 64 KiB Wasm page | +| libc heap high-water mark in QEMU | 2,968 | Within the 16 KiB heap | + +QEMU completed two GETs and two POST echoes with short transport windows +and a delayed reader. It verified the response bytes and upload trailers, +then disconnected a pending upload and reaped all handles. The QEMU +execution uses scripted readiness and the Pico SDK result is a firmware +link. No physical Pico or radio was available. Both Cortex-M33 binaries +used LTO and the standard profile's larger stack for nested +`cabi_realloc` calls. + +| Hosted release | Idle process CPU over 3 s | Median CPU ns per `context.get` | +|---|---:|---:| +| WAMR | 17 µs | 22.34 | +| TinyWasm | 20 µs | 81.28 | + +Both idle processes waited once and saw zero I/O wakeups. The native +samples each execute ten million calls through the Wasm C API and include +the interpreter loop. The corresponding QEMU trace executed 465,016 +instructions for 1,000 calls, and 22 instructions across the simulated +`WFI` interval and timer wake. These are emulator instruction counts. +The Windows SSH host timed out during this follow-up. Earlier Windows +results in [RESULTS.md](RESULTS.md) cover the original numeric fixture. + +The per-case outcomes, workload counters, timing samples, and binary +hashes are in [wasi-http-receipts.json](wasi-http-receipts.json). diff --git a/crates/c-api/examples/stackless-http/build_wasi_http_guest.py b/crates/c-api/examples/stackless-http/build_wasi_http_guest.py new file mode 100644 index 0000000..b981cfa --- /dev/null +++ b/crates/c-api/examples/stackless-http/build_wasi_http_guest.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Build a one-page, freestanding guest from the upstream HTTP service world.""" +import argparse +from pathlib import Path +import struct +import subprocess +import zlib + +from wasi_http_link import link + + +def png(size): + def chunk(tag, data): + return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data)) + header = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>2I5B', 1, 1, 8, 0, 0, 0, 0)) + image = chunk(b'IDAT', zlib.compress(b'\0\x7f')) + chunk(b'IEND', b'') + padding = b'Comment\0' + return header + chunk(b'tEXt', padding + b'x' * (size - len(header) - len(image) - 12 - len(padding))) + image + + +def json_body(): + return b'{"data":"' + b'x' * (4096 - len(b'{"data":""}\n')) + b'"}\n' + + +def c_array(name, data): + return f'static const unsigned char {name}[] = {{\n' + '\n'.join( + ' ' + ', '.join(f'0x{v:02x}' for v in data[i:i + 16]) + ',' + for i in range(0, len(data), 16)) + '\n};\n' + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('suite', type=Path) + p.add_argument('output', type=Path) + p.add_argument('--clang', default='clang') + p.add_argument('--wasm-tools', default='wasm-tools') + a = p.parse_args() + here = Path(__file__).resolve().parent + a.output.mkdir(parents=True, exist_ok=True) + (a.output / 'upload.h').write_text(c_array('upload_body', png(16384))) + (a.output / 'payloads.h').write_text(c_array('json_body', json_body()) + c_array('png_body', png(4096))) + wit = a.suite / 'tests/rust/wasm32-wasip3/wit' + core, typed, component, linked = (a.output / f'standard.{ext}.wasm' for ext in ('core', 'typed', 'component', 'linked')) + subprocess.run([a.clang, '--target=wasm32', '-Os', '-fno-builtin', '-nostdlib', + '-Wl,--no-entry,--initial-memory=65536,--max-memory=65536,-z,stack-size=4096,--strip-all', + '-I', a.output, here / 'wasi_http_guest.c', '-o', core], check=True) + subprocess.run([a.wasm_tools, 'component', 'embed', wit, core, '--world', 'wasi:http/service@0.3.2', '-o', typed], check=True) + subprocess.run([a.wasm_tools, 'component', 'new', typed, '-o', component], check=True) + link(component, linked, a.wasm_tools) + (a.output / 'standard_handler.h').write_text(c_array('handler_wasm', linked.read_bytes())) + + +if __name__ == '__main__': + main() diff --git a/crates/c-api/examples/stackless-http/component.py b/crates/c-api/examples/stackless-http/component.py index 6475377..c51111a 100644 --- a/crates/c-api/examples/stackless-http/component.py +++ b/crates/c-api/examples/stackless-http/component.py @@ -20,6 +20,9 @@ class Unsupported(ValueError): pass +_TOKEN = re.compile(r'[^\s()]+') + + def parse(text): tokens = [] i = 0 @@ -57,7 +60,7 @@ def parse(text): i += 1 tokens.append(text[start:i]) else: - m = re.match(r'[^\s()]+', text[i:]) + m = _TOKEN.match(text, i) tokens.append(m[0]) i += len(m[0]) stack = [[]] diff --git a/crates/c-api/examples/stackless-http/http.c b/crates/c-api/examples/stackless-http/http.c index 4192c04..d87be34 100644 --- a/crates/c-api/examples/stackless-http/http.c +++ b/crates/c-api/examples/stackless-http/http.c @@ -139,3 +139,10 @@ http_native_loop(http_server *server, int32_t iterations) server->vm.current = NULL; return ok && has_result && result == iterations; } + +uint32_t +http_input_capacity(http_server *server, unsigned slot) +{ + uint32_t input = server->connections[slot].input; + return input ? p3_native_capacity(&server->vm.async, input) : 0; +} diff --git a/crates/c-api/examples/stackless-http/http.h b/crates/c-api/examples/stackless-http/http.h index 6dabf7f..3319a73 100644 --- a/crates/c-api/examples/stackless-http/http.h +++ b/crates/c-api/examples/stackless-http/http.h @@ -5,6 +5,9 @@ #ifndef STACKLESS_HTTP_H #define STACKLESS_HTTP_H #include "vm.h" +#ifdef P3_WASI_HTTP +#include "wasi_http.h" +#endif #define HTTP_CONNECTIONS 4 #define HTTP_CHUNK 1024 typedef struct http_connection { @@ -12,13 +15,28 @@ typedef struct http_connection { uint32_t input; size_t sent, length; bool live, disconnected, done; - uint8_t outcome, output[HTTP_CHUNK]; + uint8_t outcome; +#ifdef P3_WASI_HTTP + uint8_t output[HTTP_CHUNK + HTTP_FIELD_BYTES]; +#else + uint8_t output[HTTP_CHUNK]; +#endif +#ifdef P3_WASI_HTTP + uint32_t trailers_tx, response, body_left, used, response_trailers; + bool parsed, head, chunked, terminal_chunk, wire_done, saw_eof; + uint8_t request_bytes[2 * HTTP_CHUNK]; +#endif } http_connection; typedef struct http_server { p3_vm vm; http_connection connections[HTTP_CONNECTIONS]; uint32_t completed, cancelled, disconnected, callbacks; +#ifdef P3_WASI_HTTP + wasi_http http; +#endif } http_server; +uint32_t +http_input_capacity(http_server *server, unsigned slot); bool http_open(http_server *server, unsigned slot); bool diff --git a/crates/c-api/examples/stackless-http/p3.c b/crates/c-api/examples/stackless-http/p3.c index dcc8282..6c70427 100644 --- a/crates/c-api/examples/stackless-http/p3.c +++ b/crates/c-api/examples/stackless-http/p3.c @@ -177,6 +177,63 @@ p3_host_end(p3_context *cx, uint32_t h) s->native = 1; return true; } +#ifdef P3_WASI_HTTP +bool +p3_http_element(uint8_t element) +{ + return element == P3_HTTP_STATUS || element == P3_HTTP_TRAILERS; +} +bool +p3_http_lift(p3_context *cx, uint32_t h, bool future, uint8_t element) +{ + p3_slot *s = get(cx, h); + if (!s || s->kind != (future ? FUTURE_RX : STREAM_RX) || s->native + || s->state != IDLE || s->set || s->element != element) + return fail(cx, "invalid owned HTTP endpoint"); + s->native = 1; + return true; +} +bool +p3_http_lower(p3_context *cx, uint32_t h) +{ + p3_slot *s = get(cx, h); + if (!s || !s->native || s->state != IDLE + || (s->kind != FUTURE_RX && s->kind != STREAM_RX)) + return fail(cx, "invalid HTTP endpoint lowering"); + s->native = 0; + return true; +} +bool +p3_http_finish(p3_context *cx, uint32_t h, uint8_t *value, bool *complete) +{ + p3_slot *s = get(cx, h); + if (!s || !s->native || s->state != IDLE || !p3_http_element(s->element)) + return fail(cx, "invalid HTTP future endpoint"); + *complete = false; + if (s->peer) { + p3_slot *peer = get(cx, s->peer); + if (!peer || !peer->buffer || peer->pending) + return true; + uint8_t *guest = range(cx, peer->ptr, P3_HTTP_RESULT_SIZE); + if (!guest) + return false; + if (s->kind == FUTURE_TX) + memcpy(guest, value, P3_HTTP_RESULT_SIZE); + else + memcpy(value, guest, P3_HTTP_RESULT_SIZE); + peer->progress = 1; + peer->buffer = 0; + cx->bytes += P3_HTTP_RESULT_SIZE; + ++cx->transfers; + notify(cx, peer); + } + else if (s->kind == FUTURE_RX) + return fail(cx, "HTTP future producer closed before completion"); + s->state = DONE; + *complete = true; + return p3_native_drop(cx, h); +} +#endif bool p3_lift_pair(p3_context *cx, uint32_t stream, uint32_t future) { @@ -262,7 +319,11 @@ p3_call(p3_context *cx, enum p3_op op, bool future, uint8_t element, unsigned kind = future ? FUTURE_RX : STREAM_RX; cx->error = NULL; *out = 0; - if (element != 0 && element != 1 && element != 2 && element != 4) + bool http = false; +#ifdef P3_WASI_HTTP + http = future && p3_http_element(element); +#endif + if (!http && element != 0 && element != 1 && element != 2 && element != 4) return fail(cx, "unsupported canonical element type"); if (op == P3_NEW || op == P3_SET_NEW) { s = allocate(cx, op == P3_SET_NEW ? SET : kind); @@ -371,8 +432,9 @@ p3_call(p3_context *cx, enum p3_op op, bool future, uint8_t element, c = 1; if (c > P3_MAX_TRANSFER) return fail(cx, "canonical transfer exceeds maximum length"); - unsigned width = element == 4 ? 4 : element != 0; - if (width && (b & (width - 1))) + unsigned width = http ? 40 : element == 4 ? 4 : element != 0; + unsigned align = http ? 8 : width; + if (align && (b & (align - 1))) return fail(cx, "unaligned canonical buffer"); if (width && !range(cx, b, c * width)) return false; diff --git a/crates/c-api/examples/stackless-http/p3.h b/crates/c-api/examples/stackless-http/p3.h index bc9fc78..5bd1bbf 100644 --- a/crates/c-api/examples/stackless-http/p3.h +++ b/crates/c-api/examples/stackless-http/p3.h @@ -12,6 +12,13 @@ #ifndef P3_HANDLES #define P3_HANDLES 64 #endif +#ifdef P3_WASI_HTTP +#define P3_HTTP_STATUS 40 +#define P3_HTTP_TRAILERS 41 +#define P3_HTTP_RESULT_SIZE 40 +bool +p3_http_element(uint8_t element); +#endif #define P3_BLOCKED UINT32_MAX #define P3_MAX_TRANSFER 0x0fffffffU enum p3_op { @@ -69,6 +76,14 @@ bool p3_take_event(p3_context *cx, uint32_t set, p3_event *event); bool p3_host_end(p3_context *cx, uint32_t end); +#ifdef P3_WASI_HTTP +bool +p3_http_lift(p3_context *cx, uint32_t h, bool future, uint8_t element); +bool +p3_http_lower(p3_context *cx, uint32_t h); +bool +p3_http_finish(p3_context *cx, uint32_t h, uint8_t *value, bool *complete); +#endif bool p3_lift_pair(p3_context *cx, uint32_t stream, uint32_t future); /* Native buffers are borrowed only for this call and never retained. */ diff --git a/crates/c-api/examples/stackless-http/pico/CMakeLists.txt b/crates/c-api/examples/stackless-http/pico/CMakeLists.txt index cf60a5a..a912dba 100644 --- a/crates/c-api/examples/stackless-http/pico/CMakeLists.txt +++ b/crates/c-api/examples/stackless-http/pico/CMakeLists.txt @@ -15,10 +15,37 @@ if(NOT WAMR_ROOT OR NOT HANDLER_INCLUDE) endif() include(../embedded-wamr.cmake) -add_executable(stackless_http ${WAMR_RUNTIME_LIB_SOURCE} ../p3.c ../vm.c ../task.c ../http.c pico_server.c) +if(P3_WASI_HTTP) + set(http_sources ../wasi_http.c ../wasi_http_server.c) + set(stack_size 8192) +else() + set(http_sources ../http.c) + set(stack_size 4096) +endif() +add_executable(stackless_http ${WAMR_RUNTIME_LIB_SOURCE} ../p3.c ../vm.c ../task.c ${http_sources} pico_server.c) +if(P3_WASI_HTTP) + target_compile_definitions(stackless_http PRIVATE P3_WASI_HTTP) + # The default RP2350 core-zero stack lives in a 4 KiB scratch bank. + # Put the larger, budgeted stack in main SRAM for nested canonical calls. + set(sdk_linker "${PICO_SDK_PATH}/src/rp2_common/pico_crt0/rp2350/memmap_default.ld") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${sdk_linker}") + file(READ "${sdk_linker}" linker) + set(sdk_stack "KEEP(*(.stack*))\n } > SCRATCH_Y") + set(sdk_stack_top "__StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);") + string(FIND "${linker}" "${sdk_stack}" stack_section) + string(FIND "${linker}" "${sdk_stack_top}" stack_top) + if(stack_section EQUAL -1 OR stack_top EQUAL -1) + message(FATAL_ERROR "Unsupported RP2350 SDK linker layout") + endif() + string(REPLACE "${sdk_stack}" "KEEP(*(.stack*))\n } > RAM" linker "${linker}") + string(REPLACE "${sdk_stack_top}" "__StackTop = ADDR(.stack_dummy) + SIZEOF(.stack_dummy);" linker "${linker}") + set(linker_path "${CMAKE_CURRENT_BINARY_DIR}/stackless_http.ld") + file(GENERATE OUTPUT "${linker_path}" CONTENT "${linker}") + pico_set_linker_script(stackless_http "${linker_path}") +endif() target_include_directories(stackless_http PRIVATE . .. ${HANDLER_INCLUDE} ${WAMR_ROOT}/core/iwasm/include) -target_compile_definitions(stackless_http PRIVATE P3_WAMR PICO_STACK_SIZE=4096 PICO_HEAP_SIZE=16384) +target_compile_definitions(stackless_http PRIVATE P3_WAMR PICO_STACK_SIZE=${stack_size} PICO_HEAP_SIZE=16384) target_link_libraries(stackless_http PRIVATE pico_stdlib pico_cyw43_arch_lwip_threadsafe_background) set_property(TARGET stackless_http PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) # GCC's LTO pass cannot see the SDK's ld --wrap references. Keep the wrappers diff --git a/crates/c-api/examples/stackless-http/pico/pico_server.c b/crates/c-api/examples/stackless-http/pico/pico_server.c index a474db2..5f06853 100644 --- a/crates/c-api/examples/stackless-http/pico/pico_server.c +++ b/crates/c-api/examples/stackless-http/pico/pico_server.c @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ #include "http.h" +#ifdef P3_WASI_HTTP +#include "standard_handler.h" +#else #include "handler.h" +#endif #include "pico/cyw43_arch.h" #include "pico/stdlib.h" #include "pico/sem.h" @@ -141,8 +145,7 @@ pump(unsigned i) panic("component start: %s", server.vm.error); progress = true; } - uint32_t limit = - c->input ? p3_native_capacity(&server.vm.async, c->input) : 0; + uint32_t limit = http_input_capacity(&server, i); if (limit > sizeof(data)) limit = sizeof(data); cyw43_arch_lwip_begin(); @@ -266,6 +269,9 @@ main(void) &server.vm, wasm_engine_new_with_args(Alloc_With_Pool, &memory), handler_wasm, sizeof(handler_wasm))) panic("component load: %s", server.vm.error); +#ifdef P3_WASI_HTTP + wasi_http_init(&server.http, &server.vm); +#endif uint64_t benchmark_start = time_us_64(); if (!http_native_loop(&server, 100000)) panic("native call benchmark"); diff --git a/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt b/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt index 09a60f2..148b2a5 100644 --- a/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt +++ b/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt @@ -12,8 +12,17 @@ if(NOT WAMR_ROOT OR NOT HANDLER_INCLUDE) message(FATAL_ERROR "Set WAMR_ROOT and HANDLER_INCLUDE") endif() include(../embedded-wamr.cmake) -add_executable(stackless_m33 ${WAMR_RUNTIME_LIB_SOURCE} ../p3.c ../vm.c ../task.c ../http.c +if(P3_WASI_HTTP) + set(http_sources ../wasi_http.c ../wasi_http_server.c) +else() + set(http_sources ../http.c) +endif() +add_executable(stackless_m33 ${WAMR_RUNTIME_LIB_SOURCE} ../p3.c ../vm.c ../task.c ${http_sources} startup.c sim.c) +if(P3_WASI_HTTP) + target_compile_definitions(stackless_m33 PRIVATE P3_WASI_HTTP) + target_link_options(stackless_m33 PRIVATE "LINKER:--defsym,__p3_stack_size=8192") +endif() target_include_directories(stackless_m33 PRIVATE .. ${HANDLER_INCLUDE} ${WAMR_ROOT}/core/iwasm/include) target_compile_definitions(stackless_m33 PRIVATE P3_WAMR P3_QEMU PICO_HEAP_SIZE=16384) target_compile_options(stackless_m33 PRIVATE -mcpu=cortex-m33 -mthumb -mfloat-abi=soft -ffunction-sections -fdata-sections) diff --git a/crates/c-api/examples/stackless-http/qemu/m33.ld b/crates/c-api/examples/stackless-http/qemu/m33.ld index e3f3eb6..a8af8d4 100644 --- a/crates/c-api/examples/stackless-http/qemu/m33.ld +++ b/crates/c-api/examples/stackless-http/qemu/m33.ld @@ -8,6 +8,7 @@ MEMORY { RAM (rwx) : ORIGIN = 0x38000000, LENGTH = 520K } ENTRY(reset) +PROVIDE(__p3_stack_size = 4K); SECTIONS { .text : { KEEP(*(.vectors)) *(.text*) *(.rodata*) . = ALIGN(4); } > XIP .ARM.exidx : { *(.ARM.exidx*) } > XIP @@ -16,6 +17,6 @@ SECTIONS { .bss (NOLOAD) : { __bss_start__ = .; *(.bss*) *(COMMON) . = ALIGN(8); __bss_end__ = .; } > RAM .net_reserve (NOLOAD) : { . += 64K; } > RAM .heap (NOLOAD) : { __end__ = .; . += 16K; } > RAM - .stack (NOLOAD) : { __StackBottom = .; . += 4K; __StackTop = .; } > RAM + .stack (NOLOAD) : { __StackBottom = .; . += __p3_stack_size; __StackTop = .; } > RAM ASSERT(__StackTop <= ORIGIN(RAM) + LENGTH(RAM), "Pico RAM budget exceeded") } diff --git a/crates/c-api/examples/stackless-http/qemu/run.py b/crates/c-api/examples/stackless-http/qemu/run.py index 9d22007..1df85a4 100644 --- a/crates/c-api/examples/stackless-http/qemu/run.py +++ b/crates/c-api/examples/stackless-http/qemu/run.py @@ -19,7 +19,7 @@ def run(*args): text=True, timeout=60) -def ram(elf): +def ram(elf, stack=4096): sections = {} for line in run('arm-none-eabi-size', '-A', elf).stdout.splitlines(): words = line.split() @@ -30,7 +30,7 @@ def ram(elf): sections[name] = size used = sum(sections.values()) if (not 131072 < used < 520 * 1024 or sections.get('.heap') != 16384 - or sections.get('.stack', sections.get('.stack_dummy')) != 4096): + or sections.get('.stack', sections.get('.stack_dummy')) != stack): raise RuntimeError(f'bad Cortex-M33 SRAM accounting: {sections}') return {'sram_reserved_bytes': used, 'sram_limit_bytes': 520 * 1024, 'sram_sections': sections} @@ -80,14 +80,17 @@ def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('elf', type=Path) p.add_argument('--pico-elf', type=Path) + p.add_argument('--wasi-http', action='store_true') p.add_argument('--qemu', default='qemu-system-arm') p.add_argument('--output', type=Path, required=True) a = p.parse_args() a.output.mkdir(parents=True, exist_ok=True) command = [a.qemu, '-M', 'mps2-an505', '-nographic', '-monitor', 'none', '-serial', 'null', '-net', 'none', '-icount', 'shift=0,sleep=off', '-kernel', a.elf.resolve()] - test = result(run(*command, '-semihosting-config', 'enable=on,target=native')) - if (test['completed'], test['cancelled'], test['disconnected'], test['live']) != (34, 1, 1, 0): + workload = run(*command, '-semihosting-config', 'enable=on,target=native') + test = result(workload) + expected = (4, 0, 1, 0) if a.wasi_http else (34, 1, 1, 0) + if (test['completed'], test['cancelled'], test['disconnected'], test['live']) != expected: raise RuntimeError(f'incomplete simulated HTTP workload: {test}') trace = a.output / 'instructions.log' count = result(run(*command, '-semihosting-config', 'enable=on,target=native,arg=count', @@ -95,12 +98,20 @@ def main(): if count['completed'] or count['idle_wakes'] != 1: raise RuntimeError('instruction-only mode did not run') ops = instructions(a.elf, trace) + stack = 8192 if a.wasi_http else 4096 report = {'qemu': run(a.qemu, '--version').stdout.splitlines()[0], 'machine': 'mps2-an505', 'elf_sha256': hashlib.sha256(a.elf.read_bytes()).hexdigest(), 'simulated_http': test, - 'm33_link': ram(a.elf), 'instruction_counts': ops, + 'm33_link': ram(a.elf, stack), 'instruction_counts': ops, 'instructions_per_context_get_import': ops['native'] / count['native_imports']} + if a.wasi_http: + row = next(line for line in (workload.stdout + workload.stderr).splitlines() + if line.startswith('WASI HTTP ')) + report['wasi_http'] = {key: int(value) for key, value in + (item.split('=') for item in row.split()[2:])} + if report['wasi_http']['resources_live']: + raise RuntimeError('HTTP resources leaked') if a.pico_elf: - report['pico_link'] = ram(a.pico_elf) + report['pico_link'] = ram(a.pico_elf, stack) report['pico_link']['elf_sha256'] = hashlib.sha256(a.pico_elf.read_bytes()).hexdigest() (a.output / 'results.json').write_text(json.dumps(report, indent=2) + '\n') print(json.dumps(report, indent=2)) diff --git a/crates/c-api/examples/stackless-http/qemu/sim.c b/crates/c-api/examples/stackless-http/qemu/sim.c index e6b84ed..854bb0a 100644 --- a/crates/c-api/examples/stackless-http/qemu/sim.c +++ b/crates/c-api/examples/stackless-http/qemu/sim.c @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ #include "http.h" +#ifdef P3_WASI_HTTP +#include "standard_handler.h" +#else #include "handler.h" +#endif #include "wasm_export.h" #include #include @@ -73,6 +77,9 @@ wait_for_irq(void) } idle_end(); } +#ifdef P3_WASI_HTTP +#include "wasi_http_sim.h" +#else static void send_request(unsigned slot, const char *request) { @@ -144,6 +151,7 @@ run_http(void) CHECK(server.completed == 34 && server.cancelled == 1 && server.disconnected == 1); } +#endif int main(void) { @@ -153,6 +161,9 @@ main(void) CHECK(p3_vm_open_with_engine( &server.vm, wasm_engine_new_with_args(Alloc_With_Pool, &mem), handler_wasm, sizeof(handler_wasm))); +#ifdef P3_WASI_HTTP + wasi_http_init(&server.http, &server.vm); +#endif char command[64] = { 0 }; uintptr_t cmd[] = { (uintptr_t)command, sizeof(command) }; semi(0x15, (uintptr_t)cmd); @@ -165,6 +176,12 @@ main(void) CHECK(ok); wait_for_irq(); CHECK(wakes == 1 && server.vm.async.live == 0); +#ifdef P3_WASI_HTTP + CHECK(server.http.live == 0); + printf("WASI HTTP server_bytes=%lu resources_peak=%lu resources_live=%lu\n", + (unsigned long)sizeof(server), (unsigned long)server.http.peak, + (unsigned long)server.http.live); +#endif mem_alloc_info_t usage = { 0 }; CHECK(wasm_runtime_get_mem_alloc_info(&usage)); CHECK(usage.highmark_size >= 65536 && usage.highmark_size <= sizeof(pool)); diff --git a/crates/c-api/examples/stackless-http/qemu/wasi_http_sim.h b/crates/c-api/examples/stackless-http/qemu/wasi_http_sim.h new file mode 100644 index 0000000..70f5d9a --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/wasi_http_sim.h @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "payloads.h" +#include "upload.h" + +typedef struct workload { + const uint8_t *bytes; + uint32_t size, uploaded, received, chunk; + unsigned state, digits, suffix; +} workload; + +static void +receive_byte(workload *w, uint8_t ch) +{ + switch (w->state) { + case 0: + { + /* Parse the first line and then scan through CRLF CRLF. */ + static const char status[] = "HTTP/1.1 200\r\n"; + CHECK(ch == (uint8_t)status[w->suffix]); + if (++w->suffix == sizeof(status) - 1) { + w->state = 1; + w->suffix = 2; + } + break; + } + case 1: + { + static const char end[] = "\r\n\r\n"; + if (ch == (uint8_t)end[w->suffix]) { + if (++w->suffix == 4) { + w->state = 2; + w->suffix = 0; + } + } + else + w->suffix = ch == '\r'; + break; + } + case 2: + if (ch == '\r') { + CHECK(w->digits && w->digits <= 4); + w->state = 3; + } + else { + CHECK( + w->digits < 4 + && ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'))); + w->chunk = + w->chunk * 16 + (ch <= '9' ? ch - '0' : ch - 'a' + 10); + ++w->digits; + } + break; + case 3: + CHECK(ch == '\n'); + w->state = w->chunk ? 4 : 6; + w->suffix = 0; + break; + case 4: + CHECK(w->received < w->size && ch == w->bytes[w->received++]); + if (!--w->chunk) { + w->state = 5; + w->suffix = 0; + } + break; + case 6: + { + const char *end = + w->size > 4096 ? "x-upload-check: complete\r\n\r\n" : "\r\n"; + CHECK(ch == (uint8_t)end[w->suffix]); + if (!end[++w->suffix]) + w->state = 7; + break; + } + case 5: + CHECK(ch == (w->suffix ? '\n' : '\r')); + if (w->suffix++) { + w->state = 2; + w->digits = 0; + } + break; + default: + CHECK(false); + } +} + +static void +run_http(void) +{ + static const char *const requests[] = { + "GET /data.json HTTP/1.1\r\nHost: localhost\r\n\r\n", + "GET /image.png HTTP/1.1\r\nHost: localhost\r\n\r\n", + "POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: " + "16384\r\n\r\n", + "POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: " + "16384\r\n\r\n" + }; + workload jobs[HTTP_CONNECTIONS] = { + { json_body, sizeof(json_body), sizeof(json_body), 0, 0, 0, 0, 0 }, + { png_body, sizeof(png_body), sizeof(png_body), 0, 0, 0, 0, 0 }, + { upload_body, sizeof(upload_body), 0, 0, 0, 0, 0, 0 }, + { upload_body, sizeof(upload_body), 0, 0, 0, 0, 0, 0 } + }; + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) { + CHECK(http_open(&server, i)); + CHECK(http_input(&server, i, (uint8_t *)requests[i], + (uint32_t)strlen(requests[i]))); + } + CHECK(!http_open(&server, 0) && !http_open(&server, HTTP_CONNECTIONS)); + unsigned complete = 0; + for (unsigned turn = 0; turn < 4096 && complete < HTTP_CONNECTIONS; + ++turn) { + for (unsigned i = 0; i < HTTP_CONNECTIONS; ++i) { + http_connection *c = &server.connections[i]; + if (!c->live) + continue; + workload *w = &jobs[i]; + CHECK(http_progress(&server, i)); + uint32_t cap = http_input_capacity(&server, i); + if (cap > 379) + cap = 379; + if (cap > w->size - w->uploaded) + cap = w->size - w->uploaded; + if (cap) { + CHECK(http_input(&server, i, (uint8_t *)w->bytes + w->uploaded, + cap)); + w->uploaded += cap; + } + /* Force backpressure on one upload while the others progress. */ + if (i != 3 || turn >= 32) { + size_t window = c->length - c->sent; + if (window > 73) + window = 73; + for (size_t j = 0; j < window; ++j) + receive_byte(w, c->output[c->sent++]); + } + if (http_finished(c)) { + CHECK(w->state == 7 && w->received == w->size); + CHECK(http_reap(&server, i)); + ++complete; + } + } + } + CHECK(complete == HTTP_CONNECTIONS); + CHECK(http_open(&server, 0)); + CHECK(http_input(&server, 0, (uint8_t *)requests[2], + (uint32_t)strlen(requests[2]))); + CHECK(http_progress(&server, 0)); + CHECK(http_disconnect(&server, 0)); + CHECK(http_progress(&server, 0)); + CHECK(http_reap(&server, 0)); + CHECK(server.completed == 4 && server.disconnected == 1); +} diff --git a/crates/c-api/examples/stackless-http/server.c b/crates/c-api/examples/stackless-http/server.c index 0d7a98e..389d08a 100644 --- a/crates/c-api/examples/stackless-http/server.c +++ b/crates/c-api/examples/stackless-http/server.c @@ -7,7 +7,11 @@ #define _DARWIN_C_SOURCE #endif #include "http.h" +#ifdef P3_WASI_HTTP +#include "standard_handler.h" +#else #include "handler.h" +#endif #include #include #include @@ -111,6 +115,15 @@ rss(void) } #endif static http_server server; +#if defined(P3_WASI_HTTP) && !defined(_WIN32) +static volatile sig_atomic_t interrupted; +static void +interrupt(int sig) +{ + (void)sig; + interrupted = 1; +} +#endif int main(int argc, char **argv) @@ -126,9 +139,39 @@ main(int argc, char **argv) return 2; #else signal(SIGPIPE, SIG_IGN); +#ifdef P3_WASI_HTTP + struct sigaction action = { 0 }; + action.sa_handler = interrupt; + sigemptyset(&action.sa_mask); + sigaction(SIGINT, &action, NULL); #endif - if (!p3_vm_open(&server.vm, handler_wasm, sizeof(handler_wasm))) +#endif + const uint8_t *module = handler_wasm; + size_t module_size = sizeof(handler_wasm); + uint8_t *file_bytes = NULL; +#ifdef P3_WASI_HTTP + if (argc > 3) { + FILE *file = fopen(argv[3], "rb"); + if (!file || fseek(file, 0, SEEK_END)) + goto fail; + long size = ftell(file); + if (size <= 0 || fseek(file, 0, SEEK_SET)) + goto fail; + module_size = (size_t)size; + file_bytes = malloc(module_size); + if (!file_bytes || fread(file_bytes, 1, module_size, file) != module_size) + goto fail; + fclose(file); + module = file_bytes; + } +#endif + bool opened = p3_vm_open(&server.vm, module, module_size); + free(file_bytes); + if (!opened) goto fail; +#ifdef P3_WASI_HTTP + wasi_http_init(&server.http, &server.vm); +#endif if (bench) { const int32_t iterations = 10000000; if (!http_native_loop(&server, 10000)) @@ -165,9 +208,13 @@ main(int argc, char **argv) printf("LISTEN %u\n", ntohs(addr.sin_port)); fflush(stdout); double wall_start = wall_time(), cpu_start = cpu_time(); - unsigned wakes = 0, waits = 0; + unsigned wakes = 0, waits = 0, rejected = 0; uint8_t input[HTTP_CHUNK]; while (wall_time() - wall_start < seconds) { +#if defined(P3_WASI_HTTP) && !defined(_WIN32) + if (interrupted) + break; +#endif pollfd_t fds[HTTP_CONNECTIONS + 1]; unsigned owners[HTTP_CONNECTIONS + 1], count = 1; fds[0] = (pollfd_t){ listener, POLLIN, 0 }; @@ -190,7 +237,7 @@ main(int argc, char **argv) if (sockets[i] == BAD_SOCKET) continue; short events = 0; - if (c->input && p3_native_capacity(&server.vm.async, c->input)) + if (http_input_capacity(&server, i)) events |= POLLIN; if (c->sent < c->length) events |= POLLOUT; @@ -217,8 +264,10 @@ main(int argc, char **argv) unsigned i = 0; while (i < HTTP_CONNECTIONS && server.connections[i].live) ++i; - if (i == HTTP_CONNECTIONS || !nonblock(fd)) + if (i == HTTP_CONNECTIONS || !nonblock(fd)) { + ++rejected; CLOSE(fd); + } else { int cap = 4096; setsockopt(fd, SOL_SOCKET, SO_SNDBUF, (const char *)&cap, @@ -237,7 +286,7 @@ main(int argc, char **argv) bool broken = (fds[j].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0; if (!broken && (fds[j].revents & POLLIN)) { - uint32_t cap = p3_native_capacity(&server.vm.async, c->input); + uint32_t cap = http_input_capacity(&server, i); if (cap > sizeof(input)) cap = sizeof(input); int n = (int)recv(sockets[i], (char *)input, (int)cap, 0); @@ -280,6 +329,12 @@ main(int argc, char **argv) || !http_reap(&server, i)) goto fail; } +#ifdef P3_WASI_HTTP + printf("HTTP resources_live=%u resources_peak=%u rejected=%u\n", + server.http.live, server.http.peak, rejected); +#else + (void)rejected; +#endif printf("RESULT completed=%u cancelled=%u disconnected=%u callbacks=%u " "transfers=%u bytes=%llu " "live=%u peak_handles=%u server_bytes=%zu rss_bytes=%zu cpu_s=%.9f " diff --git a/crates/c-api/examples/stackless-http/test_wasi_http_load.py b/crates/c-api/examples/stackless-http/test_wasi_http_load.py new file mode 100644 index 0000000..8a9aaff --- /dev/null +++ b/crates/c-api/examples/stackless-http/test_wasi_http_load.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Check concurrent standard-component HTTP/1.1 bodies, overload and recovery.""" +import argparse +from concurrent.futures import ThreadPoolExecutor +import http.client +import json +import os +import signal +import socket +import struct +import subprocess +import threading +import time + +from build_wasi_http_guest import json_body, png + +ASSETS = { + '/data.json': ('GET', 'application/json', json_body()), + '/image.png': ('GET', 'image/png', png(4096)), + '/upload': ('POST', 'image/png', png(16384)), +} + + +def request(sock, path, barrier=None): + method, content_type, expected = ASSETS[path] + body = expected if method == 'POST' else b'' + head = (f'{method} {path} HTTP/1.1\r\nHost: localhost\r\n' + f'Content-Length: {len(body)}\r\nContent-Type: {content_type}\r\n') + sock.sendall(head.encode()) + if barrier: + barrier.wait(timeout=3) + sock.sendall(b'\r\n') + for offset in range(0, len(body), 509): + sock.sendall(body[offset:offset + 509]) + response = http.client.HTTPResponse(sock) + response.begin() + assert response.status == 200, (path, response.status) + assert response.getheader('content-type') == content_type + assert response.read() == expected, path + + +def run(exe): + process = subprocess.Popen([exe, '0', '6' if os.name == 'nt' else '60'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True) + try: + line = process.stdout.readline() + assert line.startswith('LISTEN '), line + port = int(line.split()[1]) + def connect(): + return socket.create_connection(('127.0.0.1', port), timeout=3) + time.sleep(.25) + def batch(paths): + barrier = threading.Barrier(len(paths)) + def get(path): + with connect() as sock: + request(sock, path, barrier) + with ThreadPoolExecutor(max_workers=len(paths)) as pool: + list(pool.map(get, paths)) + for count in range(1, 5): + batch([('/data.json', '/image.png')[i % 2] for i in range(count)]) + batch(['/upload'] * count) + batch(['/data.json', '/image.png', '/upload', '/upload']) + failures = {} + for path in ('/data.json', '/image.png', '/upload'): + holders = [] + try: + for _ in range(4): + sock = connect() + holders.append(sock) + if path == '/upload': + sock.sendall(b'POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: 16384\r\n\r\n') + sock.sendall(ASSETS[path][2][:509]) + else: + sock.sendall(f'GET {path} HTTP/1.1\r\nHost: localhost\r\n'.encode()) + # Wait for all four sockets to be admitted before offering the fifth. + time.sleep(.05) + def probe(_): + phase = 'connect' + try: + with connect() as extra: + phase = 'established' + method = ASSETS[path][0] + length = 16384 if method == 'POST' else 0 + extra.sendall((f'{method} {path} HTTP/1.1\r\nHost: localhost\r\n' + f'Content-Length: {length}\r\n\r\n').encode()) + assert extra.recv(256) == b'', 'over-capacity connection served' + return 'established:eof' + except ConnectionResetError: + return phase + ':reset' + except BrokenPipeError: + return phase + ':broken-pipe' + except ConnectionRefusedError: + return phase + ':refused' + with ThreadPoolExecutor(max_workers=4) as pool: + failures[path] = list(pool.map(probe, range(4))) + finally: + for sock in holders: + sock.close() + time.sleep(.05) + batch([path]) + # Reset after seeing a response while a POST producer is still waiting for bytes. + with connect() as sock: + sock.sendall(b'POST /upload HTTP/1.1\r\nHost: localhost\r\nContent-Length: 16384\r\n\r\n') + sock.sendall(ASSETS['/upload'][2][:509]) + assert sock.recv(256).startswith(b'HTTP/1.1 200') + linger = 'HH' if os.name == 'nt' else 'ii' + sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack(linger, 1, 0)) + batch(['/data.json', '/image.png', '/upload']) + for headers, status in ( + (b'Content-Length: 1\r\nContent-Length: 1\r\n', 400), + (b'Content-Length: 1\r\nTransfer-Encoding: chunked\r\n', 400), + (b'Content-Length: 65537\r\n', 413), + (b'X-Too-Large: ' + b'x' * 2200 + b'\r\n', 431), + ): + with connect() as sock: + sock.sendall(b'POST /upload HTTP/1.1\r\nHost: localhost\r\n' + headers + b'\r\n') + response = http.client.HTTPResponse(sock) + response.begin() + assert response.status == status and not response.read() + with connect() as sock: + sock.sendall(b'GET /\xff HTTP/1.1\r\nHost: localhost\r\n\r\n') + response = http.client.HTTPResponse(sock) + response.begin() + assert response.status == 400 and not response.read() + batch(['/data.json']) + if os.name != 'nt': + process.send_signal(signal.SIGINT) + out, err = process.communicate(timeout=8) + assert process.returncode == 0 and not err, (out, err, process.returncode) + assert 'resources_live=0 ' in out and ' live=0 ' in out, out + return {'overload': failures, 'server': out.strip().splitlines()} + except Exception: + if process.poll() is None: + process.kill() + out, err = process.communicate() + print(out, err) + raise + + +if __name__ == '__main__': + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('server') + a = p.parse_args() + print(json.dumps(run(a.server), indent=2)) diff --git a/crates/c-api/examples/stackless-http/vm.c b/crates/c-api/examples/stackless-http/vm.c index 1be2a3c..095af0f 100644 --- a/crates/c-api/examples/stackless-http/vm.c +++ b/crates/c-api/examples/stackless-http/vm.c @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ #include "vm.h" +#ifdef P3_WASI_HTTP +#include "wasi_http.h" +#endif #include #include @@ -47,6 +50,27 @@ trap_message(p3_vm *vm, wasm_trap_t *t) wasm_byte_vec_delete(&message); wasm_trap_delete(t); } +#ifdef P3_WASI_HTTP +static wasm_trap_t * +http_import(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results) +{ + p3_binding *b = env; + return wasi_http_call(b->vm, b->http_op, args, results) + ? NULL + : trap(b->vm, b->vm->async.error ? b->vm->async.error + : "invalid WASI HTTP operation"); +} +#endif +#ifdef P3_WASI_HTTP_HOSTED +static wasm_trap_t * +unsupported_import(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results) +{ + p3_binding *b = env; + (void)args; + (void)results; + return trap(b->vm, b->unsupported); +} +#endif static wasm_trap_t * canonical(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results) { @@ -151,13 +175,23 @@ binding(p3_binding *b, const wasm_name_t *name) return false; *async_++ = 0; if (strcmp(element, "u8") && strcmp(element, "unit") - && strcmp(element, "bool") && strcmp(element, "char")) + && strcmp(element, "bool") && strcmp(element, "char") +#ifdef P3_WASI_HTTP + && strcmp(element, "http-status") && strcmp(element, "http-trailers") +#endif + ) return false; if (strcmp(async_, "0") && strcmp(async_, "1")) return false; b->element = !strcmp(element, "char") ? 4 : !strcmp(element, "bool") ? 2 : (uint8_t)!strcmp(element, "u8"); +#ifdef P3_WASI_HTTP + if (!strcmp(element, "http-status")) + b->element = P3_HTTP_STATUS; + if (!strcmp(element, "http-trailers")) + b->element = P3_HTTP_TRAILERS; +#endif b->async_ = (uint8_t)!strcmp(async_, "1"); const char *operation = text; if (!strncmp(operation, "future.", 7)) { @@ -208,12 +242,28 @@ p3_vm_open_with_engine(p3_vm *vm, wasm_engine_t *engine, const uint8_t *bytes, b->vm = vm; const wasm_functype_t *type = wasm_externtype_as_functype_const( wasm_importtype_type(types.data[i])); - ok = type && name_is(wasm_importtype_module(types.data[i]), "p3") - && binding(b, wasm_importtype_name(types.data[i])); + const wasm_name_t *module = wasm_importtype_module(types.data[i]); + const wasm_name_t *name = wasm_importtype_name(types.data[i]); + wasm_func_callback_with_env_t callback = canonical; + ok = type && name_is(module, "p3") && binding(b, name); +#ifdef P3_WASI_HTTP + if (type && name_is(module, "wasi-http")) { + ok = (b->http_op = wasi_http_binding(name, type)) != 0; + callback = http_import; + } +#endif +#ifdef P3_WASI_HTTP_HOSTED + if (type && name_is(module, "unsupported-wasi")) { + snprintf(b->unsupported, sizeof(b->unsupported), + "unsupported host import: %.*s", (int)name->size, name->data); + ok = true; + callback = unsupported_import; + } +#endif if (!ok) break; wasm_func_t *fn = - wasm_func_new_with_env(vm->store, type, canonical, b, NULL); + wasm_func_new_with_env(vm->store, type, callback, b, NULL); if (!fn) { ok = false; break; diff --git a/crates/c-api/examples/stackless-http/vm.h b/crates/c-api/examples/stackless-http/vm.h index 8cb05ae..fe72bfe 100644 --- a/crates/c-api/examples/stackless-http/vm.h +++ b/crates/c-api/examples/stackless-http/vm.h @@ -10,7 +10,9 @@ #else #include "wasm.h" #endif +#ifndef P3_IMPORTS #define P3_IMPORTS 48 +#endif #define P3_EXPORTS 32 typedef struct p3_vm p3_vm; typedef struct p3_task p3_task; @@ -18,6 +20,12 @@ typedef struct p3_binding { p3_vm *vm; enum p3_op op; uint8_t future, element, async_; +#ifdef P3_WASI_HTTP + uint8_t http_op; +#endif +#ifdef P3_WASI_HTTP_HOSTED + char unsupported[96]; +#endif } p3_binding; typedef struct p3_export { wasm_func_t *func; @@ -37,6 +45,9 @@ struct p3_vm { char error[256]; p3_task *current; bool poisoned; +#ifdef P3_WASI_HTTP + struct wasi_http *http; +#endif }; struct p3_task { p3_vm *vm; @@ -45,6 +56,9 @@ struct p3_task { bool started, returned, cancel_requested, cancel_delivered, cancelled; bool exited, yielded, failed; uint32_t callbacks; +#ifdef P3_WASI_HTTP + uint32_t http_result; +#endif }; bool p3_vm_open(p3_vm *vm, const uint8_t *bytes, size_t size); diff --git a/crates/c-api/examples/stackless-http/wasi-http-receipts.json b/crates/c-api/examples/stackless-http/wasi-http-receipts.json new file mode 100644 index 0000000..de650f4 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi-http-receipts.json @@ -0,0 +1,303 @@ +{ + "suite_commit": "9d84631e427729802085c3d172d0a63cd360a1fa", + "suite_source_commit": "de7fbe267210b52f63893270403177239b4f4a9b", + "suite_artifact_sha256": { + "http-client-chunked": "da8e537cc820c0c579ad16053420d9211097af6ef830f906da845c39cee65d09", + "http-client-headers": "1fd4ac5b7b938c1b71a05b565ae43f4ad89366ba9f5c507fbcab45f91b3bf644", + "http-client-method": "bc55679416a7e55fbee147cc34fcc47d14f0bb63a6af4afefc9b5166c6b3f73c", + "http-client-path-none": "3aee70dfa58b6cff6bb2a7d842c067bb347823507153b29e9c12d269075d0b46", + "http-client-path": "497760eed2c4b6e2b5ddc2d72933342514983b76e33e846ce73580584f97e06c", + "http-client-send-errors": "da72e38a2e524c87acff10f604b22131779915d768df666fa713370131a35162", + "http-client-sent": "f16b9f2a49ed6e50fc6dd937d027e1bde161d72c302f232a8f781dcf5a65a3ae", + "http-client-status": "cccf6ce1bb2457b46d839a4961932be9a3aa3bcd55da29cf508395f2c6d53bf8", + "http-client-trailer-error": "c5f54f31c66ef38db9d777173ef45c788600f25648348291166ebcbb93b15470", + "http-client-trailers": "610d32a7af138f80d63f791989d5e48b0ca3c31690409b852c13f3f498d14291", + "http-client": "a94e02bf57ee96a8f18a450b0dc7989916329f1918994056f271baf66c045916", + "http-fields": "dc38c0db084eb9745f6b3a8f9d67a9f7571e60cdb9ef0c76bd7056c2a4b086b1", + "http-request-options": "6bd611a8d0fc700646989cb42fa1e4b96ea244ea2bcc1237ba6a1ef3fa5cb728", + "http-request": "79909ca8274b0789e79d38cb24b3ede890c3d44408448717838393226e733945", + "http-response": "e9dce3b6ac0443acdbe7890560218d8dc4bc68e6700d80ee66dc2fcb15d4cb87", + "http-service-echo": "8d1bab20ab13e8da0eb519987dab59a2c47fee5ceffb2e7d3d7d903425177519", + "http-service-uri": "8ddb69c09c2df11f8b39686b40eef1b73934b2aba44c9845c6364c1201fe44ab", + "http-service": "786e4638843018c3c39a875dbcc58166093975da7c1d2208d3a32930e138174e" + }, + "guest_sha256": { + "standard.component.wasm": "734b44a380a53ea438d7d261c00e389b6db3f13ff3105e35665cc78011998e08", + "standard.linked.wasm": "6ecc282d19a64121b846b5b302a779505b716168af1824279e7ed7db053d1fea" + }, + "guest_bytes": { + "standard.component.wasm": 21399, + "standard.linked.wasm": 11433 + }, + "host": "macOS arm64", + "compiler": "Homebrew clang version 23.1.1", + "hosted": { + "wamr": { + "suite": [ + "UNSUPPORTED http-client-chunked wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-headers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-method wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path-none wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-send-errors wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-sent wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-status wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailer-error wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client wasi:http/client.send requires outbound HTTP transport", + "PASS http-fields", + "PASS http-request-options", + "PASS http-request", + "PASS http-response", + "PASS http-service-echo", + "PASS http-service-uri", + "PASS http-service" + ], + "tcp_load": { + "overload": { + "/data.json": [ + "established:reset", + "established:reset", + "established:reset", + "established:reset" + ], + "/image.png": [ + "established:eof", + "established:reset", + "established:eof", + "established:reset" + ], + "/upload": [ + "established:reset", + "established:reset", + "established:eof", + "established:reset" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=9 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1244 transfers=894 bytes=536434 live=0 peak_handles=49 server_bytes=89440 rss_bytes=2359296 cpu_s=0.006356000 elapsed_s=0.602980 waits=542 wakes=541" + ] + }, + "idle": "RESULT completed=0 cancelled=0 disconnected=0 callbacks=0 transfers=0 bytes=0 live=0 peak_handles=0 server_bytes=89440 rss_bytes=2310144 cpu_s=0.000017000 elapsed_s=3.001023 waits=1 wakes=0", + "native_imports_per_sample": 10000000, + "native_ns_per_import": [ + 22.302, + 23.602, + 21.997, + 23.073, + 22.344, + 22.168, + 22.455 + ], + "native_median_ns_per_import": 22.344 + }, + "tiny": { + "suite": [ + "UNSUPPORTED http-client-chunked wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-headers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-method wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path-none wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-send-errors wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-sent wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-status wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailer-error wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client wasi:http/client.send requires outbound HTTP transport", + "PASS http-fields", + "PASS http-request-options", + "PASS http-request", + "PASS http-response", + "PASS http-service-echo", + "PASS http-service-uri", + "PASS http-service" + ], + "tcp_load": { + "overload": { + "/data.json": [ + "established:reset", + "established:reset", + "established:reset", + "established:reset" + ], + "/image.png": [ + "established:reset", + "established:reset", + "established:reset", + "established:reset" + ], + "/upload": [ + "established:reset", + "established:reset", + "established:reset", + "established:eof" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=9 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1229 transfers=884 bytes=536434 live=0 peak_handles=49 server_bytes=89392 rss_bytes=7405568 cpu_s=0.007595000 elapsed_s=0.589811 waits=468 wakes=467" + ] + }, + "idle": "RESULT completed=0 cancelled=0 disconnected=0 callbacks=0 transfers=0 bytes=0 live=0 peak_handles=0 server_bytes=89392 rss_bytes=7143424 cpu_s=0.000020000 elapsed_s=3.000549 waits=1 wakes=0", + "native_imports_per_sample": 10000000, + "native_ns_per_import": [ + 81.281, + 80.835, + 81.273, + 82.583, + 80.748, + 88.242, + 86.297 + ], + "native_median_ns_per_import": 81.281 + }, + "wamr-asan": { + "suite": [ + "UNSUPPORTED http-client-chunked wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-headers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-method wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path-none wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-send-errors wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-sent wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-status wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailer-error wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client wasi:http/client.send requires outbound HTTP transport", + "PASS http-fields", + "PASS http-request-options", + "PASS http-request", + "PASS http-response", + "PASS http-service-echo", + "PASS http-service-uri", + "PASS http-service" + ], + "tcp_load": { + "overload": { + "/data.json": [ + "established:eof", + "established:eof", + "established:reset", + "established:reset" + ], + "/image.png": [ + "established:reset", + "established:eof", + "established:eof", + "established:eof" + ], + "/upload": [ + "established:reset", + "established:reset", + "established:reset", + "established:reset" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=9 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1193 transfers=860 bytes=536434 live=0 peak_handles=49 server_bytes=89440 rss_bytes=12304384 cpu_s=0.006993000 elapsed_s=0.604320 waits=427 wakes=426" + ] + } + }, + "tiny-asan": { + "suite": [ + "UNSUPPORTED http-client-chunked wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-headers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-method wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path-none wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-send-errors wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-sent wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-status wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailer-error wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client wasi:http/client.send requires outbound HTTP transport", + "PASS http-fields", + "PASS http-request-options", + "PASS http-request", + "PASS http-response", + "PASS http-service-echo", + "PASS http-service-uri", + "PASS http-service" + ], + "tcp_load": { + "overload": { + "/data.json": [ + "established:reset", + "established:eof", + "established:reset", + "established:reset" + ], + "/image.png": [ + "established:reset", + "established:reset", + "established:reset", + "established:reset" + ], + "/upload": [ + "established:eof", + "established:reset", + "established:reset", + "established:reset" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=12 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1127 transfers=816 bytes=536434 live=0 peak_handles=49 server_bytes=89392 rss_bytes=22265856 cpu_s=0.015218000 elapsed_s=0.610379 waits=367 wakes=366" + ] + } + } + }, + "cortex_m33": { + "qemu": "QEMU emulator version 11.1.3", + "machine": "mps2-an505", + "elf_sha256": "4b29f64db2f358e716d2f1204ffbddb2ba0029901d9cdcf5c03fb01f520d77f9", + "simulated_http": { + "completed": 4, + "cancelled": 0, + "disconnected": 1, + "live": 0, + "peak_handles": 41, + "pool_peak": 111712, + "pool_reserved": 131072, + "libc_peak": 2968, + "idle_wakes": 1, + "native_imports": 1000 + }, + "m33_link": { + "sram_reserved_bytes": 300128, + "sram_limit_bytes": 532480, + "sram_sections": { + ".data": 3528, + ".bss": 206488, + ".net_reserve": 65536, + ".heap": 16384, + ".stack": 8192 + } + }, + "instruction_counts": { + "native": 465016, + "idle": 22 + }, + "instructions_per_context_get_import": 465.016, + "wasi_http": { + "server_bytes": 75248, + "resources_peak": 9, + "resources_live": 0 + }, + "pico_link": { + "sram_reserved_bytes": 291072, + "sram_limit_bytes": 532480, + "sram_sections": { + ".ram_vector_table": 272, + ".uninitialized_data": 16, + ".data": 9336, + ".bss": 256872, + ".heap": 16384, + ".stack_dummy": 8192 + }, + "elf_sha256": "11fd43be62fb019a2c6b2bb9d5f4ce1a8f4b622bc6b6851642e2800b695ce0b9" + } + } +} diff --git a/crates/c-api/examples/stackless-http/wasi_http.c b/crates/c-api/examples/stackless-http/wasi_http.c new file mode 100644 index 0000000..a1248a8 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http.c @@ -0,0 +1,893 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "wasi_http.h" +#include + +enum { + RETURN_HANDLE = 1, + RETURN_RUN, + DROP_FIELDS, + DROP_REQUEST, + DROP_RESPONSE, + DROP_OPTIONS, + FIELDS_NEW, + FIELDS_CLONE, + FIELDS_FROM_LIST, + FIELDS_GET, + FIELDS_HAS, + FIELDS_SET, + FIELDS_DELETE, + FIELDS_TAKE, + FIELDS_APPEND, + FIELDS_COPY, + REQUEST_NEW, + REQUEST_CONSUME, + REQUEST_HEADERS, + METHOD_GET, + METHOD_SET, + PATH_GET, + PATH_SET, + SCHEME_GET, + SCHEME_SET, + AUTHORITY_GET, + AUTHORITY_SET, + OPTIONS_GET, + RESPONSE_NEW, + STATUS_GET, + STATUS_SET, + RESPONSE_HEADERS, + OPTIONS_NEW, + OPTIONS_CLONE, + CONNECT_GET, + FIRST_GET, + BETWEEN_GET, + CONNECT_SET, + FIRST_SET, + BETWEEN_SET +}; +enum { INVALID_SYNTAX, FORBIDDEN, IMMUTABLE, SIZE_EXCEEDED, HEADER_OK = -1 }; +static const struct { + const char *name; + uint8_t argc, result; +} bindings[] = { + [RETURN_HANDLE] = { "[return]handle", 8, 0 }, + [RETURN_RUN] = { "[return]run", 1, 0 }, + [DROP_FIELDS] = { "[drop]fields", 1, 0 }, + [DROP_REQUEST] = { "[drop]request", 1, 0 }, + [DROP_RESPONSE] = { "[drop]response", 1, 0 }, + [DROP_OPTIONS] = { "[drop]request-options", 1, 0 }, + [FIELDS_NEW] = { "[constructor]fields", 0, 1 }, + [FIELDS_CLONE] = { "[method]fields.clone", 1, 1 }, + [FIELDS_FROM_LIST] = { "[static]fields.from-list", 3, 0 }, + [FIELDS_GET] = { "[method]fields.get", 4, 0 }, + [FIELDS_HAS] = { "[method]fields.has", 3, 1 }, + [FIELDS_SET] = { "[method]fields.set", 6, 0 }, + [FIELDS_DELETE] = { "[method]fields.delete", 4, 0 }, + [FIELDS_TAKE] = { "[method]fields.get-and-delete", 4, 0 }, + [FIELDS_APPEND] = { "[method]fields.append", 6, 0 }, + [FIELDS_COPY] = { "[method]fields.copy-all", 2, 0 }, + [REQUEST_NEW] = { "[static]request.new", 7, 0 }, + [REQUEST_CONSUME] = { "[static]request.consume-body", 3, 0 }, + [REQUEST_HEADERS] = { "[method]request.get-headers", 1, 1 }, + [METHOD_GET] = { "[method]request.get-method", 2, 0 }, + [METHOD_SET] = { "[method]request.set-method", 4, 1 }, + [PATH_GET] = { "[method]request.get-path-with-query", 2, 0 }, + [PATH_SET] = { "[method]request.set-path-with-query", 4, 1 }, + [SCHEME_GET] = { "[method]request.get-scheme", 2, 0 }, + [SCHEME_SET] = { "[method]request.set-scheme", 5, 1 }, + [AUTHORITY_GET] = { "[method]request.get-authority", 2, 0 }, + [AUTHORITY_SET] = { "[method]request.set-authority", 4, 1 }, + [OPTIONS_GET] = { "[method]request.get-options", 2, 0 }, + [RESPONSE_NEW] = { "[static]response.new", 5, 0 }, + [STATUS_GET] = { "[method]response.get-status-code", 1, 1 }, + [STATUS_SET] = { "[method]response.set-status-code", 2, 1 }, + [RESPONSE_HEADERS] = { "[method]response.get-headers", 1, 1 }, + [OPTIONS_NEW] = { "[constructor]request-options", 0, 1 }, + [OPTIONS_CLONE] = { "[method]request-options.clone", 1, 1 }, + [CONNECT_GET] = { "[method]request-options.get-connect-timeout", 2, 0 }, + [FIRST_GET] = { "[method]request-options.get-first-byte-timeout", 2, 0 }, + [BETWEEN_GET] = { "[method]request-options.get-between-bytes-timeout", 2, + 0 }, + [CONNECT_SET] = { "[method]request-options.set-connect-timeout", 4, 0 }, + [FIRST_SET] = { "[method]request-options.set-first-byte-timeout", 4, 0 }, + [BETWEEN_SET] = { "[method]request-options.set-between-bytes-timeout", 4, + 0 }, +}; + +static bool +fail(wasi_http *http, const char *reason) +{ + http->vm->async.error = reason; + return false; +} +static bool +wide_arg(unsigned op, size_t i) +{ + return (op >= CONNECT_SET && i == 2) || (op == RETURN_HANDLE && i == 3); +} +uint8_t +wasi_http_binding(const wasm_name_t *name, const wasm_functype_t *type) +{ + const wasm_valtype_vec_t *params = wasm_functype_params(type); + const wasm_valtype_vec_t *results = wasm_functype_results(type); + for (unsigned i = 1; i < sizeof(bindings) / sizeof(*bindings); ++i) { + size_t len = strlen(bindings[i].name); + if ((name->size != len && (name->size != len + 1 || name->data[len])) + || memcmp(name->data, bindings[i].name, len)) + continue; + if (params->size != bindings[i].argc + || results->size != bindings[i].result + || (results->size + && wasm_valtype_kind(results->data[0]) != WASM_I32)) + return 0; + for (size_t p = 0; p < params->size; ++p) + if (wasm_valtype_kind(params->data[p]) + != (wide_arg(i, p) ? WASM_I64 : WASM_I32)) + return 0; + return (uint8_t)i; + } + return 0; +} +void +wasi_http_init(wasi_http *http, p3_vm *vm) +{ + memset(http, 0, sizeof(*http)); + http->vm = vm; + http->realloc_export = p3_vm_export(vm, "cabi_realloc"); + vm->http = http; +} +_Static_assert(HTTP_RESOURCE_SLOTS <= 255 && HTTP_FIELD_BYTES <= UINT16_MAX, + "HTTP resource index widths"); +uint32_t +wasi_http_new(wasi_http *http, uint8_t kind, bool native) +{ + for (unsigned i = 0; i < HTTP_RESOURCE_SLOTS; ++i) { + http_resource *r = &http->resources[i]; + if (r->kind || r->generation == 0xffffff) + continue; + memset(&r->as, 0, sizeof(r->as)); + r->kind = kind; + r->native = native; + r->immutable = false; + if (++http->live > http->peak) + http->peak = http->live; + return (++r->generation << 8) | (i + 1); + } + fail(http, "HTTP resource table exhausted"); + return 0; +} +http_resource * +wasi_http_get(wasi_http *http, uint32_t handle, uint8_t kind, bool native) +{ + unsigned i = (handle & 255U) - 1U; + if (i < HTTP_RESOURCE_SLOTS) { + http_resource *r = &http->resources[i]; + if (r->kind == kind && r->native == native + && r->generation == handle >> 8) + return r; + } + fail(http, "invalid HTTP resource handle or ownership"); + return NULL; +} +bool +wasi_http_pair(wasi_http *http, bool future, uint8_t element, uint32_t *reader, + uint32_t *writer) +{ + uint64_t packed; + if (!p3_call(&http->vm->async, P3_NEW, future, element, true, 0, 0, 0, + &packed)) + return false; + *reader = (uint32_t)packed; + *writer = (uint32_t)(packed >> 32); + return true; +} +static bool +defer_future(wasi_http *http, uint32_t end) +{ + if (!end) + return true; + if (http->pending_count == HTTP_PENDING_DROPS) + return fail(http, "HTTP future disposal capacity exhausted"); + http->pending[http->pending_count++] = end; + return true; +} +bool +wasi_http_sweep(wasi_http *http) +{ + size_t i = 0; + while (i < http->pending_count) { + uint8_t value[P3_HTTP_RESULT_SIZE] = { 0 }; + bool complete; + if (!p3_http_finish(&http->vm->async, http->pending[i], value, + &complete)) + return false; + if (complete) + http->pending[i] = http->pending[--http->pending_count]; + else + ++i; + } + return true; +} +bool +wasi_http_drop(wasi_http *http, uint32_t handle, uint8_t kind, bool native) +{ + http_resource *r = wasi_http_get(http, handle, kind, native); + if (!r) + return false; + if (kind == HTTP_REQUEST || kind == HTTP_RESPONSE) { + http_message *m = &r->as.message; + if ((m->fields && !wasi_http_drop(http, m->fields, HTTP_FIELDS, true)) + || (m->options + && !wasi_http_drop(http, m->options, HTTP_OPTIONS, true)) + || (m->body && !p3_native_drop(&http->vm->async, m->body)) + || (m->trailers && !p3_native_drop(&http->vm->async, m->trailers)) + || !defer_future(http, m->sent)) + return false; + } + r->kind = 0; + --http->live; + return true; +} +static uint32_t +clone(wasi_http *http, const http_resource *r, bool immutable) +{ + uint32_t handle = wasi_http_new(http, r->kind, false); + if (handle) { + http_resource *copy = wasi_http_get(http, handle, r->kind, false); + copy->as = r->as; + copy->immutable = immutable; + } + return handle; +} +static uint8_t * +memory(wasi_http *http, uint32_t ptr, uint32_t len, unsigned align) +{ + p3_context *cx = &http->vm->async; + size_t size; + uint8_t *base = cx->memory(cx->memory_env, &size); + if (!base || (ptr & (align - 1)) || ptr > size || len > size - ptr) { + fail(http, "invalid HTTP canonical memory range"); + return NULL; + } + return base + ptr; +} +static uint32_t +read32(const uint8_t *src) +{ + uint32_t n = 0; + for (unsigned i = 0; i < 4; ++i) + n |= (uint32_t)src[i] << (i * 8); + return n; +} +static void +write32(uint8_t *dst, uint32_t n) +{ + for (unsigned i = 0; i < 4; ++i) + dst[i] = (uint8_t)(n >> (i * 8)); +} +static bool +alloc(wasi_http *http, const uint8_t *bytes, uint32_t len, unsigned align, + uint32_t *offset) +{ + *offset = align; + if (!len) + return true; + int32_t args[] = { 0, 0, (int32_t)align, (int32_t)len }, result; + bool has_result; + http->allocating = true; + bool ok = p3_vm_invoke(http->vm, http->realloc_export, args, 4, &result, + &has_result); + http->allocating = false; + if (!ok || !has_result || !result) + return fail(http, "guest cabi_realloc failed"); + *offset = (uint32_t)result; + uint8_t *dst = memory(http, *offset, len, align); + if (!dst) + return false; + if (bytes) + memcpy(dst, bytes, len); + return true; +} +static unsigned +lower(unsigned ch) +{ + return ch >= 'A' && ch <= 'Z' ? ch + ('a' - 'A') : ch; +} +static bool +alpha(unsigned ch) +{ + return lower(ch) >= 'a' && lower(ch) <= 'z'; +} +static bool +digit(unsigned ch) +{ + return ch >= '0' && ch <= '9'; +} +static bool +punct(unsigned ch, const char *set) +{ + return ch && ch < 128 && strchr(set, (int)ch) != NULL; +} +static bool +token(const uint8_t *s, uint32_t len) +{ + for (uint32_t i = 0; i < len; ++i) + if (!alpha(s[i]) && !digit(s[i]) && !punct(s[i], "!#$%&'*+-.^_`|~")) + return false; + return len != 0; +} +static bool +same_name(const uint8_t *a, size_t na, const uint8_t *b, size_t nb) +{ + if (na != nb) + return false; + for (size_t i = 0; i < na; ++i) + if (lower(a[i]) != lower(b[i])) + return false; + return true; +} +static int +validate_name(const uint8_t *name, uint32_t size) +{ + static const char *const forbidden[] = { "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "proxy-connection", + "transfer-encoding", + "upgrade", + "http2-settings", + "host" }; + if (!token(name, size)) + return INVALID_SYNTAX; + for (unsigned i = 0; i < sizeof(forbidden) / sizeof(*forbidden); ++i) + if (same_name(name, size, (const uint8_t *)forbidden[i], + strlen(forbidden[i]))) + return FORBIDDEN; + return HEADER_OK; +} +int +wasi_http_append(http_fields *f, const uint8_t *name, uint32_t names, + const uint8_t *value, uint32_t values) +{ + int error = validate_name(name, names); + if (error != HEADER_OK) + return error; + if (f->count == HTTP_FIELDS_MAX + || names > (uint32_t)HTTP_FIELD_BYTES - f->size + || values > (uint32_t)HTTP_FIELD_BYTES - f->size - names) + return SIZE_EXCEEDED; + for (uint32_t i = 0; i < values; ++i) + if ((value[i] < 32 && value[i] != '\t') || value[i] == 127) + return INVALID_SYNTAX; + f->field[f->count++] = + (http_field){ f->size, (uint16_t)names, (uint16_t)values }; + for (uint32_t i = 0; i < names; ++i) + f->bytes[f->size + i] = (uint8_t)lower(name[i]); + memcpy(f->bytes + f->size + names, value, values); + f->size += (uint16_t)(names + values); + return HEADER_OK; +} +static void +delete_fields(http_fields *f, unsigned mask) +{ + unsigned count = 0, length = 0; + for (unsigned i = 0; i < f->count; ++i) { + http_field h = f->field[i]; + if (mask & (1U << i)) + continue; + unsigned size = h.name_size + h.value_size; + memmove(f->bytes + length, f->bytes + h.offset, size); + h.offset = (uint16_t)length; + f->field[count++] = h; + length += size; + } + f->count = (uint16_t)count; + f->size = (uint16_t)length; +} +static bool +output_fields(wasi_http *http, const http_fields *f, const uint8_t *selected, + unsigned count, bool all, uint32_t out) +{ + uint32_t list, width = all ? 16 : 8; + if (!alloc(http, NULL, count * width, 4, &list)) + return false; + for (unsigned i = 0; i < count; ++i) { + http_field h = f->field[selected[i]]; + const uint8_t *name = f->bytes + h.offset; + uint32_t key = 0, value; + if ((all && !alloc(http, name, h.name_size, 1, &key)) + || !alloc(http, name + h.name_size, h.value_size, 1, &value)) + return false; + uint8_t *dst = memory(http, list + i * width, width, 4); + if (!dst) + return false; + if (all) { + write32(dst, key); + write32(dst + 4, h.name_size); + dst += 8; + } + write32(dst, value); + write32(dst + 4, h.value_size); + } + uint8_t *ret = memory(http, out, 8, 4); + if (!ret) + return false; + write32(ret, list); + write32(ret + 4, count); + return true; +} +static bool +header_result(wasi_http *http, uint32_t ptr, int error) +{ + uint8_t *out = memory(http, ptr, 16, 4); + if (!out) + return false; + memset(out, 0, 16); + if (error != HEADER_OK) { + out[0] = 1; + out[4] = (uint8_t)error; + } + return true; +} +static bool +field_import(wasi_http *http, unsigned op, const uint32_t *a, uint32_t *ret) +{ + if (op == FIELDS_NEW) + return (*ret = wasi_http_new(http, HTTP_FIELDS, false)) != 0; + if (op == FIELDS_FROM_LIST) { + http_fields fields = { 0 }; + int error = HEADER_OK; + if (a[1] > HTTP_FIELDS_MAX) + error = SIZE_EXCEEDED; + for (uint32_t i = 0; i < a[1] && error == HEADER_OK; ++i) { + uint8_t *item = memory(http, a[0], a[1] * 16, 4); + if (!item) + return false; + item += i * 16; + uint32_t ns = read32(item + 4), vs = read32(item + 12); + const uint8_t *name = memory(http, read32(item), ns, 1); + const uint8_t *value = memory(http, read32(item + 8), vs, 1); + if (!name || !value) + return false; + error = wasi_http_append(&fields, name, ns, value, vs); + } + if (!header_result(http, a[2], error)) + return false; + if (error == HEADER_OK) { + uint32_t h = wasi_http_new(http, HTTP_FIELDS, false); + if (!h) + return false; + wasi_http_get(http, h, HTTP_FIELDS, false)->as.fields = fields; + write32(memory(http, a[2], 16, 4) + 4, h); + } + return true; + } + http_resource *r = wasi_http_get(http, a[0], HTTP_FIELDS, false); + if (!r) + return false; + if (op == FIELDS_CLONE) + return (*ret = clone(http, r, false)) != 0; + http_fields *fields = &r->as.fields; + const uint8_t *name = + op == FIELDS_COPY ? NULL : memory(http, a[1], a[2], 1); + if (!name && op != FIELDS_COPY) + return false; + uint8_t selected[HTTP_FIELDS_MAX]; + unsigned count = 0, mask = 0; + for (unsigned i = 0; i < fields->count; ++i) { + http_field f = fields->field[i]; + if (!name + || same_name(fields->bytes + f.offset, f.name_size, name, a[2])) { + selected[count++] = (uint8_t)i; + mask |= 1U << i; + } + } + if (op == FIELDS_HAS) { + *ret = count != 0; + return true; + } + if (op == FIELDS_GET || op == FIELDS_COPY) + return output_fields(http, fields, selected, count, op == FIELDS_COPY, + a[op == FIELDS_COPY ? 1 : 3]); + uint32_t out = a[bindings[op].argc - 1]; + int error = r->immutable ? IMMUTABLE : validate_name(name, a[2]); + if (error != HEADER_OK) + return header_result(http, out, error); + if (op == FIELDS_APPEND) { + const uint8_t *value = memory(http, a[3], a[4], 1); + return value + && header_result( + http, out, + wasi_http_append(fields, name, a[2], value, a[4])); + } + if (op == FIELDS_SET) { + http_fields copy = *fields; + delete_fields(©, mask); + if (a[4] > HTTP_FIELDS_MAX) + error = SIZE_EXCEEDED; + for (uint32_t i = 0; i < a[4] && error == HEADER_OK; ++i) { + uint8_t *list = memory(http, a[3], a[4] * 8, 4); + if (!list) + return false; + uint32_t size = read32(list + i * 8 + 4); + const uint8_t *value = memory(http, read32(list + i * 8), size, 1); + if (!value) + return false; + error = wasi_http_append(©, name, a[2], value, size); + } + if (error == HEADER_OK) + *fields = copy; + return header_result(http, out, error); + } + if (!header_result(http, out, HEADER_OK)) + return false; + if (op == FIELDS_TAKE + && !output_fields(http, fields, selected, count, false, out + 4)) + return false; + delete_fields(fields, mask); + return true; +} + +static const char *const methods[] = { "GET", "HEAD", "POST", + "PUT", "DELETE", "CONNECT", + "OPTIONS", "TRACE", "PATCH" }; +int +wasi_http_method(const uint8_t *str, uint32_t len) +{ + if (len >= sizeof(((http_message *)0)->method_other) || !token(str, len)) + return -1; + for (unsigned i = 0; i < sizeof(methods) / sizeof(*methods); ++i) + if (strlen(methods[i]) == len && !memcmp(methods[i], str, len)) + return (int)i; + return 9; +} +static bool +utf8(const uint8_t *str, uint32_t len) +{ + for (uint32_t i = 0; i < len; ++i) { + unsigned lead = str[i]; + if (lead < 0x80) + continue; + if (lead < 0xc2 || lead > 0xf4) + return false; + unsigned tail = lead >= 0xf0 ? 3 : lead >= 0xe0 ? 2 : 1; + if (tail >= len - i) + return false; + unsigned next = str[i + 1]; + if ((lead == 0xe0 && next < 0xa0) || (lead == 0xed && next >= 0xa0) + || (lead == 0xf0 && next < 0x90) || (lead == 0xf4 && next >= 0x90)) + return false; + while (tail--) + if ((str[++i] & 0xc0) != 0x80) + return false; + } + return true; +} +bool +wasi_http_path(const uint8_t *str, uint32_t len) +{ + if (len >= HTTP_URI_MAX || !utf8(str, len) + || (len && str[0] != '/' && str[0] != '?' + && (len != 1 || str[0] != '*'))) + return false; + for (uint32_t i = 0; i < len; ++i) + if (!(str[i] >= 128 || alpha(str[i]) || digit(str[i]) + || punct(str[i], "-._~%!$&'()*+,;=:@/?"))) + return false; + return true; +} +bool +wasi_http_authority(const uint8_t *str, uint32_t len) +{ + if (!len || len >= HTTP_URI_MAX) + return false; + uint32_t host = 0; + for (uint32_t i = 0; i < len; ++i) + if (str[i] == '@') { + if (host || !i || str[0] == ':') + return false; + host = i + 1; + } + else if (!alpha(str[i]) && !digit(str[i]) + && !punct(str[i], "-._~%!$&'()*+,;=:")) + return false; + if (host == len || str[host] == ':') + return false; + bool port = false; + for (uint32_t i = host; i < len; ++i) { + if (port && !digit(str[i])) + return false; + if (str[i] == ':') { + if (i + 1 == len) + return false; + port = true; + } + } + return true; +} +static bool +string_result(wasi_http *http, uint32_t ptr, uint32_t tag, const char *str, + unsigned header) +{ + uint32_t len = (uint32_t)strlen(str), offset; + if (!alloc(http, (const uint8_t *)str, len, 1, &offset)) + return false; + uint8_t *out = memory(http, ptr, header + 8, 4); + if (!out) + return false; + write32(out, tag); + if (header == 8) + write32(out + 4, tag ? tag - 1 : 0); + write32(out + header, offset); + write32(out + header + 4, len); + if (header == 8) + write32(out, tag != 0); + return true; +} +static void +copy_text(char *dst, const uint8_t *src, size_t len) +{ + if (len) + memcpy(dst, src, len); + dst[len] = 0; +} +static bool +options_import(wasi_http *http, unsigned op, const uint32_t *a, + uint64_t timeout, uint32_t *ret) +{ + if (op == OPTIONS_NEW) + return (*ret = wasi_http_new(http, HTTP_OPTIONS, false)) != 0; + http_resource *r = wasi_http_get(http, a[0], HTTP_OPTIONS, false); + if (!r) + return false; + if (op == OPTIONS_CLONE) + return (*ret = clone(http, r, false)) != 0; + bool set = op >= CONNECT_SET; + unsigned index = op - (set ? CONNECT_SET : CONNECT_GET); + uint8_t mask = (uint8_t)(1U << index); + uint8_t *out = memory(http, a[set ? 3 : 1], 16, set ? 4 : 8); + if (!out) + return false; + if (set) { + out[0] = r->immutable; + out[4] = 1; /* request-options-error.immutable */ + if (!r->immutable) { + r->as.options.mask = + (uint8_t)((r->as.options.mask & ~mask) | (a[1] ? mask : 0)); + r->as.options.timeout[index] = timeout; + } + } + else { + out[0] = (r->as.options.mask & mask) != 0; + for (unsigned i = 0; i < 8; ++i) + out[8 + i] = (uint8_t)(r->as.options.timeout[index] >> (8 * i)); + } + return true; +} +static bool +message_new(wasi_http *http, bool request, const uint32_t *a) +{ + p3_context *cx = &http->vm->async; + http_resource *headers = wasi_http_get(http, a[0], HTTP_FIELDS, false); + http_resource *options = NULL; + if (request && a[4]) + options = wasi_http_get(http, a[5], HTTP_OPTIONS, false); + uint8_t *out = memory(http, a[request ? 6 : 4], 8, 4); + if (!headers || (request && a[4] && !options) || !out || a[1] > 1 + || (request && a[4] > 1)) + return false; + uint32_t h = + wasi_http_new(http, request ? HTTP_REQUEST : HTTP_RESPONSE, false); + if (!h) + return false; + http_message *m = + &wasi_http_get(http, h, request ? HTTP_REQUEST : HTTP_RESPONSE, false) + ->as.message; + uint32_t sent; + if (!p3_http_lift(cx, a[3], true, P3_HTTP_TRAILERS) + || (a[1] && !p3_http_lift(cx, a[2], false, 1)) + || !wasi_http_pair(http, true, P3_HTTP_STATUS, &sent, &m->sent) + || !p3_host_end(cx, m->sent)) + return false; + headers->native = true; + if (options) + options->native = true; + m->fields = a[0]; + m->options = options ? a[5] : 0; + m->body = a[1] ? a[2] : 0; + m->trailers = a[3]; + m->status = 200; + write32(out, h); + write32(out + 4, sent); + return true; +} +static bool +message_import(wasi_http *http, unsigned op, const uint32_t *a, uint32_t *ret) +{ + if (op == REQUEST_NEW || op == RESPONSE_NEW) + return message_new(http, op == REQUEST_NEW, a); + bool response = op >= STATUS_GET; + uint8_t kind = response ? HTTP_RESPONSE : HTTP_REQUEST; + http_resource *r = wasi_http_get(http, a[0], kind, false); + if (!r) + return false; + http_message *m = &r->as.message; + switch (op) { + case REQUEST_CONSUME: + { + if (!m->inbound) + return fail(http, "consume-body of an unsent request is " + "outside this transport"); + uint8_t *out = memory(http, a[2], 8, 4); + if (!out + || !p3_http_lift(&http->vm->async, a[1], true, P3_HTTP_STATUS) + || !defer_future(http, a[1]) + || !p3_http_lower(&http->vm->async, m->body) + || !p3_http_lower(&http->vm->async, m->trailers)) + return false; + write32(out, m->body); + write32(out + 4, m->trailers); + m->body = m->trailers = 0; + return wasi_http_drop(http, a[0], kind, false); + } + case REQUEST_HEADERS: + case RESPONSE_HEADERS: + { + http_resource *headers = + wasi_http_get(http, m->fields, HTTP_FIELDS, true); + return headers && (*ret = clone(http, headers, true)) != 0; + } + case OPTIONS_GET: + { + uint8_t *out = memory(http, a[1], 8, 4); + if (!out) + return false; + write32(out, m->options != 0); + if (m->options) { + http_resource *options = + wasi_http_get(http, m->options, HTTP_OPTIONS, true); + uint32_t copy = options ? clone(http, options, true) : 0; + if (!copy) + return false; + write32(out + 4, copy); + } + return true; + } + case STATUS_GET: + *ret = m->status; + return true; + case STATUS_SET: + *ret = (uint16_t)a[1] < 100 || (uint16_t)a[1] > 599; + if (!*ret) + m->status = (uint16_t)a[1]; + return true; + case METHOD_GET: + return string_result(http, a[1], m->method, m->method_other, 4); + case SCHEME_GET: + return string_result(http, a[1], m->scheme, m->scheme_other, 8); + case PATH_GET: + return string_result(http, a[1], m->path[0] != 0, m->path, 4); + case AUTHORITY_GET: + return string_result(http, a[1], m->authority[0] != 0, m->authority, + 4); + case METHOD_SET: + { + if (a[1] > 9) + return fail(http, "invalid HTTP method discriminant"); + const uint8_t *text = + a[1] == 9 ? memory(http, a[2], a[3], 1) : NULL; + if (a[1] == 9 && !text) + return false; + int method = text ? wasi_http_method(text, a[3]) : (int)a[1]; + *ret = method < 0; + if (!*ret) { + m->method = (uint8_t)method; + copy_text(m->method_other, text, method == 9 ? a[3] : 0); + } + return true; + } + case SCHEME_SET: + { + if (a[1] > 1 || (a[1] && a[2] > 2)) + return fail(http, "invalid HTTP scheme discriminant"); + unsigned scheme = a[1] ? a[2] + 1 : 0; + const uint8_t *text = + scheme == 3 ? memory(http, a[3], a[4], 1) : NULL; + if (scheme == 3 && !text) + return false; + if (text) { + *ret = + !a[4] || a[4] >= sizeof(m->scheme_other) || !alpha(text[0]); + for (unsigned i = 1; !*ret && i < a[4]; ++i) + *ret = !alpha(text[i]) && !digit(text[i]) + && !punct(text[i], "+-."); + if (*ret) + return true; + if (a[4] == 4 && !memcmp(text, "http", 4)) + scheme = 1; + if (a[4] == 5 && !memcmp(text, "https", 5)) + scheme = 2; + } + m->scheme = (uint8_t)scheme; + copy_text(m->scheme_other, text, scheme == 3 ? a[4] : 0); + return true; + } + case PATH_SET: + case AUTHORITY_SET: + { + if (a[1] > 1) + return fail(http, "invalid HTTP string option"); + bool path = op == PATH_SET; + char *dst = path ? m->path : m->authority; + if (!a[1]) { + dst[0] = 0; + return true; + } + const uint8_t *text = memory(http, a[2], a[3], 1); + if (!text) + return false; + *ret = !(path ? wasi_http_path(text, a[3]) + : wasi_http_authority(text, a[3])); + if (!*ret) + copy_text(dst, a[3] || !path ? text : (const uint8_t *)"/", + a[3] || !path ? a[3] : 1); + return true; + } + default: + return false; + } +} +bool +wasi_http_call(p3_vm *vm, uint8_t op, const wasm_val_vec_t *args, + wasm_val_vec_t *results) +{ + wasi_http *http = vm->http; + if (!http) + return false; + if (http->allocating) + return fail(http, "HTTP import reentered through guest cabi_realloc"); + vm->async.error = NULL; + uint32_t a[8] = { 0 }, ret = 0; + uint64_t timeout = 0; + for (size_t i = 0; i < args->size; ++i) { + if (wide_arg(op, i)) + timeout = (uint64_t)args->data[i].of.i64; + else + a[i] = (uint32_t)args->data[i].of.i32; + } + bool ok; + if (op <= RETURN_RUN) { + p3_task *task = vm->current; + if (!task || task->returned || task->cancelled || a[0] > 1) + return fail(http, "invalid WASI task return"); + task->http_result = a[0]; + if (op == RETURN_HANDLE) { + if (a[0]) + return fail(http, "HTTP handler returned an error"); + http_resource *r = wasi_http_get(http, a[1], HTTP_RESPONSE, false); + if (!r) + return false; + r->native = true; + task->http_result = a[1]; + } + task->returned = true; + ok = true; + } + else if (op <= DROP_OPTIONS) + ok = wasi_http_drop(http, a[0], + (uint8_t)(op - DROP_FIELDS + HTTP_FIELDS), false); + else if (op <= FIELDS_COPY) + ok = field_import(http, op, a, &ret); + else if (op <= RESPONSE_HEADERS) + ok = message_import(http, op, a, &ret); + else + ok = options_import(http, op, a, timeout, &ret); + if (ok && results->size) { + results->data[0].kind = WASM_I32; + results->data[0].of.i32 = (int32_t)ret; + } + return ok; +} diff --git a/crates/c-api/examples/stackless-http/wasi_http.h b/crates/c-api/examples/stackless-http/wasi_http.h new file mode 100644 index 0000000..a7f39ea --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_WASI_HTTP_H +#define STACKLESS_WASI_HTTP_H +#include "vm.h" + +#ifndef HTTP_RESOURCE_SLOTS +#define HTTP_RESOURCE_SLOTS 32 +#endif +#ifndef HTTP_FIELD_BYTES +#define HTTP_FIELD_BYTES 1536 +#endif +#define HTTP_FIELDS_MAX 12 +#define HTTP_URI_MAX 512 +#define HTTP_PENDING_DROPS 16 +enum http_kind { HTTP_FIELDS = 1, HTTP_REQUEST, HTTP_RESPONSE, HTTP_OPTIONS }; + +typedef struct http_field { + uint16_t offset, name_size, value_size; +} http_field; +typedef struct http_fields { + uint8_t bytes[HTTP_FIELD_BYTES]; + http_field field[HTTP_FIELDS_MAX]; + uint16_t size, count; +} http_fields; +typedef struct http_message { + char path[HTTP_URI_MAX], authority[HTTP_URI_MAX], method_other[64], + scheme_other[64]; + uint32_t fields, options, body, trailers, sent; + uint16_t status; + uint8_t method, scheme; + bool inbound; +} http_message; +typedef struct http_options { + uint64_t timeout[3]; + uint8_t mask; +} http_options; +typedef struct http_resource { + union { + http_fields fields; + http_message message; + http_options options; + } as; + uint32_t generation; + uint8_t kind; + bool native, immutable; +} http_resource; +typedef struct wasi_http { + p3_vm *vm; + const p3_export *realloc_export; + http_resource resources[HTTP_RESOURCE_SLOTS]; + uint32_t pending[HTTP_PENDING_DROPS], pending_count, live, peak; + bool allocating; +} wasi_http; + +void +wasi_http_init(wasi_http *http, p3_vm *vm); +uint8_t +wasi_http_binding(const wasm_name_t *name, const wasm_functype_t *type); +bool +wasi_http_call(p3_vm *vm, uint8_t op, const wasm_val_vec_t *args, + wasm_val_vec_t *results); +bool +wasi_http_sweep(wasi_http *http); +uint32_t +wasi_http_new(wasi_http *http, uint8_t kind, bool native); +http_resource * +wasi_http_get(wasi_http *http, uint32_t handle, uint8_t kind, bool native); +bool +wasi_http_drop(wasi_http *http, uint32_t handle, uint8_t kind, bool native); +bool +wasi_http_pair(wasi_http *http, bool future, uint8_t element, uint32_t *reader, + uint32_t *writer); +int +wasi_http_append(http_fields *fields, const uint8_t *name, uint32_t names, + const uint8_t *value, uint32_t values); +int +wasi_http_method(const uint8_t *str, uint32_t len); +bool +wasi_http_path(const uint8_t *str, uint32_t len); +bool +wasi_http_authority(const uint8_t *str, uint32_t len); +#endif diff --git a/crates/c-api/examples/stackless-http/wasi_http_check.c b/crates/c-api/examples/stackless-http/wasi_http_check.c new file mode 100644 index 0000000..f06444e --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_check.c @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "wasi_http.h" +#include +#include + +static p3_vm vm; +static wasi_http http; + +int +main(int argc, char **argv) +{ + if (argc != 2) + return 2; + FILE *file = fopen(argv[1], "rb"); + if (!file || fseek(file, 0, SEEK_END)) + return 2; + long size = ftell(file); + if (size <= 0 || fseek(file, 0, SEEK_SET)) + return 2; + uint8_t *bytes = malloc((size_t)size); + if (!bytes || fread(bytes, 1, (size_t)size, file) != (size_t)size) + return 2; + fclose(file); + bool ok = p3_vm_open(&vm, bytes, (size_t)size); + free(bytes); + if (ok) { + wasi_http_init(&http, &vm); + p3_task task; + p3_task_init(&task, &vm, "command-event"); + ok = p3_task_start(&task, "command", NULL, 0); + for (unsigned i = 0; ok && i < 10000 && !task.exited; ++i) { + ok = wasi_http_sweep(&http) && p3_task_ready(&task) + && p3_task_step(&task); + } + ok = ok && wasi_http_sweep(&http) && task.exited && task.returned + && !task.http_result && !http.live && !vm.async.live; + } + if (ok) + printf("PASS %s resources=%u handles=%u\n", argv[1], http.peak, + vm.async.peak); + else + fprintf(stderr, "FAIL %s: %s %s resources=%u handles=%u\n", argv[1], + vm.error, vm.async.error ? vm.async.error : "", http.live, + vm.async.live); + p3_vm_close(&vm); + return ok ? 0 : 1; +} diff --git a/crates/c-api/examples/stackless-http/wasi_http_guest.c b/crates/c-api/examples/stackless-http/wasi_http_guest.c new file mode 100644 index 0000000..2f8a6e2 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_guest.c @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include +#include +#include "payloads.h" + +#define IMPORT(module, name) \ + __attribute__((import_module(module), import_name(name))) +#define EXPORT(name) __attribute__((export_name(name))) +#define TYPES "wasi:http/types@0.3.2" +#define HANDLER "wasi:http/handler@0.3.2" +#define BLOCKED UINT32_MAX +#define CHUNK 768 +#define CONNECTIONS 4 + +IMPORT(TYPES, "[constructor]fields") uint32_t fields_new(void); +IMPORT(TYPES, "[method]fields.append") +void append(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); +IMPORT(TYPES, "[method]request.get-method") void get_method(uint32_t, uint32_t); +IMPORT(TYPES, "[method]request.get-path-with-query") +void get_path(uint32_t, uint32_t); +IMPORT(TYPES, "[resource-drop]request") void drop_request(uint32_t); +IMPORT(TYPES, "[static]request.consume-body") +void consume_body(uint32_t, uint32_t, uint32_t); +IMPORT(TYPES, "[static]response.new") +void response_new(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); +IMPORT(TYPES, "[method]response.set-status-code") +uint32_t set_status(uint32_t, uint32_t); +IMPORT("[export]" HANDLER, "[task-return]handle") +void +task_return(uint32_t, uint32_t, uint32_t, uint64_t, uint32_t, uint32_t, + uint32_t, uint32_t); + +#define BODY_READ "[static]request.consume-body" +#define BODY_WRITE "[static]response.new" +#define STATUS_FUTURE "[static]request.consume-body" +#define TRAILERS_FUTURE "[static]response.new" +IMPORT(TYPES, "[stream-new-0]" BODY_WRITE) uint64_t stream_new(void); +IMPORT(TYPES, "[async-lower][stream-read-1]" BODY_READ) +uint32_t +stream_read(uint32_t, uint32_t, uint32_t); +IMPORT(TYPES, "[async-lower][stream-write-0]" BODY_WRITE) +uint32_t +stream_write(uint32_t, uint32_t, uint32_t); +IMPORT(TYPES, "[stream-drop-readable-1]" BODY_READ) void drop_read(uint32_t); +IMPORT(TYPES, "[stream-drop-writable-0]" BODY_WRITE) void drop_write(uint32_t); +IMPORT(TYPES, "[future-new-0]" STATUS_FUTURE) uint64_t status_new(void); +IMPORT(TYPES, "[async-lower][future-write-0]" STATUS_FUTURE) +uint32_t +status_write(uint32_t, uint32_t); +IMPORT(TYPES, "[future-drop-writable-0]" STATUS_FUTURE) +void status_drop_write(uint32_t); +IMPORT(TYPES, "[future-drop-readable-2]" BODY_WRITE) +void status_drop_read(uint32_t); +IMPORT(TYPES, "[future-new-1]" TRAILERS_FUTURE) uint64_t trailers_new(void); +IMPORT(TYPES, "[async-lower][future-write-1]" TRAILERS_FUTURE) +uint32_t +trailers_write(uint32_t, uint32_t); +IMPORT(TYPES, "[async-lower][future-read-2]" BODY_READ) +uint32_t +trailers_read(uint32_t, uint32_t); +IMPORT(TYPES, "[future-drop-writable-1]" TRAILERS_FUTURE) +void trailers_drop_write(uint32_t); +IMPORT(TYPES, "[future-drop-readable-2]" BODY_READ) +void trailers_drop_read(uint32_t); +IMPORT("$root", "[waitable-set-new]") uint32_t set_new(void); +IMPORT("$root", "[waitable-set-drop]") void set_drop(uint32_t); +IMPORT("$root", "[waitable-join]") void join(uint32_t, uint32_t); +IMPORT("$root", "[context-get-0]") uint32_t context_get(void); +IMPORT("$root", "[context-set-0]") void context_set(uint32_t); + +enum phase { READ = 1, WRITE, TRAILERS_READ, STATUS_WRITE, TRAILERS_WRITE }; +typedef struct request { + uint32_t set, rx, tx, input_status, input_trailers, output_trailers; + uint32_t offset, size; + const uint8_t *bytes; + enum phase phase; + uint8_t upload[CHUNK]; + _Alignas(8) uint8_t result[40]; + uint8_t eof, lost; +} Request; + +static Request active[CONNECTIONS]; +static uint8_t path[512]; + +static uint32_t +ptr(const void *p) +{ + return (uint32_t)(uintptr_t)p; +} +EXPORT("cabi_realloc") +uint32_t +cabi_realloc(uint32_t old, uint32_t old_size, uint32_t align, uint32_t size) +{ + /* Only the path getter allocates in this synchronous, bounded demo. */ + if (old || old_size || align != 1 || size > sizeof(path)) + __builtin_trap(); + return ptr(path); +} +static int +same(const uint8_t *text, uint32_t size, const char *expected, uint32_t len) +{ + if (size != len) + return 0; + for (uint32_t i = 0; i < len; ++i) + if (text[i] != (uint8_t)expected[i]) + return 0; + return 1; +} +static uint32_t +wait(Request *r) +{ + return (r->set << 4) | 2; +} +static uint32_t +yield(void) +{ + return 1; +} +static uint32_t +write_trailers(Request *r) +{ + r->result[0] = r->result[8] = 0; + if (r->input_status && !r->lost) { + uint32_t trailers = fields_new(); + uint32_t header_result[4]; + append(trailers, ptr("x-upload-check"), 14, ptr("complete"), 8, + ptr(header_result)); + if (header_result[0]) + __builtin_trap(); + r->result[8] = 1; + for (unsigned i = 0; i < 4; ++i) + r->result[12 + i] = (uint8_t)(trailers >> (i * 8)); + } + r->phase = TRAILERS_WRITE; + uint32_t status = trailers_write(r->output_trailers, ptr(r->result)); + if (status == BLOCKED) + return wait(r); + trailers_drop_write(r->output_trailers); + set_drop(r->set); + r->set = 0; + return 0; +} +static uint32_t +write_status(Request *r, uint32_t status) +{ + trailers_drop_read(r->input_trailers); + r->input_trailers = 0; + r->result[0] = r->lost || status != 0 || r->result[0] || r->result[8]; + r->result[8] = r->result[0] ? 7 : 0; + r->lost = r->result[0]; + r->phase = STATUS_WRITE; + if (status_write(r->input_status, ptr(r->result)) == BLOCKED) + return wait(r); + status_drop_write(r->input_status); + return write_trailers(r); +} +static uint32_t +finish(Request *r) +{ + drop_write(r->tx); + r->tx = 0; + if (!r->input_trailers) + return write_trailers(r); + drop_read(r->rx); + r->rx = 0; + if (r->lost) + return write_status(r, 1); + r->phase = TRAILERS_READ; + uint32_t status = trailers_read(r->input_trailers, ptr(r->result)); + return status == BLOCKED ? wait(r) : write_status(r, status); +} +static uint32_t +write_body(Request *r, uint32_t status) +{ + r->offset += status >> 4; + if ((status & 15) == 1) { + r->lost = 1; + return finish(r); + } + if (r->offset == r->size) { + if (!r->rx || r->eof) + return finish(r); + r->phase = READ; + return yield(); + } + uint32_t size = r->size - r->offset; + r->phase = WRITE; + status = stream_write(r->tx, ptr(r->bytes + r->offset), + size > CHUNK ? CHUNK : size); + if (status == BLOCKED) + return wait(r); + if ((status & 15) == 1) { + r->lost = 1; + return finish(r); + } + r->offset += status >> 4; + return yield(); +} +static uint32_t +read_body(Request *r, uint32_t status) +{ + if (status >> 4) { + r->eof = (status & 15) == 1; + r->bytes = r->upload; + r->size = status >> 4; + r->offset = 0; + return write_body(r, 0); + } + if ((status & 15) == 1) + return finish(r); + r->phase = READ; + status = stream_read(r->rx, ptr(r->upload), sizeof(r->upload)); + if (status == BLOCKED) + return wait(r); + return read_body(r, status); +} +EXPORT("[async-lift]" HANDLER "#handle") +uint32_t +handle(uint32_t request) +{ + unsigned slot = 0; + while (slot < CONNECTIONS && active[slot].set) + ++slot; + if (slot == CONNECTIONS) + __builtin_trap(); + Request *r = &active[slot]; + r->set = set_new(); + r->rx = r->offset = r->size = r->eof = r->lost = 0; + r->input_status = r->input_trailers = 0; + r->bytes = NULL; + context_set(slot); + + uint32_t method[3], route[3], fields = fields_new(), status = 404; + get_method(request, ptr(method)); + get_path(request, ptr(route)); + const char *type = "application/octet-stream"; + uint32_t type_len = 24; + const uint8_t *text = (const uint8_t *)(uintptr_t)route[1]; + if (method[0] == 0 && route[0] && same(text, route[2], "/data.json", 10)) { + status = 200; + type = "application/json"; + type_len = 16; + r->bytes = json_body; + r->size = sizeof(json_body); + } + else if (method[0] == 0 && route[0] + && same(text, route[2], "/image.png", 10)) { + status = 200; + type = "image/png"; + type_len = 9; + r->bytes = png_body; + r->size = sizeof(png_body); + } + else if (method[0] == 2 && route[0] && same(text, route[2], "/upload", 7)) { + status = 200; + type = "image/png"; + type_len = 9; + uint64_t done = status_new(); + uint32_t ends[2]; + r->input_status = (uint32_t)(done >> 32); + join(r->input_status, r->set); + consume_body(request, (uint32_t)done, ptr(ends)); + r->rx = ends[0]; + r->input_trailers = ends[1]; + join(r->rx, r->set); + join(r->input_trailers, r->set); + } + if (!r->rx) + drop_request(request); + append(fields, ptr("content-type"), 12, ptr(type), type_len, + ptr(r->result)); + if (r->result[0]) + __builtin_trap(); + uint64_t body = stream_new(), trailers = trailers_new(); + uint32_t response[2]; + r->tx = (uint32_t)(body >> 32); + r->output_trailers = (uint32_t)(trailers >> 32); + join(r->tx, r->set); + join(r->output_trailers, r->set); + response_new(fields, 1, (uint32_t)body, (uint32_t)trailers, ptr(response)); + status_drop_read(response[1]); + if (set_status(response[0], status)) + __builtin_trap(); + task_return(0, response[0], 0, 0, 0, 0, 0, 0); + return r->rx ? read_body(r, 0) : write_body(r, 0); +} +EXPORT("[callback][async-lift]" HANDLER "#handle") +uint32_t +handle_event(uint32_t code, uint32_t end, uint32_t status) +{ + uint32_t slot = context_get(); + if (slot >= CONNECTIONS) + __builtin_trap(); + Request *r = &active[slot]; + if (r->phase == READ && (!code || (code == 2 && end == r->rx))) + return read_body(r, status); + if (r->phase == WRITE && (!code || (code == 3 && end == r->tx))) + return write_body(r, status); + if (r->phase == TRAILERS_READ && code == 4 && end == r->input_trailers) + return write_status(r, status); + if (r->phase == STATUS_WRITE && code == 5 && end == r->input_status) { + status_drop_write(r->input_status); + return write_trailers(r); + } + if (r->phase == TRAILERS_WRITE && code == 5 && end == r->output_trailers) { + trailers_drop_write(r->output_trailers); + set_drop(r->set); + r->set = 0; + return 0; + } + __builtin_trap(); +} +EXPORT("native-loop") +uint32_t +native_loop(uint32_t iterations) +{ + uint32_t total = 0; + for (uint32_t i = 0; i < iterations; ++i) + total += context_get(); + return total; +} diff --git a/crates/c-api/examples/stackless-http/wasi_http_link.py b/crates/c-api/examples/stackless-http/wasi_http_link.py new file mode 100644 index 0000000..3690612 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_link.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Resolve a validated, single-instance wit-component HTTP guest offline.""" +import argparse +import json +from pathlib import Path +import subprocess +import tempfile + +from component import OPS, Unsupported, children, emit, one, parse + +HTTP = '"wasi:http/types@0.3.2"' +CLIENT = '"wasi:http/client@0.3.2"' + + +def named(node, name): + return one([x[2] for x in children(node, ['export']) if x[1] == name], + f'export {name}') + + +def resolve(component): + if any(node[1] == CLIENT for node in children(component, ['import'])): + raise Unsupported('wasi:http/client.send requires outbound HTTP transport') + modules = children(component, ['core', 'module']) + if len(modules) not in (1, 3): + raise Unsupported('expected one guest with an optional wit-component shim') + guest = modules[0] + instances, core_funcs, functions, types, imports = [], [], [], [], [] + guest_instance = fixups = shim = None + tables = [] + exports = {} + for node in component[1:]: + match node: + case ['import', interface, ['instance', *_]]: + imports.append(interface) + case ['instance', *_] | ['export', _, ['instance', _], *_]: + imports.append(None) + case ['type', ty]: + types.append(ty) + case ['alias', 'export', index, name, ['type']]: + types.append((imports[int(index)], name)) + case ['alias', 'export', index, name, ['func']]: + functions.append((imports[int(index)], name)) + case ['alias', 'core', 'export', index, name, ['core', 'table']]: + tables.append((int(index), name)) + case ['alias', 'core', 'export', index, name, ['core', 'func']]: + core_funcs.append(('alias', int(index), name)) + case ['core', 'func', ['canon', op, *options]]: + if op == 'lower': + interface, name = functions[int(options[0][1])] + if interface is None: + raise Unsupported('canonical lower must target a host import') + target = ('wasi-http', json.loads(name)) if interface == HTTP else ('unsupported-wasi', json.loads(interface) + '#' + json.loads(name)) + elif op == 'resource.drop': + interface, name = types[int(options[0])] + target = ('wasi-http', '[drop]' + json.loads(name)) if interface == HTTP else ('unsupported-wasi', json.loads(interface) + '#drop-' + json.loads(name)) + elif op in OPS: + element = 'unit' + if op.startswith(('future.', 'stream.')): + ty = types[int(options[0])] + if ty == ['stream', 'u8']: + element = 'u8' + elif ty[0] == 'future' and len(ty) == 2: + result = types[int(ty[1])] + error = result[-1] + if result[0] != 'result' or not isinstance(error, list) or error[0] != 'error' or types[int(error[1])] != (HTTP, '"error-code"'): + raise Unsupported('unsupported future payload ' + repr(result)) + element = 'http-status' + if len(result) > 2: + option = types[int(result[1])] + if option[0] != 'option' or types[int(types[int(option[1])][1])] not in ((HTTP, '"fields"'), (HTTP, '"trailers"')): + raise Unsupported('unsupported HTTP future option') + element = 'http-trailers' + else: + raise Unsupported('unsupported endpoint type ' + repr(ty)) + if op == 'task.return' and options and options[0][0] == 'result': + result = types[int(options[0][1])] + target = ('wasi-http', '[return]run' if result == ['result'] else '[return]handle') + else: + target = ('p3', f'{op}:{element}:{int("async" in options)}') + else: + target = ('unsupported-wasi', op) + core_funcs.append(target) + case ['core', 'instance', ['instantiate', index, *bindings]]: + index = int(index) + bound = {name: int(instance) for _, name, (_, instance) in bindings} + if index == 0 and guest_instance is None: + guest_instance = len(instances) + instances.append(bound) + elif index == 1 and shim is None and not bound: + shim = len(instances) + instances.append(None) + elif index == 2 and fixups is None and set(bound) == {'"actual"', '"shim"'} and bound['"shim"'] == shim: + fixups = (instances[bound['"actual"']], '"actual"', '"shim"') + instances.append(None) + elif index == 2 and fixups is None and set(bound) == {'""'}: + exports_ = instances[bound['""']].copy() + table = exports_.pop('"$imports"') + if table[0] != 'table' or tables[table[1]] != (shim, '"$imports"'): + raise Unsupported('fixup must initialize the shim table') + fixups = (exports_, '""', '""') + instances.append(None) + else: + raise Unsupported('unexpected core instantiation') + case ['core', 'instance', *bindings]: + instances.append({name: (kind, int(idx)) for _, name, (kind, idx) in bindings}) + case ['func', *parts]: + lift = one(children(parts, ['canon', 'lift']), 'canonical lift') + alias = core_funcs[int(lift[2][2])] + if alias[:2] != ('alias', guest_instance): + raise Unsupported('lift must resolve to a guest function') + name = json.loads(alias[2]) + if name == 'wasi:cli/run@0.2.0#run': + exports['run'] = alias[2] + async_exports = {'[async-lift]wasi:http/handler@0.3.2#handle': 'handle', + '[async-lift]wasi:cli/run@0.3.2#run': 'command'} + if name in async_exports: + entry = async_exports[name] + exports[entry] = alias[2] + callback = one(children(lift, ['callback']), 'handler callback') + cb = core_funcs[int(callback[1])] + if cb[:2] != ('alias', guest_instance): + raise Unsupported('handler callback must resolve to guest function') + exports[entry + '-event'] = cb[2] + functions.append(None) + case _: + pass + + if len(modules) == 3: + if fixups is None: + raise Unsupported('missing shim fixup bindings') + bindings, function_ns, table_ns = fixups + validate_shim(modules[1], modules[2], bindings, function_ns, table_ns) + if guest_instance is None or not exports: + raise Unsupported('missing supported component entry point') + + output = ['module'] + for item in guest[2:]: + if item[0] == 'import': + _, module, name, desc = item + if desc[0] != 'func': + raise Unsupported('core imports must all be functions') + kind, idx = instances[instances[guest_instance][module]][name] + if kind != 'func': + raise Unsupported('core function import resolved to a non-function') + binding = core_funcs[idx] + if binding[0] == 'alias': + if binding[1] != shim or fixups is None: + raise Unsupported('import alias must point at a validated shim') + kind, idx = fixups[0][binding[2]] + if kind != 'func': + raise Unsupported('shim fixup must supply a function') + binding = core_funcs[idx] + if binding[0] == 'alias': + raise Unsupported('unresolved core import') + output.append(['import', json.dumps(binding[0]), json.dumps(binding[1]), desc]) + elif item[0] == 'start': + output.append(['export', '"__p3_start"', ['func', item[1]]]) + elif item[0] != 'export': + output.append(item) + one(children(guest, ['memory']), 'guest memory') + output.append(['export', '"__p3_memory"', ['memory', '0']]) + for name, original in exports.items(): + output.append(['export', json.dumps(name), named(guest, original)]) + alloc = [x for x in children(guest, ['export']) if x[1] in ('"cabi_realloc"', '"cabi_realloc_wit_bindgen_0_60_0"')] + if any(x[1] == '"native-loop"' for x in children(guest, ['export'])): + output.append(['export', '"native-loop"', named(guest, '"native-loop"')]) + if alloc: + output.append(['export', '"cabi_realloc"', alloc[0][2]]) + return output + + +def validate_shim(shim, fixup, bindings, function_ns, table_ns): + count = len(bindings) + table = ['table', str(count), str(count), 'funcref'] + if one(children(shim, ['table']), 'shim table') != table: + raise Unsupported('unexpected shim table') + if any(n[0] not in {'type', 'table', 'export', 'func'} for n in shim[2:]): + raise Unsupported('unexpected shim code') + funcs = children(shim, ['func']) + if len(funcs) != count or named(shim, '"$imports"') != ['table', '0']: + raise Unsupported('unexpected shim exports') + for i, func in enumerate(funcs): + idx = str(i) + use = func[1] + signature = children(shim, ['type'])[int(use[1])][1] + params = sum(len(n) - 1 for n in children(signature, ['param'])) + start = 2 + while isinstance(func[start], list) and func[start][0] in {'param', 'result'}: + start += 1 + code = func[start:] + expected = [token for p in range(params) for token in ('local.get', str(p))] + expected += ['i32.const', idx, 'call_indirect', use] + if code != expected or named(shim, json.dumps(idx)) != ['func', idx]: + raise Unsupported('unexpected shim trampoline') + imports = children(fixup, ['import']) + if (any(n[0] not in {'type', 'import', 'elem'} for n in fixup[2:]) + or len(imports) != count + 1 or imports[-1] != ['import', table_ns, '"$imports"', table] + or any(imp[:3] != ['import', function_ns, json.dumps(str(i))] or imp[3][0] != 'func' + for i, imp in enumerate(imports[:-1])) + or one(children(fixup, ['elem']), 'shim element initializer') + != ['elem', ['i32.const', '0'], 'func', *map(str, range(count))]): + raise Unsupported('unexpected shim fixup module') + + +def link(source, output, tool): + with tempfile.TemporaryDirectory() as tmp: + stripped = Path(tmp) / 'numeric.wasm' + wat = Path(tmp) / 'linked.wat' + subprocess.run([tool, 'validate', '--features', 'all', source], check=True) + subprocess.run([tool, 'strip', '-a', source, '-o', stripped], check=True) + component = one(parse(subprocess.check_output([tool, 'print', stripped], text=True)), 'component') + wat.write_text(emit(resolve(component))) + subprocess.run([tool, 'parse', wat, '-o', output], check=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('source', type=Path) + parser.add_argument('output', type=Path) + parser.add_argument('--wasm-tools', default='wasm-tools') + args = parser.parse_args() + link(args.source, args.output, args.wasm_tools) diff --git a/crates/c-api/examples/stackless-http/wasi_http_server.c b/crates/c-api/examples/stackless-http/wasi_http_server.c new file mode 100644 index 0000000..e444b2c --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_server.c @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "http.h" +#include +#include + +#define HTTP_MAX_BODY (64 * 1024U) + +static bool +close_end(http_server *server, uint32_t *end) +{ + if (*end && !p3_native_drop(&server->vm.async, *end)) + return false; + *end = 0; + return true; +} +static bool +send_future(http_server *server, uint32_t *end, bool error) +{ + if (!*end) + return true; + uint8_t value[P3_HTTP_RESULT_SIZE] = { 0 }; + if (error) { + value[0] = 1; + value[8] = 7; /* connection-terminated */ + } + bool complete; + if (!p3_http_finish(&server->vm.async, *end, value, &complete)) + return false; + if (complete) + *end = 0; + return true; +} +static bool +receive_trailers(http_server *server, http_connection *c, uint32_t *end) +{ + if (!*end) + return true; + uint8_t value[P3_HTTP_RESULT_SIZE]; + bool complete; + if (!p3_http_finish(&server->vm.async, *end, value, &complete)) + return false; + if (!complete) + return true; + *end = 0; + if (value[0] || value[8] > 1) { + server->vm.async.error = "response trailers resolved to an error"; + return false; + } + if (value[8]) { + uint32_t h = 0; + for (unsigned i = 0; i < 4; ++i) + h |= (uint32_t)value[12 + i] << (8 * i); + http_resource *fields = + wasi_http_get(&server->http, h, HTTP_FIELDS, false); + if (!fields) + return false; + fields->native = true; + c->response_trailers = h; + } + return true; +} +bool +http_open(http_server *server, unsigned slot) +{ + if (slot >= HTTP_CONNECTIONS || server->connections[slot].live) + return false; + http_connection *c = &server->connections[slot]; + memset(c, 0, sizeof(*c)); + c->live = true; + return true; +} +static bool +protocol_error(http_connection *c, unsigned code) +{ + const char *phrase = code == 431 ? "Request Header Fields Too Large" + : code == 413 ? "Content Too Large" + : "Bad Request"; + int n = snprintf( + (char *)c->output, sizeof(c->output), + "HTTP/1.1 %u %s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + code, phrase); + if (n < 0 || (size_t)n >= sizeof(c->output)) + return false; + c->length = (size_t)n; + c->done = true; + return true; +} +static const uint8_t * +crlf(const uint8_t *start, const uint8_t *end) +{ + while (end - start >= 2) { + if (start[0] == '\r' && start[1] == '\n') + return start; + ++start; + } + return NULL; +} +static bool +name_is(const uint8_t *p, size_t len, const char *name) +{ + if (len != strlen(name)) + return false; + for (size_t i = 0; i < len; ++i) { + unsigned ch = p[i]; + if (ch >= 'A' && ch <= 'Z') + ch += 'a' - 'A'; + if (ch != (unsigned char)name[i]) + return false; + } + return true; +} +static unsigned +parse_request(http_connection *c, http_message *m, http_fields *fields, + unsigned header) +{ + const uint8_t *start = c->request_bytes, *end = start + header; + const uint8_t *line = crlf(start, end); + const uint8_t *space = + line ? memchr(start, ' ', (size_t)(line - start)) : NULL; + if (!space) + return 400; + const uint8_t *path = space + 1; + const uint8_t *version = memchr(path, ' ', (size_t)(line - path)); + int method = wasi_http_method(start, (uint32_t)(space - start)); + if (method < 0 || !version || version == path || line - version != 9 + || memcmp(version, " HTTP/1.1", 9) + || !wasi_http_path(path, (uint32_t)(version - path))) + return 400; + m->method = (uint8_t)method; + c->head = method == 1; + if (method == 9) + memcpy(m->method_other, start, (size_t)(space - start)); + memcpy(m->path, path, (size_t)(version - path)); + m->scheme = 1; + bool length = false; + for (start = line + 2; start != end - 2; start = line + 2) { + line = crlf(start, end); + const uint8_t *colon = + line ? memchr(start, ':', (size_t)(line - start)) : NULL; + if (!colon || colon == start) + return 400; + const uint8_t *value = colon + 1, *last = line; + while (value < last && (*value == ' ' || *value == '\t')) + ++value; + while (last > value && (last[-1] == ' ' || last[-1] == '\t')) + --last; + uint32_t names = (uint32_t)(colon - start), + values = (uint32_t)(last - value); + if (name_is(start, names, "content-length")) { + if (length || !values) + return 400; + length = true; + for (const uint8_t *p = value; p < last; ++p) { + if (*p < '0' || *p > '9') + return 400; + c->body_left = c->body_left * 10 + *p - '0'; + if (c->body_left > HTTP_MAX_BODY) + return 413; + } + } + else if (name_is(start, names, "transfer-encoding")) + return 400; /* Reject ambiguous framing and chunked uploads. */ + else if (name_is(start, names, "host")) { + if (m->authority[0] || memchr(value, '@', values) + || !wasi_http_authority(value, values)) + return 400; + memcpy(m->authority, value, values); + } + else if (!name_is(start, names, "connection")) { + int err = wasi_http_append(fields, start, names, value, values); + if (err != -1) + return err == 3 ? 431 : 400; + } + } + if (!m->authority[0]) + return 400; + return 0; +} +static bool +start_request(http_server *server, http_connection *c, unsigned header) +{ + wasi_http *http = &server->http; + p3_context *cx = &server->vm.async; + uint32_t request = wasi_http_new(http, HTTP_REQUEST, false); + uint32_t fields = wasi_http_new(http, HTTP_FIELDS, true); + if (!request || !fields) + return false; + http_message *m = + &wasi_http_get(http, request, HTTP_REQUEST, false)->as.message; + m->fields = fields; + m->inbound = true; + unsigned code = parse_request( + c, m, &wasi_http_get(http, fields, HTTP_FIELDS, true)->as.fields, + header); + if (!code && c->used - header > c->body_left) + code = 400; + if (code) + return wasi_http_drop(http, request, HTTP_REQUEST, false) + && protocol_error(c, code); + c->used -= header; + memmove(c->request_bytes, c->request_bytes + header, c->used); + c->parsed = true; + if (!wasi_http_pair(http, false, 1, &m->body, &c->input) + || !wasi_http_pair(http, true, P3_HTTP_TRAILERS, &m->trailers, + &c->trailers_tx) + || !p3_host_end(cx, c->input) || !p3_host_end(cx, m->body) + || !p3_host_end(cx, c->trailers_tx) || !p3_host_end(cx, m->trailers)) + return false; + p3_task_init(&c->task, &server->vm, "handle-event"); + int32_t args[] = { (int32_t)request }; + return p3_task_start(&c->task, "handle", args, 1); +} +uint32_t +http_input_capacity(http_server *server, unsigned slot) +{ + http_connection *c = &server->connections[slot]; + if (c->done || c->disconnected || c->saw_eof) + return 0; + if (!c->parsed) + return (uint32_t)sizeof(c->request_bytes) - c->used; + if (c->used || !c->input) + return 0; + uint32_t cap = p3_native_capacity(&server->vm.async, c->input); + return cap < c->body_left ? cap : c->body_left; +} +bool +http_input(http_server *server, unsigned slot, uint8_t *data, uint32_t size) +{ + http_connection *c = &server->connections[slot]; + if (!c->live || size > http_input_capacity(server, slot)) + return false; + if (c->parsed) { + uint32_t result; + if (!p3_native_transfer(&server->vm.async, c->input, data, size, + &result) + || result != size << 4) + return false; + c->body_left -= size; + return true; + } + uint32_t old = c->used; + memcpy(c->request_bytes + old, data, size); + c->used += size; + for (unsigned i = old > 3 ? old - 3 : 0; i + 4 <= c->used; ++i) + if (!memcmp(c->request_bytes + i, "\r\n\r\n", 4)) + return start_request(server, c, i + 4); + return c->used < sizeof(c->request_bytes) || protocol_error(c, 431); +} +bool +http_end_input(http_server *server, unsigned slot) +{ + http_connection *c = &server->connections[slot]; + c->saw_eof = true; + return close_end(server, &c->input); +} +bool +http_disconnect(http_server *server, unsigned slot) +{ + http_connection *c = &server->connections[slot]; + if (c->disconnected) + return true; + c->disconnected = true; + c->sent = c->length; + if (!http_end_input(server, slot)) + return false; + if (!c->parsed) + c->done = true; + else if (!c->task.returned && !c->task.exited) + p3_task_request_cancel(&c->task); + return true; +} +static bool +wire_fields(http_connection *c, const http_fields *f) +{ + for (unsigned i = 0; i < f->count; ++i) { + http_field h = f->field[i]; + const uint8_t *name = f->bytes + h.offset; + if (name_is(name, h.name_size, "content-length")) + continue; + size_t count = (size_t)h.name_size + h.value_size + 4; + if (count > sizeof(c->output) - c->length) + return false; + uint8_t *dst = c->output + c->length; + memcpy(dst, name, h.name_size); + memcpy(dst + h.name_size, ": ", 2); + memcpy(dst + h.name_size + 2, name + h.name_size, h.value_size); + memcpy(dst + count - 2, "\r\n", 2); + c->length += count; + } + return true; +} +static bool +wire_headers(http_server *server, http_connection *c, const http_message *m) +{ + http_resource *headers = + wasi_http_get(&server->http, m->fields, HTTP_FIELDS, true); + if (!headers) + return false; + int n = snprintf((char *)c->output, sizeof(c->output), "HTTP/1.1 %u\r\n", + m->status); + if (n < 0 || (size_t)n >= sizeof(c->output)) + return false; + c->length = (size_t)n; + bool body = + !c->head && m->status >= 200 && m->status != 204 && m->status != 304; + c->chunked = m->body && body; + if (!wire_fields(c, &headers->as.fields)) + return false; + const char *framing = c->chunked ? "Transfer-Encoding: chunked\r\n" + : body ? "Content-Length: 0\r\n" + : ""; + n = snprintf((char *)c->output + c->length, sizeof(c->output) - c->length, + "%sConnection: close\r\n\r\n", framing); + if (n < 0 || (size_t)n >= sizeof(c->output) - c->length) + return false; + c->length += (size_t)n; + return true; +} +static bool +wire_body(http_server *server, http_connection *c, http_message *m) +{ + if (!m->body) + return true; + if (c->disconnected || !c->chunked) + return close_end(server, &m->body); + if (c->sent != c->length) + return true; + uint32_t status; + if (!p3_native_transfer(&server->vm.async, m->body, c->output + 6, + HTTP_CHUNK - 8, &status)) + return false; + if (status == P3_BLOCKED) + return true; + if ((status & 15U) == 1 && !close_end(server, &m->body)) + return false; + uint32_t count = status >> 4; + if (!count) + return true; + char prefix[7]; + if (snprintf(prefix, sizeof(prefix), "%04x\r\n", (unsigned)count) != 6) + return false; + memcpy(c->output, prefix, 6); + memcpy(c->output + 6 + count, "\r\n", 2); + c->sent = 0; + c->length = 8 + count; + return true; +} +static bool +wire_finish(http_server *server, http_connection *c, http_message *m) +{ + if (c->chunked && !c->terminal_chunk && !c->disconnected) { + memcpy(c->output, "0\r\n", 3); + c->sent = 0; + c->length = 3; + if (c->response_trailers) { + http_resource *r = wasi_http_get( + &server->http, c->response_trailers, HTTP_FIELDS, true); + if (!r || !wire_fields(c, &r->as.fields)) + return false; + } + memcpy(c->output + c->length, "\r\n", 2); + c->length += 2; + c->terminal_chunk = true; + return true; + } + if (!send_future(server, &m->sent, c->disconnected)) + return false; + c->wire_done = true; + return true; +} +bool +http_progress(http_server *server, unsigned slot) +{ + http_connection *c = &server->connections[slot]; + if (c->done || !c->parsed) + return true; + for (unsigned i = 0; i < 64; ++i) { + p3_context *cx = &server->vm.async; + if (c->used && c->input) { + uint32_t cap = p3_native_capacity(cx, c->input); + uint32_t take = c->used < cap ? c->used : cap; + if (take) { + uint32_t result; + if (!p3_native_transfer(cx, c->input, c->request_bytes, take, + &result) + || result != take << 4) + return false; + c->used -= take; + c->body_left -= take; + memmove(c->request_bytes, c->request_bytes + take, c->used); + } + } + if (!c->body_left && !close_end(server, &c->input)) + return false; + if ((!c->input || c->disconnected) + && !send_future(server, &c->trailers_tx, c->disconnected)) + return false; + if (!c->response && c->task.returned && !c->wire_done) { + c->response = c->task.http_result; + http_resource *r = + wasi_http_get(&server->http, c->response, HTTP_RESPONSE, true); + if (!r + || (!c->disconnected + && !wire_headers(server, c, &r->as.message))) + return false; + } + if (c->response) { + http_resource *r = + wasi_http_get(&server->http, c->response, HTTP_RESPONSE, true); + if (!r) + return false; + http_message *m = &r->as.message; + if (!wire_body(server, c, m) + || !receive_trailers(server, c, &m->trailers)) + return false; + if (!m->body && !m->trailers && c->sent == c->length + && !wire_finish(server, c, m)) + return false; + } + if (!wasi_http_sweep(&server->http)) + return false; + if (!p3_task_ready(&c->task)) + break; + if (!p3_task_step(&c->task)) + return false; + ++server->callbacks; + } + if (c->task.exited && (c->wire_done || c->disconnected)) { + if (!close_end(server, &c->input) + || !send_future(server, &c->trailers_tx, c->disconnected)) + return false; + if (c->response_trailers + && !wasi_http_drop(&server->http, c->response_trailers, HTTP_FIELDS, + true)) + return false; + if (c->response) { + if (!wasi_http_drop(&server->http, c->response, HTTP_RESPONSE, + true)) + return false; + c->response = 0; + } + if (!wasi_http_sweep(&server->http)) + return false; + c->done = true; + } + return true; +} +bool +http_finished(const http_connection *c) +{ + return c->done && c->sent == c->length; +} +bool +http_reap(http_server *server, unsigned slot) +{ + http_connection *c = &server->connections[slot]; + if (!c->live || !http_finished(c)) + return false; + if (c->task.cancelled) + ++server->cancelled; + else if (c->disconnected) + ++server->disconnected; + else + ++server->completed; + c->live = false; + return true; +} +bool +http_native_loop(http_server *server, int32_t iterations) +{ + if (server->vm.current || iterations <= 0) + return false; + p3_task task = { .vm = &server->vm, .context = 1 }; + server->vm.current = &task; + int32_t result; + bool has_result; + bool ok = p3_vm_call(&server->vm, "native-loop", &iterations, 1, &result, + &has_result); + server->vm.current = NULL; + return ok && has_result && result == iterations; +} diff --git a/crates/c-api/examples/stackless-http/wasi_http_tests.py b/crates/c-api/examples/stackless-http/wasi_http_tests.py new file mode 100644 index 0000000..2e086b5 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_tests.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Run pinned upstream WASI 0.3 HTTP guests and their HTTP service assertions.""" +import argparse +import http.client +import json +import os +from pathlib import Path +import signal +import socket +import subprocess +import tempfile +import time + +from component import Unsupported +from wasi_http_link import link + +SUITE = '9d84631e427729802085c3d172d0a63cd360a1fa' + + +def service(exe, wasm, config, cancel_probe=False): + process = subprocess.Popen([exe, '0', '6' if os.name == 'nt' else '30', wasm], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True) + try: + for op in config['operations']: + match op['type']: + case 'run': + line = process.stdout.readline() + if not line.startswith('LISTEN '): + raise RuntimeError(line) + port = int(line.split()[1]) + case 'request': + conn = http.client.HTTPConnection('127.0.0.1', port, timeout=3) + try: + conn.request(op['method'], op['path'], op.get('body'), op.get('headers', {})) + res = conn.getresponse() + expected = op['response'] + assert res.status == expected['status'], (op, res.status) + for name, value in expected.get('headers', {}).items(): + assert res.getheader(name) == value, (op, name, res.getheader(name)) + body = res.read() + if 'body' in expected: + assert body == expected['body'].encode(), (op, body) + finally: + conn.close() + case 'kill' if op['signal'] == 'SIGINT': + if cancel_probe: + with socket.create_connection(('127.0.0.1', port), timeout=3) as conn: + conn.sendall(b'POST /echo HTTP/1.1\r\nHost: localhost\r\n' + b'Content-Length: 32\r\n\r\nabc') + time.sleep(.05) + time.sleep(.05) + if os.name != 'nt': + process.send_signal(signal.SIGINT) + case 'wait': + out, err = process.communicate(timeout=8) + if (process.returncode or err or 'resources_live=0 ' not in out + or ' live=0 ' not in out + or (cancel_probe and ' cancelled=1 ' not in out)): + raise RuntimeError((out, err, process.returncode)) + case _: + raise ValueError('Unsupported test operation ' + str(op)) + except Exception as error: + if process.poll() is None: + process.kill() + out, err = process.communicate() + raise RuntimeError(f'{error}; server stdout={out!r}, stderr={err!r}') from error + finally: + if process.poll() is None: + process.kill() + process.wait() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('suite', type=Path) + parser.add_argument('build', type=Path) + parser.add_argument('--wasm-tools', default='wasm-tools') + args = parser.parse_args() + commit = subprocess.check_output(['git', '-C', args.suite, 'rev-parse', 'HEAD'], text=True).strip() + if commit != SUITE: + parser.error('Expected artifact revision ' + SUITE) + directory = args.suite / 'tests/rust/testsuite/wasm32-wasip3' + exe = '.exe' if os.name == 'nt' else '' + failures = 0 + with tempfile.TemporaryDirectory() as tmp: + for wasm in sorted(directory.glob('http-*.wasm')): + config = json.loads(wasm.with_suffix('.json').read_text()) + linked = Path(tmp) / wasm.name + try: + link(wasm, linked, args.wasm_tools) + if config.get('world') == 'wasi:http/service': + service(args.build / ('wasi_http_server' + exe), linked, config, + cancel_probe=wasm.stem == 'http-service-echo') + else: + subprocess.run([args.build / ('wasi_http_check' + exe), linked], check=True, + capture_output=True, text=True) + print('PASS', wasm.stem) + except Unsupported as error: + if str(error) != 'wasi:http/client.send requires outbound HTTP transport': + print('FAIL', wasm.stem, error) + failures += 1 + else: + print('UNSUPPORTED', wasm.stem, error) + except (OSError, ValueError, AssertionError, RuntimeError, subprocess.CalledProcessError) as error: + print('FAIL', wasm.stem, error, getattr(error, 'stderr', '')) + failures += 1 + return bool(failures) + + +if __name__ == '__main__': + raise SystemExit(main()) From 86caa8ed5afbe1639409bab0ebf532d915edb6a8 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 22:53:26 -0800 Subject: [PATCH 12/20] fix: wake only pending transfers on peer closure --- crates/c-api/examples/stackless-http/p3.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/c-api/examples/stackless-http/p3.c b/crates/c-api/examples/stackless-http/p3.c index 6c70427..7ea7d9f 100644 --- a/crates/c-api/examples/stackless-http/p3.c +++ b/crates/c-api/examples/stackless-http/p3.c @@ -409,7 +409,9 @@ p3_call(p3_context *cx, enum p3_op op, bool future, uint8_t element, if (s->peer) { other = get(cx, s->peer); other->peer = 0; - if (other->state != DONE && !other->pending) + /* Peer closure completes an in-flight operation. An idle end + * observes closure when it starts its next operation. */ + if (other->state == COPYING && !other->pending) notify(cx, other); } release(cx, s); From 04957b0a00800733f7cfe0774c4edfb125966e5e Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 22:53:26 -0800 Subject: [PATCH 13/20] feat: add a bounded HTTP/2 service fixture --- .../examples/stackless-http/CMakeLists.txt | 16 +- crates/c-api/examples/stackless-http/HTTP2.md | 89 +++ .../c-api/examples/stackless-http/README.md | 3 + .../stackless-http/build_wasi_http_guest.py | 3 +- crates/c-api/examples/stackless-http/h2.c | 555 ++++++++++++++++++ crates/c-api/examples/stackless-http/h2.h | 58 ++ .../c-api/examples/stackless-http/h2_server.c | 204 +++++++ crates/c-api/examples/stackless-http/p3.c | 6 + crates/c-api/examples/stackless-http/p3.h | 2 + .../stackless-http/pico/CMakeLists.txt | 2 +- .../stackless-http/qemu/CMakeLists.txt | 25 +- .../examples/stackless-http/qemu/h2_inputs.py | 35 ++ .../examples/stackless-http/qemu/h2_sim.c | 205 +++++++ .../c-api/examples/stackless-http/qemu/m33.ld | 3 +- .../stackless-http/qemu/nghttp2_newlib.h | 30 + .../c-api/examples/stackless-http/qemu/run.py | 4 +- .../examples/stackless-http/qemu/run_h2.py | 38 ++ crates/c-api/examples/stackless-http/server.c | 99 +--- .../examples/stackless-http/socket_loop.h | 106 ++++ .../examples/stackless-http/test_wasi_h2.py | 253 ++++++++ .../stackless-http/test_wasi_http_load.py | 14 +- .../c-api/examples/stackless-http/wasi_http.h | 2 + .../examples/stackless-http/wasi_http_guest.c | 2 + .../examples/stackless-http/wasi_http_io.c | 77 +++ .../examples/stackless-http/wasi_http_io.h | 18 + .../stackless-http/wasi_http_server.c | 84 +-- 26 files changed, 1745 insertions(+), 188 deletions(-) create mode 100644 crates/c-api/examples/stackless-http/HTTP2.md create mode 100644 crates/c-api/examples/stackless-http/h2.c create mode 100644 crates/c-api/examples/stackless-http/h2.h create mode 100644 crates/c-api/examples/stackless-http/h2_server.c create mode 100644 crates/c-api/examples/stackless-http/qemu/h2_inputs.py create mode 100644 crates/c-api/examples/stackless-http/qemu/h2_sim.c create mode 100644 crates/c-api/examples/stackless-http/qemu/nghttp2_newlib.h create mode 100644 crates/c-api/examples/stackless-http/qemu/run_h2.py create mode 100644 crates/c-api/examples/stackless-http/socket_loop.h create mode 100644 crates/c-api/examples/stackless-http/test_wasi_h2.py create mode 100644 crates/c-api/examples/stackless-http/wasi_http_io.c create mode 100644 crates/c-api/examples/stackless-http/wasi_http_io.h diff --git a/crates/c-api/examples/stackless-http/CMakeLists.txt b/crates/c-api/examples/stackless-http/CMakeLists.txt index 66beab3..3ab22d2 100644 --- a/crates/c-api/examples/stackless-http/CMakeLists.txt +++ b/crates/c-api/examples/stackless-http/CMakeLists.txt @@ -16,10 +16,22 @@ target_compile_definitions(wasi_http_check PRIVATE P3_WASI_HTTP P3_WASI_HTTP_HOS target_compile_definitions(component_check PRIVATE P3_HANDLES=128) set(targets stackless_http component_check wasi_http_check) if(P3_WASI_HANDLER_INCLUDE) - add_executable(wasi_http_server p3.c vm.c task.c wasi_http.c wasi_http_server.c server.c) + add_executable(wasi_http_server p3.c vm.c task.c wasi_http.c wasi_http_io.c wasi_http_server.c server.c) target_include_directories(wasi_http_server PRIVATE ${P3_WASI_HANDLER_INCLUDE}) target_compile_definitions(wasi_http_server PRIVATE P3_WASI_HTTP P3_WASI_HTTP_HOSTED P3_IMPORTS=128) - list(APPEND targets wasi_http_server wasi_http_check) + list(APPEND targets wasi_http_server) +endif() +if(P3_H2_HANDLER_INCLUDE) + if(NOT P3_NGHTTP2_INCLUDE OR NOT P3_NGHTTP2_LIBRARY) + message(FATAL_ERROR "Set P3_NGHTTP2_INCLUDE and P3_NGHTTP2_LIBRARY") + endif() + add_executable(wasi_h2_server p3.c vm.c task.c wasi_http.c wasi_http_io.c h2.c h2_server.c) + target_include_directories(wasi_h2_server PRIVATE ${P3_H2_HANDLER_INCLUDE} ${P3_NGHTTP2_INCLUDE}) + target_link_libraries(wasi_h2_server PRIVATE ${P3_NGHTTP2_LIBRARY}) + target_compile_definitions(wasi_h2_server PRIVATE P3_WASI_HTTP NGHTTP2_STATICLIB NGHTTP2_NO_SSIZE_T + P3_HANDLES=240 HTTP_RESOURCE_SLOTS=64 HTTP_PENDING_DROPS=48 + H2_STREAMS=16 H2_PER_CONNECTION=8 H2_CONNECTIONS=4 H2_CONNECTION_HEAP=65536) + list(APPEND targets wasi_h2_server) endif() foreach(target IN LISTS targets) target_include_directories(${target} PRIVATE ${P3_RUNTIME_INCLUDE} ${P3_HANDLER_INCLUDE}) diff --git a/crates/c-api/examples/stackless-http/HTTP2.md b/crates/c-api/examples/stackless-http/HTTP2.md new file mode 100644 index 0000000..4a47a1b --- /dev/null +++ b/crates/c-api/examples/stackless-http/HTTP2.md @@ -0,0 +1,89 @@ +# Bounded HTTP/2 fixture + +This opt-in transport serves the same `wasi:http@0.3.2` component using +HTTP/2 prior knowledge over loopback TCP. nghttp2 owns framing, HPACK and +protocol flow control. The embedder maps each stream to an independent +WASI task and keeps one shared guest instance. A 2 KiB receive ring per +stream bounds pending uploads. The server returns window credit when the +guest consumes bytes or the transport discards them after cancellation. +Response production follows nghttp2's send windows. A blocked stream +defers its producer while siblings can continue. + +The hosted fixture admits four connections, eight streams per connection, +and sixteen across the process. Each nghttp2 connection has a 64 KiB +allocation cap. The Cortex-M33 fixture uses two connections, four +aggregate streams, and a 28 KiB protocol allocation cap per connection. +Its libc heap and Wasm pool are separately bounded by the application +linker and allocator. The fixture covers cleartext prior knowledge, +request bodies, response trailers and stream resets. TLS/ALPN, upgrade +negotiation and inbound request trailers are outside this profile. + +## Review scope + +The shared C continuation contract is the runtime-facing proposal. A later +commit should move the WASI bindings, offline component linker, scheduler, +HTTP/1.1 and HTTP/2 transports, example guests, and test harnesses to an +out-of-tree consumer. Both drafts carry these pieces so reviewers can +trace the seam end to end and reproduce the evidence against either +runtime. The opt-in example targets own all HTTP dependencies. + +## Build and run + +Use the pinned WASI suite and hosted adapter build from +[WASI_HTTP.md](WASI_HTTP.md). The host fixture needs a sixteen-task guest +and nghttp2 v1.70.0 at `85e300c79fb6dbcfa9c1013215c8710c1c2cd3d2`. +The independent client uses `h2==4.3.0`. + +```sh +python3 -m venv "$CLIENT" +"$CLIENT/bin/pip" install 'h2==4.3.0' +python3 build_wasi_http_guest.py "$SUITE" "$H2_GUEST" --tasks 16 + +cmake -S "$NGHTTP2" -B "$NG_HOST" -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_LIB_ONLY=ON -DBUILD_TESTING=OFF \ + -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON +cmake --build "$NG_HOST" +cmake -S . -B "$ADAPTER" -DP3_H2_HANDLER_INCLUDE="$H2_GUEST" \ + "-DP3_NGHTTP2_INCLUDE=$NGHTTP2/lib/includes;$NG_HOST/lib/includes" \ + -DP3_NGHTTP2_LIBRARY="$NG_HOST/lib/libnghttp2.a" +cmake --build "$ADAPTER" --target wasi_h2_server +"$CLIENT/bin/python" test_wasi_h2.py "$ADAPTER/wasi_h2_server" --rounds 24 +``` + +Use the MSVC x64 developer environment on Windows. With the Ninja +generator, point `P3_NGHTTP2_LIBRARY` at `lib/nghttp2.lib` and run the +Python client against `wasi_h2_server.exe`. The overload checks distinguish +the peer SETTINGS stream limit, an on-wire `REFUSED_STREAM` at the global +limit, and TCP closure when all connection slots are reserved. + +For Cortex-M33, build the regular four-task standard guest in +`PICO_GUEST` and generate the client prefaces. The newlib compatibility +header supplies byte-order conversions for the emulated bare-metal +build. Use an LTO-aware archive index with the embedded GCC toolchain. + +```sh +python3 build_wasi_http_guest.py "$SUITE" "$PICO_GUEST" +"$CLIENT/bin/python" qemu/h2_inputs.py "$PICO_GUEST/h2_inputs.h" +cmake -G Ninja -S "$NGHTTP2" -B "$NG_ARM" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_SYSTEM_NAME=Generic -DCMAKE_C_COMPILER=arm-none-eabi-gcc \ + -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY -DSIZEOF_SSIZE_T=4 \ + "-DCMAKE_C_FLAGS=-mcpu=cortex-m33 -mthumb -mfloat-abi=soft -ffunction-sections -fdata-sections -flto -include $PWD/qemu/nghttp2_newlib.h" \ + -DENABLE_LIB_ONLY=ON -DBUILD_TESTING=OFF \ + -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON +cmake --build "$NG_ARM" +arm-none-eabi-gcc-ranlib "$NG_ARM/lib/libnghttp2.a" +cmake -G Ninja -S qemu -B "$M33_BUILD" -DCMAKE_BUILD_TYPE=Release \ + -DWAMR_ROOT="$WAMR_ROOT" -DHANDLER_INCLUDE="$PICO_GUEST" -DP3_WASI_H2=ON \ + -DP3_NGHTTP2_INCLUDE="$NGHTTP2/lib/includes" \ + -DP3_NGHTTP2_CONFIG_INCLUDE="$NG_ARM/lib/includes" \ + -DP3_NGHTTP2_LIBRARY="$NG_ARM/lib/libnghttp2.a" +cmake --build "$M33_BUILD" +python3 qemu/run_h2.py "$M33_BUILD/stackless_m33.elf" +``` + +The QEMU workload passes HTTP/2 frames through the same nghttp2 +transport. It verifies all response bytes, terminal trailer frames and stream +completion while holding one stream's output window closed. The +emulated binary accounts for the 64 KiB network reserve in its 520 KiB +SRAM region. This target uses scripted transport readiness. The Pico SDK +Wi-Fi firmware in the existing sample continues to use HTTP/1.1. diff --git a/crates/c-api/examples/stackless-http/README.md b/crates/c-api/examples/stackless-http/README.md index a294f14..9f13030 100644 --- a/crates/c-api/examples/stackless-http/README.md +++ b/crates/c-api/examples/stackless-http/README.md @@ -148,3 +148,6 @@ Foreground time excludes sleeping time and is not total CPU or power. See [WASI_HTTP.md](WASI_HTTP.md) for the optional standard WASI 0.3 service adapter, pinned suite runner, and concurrent HTTP workload. + +The [HTTP/2 fixture](HTTP2.md) reuses the standard service adapter and +exercises independent streams, admission limits and the Cortex-M33 budget. diff --git a/crates/c-api/examples/stackless-http/build_wasi_http_guest.py b/crates/c-api/examples/stackless-http/build_wasi_http_guest.py index b981cfa..f1c630c 100644 --- a/crates/c-api/examples/stackless-http/build_wasi_http_guest.py +++ b/crates/c-api/examples/stackless-http/build_wasi_http_guest.py @@ -34,6 +34,7 @@ def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('suite', type=Path) p.add_argument('output', type=Path) + p.add_argument('--tasks', type=int, choices=(4, 16), default=4) p.add_argument('--clang', default='clang') p.add_argument('--wasm-tools', default='wasm-tools') a = p.parse_args() @@ -45,7 +46,7 @@ def main(): core, typed, component, linked = (a.output / f'standard.{ext}.wasm' for ext in ('core', 'typed', 'component', 'linked')) subprocess.run([a.clang, '--target=wasm32', '-Os', '-fno-builtin', '-nostdlib', '-Wl,--no-entry,--initial-memory=65536,--max-memory=65536,-z,stack-size=4096,--strip-all', - '-I', a.output, here / 'wasi_http_guest.c', '-o', core], check=True) + f'-DCONNECTIONS={a.tasks}', '-I', a.output, here / 'wasi_http_guest.c', '-o', core], check=True) subprocess.run([a.wasm_tools, 'component', 'embed', wit, core, '--world', 'wasi:http/service@0.3.2', '-o', typed], check=True) subprocess.run([a.wasm_tools, 'component', 'new', typed, '-o', component], check=True) link(component, linked, a.wasm_tools) diff --git a/crates/c-api/examples/stackless-http/h2.c b/crates/c-api/examples/stackless-http/h2.c new file mode 100644 index 0000000..fdfeb13 --- /dev/null +++ b/crates/c-api/examples/stackless-http/h2.c @@ -0,0 +1,555 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "h2.h" +#include "wasi_http_io.h" +#include +#include + +typedef union allocation { +#if defined(_MSC_VER) && defined(_WIN64) + __declspec(align(16)) size_t size; +#elif defined(_MSC_VER) + __declspec(align(8)) size_t size; +#else + max_align_t alignment; + size_t size; +#endif +} allocation; + +static void * +allocate(size_t size, void *user) +{ + h2_connection *c = user; + h2_server *s = c->server; + size_t available = H2_CONNECTION_HEAP - c->heap; + if (size > available || sizeof(allocation) > available - size) + return NULL; + size += sizeof(allocation); + allocation *p = malloc(size); + if (!p) + return NULL; + p->size = size; + c->heap += size; + s->heap += size; + if (s->heap > s->heap_peak) + s->heap_peak = s->heap; + return p + 1; +} +static void +release(void *ptr, void *user) +{ + if (!ptr) + return; + h2_connection *c = user; + allocation *p = (allocation *)ptr - 1; + c->heap -= p->size; + c->server->heap -= p->size; + free(p); +} +static void * +allocate_zero(size_t n, size_t size, void *user) +{ + if (size && n > SIZE_MAX / size) + return NULL; + void *p = allocate(n * size, user); + if (p) + memset(p, 0, n * size); + return p; +} +static void * +resize(void *ptr, size_t size, void *user) +{ + if (!ptr) + return allocate(size, user); + h2_connection *c = user; + h2_server *s = c->server; + allocation *p = (allocation *)ptr - 1; + size_t old = p->size, available = H2_CONNECTION_HEAP - (c->heap - old); + if (size > available || sizeof(*p) > available - size) + return NULL; + size += sizeof(*p); + p = realloc(p, size); + if (!p) + return NULL; + p->size = size; + c->heap = c->heap - old + size; + s->heap = s->heap - old + size; + if (s->heap > s->heap_peak) + s->heap_peak = s->heap; + return p + 1; +} +static h2_stream * +stream(nghttp2_session *session, int32_t id) +{ + return nghttp2_session_get_stream_user_data(session, id); +} +static int +reject(nghttp2_session *session, int32_t id, uint32_t code) +{ + if (nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, id, code)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; +} +static int +begin_headers(nghttp2_session *session, const nghttp2_frame *frame, void *user) +{ + h2_connection *c = user; + h2_server *s = c->server; + int32_t id = frame->hd.stream_id; + if (frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + ++s->protocol_errors; + return reject(session, id, NGHTTP2_PROTOCOL_ERROR); + } + h2_stream *slot = NULL; + for (unsigned i = 0; i < H2_STREAMS; ++i) + if (!s->streams[i].connection) { + slot = &s->streams[i]; + break; + } + if (!slot) { + return reject(session, id, NGHTTP2_REFUSED_STREAM); + } + memset(slot, 0, sizeof(*slot)); + slot->connection = c; + slot->id = id; + if (++s->live > s->peak) + s->peak = s->live; + slot->request = wasi_http_new(&s->http, HTTP_REQUEST, false); + if (!slot->request + || nghttp2_session_set_stream_user_data(session, id, slot)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + http_resource *r = + wasi_http_get(&s->http, slot->request, HTTP_REQUEST, false); + r->as.message.inbound = true; + r->as.message.fields = wasi_http_new(&s->http, HTTP_FIELDS, true); + return r->as.message.fields ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE; +} +static bool +eq(const uint8_t *name, size_t size, const char *text) +{ + return size == strlen(text) && !memcmp(name, text, size); +} +static int +header(nghttp2_session *session, const nghttp2_frame *frame, + const uint8_t *name, size_t names, const uint8_t *value, size_t values, + uint8_t flags, void *user) +{ + (void)flags; + h2_connection *c = user; + h2_stream *x = stream(session, frame->hd.stream_id); + if (!x) + return 0; + wasi_http *http = &c->server->http; + http_resource *r = wasi_http_get(http, x->request, HTTP_REQUEST, false); + if (!r) + return NGHTTP2_ERR_CALLBACK_FAILURE; + http_message *m = &r->as.message; + if (eq(name, names, ":method")) { + int method = wasi_http_method(value, (uint32_t)values); + if (method < 0 || method == 5) + goto invalid; + x->method = true; + m->method = (uint8_t)method; + if (method == 9) + memcpy(m->method_other, value, values); + } + else if (eq(name, names, ":scheme")) { + if (!eq(value, values, "http")) + goto invalid; + x->scheme = true; + m->scheme = 1; + } + else if (eq(name, names, ":authority") || eq(name, names, "host")) { + if (m->authority[0] || memchr(value, '@', values) + || !wasi_http_authority(value, (uint32_t)values)) + goto invalid; + memcpy(m->authority, value, values); + } + else if (eq(name, names, ":path")) { + if (!values || !wasi_http_path(value, (uint32_t)values)) + goto invalid; + memcpy(m->path, value, values); + } + else { + http_resource *fields = + wasi_http_get(http, m->fields, HTTP_FIELDS, true); + if (!fields) + return NGHTTP2_ERR_CALLBACK_FAILURE; + if (wasi_http_append(&fields->as.fields, name, (uint32_t)names, value, + (uint32_t)values) + != -1) + goto invalid; + } + return 0; +invalid: + ++c->server->protocol_errors; + return reject(session, frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); +} +static int +frame_received(nghttp2_session *session, const nghttp2_frame *frame, void *user) +{ + h2_connection *c = user; + h2_stream *x = stream(session, frame->hd.stream_id); + if (!x) + return 0; + if (frame->hd.type == NGHTTP2_HEADERS + && frame->headers.cat == NGHTTP2_HCAT_REQUEST) { + wasi_http *http = &c->server->http; + http_resource *r = wasi_http_get(http, x->request, HTTP_REQUEST, false); + if (!r) + return NGHTTP2_ERR_CALLBACK_FAILURE; + if (!x->method || !x->scheme || !r->as.message.path[0] + || !r->as.message.authority[0]) { + ++c->server->protocol_errors; + return nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, x->id, + NGHTTP2_PROTOCOL_ERROR); + } + x->send_body = r->as.message.method != 1; + if (!http_start_task(http, &x->task, x->request, &x->input, + &x->input_trailers)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + x->request = 0; /* Ownership passed to the guest. */ + } + if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) + && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) + x->input_end = true; + return 0; +} +static int +data_received(nghttp2_session *session, uint8_t flags, int32_t id, + const uint8_t *data, size_t len, void *user) +{ + (void)flags; + h2_connection *c = user; + h2_stream *x = stream(session, id); + if (!len) + return 0; + if (!x || x->aborted) + return nghttp2_session_consume_connection(session, len); + if (len > H2_WINDOW - (size_t)x->queued + || len > H2_MAX_BODY - x->received) { + ++c->server->protocol_errors; + x->aborted = true; + if (nghttp2_session_consume_connection(session, len)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + return nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, id, + NGHTTP2_ENHANCE_YOUR_CALM); + } + size_t write_at = (x->read_at + x->queued) % H2_WINDOW; + size_t first = len < H2_WINDOW - write_at ? len : H2_WINDOW - write_at; + memcpy(x->input_bytes + write_at, data, first); + memcpy(x->input_bytes, data + first, len - first); + x->queued += (uint16_t)len; + x->received += (uint32_t)len; + if (x->queued > c->server->input_peak) + c->server->input_peak = x->queued; + return 0; +} +static int +stream_closed(nghttp2_session *session, int32_t id, uint32_t error, void *user) +{ + (void)user; + h2_stream *x = stream(session, id); + if (x) { + x->closed = true; + x->aborted |= error != NGHTTP2_NO_ERROR; + } + return 0; +} +static int +frame_sent(nghttp2_session *session, const nghttp2_frame *frame, void *user) +{ + h2_connection *c = user; + if (frame->hd.type == NGHTTP2_RST_STREAM + && frame->rst_stream.error_code == NGHTTP2_REFUSED_STREAM) + ++c->server->refused; + h2_stream *x = stream(session, frame->hd.stream_id); + if (x + && (frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) + && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) + x->local_end = true; + return 0; +} +static size_t +wire_fields(const http_fields *f, nghttp2_nv *nv) +{ + size_t count = 0; + for (unsigned i = 0; i < f->count; ++i) { + http_field field = f->field[i]; + uint8_t *name = (uint8_t *)f->bytes + field.offset; + /* The transport determines framing, as in the HTTP/1.1 fixture. */ + if (eq(name, field.name_size, "content-length")) + continue; + nv[count++] = + (nghttp2_nv){ name, name + field.name_size, field.name_size, + field.value_size, NGHTTP2_NV_FLAG_NONE }; + } + return count; +} +static http_message * +response(h2_server *s, h2_stream *x) +{ + http_resource *r = + wasi_http_get(&s->http, x->response, HTTP_RESPONSE, true); + return r ? &r->as.message : NULL; +} +static nghttp2_ssize +read_body(nghttp2_session *session, int32_t id, uint8_t *buf, size_t length, + uint32_t *flags, nghttp2_data_source *source, void *user) +{ + h2_connection *c = user; + h2_stream *x = source->ptr; + h2_server *s = c->server; + http_message *m = response(s, x); + if (!m) + return NGHTTP2_ERR_CALLBACK_FAILURE; + if (m->body) { + uint32_t status; + uint32_t take = length < 1024 ? (uint32_t)length : 1024; + if (!p3_native_transfer(&s->vm.async, m->body, buf, take, &status)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + if (status != P3_BLOCKED) { + if ((status & 15) == 1 && !http_close_end(&s->http, &m->body)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + if (status >> 4) + return (nghttp2_ssize)(status >> 4); + } + } + if (m->body || m->trailers) { + x->deferred = true; + return NGHTTP2_ERR_DEFERRED; + } + *flags |= NGHTTP2_DATA_FLAG_EOF; + if (x->output_trailers && x->send_body) { + http_resource *r = + wasi_http_get(&s->http, x->output_trailers, HTTP_FIELDS, true); + if (!r) + return NGHTTP2_ERR_CALLBACK_FAILURE; + nghttp2_nv nv[HTTP_FIELDS_MAX]; + size_t n = wire_fields(&r->as.fields, nv); + if (n) { + if (nghttp2_submit_trailer(session, id, nv, n)) + return NGHTTP2_ERR_CALLBACK_FAILURE; + *flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + } + } + return 0; +} +static bool +submit_response(h2_server *s, h2_stream *x, http_message *m) +{ + http_resource *r = wasi_http_get(&s->http, m->fields, HTTP_FIELDS, true); + if (!r) + return false; + uint8_t status[] = { (uint8_t)('0' + m->status / 100), + (uint8_t)('0' + m->status / 10 % 10), + (uint8_t)('0' + m->status % 10) }; + nghttp2_nv nv[1 + HTTP_FIELDS_MAX] = { { (uint8_t *)":status", status, 7, + sizeof(status), + NGHTTP2_NV_FLAG_NONE } }; + size_t n = 1 + wire_fields(&r->as.fields, nv + 1); + x->send_body &= m->status >= 200 && m->status != 204 && m->status != 304; + nghttp2_data_provider2 provider = { .source.ptr = x, + .read_callback = read_body }; + return !nghttp2_submit_response2(x->connection->session, x->id, nv, n, + &provider); +} +bool +h2_open(h2_server *s, unsigned slot) +{ + h2_connection *c = &s->connections[slot]; + /* A closed socket's index cannot be reused until its detached tasks reap. + */ + for (unsigned i = 0; i < H2_STREAMS; ++i) + if (s->streams[i].connection == c) + return false; + *c = (h2_connection){ .server = s }; + nghttp2_session_callbacks *cb; + nghttp2_option *opt; + if (nghttp2_session_callbacks_new(&cb)) + return false; + if (nghttp2_option_new(&opt)) { + nghttp2_session_callbacks_del(cb); + return false; + } + nghttp2_session_callbacks_set_on_begin_headers_callback(cb, begin_headers); + nghttp2_session_callbacks_set_on_header_callback(cb, header); + nghttp2_session_callbacks_set_on_frame_recv_callback(cb, frame_received); + nghttp2_session_callbacks_set_on_frame_send_callback(cb, frame_sent); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(cb, + data_received); + nghttp2_session_callbacks_set_on_stream_close_callback(cb, stream_closed); + nghttp2_option_set_no_auto_window_update(opt, 1); + nghttp2_option_set_max_deflate_dynamic_table_size(opt, 0); + nghttp2_option_set_max_send_header_block_length(opt, 2 * 1024); + nghttp2_option_set_max_continuations(opt, 2); + nghttp2_option_set_max_settings(opt, 16); + nghttp2_option_set_max_outbound_ack(opt, 16); + nghttp2_mem mem = { c, allocate, release, allocate_zero, resize }; + int err = nghttp2_session_server_new3(&c->session, cb, c, opt, &mem); + nghttp2_option_del(opt); + nghttp2_session_callbacks_del(cb); + const nghttp2_settings_entry settings[] = { + { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, H2_PER_CONNECTION }, + { NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, H2_WINDOW }, + { NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, 0 }, + { NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, 2048 }, + }; + if (err + || nghttp2_submit_settings(c->session, NGHTTP2_FLAG_NONE, settings, + sizeof(settings) / sizeof(*settings))) { + h2_close(c); + return false; + } + return true; +} +void +h2_close(h2_connection *c) +{ + if (c->session) + nghttp2_session_del(c->session); + c->session = NULL; + c->wire_size = 0; + for (unsigned i = 0; i < H2_STREAMS; ++i) { + h2_stream *x = &c->server->streams[i]; + if (x->connection == c) { + x->aborted |= !(x->closed && x->local_end); + x->closed = true; + } + } +} +bool +h2_receive(h2_connection *c, const uint8_t *bytes, size_t size) +{ + return nghttp2_session_mem_recv2(c->session, bytes, size) + == (nghttp2_ssize)size; +} +bool +h2_output(h2_connection *c) +{ + if (c->failed) + return false; + if (c->wire_size) + return true; + nghttp2_ssize len = nghttp2_session_mem_send2(c->session, &c->wire); + if (len < 0) + return false; + c->wire_size = (size_t)len; + return true; +} +static bool +pump_input(h2_server *s, h2_stream *x) +{ + nghttp2_session *session = x->connection->session; + p3_context *cx = &s->vm.async; + while (x->queued) { + bool discard = + x->aborted || !x->input || !p3_http_peer_open(cx, x->input); + uint32_t n = x->queued; + if (!discard) { + uint32_t cap = p3_native_capacity(cx, x->input), status; + if (!cap) + break; + if (n > cap) + n = cap; + if (n > (uint32_t)H2_WINDOW - x->read_at) + n = H2_WINDOW - x->read_at; + if (!p3_native_transfer(cx, x->input, x->input_bytes + x->read_at, + n, &status) + || status != n << 4) + return false; + } + x->read_at = (uint16_t)((x->read_at + n) % H2_WINDOW); + x->queued -= (uint16_t)n; + if (session + && (x->closed || x->aborted + ? nghttp2_session_consume_connection(session, n) + : nghttp2_session_consume(session, x->id, n))) + return false; + } + if (x->aborted || (x->input_end && !x->queued)) { + if (!http_close_end(&s->http, &x->input) + || !http_send_future(&s->http, &x->input_trailers, x->aborted)) + return false; + } + return true; +} +static bool +pump_stream(h2_server *s, h2_stream *x, bool *ready) +{ + if (x->connection->session && x->connection->failed) + return true; + p3_context *cx = &s->vm.async; + if (!pump_input(s, x)) + return false; + if (x->aborted && x->task.started && !x->task.exited && !x->task.returned) + p3_task_request_cancel(&x->task); + if (x->task.returned && !x->response) { + x->response = x->task.http_result; + http_message *m = response(s, x); + if (!m) + return false; + if (!x->aborted && !submit_response(s, x, m)) { + x->connection->failed = true; + return true; + } + } + if (x->response) { + http_message *m = response(s, x); + if (!m + || ((x->aborted || !x->send_body) + && !http_close_end(&s->http, &m->body)) + || !http_receive_trailers(&s->http, &m->trailers, + &x->output_trailers)) + return false; + if (x->deferred && !x->closed && !x->aborted + && (m->body ? (!p3_http_peer_open(cx, m->body) + || p3_native_capacity(cx, m->body)) + : !m->trailers)) { + if (nghttp2_session_resume_data(x->connection->session, x->id)) + return false; + x->deferred = false; + } + if ((x->aborted || (x->local_end && !x->connection->wire_size)) + && !http_send_future(&s->http, &m->sent, x->aborted)) + return false; + } + if (!wasi_http_sweep(&s->http)) + return false; + if (p3_task_ready(&x->task)) { + ++s->callbacks; + if (!p3_task_step(&x->task)) + return false; + *ready = true; + } + if (!x->closed || x->connection->wire_size + || (x->task.started && !x->task.exited)) + return true; + if ((x->request + && !wasi_http_drop(&s->http, x->request, HTTP_REQUEST, false)) + || (x->response + && !wasi_http_drop(&s->http, x->response, HTTP_RESPONSE, true)) + || (x->output_trailers + && !wasi_http_drop(&s->http, x->output_trailers, HTTP_FIELDS, true)) + || !http_close_end(&s->http, &x->input) + || !http_send_future(&s->http, &x->input_trailers, x->aborted)) + return false; + if (x->aborted) + ++s->reset; + else + ++s->completed; + x->connection = NULL; + --s->live; + return wasi_http_sweep(&s->http); +} +bool +h2_pump(h2_server *s, bool *ready) +{ + *ready = false; + for (unsigned i = 0; i < H2_STREAMS; ++i) + if (s->streams[i].connection && !pump_stream(s, &s->streams[i], ready)) + return false; + return true; +} diff --git a/crates/c-api/examples/stackless-http/h2.h b/crates/c-api/examples/stackless-http/h2.h new file mode 100644 index 0000000..5af6848 --- /dev/null +++ b/crates/c-api/examples/stackless-http/h2.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_H2_H +#define STACKLESS_H2_H +#include "wasi_http.h" +#include + +#ifndef H2_STREAMS +#define H2_STREAMS 4 +#define H2_PER_CONNECTION 4 +#define H2_CONNECTIONS 2 +#define H2_CONNECTION_HEAP (28 * 1024) +#endif +#define H2_WINDOW 2048 +#define H2_MAX_BODY (64 * 1024) + +typedef struct h2_server h2_server; +typedef struct h2_connection { + h2_server *server; + nghttp2_session *session; + const uint8_t *wire; + size_t wire_size, heap; + bool failed; +} h2_connection; +typedef struct h2_stream { + h2_connection *connection; + p3_task task; + int32_t id; + uint32_t request, input, input_trailers, response, output_trailers, + received; + uint16_t read_at, queued; + bool input_end, local_end, closed, aborted, deferred, send_body; + bool method, scheme; + uint8_t input_bytes[H2_WINDOW]; +} h2_stream; +struct h2_server { + p3_vm vm; + wasi_http http; + h2_connection connections[H2_CONNECTIONS]; + h2_stream streams[H2_STREAMS]; + size_t heap, heap_peak; + unsigned live, peak, completed, reset, refused, protocol_errors; + unsigned callbacks, input_peak; +}; + +bool +h2_open(h2_server *server, unsigned slot); +void +h2_close(h2_connection *c); +bool +h2_receive(h2_connection *c, const uint8_t *bytes, size_t size); +bool +h2_output(h2_connection *c); +bool +h2_pump(h2_server *server, bool *ready); +#endif diff --git a/crates/c-api/examples/stackless-http/h2_server.c b/crates/c-api/examples/stackless-http/h2_server.c new file mode 100644 index 0000000..8675648 --- /dev/null +++ b/crates/c-api/examples/stackless-http/h2_server.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef _WIN32 +#define _POSIX_C_SOURCE 200809L +#define _DARWIN_C_SOURCE +#endif +#include "h2.h" +#include "standard_handler.h" +#include +#include +#include +#include +#include "socket_loop.h" + +static h2_server server; +#ifndef _WIN32 +static volatile sig_atomic_t stopping; +static void +stop(int sig) +{ + (void)sig; + stopping = 1; +} +#endif + +static void +disconnect(unsigned slot, socket_t *fd) +{ + CLOSE(*fd); + *fd = BAD_SOCKET; + h2_close(&server.connections[slot]); +} + +int +main(int argc, char **argv) +{ + int port = argc > 1 ? atoi(argv[1]) : 0; + double seconds = argc > 2 ? atof(argv[2]) : 8; + if (port < 0 || port > 65535 || seconds < 1 || seconds > 300) + return 2; +#ifdef _WIN32 + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa)) + return 2; +#else + struct sigaction action = { 0 }; + action.sa_handler = stop; + sigemptyset(&action.sa_mask); + sigaction(SIGINT, &action, NULL); + signal(SIGPIPE, SIG_IGN); +#endif + if (!p3_vm_open(&server.vm, handler_wasm, sizeof(handler_wasm))) + goto fail; + wasi_http_init(&server.http, &server.vm); + socket_t listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listener == BAD_SOCKET || !nonblock(listener)) + goto fail; + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons((uint16_t)port); + if (bind(listener, (struct sockaddr *)&addr, sizeof(addr)) + || listen(listener, H2_CONNECTIONS)) + goto fail; +#ifdef _WIN32 + int addrlen = sizeof(addr); +#else + socklen_t addrlen = sizeof(addr); +#endif + if (getsockname(listener, (struct sockaddr *)&addr, &addrlen)) + goto fail; + socket_t sockets[H2_CONNECTIONS]; + for (unsigned i = 0; i < H2_CONNECTIONS; ++i) + sockets[i] = BAD_SOCKET; + printf("LISTEN %u connections=%u streams=%u per_connection=%u\n", + ntohs(addr.sin_port), H2_CONNECTIONS, H2_STREAMS, H2_PER_CONNECTION); + fflush(stdout); + double start = wall_time(), cpu = cpu_time(); + unsigned rejects = 0, failures = 0, waits = 0, wakes = 0; + while (wall_time() - start < seconds) { +#ifndef _WIN32 + if (stopping) + break; +#endif + bool ready; + if (!h2_pump(&server, &ready)) + goto fail; + pollfd_t fds[H2_CONNECTIONS + 1] = { { listener, POLLIN, 0 } }; + unsigned owners[H2_CONNECTIONS + 1], count = 1; + for (unsigned i = 0; i < H2_CONNECTIONS; ++i) { + if (sockets[i] == BAD_SOCKET) + continue; + h2_connection *c = &server.connections[i]; + if (!h2_output(c)) { + ++failures; + disconnect(i, &sockets[i]); + ready = true; + continue; + } + fds[count] = + (pollfd_t){ sockets[i], + (short)(POLLIN | (c->wire_size ? POLLOUT : 0)), 0 }; + owners[count++] = i; + } + double left = seconds - (wall_time() - start); + if (left <= 0) + break; + ++waits; + int n = poll_sockets(fds, count, ready ? 0 : (int)(left * 1000) + 1); + if (n < 0 && !again()) + goto fail; + if (n <= 0) + continue; + ++wakes; + if (fds[0].revents & POLLIN) { + socket_t fd = accept(listener, NULL, NULL); + if (fd != BAD_SOCKET) { + unsigned slot = 0; + if (nonblock(fd)) { + while (slot < H2_CONNECTIONS + && (sockets[slot] != BAD_SOCKET + || !h2_open(&server, slot))) + ++slot; + } + else + slot = H2_CONNECTIONS; + if (slot == H2_CONNECTIONS) { + ++rejects; + CLOSE(fd); + } + else { + int enabled = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, + (const char *)&enabled, sizeof(enabled)); + sockets[slot] = fd; + } + } + else if (!again()) + goto fail; + } + for (unsigned j = 1; j < count; ++j) { + unsigned i = owners[j]; + h2_connection *c = &server.connections[i]; + bool broken = + (fds[j].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0; + if (!broken && (fds[j].revents & POLLIN)) { + uint8_t input[4096]; + n = (int)recv(sockets[i], (char *)input, sizeof(input), 0); + if (n > 0) { + broken = !h2_receive(c, input, (size_t)n); + failures += broken; + } + else + broken = !n || !again(); + } + if (!broken && (fds[j].revents & POLLOUT)) { + n = (int)send(sockets[i], (const char *)c->wire, + (int)c->wire_size, 0); + if (n > 0) { + c->wire += n; + c->wire_size -= (size_t)n; + } + else + broken = n < 0 && !again(); + } + if (broken) + disconnect(i, &sockets[i]); + } + } + CLOSE(listener); + for (unsigned i = 0; i < H2_CONNECTIONS; ++i) + if (sockets[i] != BAD_SOCKET) + disconnect(i, &sockets[i]); + bool ready; + for (unsigned i = 0; i < 128 && server.live; ++i) + if (!h2_pump(&server, &ready)) + goto fail; + printf( + "H2 completed=%u reset=%u refused=%u connections_rejected=%u " + "protocol_errors=%u " + "connection_errors=%u peak_streams=%u peak_handles=%u " + "peak_resources=%u " + "queue_peak=%u heap_peak=%zu live=%u handles=%u resources=%u heap=%zu " + "rss_bytes=%zu cpu_s=%.9f elapsed_s=%.6f waits=%u wakes=%u " + "callbacks=%u\n", + server.completed, server.reset, server.refused, rejects, + server.protocol_errors, failures, server.peak, server.vm.async.peak, + server.http.peak, server.input_peak, server.heap_peak, server.live, + server.vm.async.live, server.http.live, server.heap, rss(), + cpu_time() - cpu, wall_time() - start, waits, wakes, server.callbacks); + p3_vm_close(&server.vm); +#ifdef _WIN32 + WSACleanup(); +#endif + return server.live || server.vm.async.live || server.http.live + || server.heap; +fail: + fprintf(stderr, "HTTP/2 failed: %s %s\n", server.vm.error, + server.vm.async.error ? server.vm.async.error : ""); + return 1; +} diff --git a/crates/c-api/examples/stackless-http/p3.c b/crates/c-api/examples/stackless-http/p3.c index 7ea7d9f..4e536e8 100644 --- a/crates/c-api/examples/stackless-http/p3.c +++ b/crates/c-api/examples/stackless-http/p3.c @@ -194,6 +194,12 @@ p3_http_lift(p3_context *cx, uint32_t h, bool future, uint8_t element) return true; } bool +p3_http_peer_open(p3_context *cx, uint32_t h) +{ + p3_slot *s = get(cx, h); + return s && s->native && s->peer; +} +bool p3_http_lower(p3_context *cx, uint32_t h) { p3_slot *s = get(cx, h); diff --git a/crates/c-api/examples/stackless-http/p3.h b/crates/c-api/examples/stackless-http/p3.h index 5bd1bbf..6cb6347 100644 --- a/crates/c-api/examples/stackless-http/p3.h +++ b/crates/c-api/examples/stackless-http/p3.h @@ -80,6 +80,8 @@ p3_host_end(p3_context *cx, uint32_t end); bool p3_http_lift(p3_context *cx, uint32_t h, bool future, uint8_t element); bool +p3_http_peer_open(p3_context *cx, uint32_t h); +bool p3_http_lower(p3_context *cx, uint32_t h); bool p3_http_finish(p3_context *cx, uint32_t h, uint8_t *value, bool *complete); diff --git a/crates/c-api/examples/stackless-http/pico/CMakeLists.txt b/crates/c-api/examples/stackless-http/pico/CMakeLists.txt index a912dba..9940314 100644 --- a/crates/c-api/examples/stackless-http/pico/CMakeLists.txt +++ b/crates/c-api/examples/stackless-http/pico/CMakeLists.txt @@ -16,7 +16,7 @@ endif() include(../embedded-wamr.cmake) if(P3_WASI_HTTP) - set(http_sources ../wasi_http.c ../wasi_http_server.c) + set(http_sources ../wasi_http.c ../wasi_http_io.c ../wasi_http_server.c) set(stack_size 8192) else() set(http_sources ../http.c) diff --git a/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt b/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt index 148b2a5..c0b0198 100644 --- a/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt +++ b/crates/c-api/examples/stackless-http/qemu/CMakeLists.txt @@ -12,19 +12,34 @@ if(NOT WAMR_ROOT OR NOT HANDLER_INCLUDE) message(FATAL_ERROR "Set WAMR_ROOT and HANDLER_INCLUDE") endif() include(../embedded-wamr.cmake) -if(P3_WASI_HTTP) - set(http_sources ../wasi_http.c ../wasi_http_server.c) +set(sim_source sim.c) +set(heap_size 16384) +if(P3_WASI_H2) + if(NOT P3_NGHTTP2_INCLUDE OR NOT P3_NGHTTP2_CONFIG_INCLUDE OR NOT P3_NGHTTP2_LIBRARY) + message(FATAL_ERROR "Set P3_NGHTTP2_INCLUDE, P3_NGHTTP2_CONFIG_INCLUDE and P3_NGHTTP2_LIBRARY") + endif() + set(http_sources ../wasi_http.c ../wasi_http_io.c ../h2.c) + set(sim_source h2_sim.c) + set(heap_size 65536) +elseif(P3_WASI_HTTP) + set(http_sources ../wasi_http.c ../wasi_http_io.c ../wasi_http_server.c) else() set(http_sources ../http.c) endif() add_executable(stackless_m33 ${WAMR_RUNTIME_LIB_SOURCE} ../p3.c ../vm.c ../task.c ${http_sources} - startup.c sim.c) -if(P3_WASI_HTTP) + startup.c ${sim_source}) +if(P3_WASI_HTTP OR P3_WASI_H2) target_compile_definitions(stackless_m33 PRIVATE P3_WASI_HTTP) target_link_options(stackless_m33 PRIVATE "LINKER:--defsym,__p3_stack_size=8192") endif() +if(P3_WASI_H2) + target_include_directories(stackless_m33 PRIVATE ${P3_NGHTTP2_INCLUDE} ${P3_NGHTTP2_CONFIG_INCLUDE}) + target_compile_definitions(stackless_m33 PRIVATE NGHTTP2_STATICLIB NGHTTP2_NO_SSIZE_T) + target_link_libraries(stackless_m33 PRIVATE ${P3_NGHTTP2_LIBRARY}) + target_link_options(stackless_m33 PRIVATE "LINKER:--defsym,__p3_heap_size=${heap_size}") +endif() target_include_directories(stackless_m33 PRIVATE .. ${HANDLER_INCLUDE} ${WAMR_ROOT}/core/iwasm/include) -target_compile_definitions(stackless_m33 PRIVATE P3_WAMR P3_QEMU PICO_HEAP_SIZE=16384) +target_compile_definitions(stackless_m33 PRIVATE P3_WAMR P3_QEMU PICO_HEAP_SIZE=${heap_size}) target_compile_options(stackless_m33 PRIVATE -mcpu=cortex-m33 -mthumb -mfloat-abi=soft -ffunction-sections -fdata-sections) target_link_options(stackless_m33 PRIVATE -mcpu=cortex-m33 -mthumb -mfloat-abi=soft -nostartfiles --specs=nosys.specs -T${CMAKE_CURRENT_LIST_DIR}/m33.ld -Wl,--gc-sections,-Map=stackless_m33.map) diff --git a/crates/c-api/examples/stackless-http/qemu/h2_inputs.py b/crates/c-api/examples/stackless-http/qemu/h2_inputs.py new file mode 100644 index 0000000..bbed4e5 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/h2_inputs.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Generate the HTTP/2 request frames used by the Cortex-M33 fixture.""" +from pathlib import Path +import sys + +from hpack import Encoder +from hyperframe.frame import HeadersFrame, SettingsFrame + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from build_wasi_http_guest import c_array + + +def requests(path): + encoder = Encoder() + encoder.header_table_size = 0 + settings = SettingsFrame(0) + settings.settings = {SettingsFrame.INITIAL_WINDOW_SIZE: 0} + data = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + settings.serialize() + for stream, method, route, size in ((1, 'GET', path, 0), (3, 'POST', '/upload', 16384)): + frame = HeadersFrame(stream) + frame.data = encoder.encode([(':method', method), (':scheme', 'http'), + (':authority', 'localhost'), (':path', route), + ('content-length', str(size))]) + frame.flags.add('END_HEADERS') + if not size: + frame.flags.add('END_STREAM') + data += frame.serialize() + return data + + +output = Path(sys.argv[1]) +output.write_text(c_array('h2_json_requests', requests('/data.json')) + + c_array('h2_png_requests', requests('/image.png'))) diff --git a/crates/c-api/examples/stackless-http/qemu/h2_sim.c b/crates/c-api/examples/stackless-http/qemu/h2_sim.c new file mode 100644 index 0000000..5f5dec9 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/h2_sim.c @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "h2.h" +#include "h2_inputs.h" +#include "payloads.h" +#include "standard_handler.h" +#include "upload.h" +#include "wasm_export.h" +#include +#include +#include + +static h2_server server; +static _Alignas(16) uint8_t pool[128 * 1024]; +int +pico_libc_peak(void); +void +systick(void) +{ +} +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + printf("H2 M33 FAIL %d %s: %s %s\n", __LINE__, #condition, \ + server.vm.error, \ + server.vm.async.error ? server.vm.async.error : ""); \ + fflush(stdout); \ + exit(1); \ + } \ + } while (0) + +typedef struct job { + const uint8_t *body; + uint32_t size, offset; + bool headers, end, trailers; +} job; +typedef struct peer { + job jobs[2]; + uint32_t upload, window, stream_window; +} peer; +static uint32_t +be32(const uint8_t *p) +{ + return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 + | p[3]; +} +static void +feed(h2_connection *c, uint8_t type, uint8_t flags, uint8_t stream, + const uint8_t *payload, uint32_t length) +{ + uint8_t frame[9 + 379] = { + 0, (uint8_t)(length >> 8), (uint8_t)length, type, flags, 0, 0, 0, stream + }; + CHECK(length <= sizeof(frame) - 9); + if (length) + memcpy(frame + 9, payload, length); + CHECK(h2_receive(c, frame, 9 + length)); +} +static void +allow_response(h2_connection *c, uint8_t stream) +{ + static const uint8_t credit[] = { 0, 0, 0x50, 0 }; + feed(c, NGHTTP2_WINDOW_UPDATE, 0, stream, credit, sizeof(credit)); +} +static void +drain(h2_connection *c, peer *p) +{ + CHECK(h2_output(c)); + while (c->wire_size) { + const uint8_t *frame = c->wire; + CHECK(c->wire_size >= 9); + uint32_t length = + (uint32_t)frame[0] << 16 | (uint32_t)frame[1] << 8 | frame[2]; + uint32_t id = be32(frame + 5); + CHECK(length == c->wire_size - 9); + const uint8_t *data = frame + 9; + switch (frame[3]) { + case NGHTTP2_WINDOW_UPDATE: + CHECK(length == 4 && (id == 0 || id == 3)); + if (id) + p->stream_window += be32(data); + else + p->window += be32(data); + break; + case NGHTTP2_HEADERS: + { + CHECK(id == 1 || id == 3); + job *j = &p->jobs[id >> 1]; + CHECK(!j->end && (frame[4] & NGHTTP2_FLAG_END_HEADERS)); + if (j->headers) { + CHECK(id == 3 && (frame[4] & NGHTTP2_FLAG_END_STREAM)); + j->trailers = j->end = true; + } + j->headers = true; + break; + } + case NGHTTP2_DATA: + { + CHECK(id == 1 || id == 3); + job *j = &p->jobs[id >> 1]; + CHECK(j->headers && !j->end && length <= j->size - j->offset); + CHECK(!memcmp(data, j->body + j->offset, length)); + j->offset += length; + j->end = (frame[4] & NGHTTP2_FLAG_END_STREAM) != 0; + break; + } + case NGHTTP2_SETTINGS: + if (!(frame[4] & NGHTTP2_FLAG_ACK)) + feed(c, NGHTTP2_SETTINGS, NGHTTP2_FLAG_ACK, 0, NULL, 0); + break; + default: + CHECK(false); + } + c->wire_size = 0; + CHECK(h2_output(c)); + } +} +int +main(void) +{ + MemAllocOption mem = { 0 }; + mem.pool.heap_buf = pool; + mem.pool.heap_size = sizeof(pool); + CHECK(p3_vm_open_with_engine( + &server.vm, wasm_engine_new_with_args(Alloc_With_Pool, &mem), + handler_wasm, sizeof(handler_wasm))); + wasi_http_init(&server.http, &server.vm); + peer peers[2] = { + { .jobs = { { json_body, sizeof(json_body) }, + { upload_body, sizeof(upload_body) } }, + .window = 65535, + .stream_window = H2_WINDOW }, + { .jobs = { { png_body, sizeof(png_body) }, + { upload_body, sizeof(upload_body) } }, + .window = 65535, + .stream_window = H2_WINDOW }, + }; + for (unsigned i = 0; i < 2; ++i) + CHECK(h2_open(&server, i)); + CHECK(h2_receive(&server.connections[0], h2_json_requests, + sizeof(h2_json_requests))); + CHECK(h2_receive(&server.connections[1], h2_png_requests, + sizeof(h2_png_requests))); + allow_response(&server.connections[0], 1); + allow_response(&server.connections[1], 1); + allow_response(&server.connections[0], 3); + bool ready, progressed_while_blocked = false; + for (unsigned tick = 0; tick < 8192 && server.live; ++tick) { + CHECK(h2_pump(&server, &ready)); + for (unsigned i = 0; i < 2; ++i) { + h2_connection *c = &server.connections[i]; + peer *p = &peers[i]; + drain(c, p); + if (p->upload == sizeof(upload_body)) + continue; + uint32_t n = sizeof(upload_body) - p->upload; + if (n > p->window) + n = p->window; + if (n > p->stream_window) + n = p->stream_window; + if (n > 379) + n = 379; + if (!n) + continue; + bool end = p->upload + n == sizeof(upload_body); + feed(c, NGHTTP2_DATA, end ? NGHTTP2_FLAG_END_STREAM : 0, 3, + upload_body + p->upload, n); + p->window -= n; + p->stream_window -= n; + p->upload += n; + } + if (!progressed_while_blocked && peers[0].jobs[1].end) { + CHECK(!peers[1].jobs[1].offset && !peers[1].jobs[1].end); + progressed_while_blocked = true; + allow_response(&server.connections[1], 3); + } + } + CHECK(progressed_while_blocked && server.completed == 4 + && server.peak == 4); + for (unsigned i = 0; i < 2; ++i) { + CHECK(peers[i].jobs[0].offset == peers[i].jobs[0].size + && peers[i].jobs[0].end); + CHECK(peers[i].jobs[1].offset == peers[i].jobs[1].size + && peers[i].jobs[1].trailers); + h2_close(&server.connections[i]); + } + CHECK(!server.live && !server.vm.async.live && !server.http.live + && !server.heap); + mem_alloc_info_t usage = { 0 }; + CHECK(wasm_runtime_get_mem_alloc_info(&usage)); + printf("H2 M33 PASS completed=%lu peak_streams=%lu peak_handles=%lu " + "peak_resources=%lu " + "input_peak=%lu h2_heap_peak=%lu libc_peak=%d pool_peak=%lu " + "server_bytes=%lu\n", + (unsigned long)server.completed, (unsigned long)server.peak, + (unsigned long)server.vm.async.peak, (unsigned long)server.http.peak, + (unsigned long)server.input_peak, (unsigned long)server.heap_peak, + pico_libc_peak(), (unsigned long)usage.highmark_size, + (unsigned long)sizeof(server)); + p3_vm_close(&server.vm); + fflush(stdout); + return 0; +} diff --git a/crates/c-api/examples/stackless-http/qemu/m33.ld b/crates/c-api/examples/stackless-http/qemu/m33.ld index a8af8d4..039dee5 100644 --- a/crates/c-api/examples/stackless-http/qemu/m33.ld +++ b/crates/c-api/examples/stackless-http/qemu/m33.ld @@ -9,6 +9,7 @@ MEMORY { } ENTRY(reset) PROVIDE(__p3_stack_size = 4K); +PROVIDE(__p3_heap_size = 16K); SECTIONS { .text : { KEEP(*(.vectors)) *(.text*) *(.rodata*) . = ALIGN(4); } > XIP .ARM.exidx : { *(.ARM.exidx*) } > XIP @@ -16,7 +17,7 @@ SECTIONS { __data_load__ = LOADADDR(.data); .bss (NOLOAD) : { __bss_start__ = .; *(.bss*) *(COMMON) . = ALIGN(8); __bss_end__ = .; } > RAM .net_reserve (NOLOAD) : { . += 64K; } > RAM - .heap (NOLOAD) : { __end__ = .; . += 16K; } > RAM + .heap (NOLOAD) : { __end__ = .; . += __p3_heap_size; } > RAM .stack (NOLOAD) : { __StackBottom = .; . += __p3_stack_size; __StackTop = .; } > RAM ASSERT(__StackTop <= ORIGIN(RAM) + LENGTH(RAM), "Pico RAM budget exceeded") } diff --git a/crates/c-api/examples/stackless-http/qemu/nghttp2_newlib.h b/crates/c-api/examples/stackless-http/qemu/nghttp2_newlib.h new file mode 100644 index 0000000..1e92bbb --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/nghttp2_newlib.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_NGHTTP2_NEWLIB_H +#define STACKLESS_NGHTTP2_NEWLIB_H +#include + +/* The AN505 build uses little-endian newlib without socket headers. */ +static inline uint16_t +htons(uint16_t value) +{ + return __builtin_bswap16(value); +} +static inline uint16_t +ntohs(uint16_t value) +{ + return __builtin_bswap16(value); +} +static inline uint32_t +htonl(uint32_t value) +{ + return __builtin_bswap32(value); +} +static inline uint32_t +ntohl(uint32_t value) +{ + return __builtin_bswap32(value); +} +#endif diff --git a/crates/c-api/examples/stackless-http/qemu/run.py b/crates/c-api/examples/stackless-http/qemu/run.py index 1df85a4..a4ccd9d 100644 --- a/crates/c-api/examples/stackless-http/qemu/run.py +++ b/crates/c-api/examples/stackless-http/qemu/run.py @@ -19,7 +19,7 @@ def run(*args): text=True, timeout=60) -def ram(elf, stack=4096): +def ram(elf, stack=4096, heap=16384): sections = {} for line in run('arm-none-eabi-size', '-A', elf).stdout.splitlines(): words = line.split() @@ -29,7 +29,7 @@ def ram(elf, stack=4096): if size and (0x20000000 <= address < 0x20082000 or 0x38000000 <= address < 0x38082000): sections[name] = size used = sum(sections.values()) - if (not 131072 < used < 520 * 1024 or sections.get('.heap') != 16384 + if (not 131072 < used < 520 * 1024 or sections.get('.heap') != heap or sections.get('.stack', sections.get('.stack_dummy')) != stack): raise RuntimeError(f'bad Cortex-M33 SRAM accounting: {sections}') return {'sram_reserved_bytes': used, 'sram_limit_bytes': 520 * 1024, 'sram_sections': sections} diff --git a/crates/c-api/examples/stackless-http/qemu/run_h2.py b/crates/c-api/examples/stackless-http/qemu/run_h2.py new file mode 100644 index 0000000..e02116d --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/run_h2.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Run HTTP/2 frames through the bounded Cortex-M33 server and record SRAM.""" +import argparse +import hashlib +import json +from pathlib import Path + +from run import ram, run + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('elf', type=Path) + p.add_argument('--qemu', default='qemu-system-arm') + args = p.parse_args() + elf = args.elf.resolve() + result = run(args.qemu, '-M', 'mps2-an505', '-nographic', '-monitor', 'none', + '-serial', 'null', '-net', 'none', '-icount', 'shift=0,sleep=off', + '-kernel', elf, '-semihosting-config', 'enable=on,target=native') + row = next(line for line in (result.stdout + result.stderr).splitlines() + if line.startswith('H2 M33 PASS ')) + counters = {k: int(v) for k, v in (word.split('=') for word in row.split()[3:])} + assert counters['completed'] == counters['peak_streams'] == 4 + assert counters['input_peak'] <= 2048 and counters['libc_peak'] <= 65536 + assert counters['pool_peak'] <= 131072 + print(json.dumps({ + 'qemu': run(args.qemu, '--version').stdout.splitlines()[0], + 'machine': 'mps2-an505', + 'elf_sha256': hashlib.sha256(elf.read_bytes()).hexdigest(), + 'memory': ram(elf, 8192, 65536), + 'workload': counters, + }, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/crates/c-api/examples/stackless-http/server.c b/crates/c-api/examples/stackless-http/server.c index 389d08a..9d626f9 100644 --- a/crates/c-api/examples/stackless-http/server.c +++ b/crates/c-api/examples/stackless-http/server.c @@ -16,104 +16,7 @@ #include #include #include -#ifdef _WIN32 -#include -#include -#include -#include -typedef SOCKET socket_t; -typedef WSAPOLLFD pollfd_t; -#define BAD_SOCKET INVALID_SOCKET -#define poll_sockets WSAPoll -#define CLOSE closesocket -static bool -nonblock(socket_t fd) -{ - u_long mode = 1; - return ioctlsocket(fd, FIONBIO, &mode) == 0; -} -static bool -again(void) -{ - int e = WSAGetLastError(); - return e == WSAEWOULDBLOCK || e == WSAEINTR; -} -static double -wall_time(void) -{ - return GetTickCount64() / 1000.0; -} -static double -cpu_time(void) -{ - FILETIME create, exit, kernel, user; - GetProcessTimes(GetCurrentProcess(), &create, &exit, &kernel, &user); - ULARGE_INTEGER k, u; - k.LowPart = kernel.dwLowDateTime; - k.HighPart = kernel.dwHighDateTime; - u.LowPart = user.dwLowDateTime; - u.HighPart = user.dwHighDateTime; - return (double)(k.QuadPart + u.QuadPart) / 10000000.0; -} -static size_t -rss(void) -{ - PROCESS_MEMORY_COUNTERS m; - return GetProcessMemoryInfo(GetCurrentProcess(), &m, sizeof(m)) - ? m.PeakWorkingSetSize - : 0; -} -#else -#include -#include -#include -#include -#include -#include -#include -#include -typedef int socket_t; -typedef struct pollfd pollfd_t; -#define BAD_SOCKET (-1) -#define CLOSE close -#define poll_sockets poll -static bool -nonblock(socket_t fd) -{ - return fcntl(fd, F_SETFL, O_NONBLOCK) == 0; -} -static bool -again(void) -{ - return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR; -} -static double -wall_time(void) -{ - struct timespec t; - clock_gettime(CLOCK_MONOTONIC, &t); - return t.tv_sec + t.tv_nsec * 1e-9; -} -static double -cpu_time(void) -{ - struct timespec t; - clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t); - return t.tv_sec + t.tv_nsec * 1e-9; -} -static size_t -rss(void) -{ - struct rusage r; - if (getrusage(RUSAGE_SELF, &r)) - return 0; -#ifdef __APPLE__ - return (size_t)r.ru_maxrss; -#else - return (size_t)r.ru_maxrss * 1024; -#endif -} -#endif +#include "socket_loop.h" static http_server server; #if defined(P3_WASI_HTTP) && !defined(_WIN32) static volatile sig_atomic_t interrupted; diff --git a/crates/c-api/examples/stackless-http/socket_loop.h b/crates/c-api/examples/stackless-http/socket_loop.h new file mode 100644 index 0000000..897cb85 --- /dev/null +++ b/crates/c-api/examples/stackless-http/socket_loop.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef STACKLESS_SOCKET_LOOP_H +#define STACKLESS_SOCKET_LOOP_H +#ifdef _WIN32 +#include +#include +#include +#include +typedef SOCKET socket_t; +typedef WSAPOLLFD pollfd_t; +#define BAD_SOCKET INVALID_SOCKET +#define poll_sockets WSAPoll +#define CLOSE closesocket +static bool +nonblock(socket_t fd) +{ + u_long mode = 1; + return ioctlsocket(fd, FIONBIO, &mode) == 0; +} +static bool +again(void) +{ + int e = WSAGetLastError(); + return e == WSAEWOULDBLOCK || e == WSAEINTR; +} +static double +wall_time(void) +{ + return GetTickCount64() / 1000.0; +} +static double +cpu_time(void) +{ + FILETIME create, exit, kernel, user; + GetProcessTimes(GetCurrentProcess(), &create, &exit, &kernel, &user); + ULARGE_INTEGER k, u; + k.LowPart = kernel.dwLowDateTime; + k.HighPart = kernel.dwHighDateTime; + u.LowPart = user.dwLowDateTime; + u.HighPart = user.dwHighDateTime; + return (double)(k.QuadPart + u.QuadPart) / 10000000.0; +} +static size_t +rss(void) +{ + PROCESS_MEMORY_COUNTERS m; + return GetProcessMemoryInfo(GetCurrentProcess(), &m, sizeof(m)) + ? m.PeakWorkingSetSize + : 0; +} +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef int socket_t; +typedef struct pollfd pollfd_t; +#define BAD_SOCKET (-1) +#define CLOSE close +#define poll_sockets poll +static bool +nonblock(socket_t fd) +{ + return fcntl(fd, F_SETFL, O_NONBLOCK) == 0; +} +static bool +again(void) +{ + return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR; +} +static double +wall_time(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} +static double +cpu_time(void) +{ + struct timespec t; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} +static size_t +rss(void) +{ + struct rusage r; + if (getrusage(RUSAGE_SELF, &r)) + return 0; +#ifdef __APPLE__ + return (size_t)r.ru_maxrss; +#else + return (size_t)r.ru_maxrss * 1024; +#endif +} +#endif +#endif diff --git a/crates/c-api/examples/stackless-http/test_wasi_h2.py b/crates/c-api/examples/stackless-http/test_wasi_h2.py new file mode 100644 index 0000000..7fff90c --- /dev/null +++ b/crates/c-api/examples/stackless-http/test_wasi_h2.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Exercise cleartext HTTP/2 against the shared WASI HTTP service.""" +import argparse +import json +import os +import select +import signal +import socket +import subprocess +import time + +import h2.config +import h2.connection +import h2.events +import h2.exceptions +from h2.settings import SettingCodes + +from build_wasi_http_guest import json_body, png + +ASSETS = { + '/data.json': ('GET', 'application/json', json_body()), + '/image.png': ('GET', 'image/png', png(4096)), + '/upload': ('POST', 'image/png', png(16384)), +} + + +class Client: + def __init__(self, port): + self.sock = socket.create_connection(('127.0.0.1', port), timeout=3) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.h2 = h2.connection.H2Connection(h2.config.H2Configuration(header_encoding='ascii')) + self.jobs = {} + self.settings = 0 + self.h2.initiate_connection() + try: + self.flush() + except OSError: + self.sock.close() + raise + + def flush(self): + data = self.h2.data_to_send() + if data: + self.sock.sendall(data) + + def open(self, path): + method, mime, expected = ASSETS[path] + stream = self.h2.get_next_available_stream_id() + body = expected if method == 'POST' else b'' + self.h2.send_headers(stream, [ + (':method', method), (':scheme', 'http'), (':authority', 'localhost'), + (':path', path), ('content-type', mime), ('content-length', str(len(body))), + ], end_stream=not body) + self.jobs[stream] = {'path': path, 'send': body, 'at': 0, 'received': bytearray(), + 'headers': None, 'trailers': {}, 'done': False, 'reset': None} + return stream + + def read(self): + data = self.sock.recv(65536) + if not data: + raise EOFError('HTTP/2 connection ended') + for ev in self.h2.receive_data(data): + if isinstance(ev, h2.events.SettingsAcknowledged): + self.settings += 1 + elif isinstance(ev, h2.events.ConnectionTerminated): + raise RuntimeError(f'GOAWAY {ev.error_code}') + elif isinstance(ev, h2.events.ResponseReceived): + self.jobs[ev.stream_id]['headers'] = dict(ev.headers) + elif isinstance(ev, h2.events.TrailersReceived): + self.jobs[ev.stream_id]['trailers'] = dict(ev.headers) + elif isinstance(ev, h2.events.DataReceived): + self.jobs[ev.stream_id]['received'].extend(ev.data) + self.h2.acknowledge_received_data(ev.flow_controlled_length, ev.stream_id) + elif isinstance(ev, h2.events.StreamEnded): + self.jobs[ev.stream_id]['done'] = True + elif isinstance(ev, h2.events.StreamReset): + self.jobs[ev.stream_id]['reset'] = int(ev.error_code) + + def upload(self): + for sid, j in self.jobs.items(): + if j['reset'] is not None or j['at'] == len(j['send']): + continue + n = min(self.h2.local_flow_control_window(sid), len(j['send']) - j['at']) + while n > 0: + take = min(n, 509) + end = j['at'] + take + self.h2.send_data(sid, j['send'][j['at']:end], end_stream=end == len(j['send'])) + j['at'] = end + n -= take + self.flush() + + def check(self, sid): + j = self.jobs.pop(sid) + method, mime, expected = ASSETS[j['path']] + assert j['headers'][':status'] == '200' and j['headers']['content-type'] == mime, j + assert j['received'] == expected, (j['path'], len(j['received'])) + if method == 'POST': + assert j['trailers']['x-upload-check'] == 'complete', j['trailers'] + return len(expected) + + +def drive(clients, complete): + deadline = time.monotonic() + 5 + while not complete(): + for c in clients: + c.upload() + left = deadline - time.monotonic() + if left <= 0: + raise TimeoutError([(j['path'], j['at'], len(j['received']), j['done'], j['reset']) + for c in clients for j in c.jobs.values()]) + ready, _, _ = select.select([c.sock for c in clients], [], [], left) + for c in clients: + if c.sock in ready: + c.read() + c.flush() + + +def run(exe, rounds): + p = subprocess.Popen([exe, '0', '20' if os.name == 'nt' else '90'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + clients = [] + try: + line = p.stdout.readline().split() + assert line[0] == 'LISTEN', line + port = int(line[1]) + caps = {k: int(v) for k, v in (x.split('=') for x in line[2:])} + + retries = 0 + + def connect(n): + nonlocal retries + for attempt in range(20): + new = [] + try: + for _ in range(n): + c = Client(port) + new.append(c) + clients.append(c) + drive(new, lambda: all(c.settings for c in new)) + return new + except (ConnectionResetError, ConnectionAbortedError, BrokenPipeError, EOFError): + close(new) + if attempt == 19: + raise + # TCP accept may race the previous connection's reaping. + retries += 1 + time.sleep(.02) + + def close(cs): + for c in cs: + c.sock.close() + clients.remove(c) + + def batch(cs, paths): + jobs = [(c, c.open(path)) for c, group in zip(cs, paths) for path in group] + drive(cs, lambda: all(c.jobs[i]['done'] or c.jobs[i]['reset'] is not None for c, i in jobs)) + assert all(c.jobs[i]['done'] for c, i in jobs), [c.jobs[i]['reset'] for c, i in jobs] + return sum(c.check(i) for c, i in jobs) + + rows = [] + workloads = [('mixed', ('/data.json', '/image.png', '/upload')), + ('GET', ('/data.json', '/image.png')), ('POST', ('/upload',))] + for connections in (1, 2, 4): + cs = connect(connections) + maximum = min(caps['per_connection'], caps['streams'] // connections) + for concurrency in sorted({1, maximum // 2, maximum}): + for name, paths in workloads: + start = time.perf_counter() + transferred = 0 + for wave in range(rounds): + groups = [[paths[(wave + i + j) % len(paths)] for j in range(concurrency)] + for i in range(connections)] + transferred += batch(cs, groups) + elapsed = time.perf_counter() - start + rows.append({'workload': name, 'connections': connections, + 'streams_per_connection': concurrency, 'responses': rounds * connections * concurrency, + 'response_bytes': transferred, 'seconds': elapsed}) + close(cs) + + held = connect(4) + for c in held: + c.h2.update_settings({SettingCodes.INITIAL_WINDOW_SIZE: 0}) + c.flush() + drive(held, lambda: all(c.settings >= 2 for c in held)) + first = [held[0].open('/image.png') for _ in range(caps['per_connection'])] + drive(held, lambda: all(held[0].jobs[i]['headers'] for i in first)) + try: + held[0].open('/data.json') + except h2.exceptions.TooManyStreamsError: + pass + else: + raise AssertionError('peer SETTINGS did not cap per-connection streams') + second = [held[1].open('/upload') for _ in range(caps['streams'] - len(first))] + drive(held, lambda: all(held[1].jobs[i]['headers'] for i in second)) + overflow = held[2].open('/data.json') + drive(held, lambda: held[2].jobs[overflow]['reset'] is not None) + assert held[2].jobs.pop(overflow)['reset'] == 7 # REFUSED_STREAM + connection_failure = None + try: + with socket.create_connection(('127.0.0.1', port), timeout=3) as extra: + extra.sendall(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n') + assert extra.recv(1) == b'' + connection_failure = 'EOF' + except ConnectionResetError: + connection_failure = 'reset' + except ConnectionAbortedError: + connection_failure = 'abort' + # One POST drains while siblings remain blocked by their own windows. + target = second.pop() + held[1].h2.increment_flow_control_window(20000, target) + drive(held, lambda: held[1].jobs[target]['done']) + held[1].check(target) + assert all(not held[0].jobs[i]['received'] for i in first) + assert all(not held[1].jobs[i]['received'] for i in second) + for c, ids in zip(held, (first, second)): + for i in ids: + c.h2.reset_stream(i) + del c.jobs[i] + c.flush() + close(held) + recovered = connect(1) + batch(recovered, [['/data.json', '/image.png', '/upload']]) + close(recovered) + + if os.name != 'nt': + p.send_signal(signal.SIGINT) + out, err = p.communicate(timeout=25) + assert p.returncode == 0 and not err, (out, err, p.returncode) + row = next(line for line in out.splitlines() if line.startswith('H2 ')) + server = {k: float(v) if '.' in v else int(v) + for k, v in (word.split('=') for word in row.split()[1:])} + assert all(server[k] == 0 for k in ('live', 'handles', 'resources', 'heap')) + assert server['refused'] >= 1 and server['connections_rejected'] >= 1 + assert server['peak_streams'] == caps['streams'] and server['queue_peak'] <= 2048 + return {'limits': caps, 'overload': {'per_connection': 'peer SETTINGS limit', + 'global': 'REFUSED_STREAM', 'tcp_after_establishment': connection_failure}, + 'reconnect_retries': retries, 'workloads': rows, 'server': server} + except Exception: + for c in clients: + c.sock.close() + p.kill() + print(p.communicate()) + raise + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('server') + parser.add_argument('--rounds', type=int, default=24) + args = parser.parse_args() + print(json.dumps(run(args.server, args.rounds), indent=2)) diff --git a/crates/c-api/examples/stackless-http/test_wasi_http_load.py b/crates/c-api/examples/stackless-http/test_wasi_http_load.py index 8a9aaff..24fa4c7 100644 --- a/crates/c-api/examples/stackless-http/test_wasi_http_load.py +++ b/crates/c-api/examples/stackless-http/test_wasi_http_load.py @@ -89,6 +89,8 @@ def probe(_): return 'established:eof' except ConnectionResetError: return phase + ':reset' + except ConnectionAbortedError: + return phase + ':abort' except BrokenPipeError: return phase + ':broken-pipe' except ConnectionRefusedError: @@ -108,14 +110,16 @@ def probe(_): linger = 'HH' if os.name == 'nt' else 'ii' sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack(linger, 1, 0)) batch(['/data.json', '/image.png', '/upload']) + request_line = b'POST /upload HTTP/1.1\r\nHost: localhost\r\n' + full_header = b'X-Too-Large: '.ljust(2048 - len(request_line), b'x') for headers, status in ( - (b'Content-Length: 1\r\nContent-Length: 1\r\n', 400), - (b'Content-Length: 1\r\nTransfer-Encoding: chunked\r\n', 400), - (b'Content-Length: 65537\r\n', 413), - (b'X-Too-Large: ' + b'x' * 2200 + b'\r\n', 431), + (b'Content-Length: 1\r\nContent-Length: 1\r\n\r\n', 400), + (b'Content-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n', 400), + (b'Content-Length: 65537\r\n\r\n', 413), + (full_header, 431), ): with connect() as sock: - sock.sendall(b'POST /upload HTTP/1.1\r\nHost: localhost\r\n' + headers + b'\r\n') + sock.sendall(request_line + headers) response = http.client.HTTPResponse(sock) response.begin() assert response.status == status and not response.read() diff --git a/crates/c-api/examples/stackless-http/wasi_http.h b/crates/c-api/examples/stackless-http/wasi_http.h index a7f39ea..2345992 100644 --- a/crates/c-api/examples/stackless-http/wasi_http.h +++ b/crates/c-api/examples/stackless-http/wasi_http.h @@ -14,7 +14,9 @@ #endif #define HTTP_FIELDS_MAX 12 #define HTTP_URI_MAX 512 +#ifndef HTTP_PENDING_DROPS #define HTTP_PENDING_DROPS 16 +#endif enum http_kind { HTTP_FIELDS = 1, HTTP_REQUEST, HTTP_RESPONSE, HTTP_OPTIONS }; typedef struct http_field { diff --git a/crates/c-api/examples/stackless-http/wasi_http_guest.c b/crates/c-api/examples/stackless-http/wasi_http_guest.c index 2f8a6e2..357083d 100644 --- a/crates/c-api/examples/stackless-http/wasi_http_guest.c +++ b/crates/c-api/examples/stackless-http/wasi_http_guest.c @@ -13,7 +13,9 @@ #define HANDLER "wasi:http/handler@0.3.2" #define BLOCKED UINT32_MAX #define CHUNK 768 +#ifndef CONNECTIONS #define CONNECTIONS 4 +#endif IMPORT(TYPES, "[constructor]fields") uint32_t fields_new(void); IMPORT(TYPES, "[method]fields.append") diff --git a/crates/c-api/examples/stackless-http/wasi_http_io.c b/crates/c-api/examples/stackless-http/wasi_http_io.c new file mode 100644 index 0000000..cb3b419 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_io.c @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "wasi_http_io.h" + +bool +http_close_end(wasi_http *http, uint32_t *end) +{ + if (*end && !p3_native_drop(&http->vm->async, *end)) + return false; + *end = 0; + return true; +} +bool +http_send_future(wasi_http *http, uint32_t *end, bool error) +{ + if (!*end) + return true; + uint8_t value[P3_HTTP_RESULT_SIZE] = { 0 }; + if (error) { + value[0] = 1; + value[8] = 7; /* connection-terminated */ + } + bool complete; + if (!p3_http_finish(&http->vm->async, *end, value, &complete)) + return false; + if (complete) + *end = 0; + return true; +} +bool +http_receive_trailers(wasi_http *http, uint32_t *end, uint32_t *trailers) +{ + if (!*end) + return true; + uint8_t value[P3_HTTP_RESULT_SIZE]; + bool complete; + if (!p3_http_finish(&http->vm->async, *end, value, &complete)) + return false; + if (!complete) + return true; + *end = 0; + if (value[0] || value[8] > 1) { + http->vm->async.error = "response trailers resolved to an error"; + return false; + } + if (value[8]) { + uint32_t h = 0; + for (unsigned i = 0; i < 4; ++i) + h |= (uint32_t)value[12 + i] << (8 * i); + http_resource *fields = wasi_http_get(http, h, HTTP_FIELDS, false); + if (!fields) + return false; + fields->native = true; + *trailers = h; + } + return true; +} +bool +http_start_task(wasi_http *http, p3_task *task, uint32_t request, + uint32_t *body, uint32_t *trailers) +{ + p3_context *cx = &http->vm->async; + http_resource *r = wasi_http_get(http, request, HTTP_REQUEST, false); + if (!r) + return false; + http_message *m = &r->as.message; + if (!wasi_http_pair(http, false, 1, &m->body, body) + || !wasi_http_pair(http, true, P3_HTTP_TRAILERS, &m->trailers, trailers) + || !p3_host_end(cx, *body) || !p3_host_end(cx, m->body) + || !p3_host_end(cx, *trailers) || !p3_host_end(cx, m->trailers)) + return false; + p3_task_init(task, http->vm, "handle-event"); + int32_t args[] = { (int32_t)request }; + return p3_task_start(task, "handle", args, 1); +} diff --git a/crates/c-api/examples/stackless-http/wasi_http_io.h b/crates/c-api/examples/stackless-http/wasi_http_io.h new file mode 100644 index 0000000..db33841 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_io.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#ifndef WASI_HTTP_IO_H +#define WASI_HTTP_IO_H +#include "wasi_http.h" + +bool +http_start_task(wasi_http *http, p3_task *task, uint32_t request, + uint32_t *body, uint32_t *trailers); +bool +http_close_end(wasi_http *http, uint32_t *end); +bool +http_send_future(wasi_http *http, uint32_t *end, bool error); +bool +http_receive_trailers(wasi_http *http, uint32_t *end, uint32_t *trailers); +#endif diff --git a/crates/c-api/examples/stackless-http/wasi_http_server.c b/crates/c-api/examples/stackless-http/wasi_http_server.c index e444b2c..ab1fbfe 100644 --- a/crates/c-api/examples/stackless-http/wasi_http_server.c +++ b/crates/c-api/examples/stackless-http/wasi_http_server.c @@ -3,65 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ #include "http.h" +#include "wasi_http_io.h" #include #include #define HTTP_MAX_BODY (64 * 1024U) -static bool -close_end(http_server *server, uint32_t *end) -{ - if (*end && !p3_native_drop(&server->vm.async, *end)) - return false; - *end = 0; - return true; -} -static bool -send_future(http_server *server, uint32_t *end, bool error) -{ - if (!*end) - return true; - uint8_t value[P3_HTTP_RESULT_SIZE] = { 0 }; - if (error) { - value[0] = 1; - value[8] = 7; /* connection-terminated */ - } - bool complete; - if (!p3_http_finish(&server->vm.async, *end, value, &complete)) - return false; - if (complete) - *end = 0; - return true; -} -static bool -receive_trailers(http_server *server, http_connection *c, uint32_t *end) -{ - if (!*end) - return true; - uint8_t value[P3_HTTP_RESULT_SIZE]; - bool complete; - if (!p3_http_finish(&server->vm.async, *end, value, &complete)) - return false; - if (!complete) - return true; - *end = 0; - if (value[0] || value[8] > 1) { - server->vm.async.error = "response trailers resolved to an error"; - return false; - } - if (value[8]) { - uint32_t h = 0; - for (unsigned i = 0; i < 4; ++i) - h |= (uint32_t)value[12 + i] << (8 * i); - http_resource *fields = - wasi_http_get(&server->http, h, HTTP_FIELDS, false); - if (!fields) - return false; - fields->native = true; - c->response_trailers = h; - } - return true; -} bool http_open(http_server *server, unsigned slot) { @@ -183,7 +130,6 @@ static bool start_request(http_server *server, http_connection *c, unsigned header) { wasi_http *http = &server->http; - p3_context *cx = &server->vm.async; uint32_t request = wasi_http_new(http, HTTP_REQUEST, false); uint32_t fields = wasi_http_new(http, HTTP_FIELDS, true); if (!request || !fields) @@ -203,15 +149,7 @@ start_request(http_server *server, http_connection *c, unsigned header) c->used -= header; memmove(c->request_bytes, c->request_bytes + header, c->used); c->parsed = true; - if (!wasi_http_pair(http, false, 1, &m->body, &c->input) - || !wasi_http_pair(http, true, P3_HTTP_TRAILERS, &m->trailers, - &c->trailers_tx) - || !p3_host_end(cx, c->input) || !p3_host_end(cx, m->body) - || !p3_host_end(cx, c->trailers_tx) || !p3_host_end(cx, m->trailers)) - return false; - p3_task_init(&c->task, &server->vm, "handle-event"); - int32_t args[] = { (int32_t)request }; - return p3_task_start(&c->task, "handle", args, 1); + return http_start_task(http, &c->task, request, &c->input, &c->trailers_tx); } uint32_t http_input_capacity(http_server *server, unsigned slot) @@ -254,7 +192,7 @@ http_end_input(http_server *server, unsigned slot) { http_connection *c = &server->connections[slot]; c->saw_eof = true; - return close_end(server, &c->input); + return http_close_end(&server->http, &c->input); } bool http_disconnect(http_server *server, unsigned slot) @@ -325,7 +263,7 @@ wire_body(http_server *server, http_connection *c, http_message *m) if (!m->body) return true; if (c->disconnected || !c->chunked) - return close_end(server, &m->body); + return http_close_end(&server->http, &m->body); if (c->sent != c->length) return true; uint32_t status; @@ -334,7 +272,7 @@ wire_body(http_server *server, http_connection *c, http_message *m) return false; if (status == P3_BLOCKED) return true; - if ((status & 15U) == 1 && !close_end(server, &m->body)) + if ((status & 15U) == 1 && !http_close_end(&server->http, &m->body)) return false; uint32_t count = status >> 4; if (!count) @@ -366,7 +304,7 @@ wire_finish(http_server *server, http_connection *c, http_message *m) c->terminal_chunk = true; return true; } - if (!send_future(server, &m->sent, c->disconnected)) + if (!http_send_future(&server->http, &m->sent, c->disconnected)) return false; c->wire_done = true; return true; @@ -393,10 +331,10 @@ http_progress(http_server *server, unsigned slot) memmove(c->request_bytes, c->request_bytes + take, c->used); } } - if (!c->body_left && !close_end(server, &c->input)) + if (!c->body_left && !http_close_end(&server->http, &c->input)) return false; if ((!c->input || c->disconnected) - && !send_future(server, &c->trailers_tx, c->disconnected)) + && !http_send_future(&server->http, &c->trailers_tx, c->disconnected)) return false; if (!c->response && c->task.returned && !c->wire_done) { c->response = c->task.http_result; @@ -414,7 +352,7 @@ http_progress(http_server *server, unsigned slot) return false; http_message *m = &r->as.message; if (!wire_body(server, c, m) - || !receive_trailers(server, c, &m->trailers)) + || !http_receive_trailers(&server->http, &m->trailers, &c->response_trailers)) return false; if (!m->body && !m->trailers && c->sent == c->length && !wire_finish(server, c, m)) @@ -429,8 +367,8 @@ http_progress(http_server *server, unsigned slot) ++server->callbacks; } if (c->task.exited && (c->wire_done || c->disconnected)) { - if (!close_end(server, &c->input) - || !send_future(server, &c->trailers_tx, c->disconnected)) + if (!http_close_end(&server->http, &c->input) + || !http_send_future(&server->http, &c->trailers_tx, c->disconnected)) return false; if (c->response_trailers && !wasi_http_drop(&server->http, c->response_trailers, HTTP_FIELDS, From 142eaf17c4246fb5052f4d7a7c0d3c0e95fe8390 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 2 Nov 2026 23:00:13 -0800 Subject: [PATCH 14/20] test: record HTTP/2 hosted and Cortex-M33 validation --- crates/c-api/examples/stackless-http/HTTP2.md | 48 ++ .../stackless-http/http2-receipts.json | 579 ++++++++++++++++++ .../stackless-http/http2-throughput.csv | 217 +++++++ 3 files changed, 844 insertions(+) create mode 100644 crates/c-api/examples/stackless-http/http2-receipts.json create mode 100644 crates/c-api/examples/stackless-http/http2-throughput.csv diff --git a/crates/c-api/examples/stackless-http/HTTP2.md b/crates/c-api/examples/stackless-http/HTTP2.md index 4a47a1b..5fd2efa 100644 --- a/crates/c-api/examples/stackless-http/HTTP2.md +++ b/crates/c-api/examples/stackless-http/HTTP2.md @@ -87,3 +87,51 @@ completion while holding one stream's output window closed. The emulated binary accounts for the 64 KiB network reserve in its 520 KiB SRAM region. This target uses scripted transport readiness. The Pico SDK Wi-Fi firmware in the existing sample continues to use HTTP/1.1. + +## Validation + +The HTTP/2 sweep ran against WAMR and TinyWasm on macOS arm64 and a +physical Windows x64 machine in release and AddressSanitizer builds. +Each run checked byte-for-byte 4,096-byte JSON and PNG downloads and +16,384-byte PNG upload echoes, including the response trailer. It tested +one, two and four connections with up to sixteen concurrent streams. +An upload progressed while sibling streams on the same connection had +zero response credit. + +Both runtimes reached eight streams per connection and sixteen overall. +The next stream at the aggregate limit received `REFUSED_STREAM`. The +fifth TCP connection established and then reached EOF or reset on macOS, +and Winsock reported an established-connection abort on Windows. The +server recovered after stream resets and closed connections. All runs +ended with zero live streams, handles, resources and protocol allocations. + +| Hosted release | Best GET responses/s | Best POST echoes/s | Best mixed responses/s | +|---|---:|---:|---:| +| macOS WAMR | 6,313 (2 × 8) | 1,529 (2 × 8) | 2,956 (2 × 8) | +| macOS TinyWasm | 6,404 (2 × 8) | 1,545 (2 × 8) | 2,897 (2 × 8) | +| Windows WAMR | 2,312 (4 × 4) | 489 (2 × 8) | 1,042 (2 × 8) | +| Windows TinyWasm | 2,366 (2 × 8) | 515 (2 × 8) | 976 (2 × 8) | + +The dimensions are connections × streams per connection. These rates +include the Python client and loopback TCP. The Mac runs used 24 rounds +per workload and the Windows runs used 10. The highest sampled mixed and +POST rates occurred at sixteen active streams on both runtimes and hosts. + +The WAMR AN505 executable reserved **339,104 bytes** of the +**532,480-byte** Pico 2 W budget, including a 64 KiB network allowance, +64 KiB libc heap, and 8 KiB native stack. The WAMR pool peaked at +111,712 of 131,072 bytes. nghttp2 allocations peaked at 40,463 bytes and +libc heap use at 45,912 bytes. The LTO build completed four streams over +two sessions with zero live handles. The HTTP/1.1 Pico Wi-Fi firmware +also linked within budget at 291,072 bytes. + +The HTTP/1.1 integration regression and seven supported official WASIp3 +HTTP cases passed on both Windows backends. The official cases ran against +the same pre-linked guests used on macOS. The eleven outbound HTTP client +cases remain outside this fixture. On Windows ASan covered WAMR and its C +adapter, and the TinyWasm C adapter. On macOS it covered both runtimes +and their C adapters. The nghttp2 static dependency used a release build +throughout. + +[Validation receipts](http2-receipts.json) and the +[throughput sweep](http2-throughput.csv) record the raw counters and timings. diff --git a/crates/c-api/examples/stackless-http/http2-receipts.json b/crates/c-api/examples/stackless-http/http2-receipts.json new file mode 100644 index 0000000..8bd203e --- /dev/null +++ b/crates/c-api/examples/stackless-http/http2-receipts.json @@ -0,0 +1,579 @@ +{ + "nghttp2_version": "1.70.0", + "nghttp2_commit": "85e300c79fb6dbcfa9c1013215c8710c1c2cd3d2", + "wasip3_suite_commit": "9d84631e427729802085c3d172d0a63cd360a1fa", + "python_h2_version": "4.3.0", + "platforms": { + "mac": "macOS arm64", + "windows": "Windows x64 10.0.26300.9711" + }, + "wasm_sha256": { + "host_16_tasks": "4653185580f6a6c2d8920409b6988add2725b381fd8788327fbb6c2dda0fb1b4", + "cortex_m33_4_tasks": "6ecc282d19a64121b846b5b302a779505b716168af1824279e7ed7db053d1fea" + }, + "qemu": { + "qemu": "QEMU emulator version 11.1.3", + "machine": "mps2-an505", + "elf_sha256": "81dbcc3cc21d0b4e38ee2eb8445d70539da266e4971ff538f84c60341dabdda2", + "memory": { + "sram_reserved_bytes": 339104, + "sram_limit_bytes": 532480, + "sram_sections": { + ".data": 3528, + ".bss": 196312, + ".net_reserve": 65536, + ".heap": 65536, + ".stack": 8192 + } + }, + "workload": { + "completed": 4, + "peak_streams": 4, + "peak_handles": 41, + "peak_resources": 9, + "input_peak": 2048, + "h2_heap_peak": 40463, + "libc_peak": 45912, + "pool_peak": 111712, + "server_bytes": 65080 + } + }, + "hosted": { + "mac-wamr-release": { + "sanitizer": "off", + "rounds_per_workload": 24, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "EOF" + }, + "reconnect_retries": 0, + "server": { + "completed": 4828, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 34, + "queue_peak": 2048, + "heap_peak": 93808, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 2818048, + "cpu_s": 0.92426, + "elapsed_s": 2.118095, + "waits": 121156, + "wakes": 95530, + "callbacks": 187099 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "http1": { + "overload": { + "/data.json": [ + "established:eof", + "established:reset", + "established:reset", + "established:reset" + ], + "/image.png": [ + "established:reset", + "established:reset", + "established:eof", + "established:eof" + ], + "/upload": [ + "established:eof", + "established:reset", + "established:reset", + "established:reset" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=9 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1271 transfers=912 bytes=536434 live=0 peak_handles=49 server_bytes=89440 rss_bytes=2408448 cpu_s=0.006862000 elapsed_s=0.605263 waits=547 wakes=546" + ] + }, + "component_model_assertions_passed": 19 + }, + "mac-wamr-asan": { + "sanitizer": "runtime and C adapter", + "rounds_per_workload": 24, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "EOF" + }, + "reconnect_retries": 0, + "server": { + "completed": 4828, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 36, + "queue_peak": 2048, + "heap_peak": 93808, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 29212672, + "cpu_s": 1.14575, + "elapsed_s": 2.140846, + "waits": 106000, + "wakes": 85696, + "callbacks": 184483 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "component_model_assertions_passed": 19 + }, + "mac-tiny-release": { + "sanitizer": "off", + "rounds_per_workload": 24, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "reset" + }, + "reconnect_retries": 0, + "server": { + "completed": 4828, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 34, + "queue_peak": 2048, + "heap_peak": 93808, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 7782400, + "cpu_s": 1.04751, + "elapsed_s": 2.116536, + "waits": 113473, + "wakes": 90977, + "callbacks": 186346 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "http1": { + "overload": { + "/data.json": [ + "established:reset", + "established:reset", + "established:reset", + "established:reset" + ], + "/image.png": [ + "established:reset", + "established:reset", + "established:eof", + "established:reset" + ], + "/upload": [ + "established:reset", + "established:reset", + "established:reset", + "established:eof" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=10 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1235 transfers=888 bytes=536434 live=0 peak_handles=49 server_bytes=89392 rss_bytes=7421952 cpu_s=0.007878000 elapsed_s=0.600589 waits=483 wakes=482" + ] + }, + "component_model_assertions_passed": 19 + }, + "mac-tiny-asan": { + "sanitizer": "runtime and C adapter", + "rounds_per_workload": 24, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "reset" + }, + "reconnect_retries": 0, + "server": { + "completed": 4828, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 35, + "queue_peak": 2048, + "heap_peak": 94055, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 141082624, + "cpu_s": 1.848798, + "elapsed_s": 2.475017, + "waits": 76612, + "wakes": 66520, + "callbacks": 178057 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "component_model_assertions_passed": 19 + }, + "windows-wamr-release": { + "sanitizer": "off", + "rounds_per_workload": 10, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "abort" + }, + "reconnect_retries": 0, + "server": { + "completed": 2014, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 34, + "queue_peak": 2048, + "heap_peak": 94317, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 5169152, + "cpu_s": 1.34375, + "elapsed_s": 20.015, + "waits": 55531, + "wakes": 40894, + "callbacks": 77388 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "http1": { + "overload": { + "/data.json": [ + "established:abort", + "established:abort", + "established:abort", + "established:abort" + ], + "/image.png": [ + "established:abort", + "established:abort", + "established:abort", + "established:reset" + ], + "/upload": [ + "established:reset", + "established:abort", + "established:abort", + "established:eof" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=11 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1133 transfers=820 bytes=536434 live=0 peak_handles=49 server_bytes=89440 rss_bytes=4943872 cpu_s=0.046875000 elapsed_s=6.016000 waits=444 wakes=443" + ] + } + }, + "windows-wamr-asan": { + "sanitizer": "runtime and C adapter", + "rounds_per_workload": 10, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "abort" + }, + "reconnect_retries": 0, + "server": { + "completed": 2014, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 36, + "queue_peak": 2048, + "heap_peak": 94317, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 29130752, + "cpu_s": 2.5, + "elapsed_s": 20.015, + "waits": 37461, + "wakes": 30722, + "callbacks": 74673 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "http1": { + "overload": { + "/data.json": [ + "established:abort", + "established:abort", + "established:abort", + "established:abort" + ], + "/image.png": [ + "established:abort", + "established:abort", + "established:abort", + "established:abort" + ], + "/upload": [ + "established:abort", + "established:abort", + "established:abort", + "established:abort" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=9 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1121 transfers=812 bytes=536434 live=0 peak_handles=49 server_bytes=89440 rss_bytes=14331904 cpu_s=0.156250000 elapsed_s=6.000000 waits=371 wakes=370" + ] + } + }, + "windows-tiny-release": { + "sanitizer": "off", + "rounds_per_workload": 10, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "abort" + }, + "reconnect_retries": 0, + "server": { + "completed": 2014, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 34, + "queue_peak": 2048, + "heap_peak": 94317, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 6336512, + "cpu_s": 1.34375, + "elapsed_s": 20.016, + "waits": 53595, + "wakes": 39600, + "callbacks": 77601 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "http1": { + "overload": { + "/data.json": [ + "established:abort", + "established:abort", + "established:reset", + "established:abort" + ], + "/image.png": [ + "established:abort", + "established:abort", + "established:abort", + "established:abort" + ], + "/upload": [ + "established:reset", + "established:reset", + "established:abort", + "established:abort" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=11 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1124 transfers=814 bytes=536434 live=0 peak_handles=49 server_bytes=89392 rss_bytes=5599232 cpu_s=0.140625000 elapsed_s=6.000000 waits=413 wakes=412" + ] + } + }, + "windows-tiny-asan": { + "sanitizer": "C adapter", + "rounds_per_workload": 10, + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "abort" + }, + "reconnect_retries": 1, + "server": { + "completed": 2014, + "reset": 15, + "refused": 1, + "connections_rejected": 2, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 39, + "queue_peak": 2048, + "heap_peak": 94613, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 75173888, + "cpu_s": 5.890625, + "elapsed_s": 20.015, + "waits": 30142, + "wakes": 26329, + "callbacks": 73839 + }, + "official_http_cases_passed": [ + "http-fields", + "http-request-options", + "http-request", + "http-response", + "http-service-echo", + "http-service-uri", + "http-service" + ], + "http1": { + "overload": { + "/data.json": [ + "established:abort", + "established:abort", + "established:reset", + "established:reset" + ], + "/image.png": [ + "established:abort", + "established:abort", + "established:reset", + "established:abort" + ], + "/upload": [ + "established:abort", + "established:abort", + "established:abort", + "established:eof" + ] + }, + "server": [ + "HTTP resources_live=0 resources_peak=12 rejected=12", + "RESULT completed=36 cancelled=0 disconnected=13 callbacks=1133 transfers=820 bytes=536434 live=0 peak_handles=49 server_bytes=89392 rss_bytes=17383424 cpu_s=0.125000000 elapsed_s=6.015000 waits=370 wakes=369" + ] + } + } + } +} diff --git a/crates/c-api/examples/stackless-http/http2-throughput.csv b/crates/c-api/examples/stackless-http/http2-throughput.csv new file mode 100644 index 0000000..010c90e --- /dev/null +++ b/crates/c-api/examples/stackless-http/http2-throughput.csv @@ -0,0 +1,217 @@ +platform,runtime,build,workload,connections,streams_per_connection,responses,response_bytes,seconds +mac,wamr,release,mixed,1,1,24,196608,0.019551125005818903 +mac,wamr,release,GET,1,1,24,98304,0.008074874989688396 +mac,wamr,release,POST,1,1,24,393216,0.03856691700639203 +mac,wamr,release,mixed,1,4,96,786432,0.05064383300486952 +mac,wamr,release,GET,1,4,96,393216,0.018207665998488665 +mac,wamr,release,POST,1,4,96,1572864,0.07447424996644258 +mac,wamr,release,mixed,1,8,192,1572864,0.07371341699035838 +mac,wamr,release,GET,1,8,192,786432,0.033171124989166856 +mac,wamr,release,POST,1,8,192,3145728,0.13484862499171868 +mac,wamr,release,mixed,2,1,48,393216,0.03265454201027751 +mac,wamr,release,GET,2,1,48,196608,0.011628917010966688 +mac,wamr,release,POST,2,1,48,786432,0.045968999969772995 +mac,wamr,release,mixed,2,4,192,1572864,0.07542712503345683 +mac,wamr,release,GET,2,4,192,786432,0.03237554099177942 +mac,wamr,release,POST,2,4,192,3145728,0.13539179199142382 +mac,wamr,release,mixed,2,8,384,3145728,0.12991620798129588 +mac,wamr,release,GET,2,8,384,1572864,0.060826708970125765 +mac,wamr,release,POST,2,8,384,6291456,0.251156083017122 +mac,wamr,release,mixed,4,1,96,786432,0.054425875016022474 +mac,wamr,release,GET,4,1,96,393216,0.020099042041692883 +mac,wamr,release,POST,4,1,96,1572864,0.08344625000609085 +mac,wamr,release,mixed,4,2,192,1572864,0.07822629198199138 +mac,wamr,release,GET,4,2,192,786432,0.034243291011080146 +mac,wamr,release,POST,4,2,192,3145728,0.13989487499929965 +mac,wamr,release,mixed,4,4,384,3145728,0.1416643330012448 +mac,wamr,release,GET,4,4,384,1572864,0.06332341697998345 +mac,wamr,release,POST,4,4,384,6291456,0.2650313749909401 +mac,wamr,asan,mixed,1,1,24,196608,0.019300082989502698 +mac,wamr,asan,GET,1,1,24,98304,0.007705875032115728 +mac,wamr,asan,POST,1,1,24,393216,0.03848537500016391 +mac,wamr,asan,mixed,1,4,96,786432,0.051587500027380884 +mac,wamr,asan,GET,1,4,96,393216,0.018959166947752237 +mac,wamr,asan,POST,1,4,96,1572864,0.07295825000619516 +mac,wamr,asan,mixed,1,8,192,1572864,0.07474295899737626 +mac,wamr,asan,GET,1,8,192,786432,0.03449887497117743 +mac,wamr,asan,POST,1,8,192,3145728,0.13392362504964694 +mac,wamr,asan,mixed,2,1,48,393216,0.03531487501459196 +mac,wamr,asan,GET,2,1,48,196608,0.011605915962718427 +mac,wamr,asan,POST,2,1,48,786432,0.04626970796380192 +mac,wamr,asan,mixed,2,4,192,1572864,0.07894020900130272 +mac,wamr,asan,GET,2,4,192,786432,0.033386291994247586 +mac,wamr,asan,POST,2,4,192,3145728,0.13438870897516608 +mac,wamr,asan,mixed,2,8,384,3145728,0.1331238750135526 +mac,wamr,asan,GET,2,8,384,1572864,0.06312925001839176 +mac,wamr,asan,POST,2,8,384,6291456,0.2527609580429271 +mac,wamr,asan,mixed,4,1,96,786432,0.05453129200031981 +mac,wamr,asan,GET,4,1,96,393216,0.019692833011504263 +mac,wamr,asan,POST,4,1,96,1572864,0.08344320801552385 +mac,wamr,asan,mixed,4,2,192,1572864,0.07882787502603605 +mac,wamr,asan,GET,4,2,192,786432,0.035500541038345546 +mac,wamr,asan,POST,4,2,192,3145728,0.14173879101872444 +mac,wamr,asan,mixed,4,4,384,3145728,0.14305279101245105 +mac,wamr,asan,GET,4,4,384,1572864,0.06286212499253452 +mac,wamr,asan,POST,4,4,384,6291456,0.26743829098995775 +mac,tiny,release,mixed,1,1,24,196608,0.018923875002656132 +mac,tiny,release,GET,1,1,24,98304,0.007473125006072223 +mac,tiny,release,POST,1,1,24,393216,0.037527334003243595 +mac,tiny,release,mixed,1,4,96,786432,0.050576250010635704 +mac,tiny,release,GET,1,4,96,393216,0.018617875000927597 +mac,tiny,release,POST,1,4,96,1572864,0.07461679197149351 +mac,tiny,release,mixed,1,8,192,1572864,0.07324574998347089 +mac,tiny,release,GET,1,8,192,786432,0.032888750021811575 +mac,tiny,release,POST,1,8,192,3145728,0.13341187499463558 +mac,tiny,release,mixed,2,1,48,393216,0.03382933302782476 +mac,tiny,release,GET,2,1,48,196608,0.011978208960499614 +mac,tiny,release,POST,2,1,48,786432,0.04799791699042544 +mac,tiny,release,mixed,2,4,192,1572864,0.0803057499579154 +mac,tiny,release,GET,2,4,192,786432,0.03397858300013468 +mac,tiny,release,POST,2,4,192,3145728,0.1348675419576466 +mac,tiny,release,mixed,2,8,384,3145728,0.13253120804438367 +mac,tiny,release,GET,2,8,384,1572864,0.059959374950267375 +mac,tiny,release,POST,2,8,384,6291456,0.24856400000862777 +mac,tiny,release,mixed,4,1,96,786432,0.056737791979685426 +mac,tiny,release,GET,4,1,96,393216,0.0192082499852404 +mac,tiny,release,POST,4,1,96,1572864,0.0839930420042947 +mac,tiny,release,mixed,4,2,192,1572864,0.07897525001317263 +mac,tiny,release,GET,4,2,192,786432,0.03505558299366385 +mac,tiny,release,POST,4,2,192,3145728,0.13625845802016556 +mac,tiny,release,mixed,4,4,384,3145728,0.13885279197711498 +mac,tiny,release,GET,4,4,384,1572864,0.06314779201056808 +mac,tiny,release,POST,4,4,384,6291456,0.26131899998290464 +mac,tiny,asan,mixed,1,1,24,196608,0.023341416963376105 +mac,tiny,asan,GET,1,1,24,98304,0.007403749972581863 +mac,tiny,asan,POST,1,1,24,393216,0.0487917079590261 +mac,tiny,asan,mixed,1,4,96,786432,0.06377466599224135 +mac,tiny,asan,GET,1,4,96,393216,0.02178537502186373 +mac,tiny,asan,POST,1,4,96,1572864,0.09204925002995878 +mac,tiny,asan,mixed,1,8,192,1572864,0.09126104199094698 +mac,tiny,asan,GET,1,8,192,786432,0.03965337498812005 +mac,tiny,asan,POST,1,8,192,3145728,0.17122812499292195 +mac,tiny,asan,mixed,2,1,48,393216,0.04167791601503268 +mac,tiny,asan,GET,2,1,48,196608,0.013626958010718226 +mac,tiny,asan,POST,2,1,48,786432,0.05933324998477474 +mac,tiny,asan,mixed,2,4,192,1572864,0.0900789579609409 +mac,tiny,asan,GET,2,4,192,786432,0.039572999987285584 +mac,tiny,asan,POST,2,4,192,3145728,0.15327025001170114 +mac,tiny,asan,mixed,2,8,384,3145728,0.15381270897341892 +mac,tiny,asan,GET,2,8,384,1572864,0.07197504199575633 +mac,tiny,asan,POST,2,8,384,6291456,0.30170870799338445 +mac,tiny,asan,mixed,4,1,96,786432,0.06843679095618427 +mac,tiny,asan,GET,4,1,96,393216,0.02292737504467368 +mac,tiny,asan,POST,4,1,96,1572864,0.08921866700984538 +mac,tiny,asan,mixed,4,2,192,1572864,0.09242091595660895 +mac,tiny,asan,GET,4,2,192,786432,0.03942562500014901 +mac,tiny,asan,POST,4,2,192,3145728,0.1496420840267092 +mac,tiny,asan,mixed,4,4,384,3145728,0.15825879201292992 +mac,tiny,asan,GET,4,4,384,1572864,0.07296891702571884 +mac,tiny,asan,POST,4,4,384,6291456,0.28252008400158957 +windows,wamr,release,mixed,1,1,10,77824,0.02261949999956414 +windows,wamr,release,GET,1,1,10,40960,0.007844499996281229 +windows,wamr,release,POST,1,1,10,163840,0.04357249999884516 +windows,wamr,release,mixed,1,4,40,323584,0.06479639999452047 +windows,wamr,release,GET,1,4,40,163840,0.022741900000255555 +windows,wamr,release,POST,1,4,40,655360,0.1041112000020803 +windows,wamr,release,mixed,1,8,80,647168,0.10732619999907911 +windows,wamr,release,GET,1,8,80,327680,0.04062749999866355 +windows,wamr,release,POST,1,8,80,1310720,0.206725700001698 +windows,wamr,release,mixed,2,1,20,155648,0.034911299997475 +windows,wamr,release,GET,2,1,20,81920,0.01343530000303872 +windows,wamr,release,POST,2,1,20,327680,0.06102259999897797 +windows,wamr,release,mixed,2,4,80,647168,0.09767420000571292 +windows,wamr,release,GET,2,4,80,327680,0.03602919999684673 +windows,wamr,release,POST,2,4,80,1310720,0.20065169999725185 +windows,wamr,release,mixed,2,8,160,1306624,0.1536192999992636 +windows,wamr,release,GET,2,8,160,655360,0.07040550000238 +windows,wamr,release,POST,2,8,160,2621440,0.32707050000317395 +windows,wamr,release,mixed,4,1,40,323584,0.06138020000071265 +windows,wamr,release,GET,4,1,40,163840,0.024029200001677964 +windows,wamr,release,POST,4,1,40,655360,0.1149442000023555 +windows,wamr,release,mixed,4,2,80,647168,0.1012808999948902 +windows,wamr,release,GET,4,2,80,327680,0.03709969999908935 +windows,wamr,release,POST,4,2,80,1310720,0.18453779999981634 +windows,wamr,release,mixed,4,4,160,1306624,0.18027189999702387 +windows,wamr,release,GET,4,4,160,655360,0.0692133999982616 +windows,wamr,release,POST,4,4,160,2621440,0.3611432000034256 +windows,wamr,asan,mixed,1,1,10,77824,0.018734699995548 +windows,wamr,asan,GET,1,1,10,40960,0.008222399999795016 +windows,wamr,asan,POST,1,1,10,163840,0.03926280000450788 +windows,wamr,asan,mixed,1,4,40,323584,0.05838650000077905 +windows,wamr,asan,GET,1,4,40,163840,0.022991899997578003 +windows,wamr,asan,POST,1,4,40,655360,0.12359699999797158 +windows,wamr,asan,mixed,1,8,80,647168,0.13146389999747043 +windows,wamr,asan,GET,1,8,80,327680,0.05012330000317888 +windows,wamr,asan,POST,1,8,80,1310720,0.20653260000108276 +windows,wamr,asan,mixed,2,1,20,155648,0.03296029999910388 +windows,wamr,asan,GET,2,1,20,81920,0.013015199998335447 +windows,wamr,asan,POST,2,1,20,327680,0.057918999998946674 +windows,wamr,asan,mixed,2,4,80,647168,0.10155469999881461 +windows,wamr,asan,GET,2,4,80,327680,0.0437510999981896 +windows,wamr,asan,POST,2,4,80,1310720,0.2269741999989492 +windows,wamr,asan,mixed,2,8,160,1306624,0.2098444000002928 +windows,wamr,asan,GET,2,8,160,655360,0.08529960000305437 +windows,wamr,asan,POST,2,8,160,2621440,0.3599079999985406 +windows,wamr,asan,mixed,4,1,40,323584,0.0634046000050148 +windows,wamr,asan,GET,4,1,40,163840,0.026358499999332707 +windows,wamr,asan,POST,4,1,40,655360,0.1144060999940848 +windows,wamr,asan,mixed,4,2,80,647168,0.11540899999818066 +windows,wamr,asan,GET,4,2,80,327680,0.04245480000099633 +windows,wamr,asan,POST,4,2,80,1310720,0.1833598000011989 +windows,wamr,asan,mixed,4,4,160,1306624,0.19067609999910928 +windows,wamr,asan,GET,4,4,160,655360,0.08681489999435144 +windows,wamr,asan,POST,4,4,160,2621440,0.40923169999587117 +windows,tiny,release,mixed,1,1,10,77824,0.023184300000139046 +windows,tiny,release,GET,1,1,10,40960,0.009077499998966232 +windows,tiny,release,POST,1,1,10,163840,0.051819200001773424 +windows,tiny,release,mixed,1,4,40,323584,0.07034429999475833 +windows,tiny,release,GET,1,4,40,163840,0.02123339999525342 +windows,tiny,release,POST,1,4,40,655360,0.09809230000246316 +windows,tiny,release,mixed,1,8,80,647168,0.09424489999946672 +windows,tiny,release,GET,1,8,80,327680,0.039556900002935436 +windows,tiny,release,POST,1,8,80,1310720,0.19647839999379357 +windows,tiny,release,mixed,2,1,20,155648,0.03322620000108145 +windows,tiny,release,GET,2,1,20,81920,0.01367970000137575 +windows,tiny,release,POST,2,1,20,327680,0.06118379999679746 +windows,tiny,release,mixed,2,4,80,647168,0.09260349999385653 +windows,tiny,release,GET,2,4,80,327680,0.040382599996519275 +windows,tiny,release,POST,2,4,80,1310720,0.1697822999994969 +windows,tiny,release,mixed,2,8,160,1306624,0.16386990000319201 +windows,tiny,release,GET,2,8,160,655360,0.06761399999959394 +windows,tiny,release,POST,2,8,160,2621440,0.31071589999919524 +windows,tiny,release,mixed,4,1,40,323584,0.06157069999608211 +windows,tiny,release,GET,4,1,40,163840,0.021194299995840993 +windows,tiny,release,POST,4,1,40,655360,0.11093049999908544 +windows,tiny,release,mixed,4,2,80,647168,0.10150560000329278 +windows,tiny,release,GET,4,2,80,327680,0.03965489999973215 +windows,tiny,release,POST,4,2,80,1310720,0.17584119999810355 +windows,tiny,release,mixed,4,4,160,1306624,0.17610119999881135 +windows,tiny,release,GET,4,4,160,655360,0.07172429999627639 +windows,tiny,release,POST,4,4,160,2621440,0.43605050000041956 +windows,tiny,asan,mixed,1,1,10,77824,0.02970630000345409 +windows,tiny,asan,GET,1,1,10,40960,0.013463700001011603 +windows,tiny,asan,POST,1,1,10,163840,0.06935349999548635 +windows,tiny,asan,mixed,1,4,40,323584,0.10531539999647066 +windows,tiny,asan,GET,1,4,40,163840,0.04439109999657376 +windows,tiny,asan,POST,1,4,40,655360,0.22102309999900172 +windows,tiny,asan,mixed,1,8,80,647168,0.20867580000049202 +windows,tiny,asan,GET,1,8,80,327680,0.0932159000003594 +windows,tiny,asan,POST,1,8,80,1310720,0.4568245999980718 +windows,tiny,asan,mixed,2,1,20,155648,0.05400499999814201 +windows,tiny,asan,GET,2,1,20,81920,0.022925700002815574 +windows,tiny,asan,POST,2,1,20,327680,0.1124924000032479 +windows,tiny,asan,mixed,2,4,80,647168,0.18621160000475356 +windows,tiny,asan,GET,2,4,80,327680,0.08018820000143023 +windows,tiny,asan,POST,2,4,80,1310720,0.3910256999952253 +windows,tiny,asan,mixed,2,8,160,1306624,0.3701952999981586 +windows,tiny,asan,GET,2,8,160,655360,0.1534479999972973 +windows,tiny,asan,POST,2,8,160,2621440,0.9355950000026496 +windows,tiny,asan,mixed,4,1,40,323584,0.12172160000045551 +windows,tiny,asan,GET,4,1,40,163840,0.04855120000138413 +windows,tiny,asan,POST,4,1,40,655360,0.23997460000100546 +windows,tiny,asan,mixed,4,2,80,647168,0.2174083000063547 +windows,tiny,asan,GET,4,2,80,327680,0.08575379999820143 +windows,tiny,asan,POST,4,2,80,1310720,0.43616849999671103 +windows,tiny,asan,mixed,4,4,160,1306624,0.4559341999993194 +windows,tiny,asan,GET,4,4,160,655360,0.16755120000016177 +windows,tiny,asan,POST,4,4,160,2621440,0.9120523999954457 From 2ffa27fe2f17528f43eaf97eaca42028c9fe26b7 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Tue, 3 Nov 2026 21:56:23 -0800 Subject: [PATCH 15/20] test: exercise GC and SIMD continuations with the shared C fixture --- .github/workflows/shared_continuations.yml | 8 +- CHANGELOG.md | 3 +- .../stackless-http/wasi_http_tests.py | 12 + .../tests/continuations/FEATURE_COMPAT.md | 107 +++ crates/c-api/tests/continuations/README.md | 26 +- .../c-api/tests/continuations/VALIDATION.md | 4 +- .../feature_native_measurements.csv | 15 + .../feature_off_measurements.csv | 15 + .../continuations/feature_off_sections.csv | 27 + .../tests/continuations/feature_receipts.json | 624 ++++++++++++++++++ crates/c-api/tests/continuations/features.c | 171 +++++ crates/c-api/tests/continuations/gc.wasm | Bin 0 -> 351 bytes crates/c-api/tests/continuations/gc.wat | 39 ++ crates/c-api/tests/continuations/run.py | 26 +- crates/c-api/tests/continuations/simd.wasm | Bin 0 -> 266 bytes crates/c-api/tests/continuations/simd.wat | 26 + 16 files changed, 1092 insertions(+), 11 deletions(-) create mode 100644 crates/c-api/tests/continuations/FEATURE_COMPAT.md create mode 100644 crates/c-api/tests/continuations/feature_native_measurements.csv create mode 100644 crates/c-api/tests/continuations/feature_off_measurements.csv create mode 100644 crates/c-api/tests/continuations/feature_off_sections.csv create mode 100644 crates/c-api/tests/continuations/feature_receipts.json create mode 100644 crates/c-api/tests/continuations/features.c create mode 100644 crates/c-api/tests/continuations/gc.wasm create mode 100644 crates/c-api/tests/continuations/gc.wat create mode 100644 crates/c-api/tests/continuations/simd.wasm create mode 100644 crates/c-api/tests/continuations/simd.wat diff --git a/.github/workflows/shared_continuations.yml b/.github/workflows/shared_continuations.yml index 456e8cb..b68b981 100644 --- a/.github/workflows/shared_continuations.yml +++ b/.github/workflows/shared_continuations.yml @@ -35,15 +35,15 @@ jobs: with: toolchain: ${{ matrix.rust }} - if: runner.os != 'Windows' - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on ${{ matrix.extra }} + run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on --gc --simd ${{ matrix.extra }} - name: Windows x64 release and C-client ASan (Rust uninstrumented) if: runner.os == 'Windows' shell: cmd run: | for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do call "%%i\Common7\Tools\VsDevCmd.bat" -arch=amd64 - python crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on ${{ matrix.extra }} + python crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-fixture --feature on --gc --simd ${{ matrix.extra }} if errorlevel 1 exit /b 1 - python crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-client-asan --client-asan ${{ matrix.extra }} + python crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-client-asan --client-asan --gc --simd ${{ matrix.extra }} - run: cargo test --locked -p tinywasm ${{ matrix.core_args }} --test host_execution - run: cargo test --locked -p tinywasm-c-api ${{ matrix.capi_args }} --lib rust-and-c-asan: @@ -58,7 +58,7 @@ jobs: with: toolchain: nightly components: rust-src - - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-full-asan --sanitize + - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-full-asan --gc --simd --sanitize miri: runs-on: ubuntu-24.04 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 10243c7..f88c141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 with checked results, rooted arguments, and access to the calling module's context, behind the opt-in `external-host-calls` feature. - Experimental numeric C continuations in `wasm_ext.h`, with a shared WAMR fixture and - disabled-feature linked-artifact measurements. + disabled-feature linked-artifact measurements. The shared fixtures also exercise guest GC + and SIMD across suspension, cancellation and indirect host calls. ## [0.12.0-beta.0] - 2026-11-01 diff --git a/crates/c-api/examples/stackless-http/wasi_http_tests.py b/crates/c-api/examples/stackless-http/wasi_http_tests.py index 2e086b5..734f8fd 100644 --- a/crates/c-api/examples/stackless-http/wasi_http_tests.py +++ b/crates/c-api/examples/stackless-http/wasi_http_tests.py @@ -77,7 +77,12 @@ def main(): parser.add_argument('suite', type=Path) parser.add_argument('build', type=Path) parser.add_argument('--wasm-tools', default='wasm-tools') + parser.add_argument('--aot-compiler', type=Path, + help='run the linked guests as WAMR AOT with software bounds checks') + parser.add_argument('--aot-gc', action='store_true', help='match a GC-enabled WAMR runtime') args = parser.parse_args() + if args.aot_gc and not args.aot_compiler: + parser.error('--aot-gc requires --aot-compiler') commit = subprocess.check_output(['git', '-C', args.suite, 'rev-parse', 'HEAD'], text=True).strip() if commit != SUITE: parser.error('Expected artifact revision ' + SUITE) @@ -90,6 +95,13 @@ def main(): linked = Path(tmp) / wasm.name try: link(wasm, linked, args.wasm_tools) + if args.aot_compiler: + compiled = linked.with_suffix('.aot') + subprocess.run([args.aot_compiler.resolve(), '--bounds-checks=1', + '--stack-bounds-checks=1', + *(['--enable-gc'] if args.aot_gc else []), + '-o', compiled, linked], check=True, capture_output=True, text=True) + linked = compiled if config.get('world') == 'wasi:http/service': service(args.build / ('wasi_http_server' + exe), linked, config, cancel_probe=wasm.stem == 'http-service-echo') diff --git a/crates/c-api/tests/continuations/FEATURE_COMPAT.md b/crates/c-api/tests/continuations/FEATURE_COMPAT.md new file mode 100644 index 0000000..f8fdec8 --- /dev/null +++ b/crates/c-api/tests/continuations/FEATURE_COMPAT.md @@ -0,0 +1,107 @@ +# Optional continuation feature compatibility + +Validated on macOS arm64 on 2026-11-03. This increment supports guest GC and SIMD +across interpreter suspension and allows ordinary AOT modules in the same WAMR +build. The shared numeric C header is unchanged. AOT functions still cannot be +suspended through `wasm_ext_call_new`; the stackless component adapter uses +ordinary calls and works with AOT. + +WAMR resumes the GC local-reference boundary along with the parked frame. The +regression first reads a live reference after resumption, allocates 2 MiB of +garbage, then checks the reference again. Without restoring that boundary it +fails the result assertion. Parent and leaf frames, repeated suspension, +indirect host calls, cancellation followed by collection, and traps are covered. +The same source fixture passes against TinyWasm's existing rooted execution API. +SIMD values stay inside the guest; reference/vector-valued C signatures remain +outside this prototype. + +WAMR now uses the instance's existing stack budget for each continuation. +`wasm_instance_new_with_args` can choose it without a new extension ABI. Tests +check that a 4 KiB budget traps on deep recursion and a 64 KiB budget completes. +The ordinary C API constructor supplies 32 KiB, replacing the prototype's +additional fixed 64 KiB per invocation. + +The AOT HTTP tests exposed an independent runtime bug: `call_indirect` treated +C API imports as native-ABI functions and could pass an invalid callback +environment. It now uses the existing C API import dispatcher. A separate base +C API regression covers callbacks with and without environments and host traps, +including AOT builds without the continuation extension or GC. + +| Check | Result | +|---|---| +| WAMR GC/SIMD interpreter continuations | Release and runtime/client ASan pass | +| WAMR ordinary AOT GC/SIMD guests, suspension rejected | Release and runtime/client ASan pass | +| TinyWasm C GC/SIMD continuations | Release and Rust/std/C ASan pass | +| WAMR base C API tests in AOT mode | 56 passed | +| WAMR continuation CTest suite with GC | 3 passed | +| TinyWasm Rust host-execution tests | 7 passed, including explicit GC while pending | +| Official WASIp3 HTTP subset through WAMR AOT | 7 passed in release and ASan, 11 outbound cases unsupported | +| Stackless HTTP/2 through WAMR AOT | Release and ASan pass, 16 streams across 4 connections | +| Windows x64 | Not rerun; SSH timed out. CI now includes GC and applicable SIMD checks | +| Apple Watch | Not deployed or qualified; hosted results do not establish watchOS execution permissions or memory limits | + +The HTTP/2 run verified 4 KiB JSON/PNG GETs, 16 KiB PNG POSTs, closed flow-control +windows, resets, stream refusal, TCP admission limits and recovery. It completed +4,828 responses and shut down with zero live handles, resources and protocol +allocations. The release process reported 3,047,424 bytes peak RSS. These are +bounded example workloads, not a budget for an arbitrary component application. +ASan instruments the runtime and C adapter, not generated AOT code; nghttp2 is +the existing release archive. Guest memory uses software bounds checks. + +## Embedded and disabled-feature footprint + +The WAMR Cortex-M33 HTTP/2 ELF is byte-identical to the previous RFC build: +`b8889f994b926dfb91799e00d74a5251b215e01b8a4759e33eb41f719762f495`. +It still passes QEMU with four streams over two connections and reserves +339,104 of 532,480 SRAM bytes, including a 64 KiB networking allowance. This +configuration leaves the continuation extension, GC, SIMD and AOT disabled. +It uses the external stackless adapter and scripted I/O; it is not Pico hardware. + +The hosted feature-off WAMR client is also byte-identical before/after this +increment: 227,096 bytes, including identical code, data, BSS and TLS sections. +Seven alternating measurements gave overlapping peak RSS ranges of +6,193,152–6,225,920 and 6,176,768–6,225,920 bytes. Native calls measured +14.39/14.26 ns and compute iterations 2.94/2.94 ns before/after. The identical +artifact establishes unchanged instructions and layouts; noisy RSS samples do +not prove exact process-memory equality. TinyWasm runtime sources and default +features are unchanged in this increment. + +For the larger WAMR build with GC/SIMD/AOT and continuations enabled, the same +ordinary-call client measured 14.61 ns/native import with interpreted code and +9.60 ns/import with AOT, medians of seven alternating runs. This isolates an +ordinary scalar import, not a pending callback or GPU/audio transfer. Both use +the existing native-call path. The HTTP copy transport is not a bulk GPU or +real-time audio interface. + +Raw data: [receipts](feature_receipts.json), [sections](feature_off_sections.csv), +[disabled measurements](feature_off_measurements.csv), +[native-call measurements](feature_native_measurements.csv). + +## Reproduce + +Use the shared `run.py` in this directory with the runtime's native compiler. +Add `--gc --simd` to test both guest features. For WAMR, also pass +`--aot-compiler /path/to/wamrc`; `--aot` alone only builds the AOT backend. +Use `--sanitize` for the separate instrumented build. On Windows use `--gc` +for WAMR; its fast interpreter has no Windows SIMD implementation. TinyWasm +Windows uses `--client-asan`, which does not instrument Rust. + +The checked receipts use Clang 23.1.1, LLVM 21 for wamrc, and Rust nightly +1.100.0 (2026-08-27). LLVM 23 cannot build this revision of wamrc. The runner +passes `--bounds-checks=1 --stack-bounds-checks=1` when compiling AOT, matching +the runtime's software-check configuration, and `--enable-gc` for a GC build. + +Build the external HTTP adapter as described in its WASI_HTTP.md with the mixed +runtime archive. Its `wasi_http_tests.py` accepts +`--aot-compiler /path/to/wamrc --aot-gc` to compile each linked official guest +and run the same assertions. For HTTP/2, compile the linked demo core with the +same flags, embed its bytes as `handler_wasm` in `standard_handler.h`, and use +that directory for `P3_H2_HANDLER_INCLUDE`. Run the unchanged `test_wasi_h2.py`. + +JIT, hardware-trap bounds checking and WAMR multi-module continuations remain +excluded. The shared extension still has one live invocation per store, one +numeric result, thread confinement, destructive cancellation and no exposed +fairness budget. AOT stack suspension, general reference-valued C calls, shared +memory, arbitrary component graphs and watchOS deployment need separate work. +All schedulers, component bindings, transport and fixtures remain consumer code +intended for later out-of-tree extraction. diff --git a/crates/c-api/tests/continuations/README.md b/crates/c-api/tests/continuations/README.md index 75c9b5c..543bbb0 100644 --- a/crates/c-api/tests/continuations/README.md +++ b/crates/c-api/tests/continuations/README.md @@ -1,9 +1,9 @@ # Shared continuation C fixture The same `fixture.c`, Wasm inputs, and `wasm_ext.h` declarations are used for TinyWasm and WAMR. -Read [READINESS.md](READINESS.md) before proposing or freezing this API: no WASIp3 -conformance subset has been executed, and the current cancellation/store contract is insufficient -to establish full component concurrency. +Read [READINESS.md](READINESS.md) for the primitive's remaining cancellation and +store-concurrency limitations. The later stackless HTTP adapter has separate +WASIp3 subset receipts. Neither establishes full component concurrency. The extension is experimental, numeric-only, thread-confined, and limited to one result. It is not an implementation of the component model or the WASI 0.3 scheduler. Only explicit pending @@ -34,7 +34,10 @@ the opt-in driver. Peak process RSS is reported; Windows also reports peak commi RSS is a noisy hosted process high-water mark, not a measurement of MCU SRAM or live heap. For WAMR, this prototype requires a single-module fast interpreter, software bounds checks, -no AOT/JIT, and no GC. The pending execution owns a dedicated 64 KiB interpreter stack. +and no JIT. Guest GC/SIMD and ordinary AOT modules can coexist with this API. +AOT functions cannot be suspended. A pending interpreter call uses the existing +instance stack budget (32 KiB by default), configurable through WAMR's existing +`wasm_instance_new_with_args` API. Unsupported build combinations fail configuration. Numeric packing uses memcpy so mixed i32/i64 sequences need not be naturally aligned. Async metadata is store-owned, independent of copied C function handles. TinyWasm reserves its store while the Rust execution borrow is live and @@ -73,3 +76,18 @@ a separate limitation. Use a fresh build directory for each mode. `edges.wasm` and `edges.h` contain the same module generated from `edges.wat`. The additional edge fixture does not change the measured `fixture.c` benchmark. + +## Feature compatibility + +`--gc --simd` adds guest GC and SIMD suspension tests with an unchanged numeric +C boundary. They cover live references and vectors in parent and leaf frames, +allocation pressure, repeated suspension, indirect host calls, cancellation and +post-resume traps. WAMR also checks 4 KiB and 64 KiB instance stack budgets. +`--aot` builds the WAMR AOT backend alongside the interpreter. Add +`--aot-compiler /path/to/wamrc` to execute compiled versions and check that the +suspension API rejects them. The runner emits software memory and stack checks +to match the runtime. WAMR fast-interpreter SIMD is unavailable on Windows. + +Regenerate the portable fixtures with `wasm-tools parse gc.wat -o gc.wasm` +and `wasm-tools parse simd.wat -o simd.wasm`. +See [FEATURE_COMPAT.md](FEATURE_COMPAT.md) for results and remaining boundaries. diff --git a/crates/c-api/tests/continuations/VALIDATION.md b/crates/c-api/tests/continuations/VALIDATION.md index dbb7585..5212da1 100644 --- a/crates/c-api/tests/continuations/VALIDATION.md +++ b/crates/c-api/tests/continuations/VALIDATION.md @@ -100,7 +100,9 @@ heap measurements. Baseline/off static RAM and instructions match, while measure process-memory values vary in pages. No claim of exactly equal runtime RSS is made. The TinyWasm C wrapper currently requires std. No physical Pico SRAM, stack, heap, or cycle qualification has been performed; cross-host figures are not MCU budgets. -The enabled WAMR invocation separately allocates a 64 KiB interpreter stack. +The original enabled WAMR invocation separately allocated a 64 KiB interpreter +stack. The later [feature compatibility update](FEATURE_COMPAT.md) uses the +existing instance budget and records fresh footprint checks. ## Safety and continuation coverage diff --git a/crates/c-api/tests/continuations/feature_native_measurements.csv b/crates/c-api/tests/continuations/feature_native_measurements.csv new file mode 100644 index 0000000..3b785ee --- /dev/null +++ b/crates/c-api/tests/continuations/feature_native_measurements.csv @@ -0,0 +1,15 @@ +mode,round,compute_ns,native_ns,peak_rss_bytes +interp,0,1.9905,14.7263,2490368 +aot,0,0.2388,9.6136,2441216 +aot,1,0.2330,9.5559,2457600 +interp,1,2.6991,14.9486,2473984 +interp,2,1.9275,14.6199,2457600 +aot,2,0.2223,9.6692,2441216 +aot,3,0.2225,9.5415,2441216 +interp,3,1.8414,14.6137,2473984 +interp,4,2.3752,14.5940,2473984 +aot,4,0.2221,9.6036,2457600 +aot,5,0.2223,9.4470,2424832 +interp,5,2.8287,14.6055,2490368 +interp,6,2.3014,14.5741,2473984 +aot,6,0.2239,9.6610,2441216 diff --git a/crates/c-api/tests/continuations/feature_off_measurements.csv b/crates/c-api/tests/continuations/feature_off_measurements.csv new file mode 100644 index 0000000..e8938db --- /dev/null +++ b/crates/c-api/tests/continuations/feature_off_measurements.csv @@ -0,0 +1,15 @@ +variant,round,compute_ns,native_ns,peak_rss_bytes,peak_commit_bytes,compute_cycles,native_cycles,cpu +before,0,3.0021,14.3854,6193152,,,, +after,0,2.9073,14.2574,6209536,,,, +after,1,2.9493,13.8604,6193152,,,, +before,1,2.8937,14.4092,6193152,,,, +before,2,2.8862,14.4814,6225920,,,, +after,2,2.9220,13.8951,6176768,,,, +after,3,2.9361,14.6151,6209536,,,, +before,3,2.9522,14.2975,6209536,,,, +before,4,2.9085,14.1835,6193152,,,, +after,4,2.9717,14.7563,6225920,,,, +after,5,3.0271,13.9622,6176768,,,, +before,5,2.9426,14.1441,6193152,,,, +before,6,2.9408,14.9534,6193152,,,, +after,6,2.9386,14.3350,6193152,,,, diff --git a/crates/c-api/tests/continuations/feature_off_sections.csv b/crates/c-api/tests/continuations/feature_off_sections.csv new file mode 100644 index 0000000..cb875af --- /dev/null +++ b/crates/c-api/tests/continuations/feature_off_sections.csv @@ -0,0 +1,27 @@ +variant,section,virtual_bytes,file_bytes,sha256 +before,,,227096,783554184715d4d6429534732107893c68b481b21c8d61dca34ae2d2baf950be +before,"__TEXT,__text",154208,154208,6d605cf4b27845eba675a35055d5445952cf3b5020fb1367de75ab9f90e78bdf +before,"__TEXT,__stubs",516,516,c194ebf3060d3109ae1ead3cff97991d7a39f1d2da6f59fb7eb9e1e0518c81de +before,"__TEXT,__cstring",9729,9729,941f837498ec2413b2658e979c8b0a93eb63305737cbe8ffb870cbaaac7d50f8 +before,"__TEXT,__const",720,720,312ce0eed39cd77d2f123a292f5e12af990b3d426088f4015b4b4d3dc366f082 +before,"__TEXT,__unwind_info",632,632,ea01d444797636d91e42580b2e8be6cf9584dd1233794c2f508f493e68914500 +before,"__DATA_CONST,__const",40,40,af9c831dfbc0f11f504d27b6909925b38d71b95da84b3820f3e89072582e4d74 +before,"__DATA_CONST,__got",360,360,4556bc72cad1258e17e4d614cfff9c01e13e1369949d02c2f7a7184cde4924f3 +before,"__DATA,__data",2232,2232,18b8a1b065a3460c870111586afd25de4207270c5b9a39f1abc0927d06bce860 +before,"__DATA,__thread_vars",48,48,501a6180e5660e83732938f859c53dee583a04942418e24fda154ffbe14629df +before,"__DATA,__thread_bss",16,0, +before,"__DATA,__bss",16,0, +before,"__DATA,__common",128,0, +after,,,227096,783554184715d4d6429534732107893c68b481b21c8d61dca34ae2d2baf950be +after,"__TEXT,__text",154208,154208,6d605cf4b27845eba675a35055d5445952cf3b5020fb1367de75ab9f90e78bdf +after,"__TEXT,__stubs",516,516,c194ebf3060d3109ae1ead3cff97991d7a39f1d2da6f59fb7eb9e1e0518c81de +after,"__TEXT,__cstring",9729,9729,941f837498ec2413b2658e979c8b0a93eb63305737cbe8ffb870cbaaac7d50f8 +after,"__TEXT,__const",720,720,312ce0eed39cd77d2f123a292f5e12af990b3d426088f4015b4b4d3dc366f082 +after,"__TEXT,__unwind_info",632,632,ea01d444797636d91e42580b2e8be6cf9584dd1233794c2f508f493e68914500 +after,"__DATA_CONST,__const",40,40,af9c831dfbc0f11f504d27b6909925b38d71b95da84b3820f3e89072582e4d74 +after,"__DATA_CONST,__got",360,360,4556bc72cad1258e17e4d614cfff9c01e13e1369949d02c2f7a7184cde4924f3 +after,"__DATA,__data",2232,2232,18b8a1b065a3460c870111586afd25de4207270c5b9a39f1abc0927d06bce860 +after,"__DATA,__thread_vars",48,48,501a6180e5660e83732938f859c53dee583a04942418e24fda154ffbe14629df +after,"__DATA,__thread_bss",16,0, +after,"__DATA,__bss",16,0, +after,"__DATA,__common",128,0, diff --git a/crates/c-api/tests/continuations/feature_receipts.json b/crates/c-api/tests/continuations/feature_receipts.json new file mode 100644 index 0000000..567afa6 --- /dev/null +++ b/crates/c-api/tests/continuations/feature_receipts.json @@ -0,0 +1,624 @@ +{ + "date": "2026-11-03", + "host": "macOS arm64, Apple M4", + "wamr_previous_rfc": "0bea1982b5034138d28bfedf8214e02d09e15892", + "tinywasm_runtime": "142eaf17c4246fb5052f4d7a7c0d3c0e95fe8390", + "toolchains": { + "runtime_c": "Clang 23.1.1", + "aot_compiler": "WAMR 2.4.3 from RFC sources, LLVM 21", + "rust": "1.100.0-nightly (e457a7b0d 2026-08-27)" + }, + "gc_regression": { + "before_root_restore": "failed numeric result assertion after collection", + "after_root_restore": "passed", + "guest_allocation_per_churn_bytes": 2097152 + }, + "tests": { + "wamr_c_api_aot_unit_cases": 56, + "wamr_continuation_ctest_cases": 3, + "tinywasm_host_execution_cases": 7, + "shared_features": [ + "gc", + "simd" + ], + "wamr_sanitizers": "runtime and C client ASan; generated AOT guest code not instrumented", + "tinywasm_sanitizers": "Rust, rebuilt std and C client ASan" + }, + "embedded": { + "before_elf_sha256": "b8889f994b926dfb91799e00d74a5251b215e01b8a4759e33eb41f719762f495", + "before_bytes": 451676, + "after_elf_sha256": "b8889f994b926dfb91799e00d74a5251b215e01b8a4759e33eb41f719762f495", + "after_bytes": 451676 + }, + "qemu": { + "qemu": "QEMU emulator version 11.1.3", + "machine": "mps2-an505", + "elf_sha256": "b8889f994b926dfb91799e00d74a5251b215e01b8a4759e33eb41f719762f495", + "memory": { + "sram_reserved_bytes": 339104, + "sram_limit_bytes": 532480, + "sram_sections": { + ".data": 3528, + ".bss": 196312, + ".net_reserve": 65536, + ".heap": 65536, + ".stack": 8192 + } + }, + "workload": { + "completed": 4, + "peak_streams": 4, + "peak_handles": 41, + "peak_resources": 9, + "input_peak": 2048, + "h2_heap_peak": 40463, + "libc_peak": 45912, + "pool_peak": 111712, + "server_bytes": 65080 + } + }, + "feature_off": { + "binary_bytes": 227096, + "sha256": "783554184715d4d6429534732107893c68b481b21c8d61dca34ae2d2baf950be", + "whole_file_identical": true + }, + "fixture_sha256": { + "features.c": "b24213774ddce7213531b2d2f2985d199261363326ae3c39e66cb2df2e1bd200", + "gc.wat": "5c1507ed8e6bc103373ad13c10deaa9f1d56d5699a1e0e80939244c92d2a21f0", + "gc.wasm": "45464ab0839f3bec369ff22d0240e6c80c57a4f30938831b84cf73f43dd0afb0", + "simd.wat": "fda6c8cc1ffd5b28a95dca34458033d046edb91798d0ec8d65759ae487e7b77b", + "simd.wasm": "61c16e257feb3422f828ea795dca8a3e1419b7e645d78e6eb743c754d20e068f" + }, + "windows": "NOT_RUN: SSH to approved host 192.168.7.20 timed out twice", + "watchOS": "NOT_RUN: hosted evidence only; no device deployment or executable-memory qualification", + "http2_aot_release": { + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "EOF" + }, + "reconnect_retries": 0, + "workloads": [ + { + "workload": "mixed", + "connections": 1, + "streams_per_connection": 1, + "responses": 24, + "response_bytes": 196608, + "seconds": 0.011877666984219104 + }, + { + "workload": "GET", + "connections": 1, + "streams_per_connection": 1, + "responses": 24, + "response_bytes": 98304, + "seconds": 0.003944458963815123 + }, + { + "workload": "POST", + "connections": 1, + "streams_per_connection": 1, + "responses": 24, + "response_bytes": 393216, + "seconds": 0.024128084012772888 + }, + { + "workload": "mixed", + "connections": 1, + "streams_per_connection": 4, + "responses": 96, + "response_bytes": 786432, + "seconds": 0.029396333964541554 + }, + { + "workload": "GET", + "connections": 1, + "streams_per_connection": 4, + "responses": 96, + "response_bytes": 393216, + "seconds": 0.009411333012394607 + }, + { + "workload": "POST", + "connections": 1, + "streams_per_connection": 4, + "responses": 96, + "response_bytes": 1572864, + "seconds": 0.04386462498223409 + }, + { + "workload": "mixed", + "connections": 1, + "streams_per_connection": 8, + "responses": 192, + "response_bytes": 1572864, + "seconds": 0.04240249999566004 + }, + { + "workload": "GET", + "connections": 1, + "streams_per_connection": 8, + "responses": 192, + "response_bytes": 786432, + "seconds": 0.016775749973021448 + }, + { + "workload": "POST", + "connections": 1, + "streams_per_connection": 8, + "responses": 192, + "response_bytes": 3145728, + "seconds": 0.07910245802486315 + }, + { + "workload": "mixed", + "connections": 2, + "streams_per_connection": 1, + "responses": 48, + "response_bytes": 393216, + "seconds": 0.017731959000229836 + }, + { + "workload": "GET", + "connections": 2, + "streams_per_connection": 1, + "responses": 48, + "response_bytes": 196608, + "seconds": 0.006010499957483262 + }, + { + "workload": "POST", + "connections": 2, + "streams_per_connection": 1, + "responses": 48, + "response_bytes": 786432, + "seconds": 0.02797908300999552 + }, + { + "workload": "mixed", + "connections": 2, + "streams_per_connection": 4, + "responses": 192, + "response_bytes": 1572864, + "seconds": 0.0442562919924967 + }, + { + "workload": "GET", + "connections": 2, + "streams_per_connection": 4, + "responses": 192, + "response_bytes": 786432, + "seconds": 0.01728833297966048 + }, + { + "workload": "POST", + "connections": 2, + "streams_per_connection": 4, + "responses": 192, + "response_bytes": 3145728, + "seconds": 0.08234191697556525 + }, + { + "workload": "mixed", + "connections": 2, + "streams_per_connection": 8, + "responses": 384, + "response_bytes": 3145728, + "seconds": 0.07647320802789181 + }, + { + "workload": "GET", + "connections": 2, + "streams_per_connection": 8, + "responses": 384, + "response_bytes": 1572864, + "seconds": 0.03312004101462662 + }, + { + "workload": "POST", + "connections": 2, + "streams_per_connection": 8, + "responses": 384, + "response_bytes": 6291456, + "seconds": 0.15352454100502655 + }, + { + "workload": "mixed", + "connections": 4, + "streams_per_connection": 1, + "responses": 96, + "response_bytes": 786432, + "seconds": 0.030300000042188913 + }, + { + "workload": "GET", + "connections": 4, + "streams_per_connection": 1, + "responses": 96, + "response_bytes": 393216, + "seconds": 0.010745332983788103 + }, + { + "workload": "POST", + "connections": 4, + "streams_per_connection": 1, + "responses": 96, + "response_bytes": 1572864, + "seconds": 0.052432125026825815 + }, + { + "workload": "mixed", + "connections": 4, + "streams_per_connection": 2, + "responses": 192, + "response_bytes": 1572864, + "seconds": 0.047529833973385394 + }, + { + "workload": "GET", + "connections": 4, + "streams_per_connection": 2, + "responses": 192, + "response_bytes": 786432, + "seconds": 0.018451458017807454 + }, + { + "workload": "POST", + "connections": 4, + "streams_per_connection": 2, + "responses": 192, + "response_bytes": 3145728, + "seconds": 0.08564958395436406 + }, + { + "workload": "mixed", + "connections": 4, + "streams_per_connection": 4, + "responses": 384, + "response_bytes": 3145728, + "seconds": 0.08542579098138958 + }, + { + "workload": "GET", + "connections": 4, + "streams_per_connection": 4, + "responses": 384, + "response_bytes": 1572864, + "seconds": 0.03629987494787201 + }, + { + "workload": "POST", + "connections": 4, + "streams_per_connection": 4, + "responses": 384, + "response_bytes": 6291456, + "seconds": 0.15942829102277756 + } + ], + "server": { + "completed": 4828, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 34, + "queue_peak": 2048, + "heap_peak": 93808, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 3047424, + "cpu_s": 0.460851, + "elapsed_s": 1.265085, + "waits": 130381, + "wakes": 101481, + "callbacks": 187501 + } + }, + "http2_aot_asan": { + "limits": { + "connections": 4, + "streams": 16, + "per_connection": 8 + }, + "overload": { + "per_connection": "peer SETTINGS limit", + "global": "REFUSED_STREAM", + "tcp_after_establishment": "reset" + }, + "reconnect_retries": 0, + "workloads": [ + { + "workload": "mixed", + "connections": 1, + "streams_per_connection": 1, + "responses": 24, + "response_bytes": 196608, + "seconds": 0.01367416704306379 + }, + { + "workload": "GET", + "connections": 1, + "streams_per_connection": 1, + "responses": 24, + "response_bytes": 98304, + "seconds": 0.004506916971877217 + }, + { + "workload": "POST", + "connections": 1, + "streams_per_connection": 1, + "responses": 24, + "response_bytes": 393216, + "seconds": 0.023854333034250885 + }, + { + "workload": "mixed", + "connections": 1, + "streams_per_connection": 4, + "responses": 96, + "response_bytes": 786432, + "seconds": 0.030946291983127594 + }, + { + "workload": "GET", + "connections": 1, + "streams_per_connection": 4, + "responses": 96, + "response_bytes": 393216, + "seconds": 0.010216917027719319 + }, + { + "workload": "POST", + "connections": 1, + "streams_per_connection": 4, + "responses": 96, + "response_bytes": 1572864, + "seconds": 0.04502429103013128 + }, + { + "workload": "mixed", + "connections": 1, + "streams_per_connection": 8, + "responses": 192, + "response_bytes": 1572864, + "seconds": 0.04451799998059869 + }, + { + "workload": "GET", + "connections": 1, + "streams_per_connection": 8, + "responses": 192, + "response_bytes": 786432, + "seconds": 0.018099499982781708 + }, + { + "workload": "POST", + "connections": 1, + "streams_per_connection": 8, + "responses": 192, + "response_bytes": 3145728, + "seconds": 0.08291399996960536 + }, + { + "workload": "mixed", + "connections": 2, + "streams_per_connection": 1, + "responses": 48, + "response_bytes": 393216, + "seconds": 0.019007292052265257 + }, + { + "workload": "GET", + "connections": 2, + "streams_per_connection": 1, + "responses": 48, + "response_bytes": 196608, + "seconds": 0.006179832969792187 + }, + { + "workload": "POST", + "connections": 2, + "streams_per_connection": 1, + "responses": 48, + "response_bytes": 786432, + "seconds": 0.028416000015567988 + }, + { + "workload": "mixed", + "connections": 2, + "streams_per_connection": 4, + "responses": 192, + "response_bytes": 1572864, + "seconds": 0.045778750034514815 + }, + { + "workload": "GET", + "connections": 2, + "streams_per_connection": 4, + "responses": 192, + "response_bytes": 786432, + "seconds": 0.018224624975118786 + }, + { + "workload": "POST", + "connections": 2, + "streams_per_connection": 4, + "responses": 192, + "response_bytes": 3145728, + "seconds": 0.08403587498469278 + }, + { + "workload": "mixed", + "connections": 2, + "streams_per_connection": 8, + "responses": 384, + "response_bytes": 3145728, + "seconds": 0.0793855829979293 + }, + { + "workload": "GET", + "connections": 2, + "streams_per_connection": 8, + "responses": 384, + "response_bytes": 1572864, + "seconds": 0.0343304579728283 + }, + { + "workload": "POST", + "connections": 2, + "streams_per_connection": 8, + "responses": 384, + "response_bytes": 6291456, + "seconds": 0.15809737500967458 + }, + { + "workload": "mixed", + "connections": 4, + "streams_per_connection": 1, + "responses": 96, + "response_bytes": 786432, + "seconds": 0.03084712498821318 + }, + { + "workload": "GET", + "connections": 4, + "streams_per_connection": 1, + "responses": 96, + "response_bytes": 393216, + "seconds": 0.01163520896807313 + }, + { + "workload": "POST", + "connections": 4, + "streams_per_connection": 1, + "responses": 96, + "response_bytes": 1572864, + "seconds": 0.04775974998483434 + }, + { + "workload": "mixed", + "connections": 4, + "streams_per_connection": 2, + "responses": 192, + "response_bytes": 1572864, + "seconds": 0.047900250006932765 + }, + { + "workload": "GET", + "connections": 4, + "streams_per_connection": 2, + "responses": 192, + "response_bytes": 786432, + "seconds": 0.019938417011871934 + }, + { + "workload": "POST", + "connections": 4, + "streams_per_connection": 2, + "responses": 192, + "response_bytes": 3145728, + "seconds": 0.08499912498518825 + }, + { + "workload": "mixed", + "connections": 4, + "streams_per_connection": 4, + "responses": 384, + "response_bytes": 3145728, + "seconds": 0.0877864999929443 + }, + { + "workload": "GET", + "connections": 4, + "streams_per_connection": 4, + "responses": 384, + "response_bytes": 1572864, + "seconds": 0.035454917000606656 + }, + { + "workload": "POST", + "connections": 4, + "streams_per_connection": 4, + "responses": 384, + "response_bytes": 6291456, + "seconds": 0.16821058298228309 + } + ], + "server": { + "completed": 4828, + "reset": 15, + "refused": 1, + "connections_rejected": 1, + "protocol_errors": 0, + "connection_errors": 0, + "peak_streams": 16, + "peak_handles": 193, + "peak_resources": 34, + "queue_peak": 2048, + "heap_peak": 93808, + "live": 0, + "handles": 0, + "resources": 0, + "heap": 0, + "rss_bytes": 36732928, + "cpu_s": 0.653471, + "elapsed_s": 1.28989, + "waits": 112640, + "wakes": 89606, + "callbacks": 186187 + } + }, + "official_http_aot": { + "release": [ + "UNSUPPORTED http-client-chunked wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-headers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-method wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path-none wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-send-errors wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-sent wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-status wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailer-error wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client wasi:http/client.send requires outbound HTTP transport", + "PASS http-fields", + "PASS http-request-options", + "PASS http-request", + "PASS http-response", + "PASS http-service-echo", + "PASS http-service-uri", + "PASS http-service" + ], + "asan": [ + "UNSUPPORTED http-client-chunked wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-headers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-method wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path-none wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-path wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-send-errors wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-sent wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-status wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailer-error wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client-trailers wasi:http/client.send requires outbound HTTP transport", + "UNSUPPORTED http-client wasi:http/client.send requires outbound HTTP transport", + "PASS http-fields", + "PASS http-request-options", + "PASS http-request", + "PASS http-response", + "PASS http-service-echo", + "PASS http-service-uri", + "PASS http-service" + ] + } +} diff --git a/crates/c-api/tests/continuations/features.c b/crates/c-api/tests/continuations/features.c new file mode 100644 index 0000000..aeab1c4 --- /dev/null +++ b/crates/c-api/tests/continuations/features.c @@ -0,0 +1,171 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#include "wasm.h" +#include "wasm_ext.h" +#include +#include +#include + +#define CHECK(x) \ + do { \ + if (!(x)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \ + exit(1); \ + } \ + } while (0) + +typedef struct Counts { + unsigned calls, cancels; +} Counts; + +static wasm_ext_status_t +pending(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results, + wasm_trap_t **trap) +{ + (void)trap; + CHECK(args->size == 1 && results->size == 1); + ((Counts *)env)->calls++; + return WASM_EXT_PENDING; +} + +static wasm_trap_t * +synchronous(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results) +{ + CHECK(args->size == 1 && results->size == 1); + ((Counts *)env)->calls++; + results->data[0].kind = WASM_I32; + results->data[0].of.i32 = args->data[0].of.i32 + 1; + return NULL; +} + +static void +cancelled(void *env) +{ + ((Counts *)env)->cancels++; +} + +int +main(int argc, char **argv) +{ + CHECK(argc == 2 || (argc == 3 && !strcmp(argv[2], "aot"))); + bool aot = argc == 3; + wasm_engine_t *engine = wasm_engine_new(); + CHECK(engine); + wasm_store_t *store = wasm_store_new(engine); + CHECK(store); + FILE *file = fopen(argv[1], "rb"); + CHECK(file && !fseek(file, 0, SEEK_END)); + long length = ftell(file); + CHECK(length > 0 && !fseek(file, 0, SEEK_SET)); + wasm_byte_vec_t bytes; + wasm_byte_vec_new_uninitialized(&bytes, (size_t)length); + CHECK(fread(bytes.data, 1, bytes.size, file) == bytes.size); + fclose(file); + wasm_module_t *module = wasm_module_new(store, &bytes); + CHECK(module); + wasm_byte_vec_delete(&bytes); + Counts counts = { 0 }; + wasm_functype_t *type = + wasm_functype_new_1_1(wasm_valtype_new_i32(), wasm_valtype_new_i32()); + wasm_func_t *host = + aot ? wasm_func_new_with_env(store, type, synchronous, &counts, NULL) + : wasm_ext_func_new(store, type, pending, &counts, cancelled); + wasm_functype_delete(type); + CHECK(host); + wasm_extern_t *items[] = { wasm_func_as_extern(host) }; + wasm_extern_vec_t imports = WASM_ARRAY_VEC(items); + wasm_trap_t *trap = NULL; + wasm_val_t arg[] = { WASM_I32_VAL(20) }, out[] = { WASM_I32_VAL(0) }; + wasm_val_t complete[] = { WASM_I32_VAL(1) }; + wasm_val_vec_t args = WASM_ARRAY_VEC(arg); + wasm_val_vec_t results = WASM_ARRAY_VEC(out); + wasm_val_vec_t completion = WASM_ARRAY_VEC(complete); + for (unsigned budget = 0; budget < 2; ++budget) { +#ifdef TEST_WAMR + wasm_instance_t *instance = wasm_instance_new_with_args( + store, module, &imports, &trap, budget ? 65536 : 4096, 0); +#else + wasm_instance_t *instance = + wasm_instance_new(store, module, &imports, &trap); +#endif + CHECK(instance && !trap); + wasm_extern_vec_t exports; + wasm_instance_exports(instance, &exports); + CHECK(exports.size == 3); + wasm_func_t *run = wasm_extern_as_func(exports.data[0]); + for (unsigned i = 0; i < 12; ++i) { + arg[0].of.i32 = 20 + (int32_t)i; + if (aot) { + CHECK(!wasm_ext_call_new(run, &args)); + trap = wasm_func_call(run, &args, &results); + CHECK(!trap); + } + else { + unsigned before = counts.calls; + wasm_ext_call_t *call = wasm_ext_call_new(run, &args); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &results, &trap) + == WASM_EXT_PENDING); + CHECK(wasm_ext_call_resume(call, &completion, &results, &trap) + == WASM_EXT_PENDING); + CHECK(wasm_ext_call_resume(call, &completion, &results, &trap) + == WASM_EXT_DONE); + CHECK(!trap && counts.calls == before + 2); + wasm_ext_call_delete(call); + call = wasm_ext_call_new(run, &args); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &results, &trap) + == WASM_EXT_PENDING); + before = counts.cancels; + wasm_ext_call_cancel(call); + wasm_ext_call_cancel(call); + wasm_ext_call_delete(call); + CHECK(counts.cancels == before + 1); + } + CHECK(out[0].of.i32 == 2 * arg[0].of.i32 + 1); + } + if (!aot) { + wasm_func_t *fail = wasm_extern_as_func(exports.data[1]); + wasm_ext_call_t *call = wasm_ext_call_new(fail, &args); + CHECK(call); + CHECK(wasm_ext_call_resume(call, NULL, &results, &trap) + == WASM_EXT_PENDING); + CHECK(wasm_ext_call_resume(call, &completion, &results, &trap) + == WASM_EXT_ERROR); + CHECK(trap); + wasm_trap_delete(trap); + trap = NULL; + wasm_ext_call_delete(call); +#ifdef TEST_WAMR + arg[0].of.i32 = 200; + call = + wasm_ext_call_new(wasm_extern_as_func(exports.data[2]), &args); + CHECK(call); + int status = wasm_ext_call_resume(call, NULL, &results, &trap); + if (budget) { + CHECK(status == WASM_EXT_PENDING && !trap); + CHECK(wasm_ext_call_resume(call, &completion, &results, &trap) + == WASM_EXT_DONE); + CHECK(!trap && out[0].of.i32 == 201); + } + else { + CHECK(status == WASM_EXT_ERROR && trap); + wasm_trap_delete(trap); + trap = NULL; + } + wasm_ext_call_delete(call); +#endif + } + wasm_extern_vec_delete(&exports); + wasm_instance_delete(instance); + } + wasm_func_delete(host); + wasm_module_delete(module); + wasm_store_delete(store); + wasm_engine_delete(engine); + printf("PASS %s: %s, calls=%u cancels=%u\n", argv[1], + aot ? "ordinary AOT, continuation rejected" + : "resume/cancel/trap/roots", + counts.calls, counts.cancels); + return 0; +} diff --git a/crates/c-api/tests/continuations/gc.wasm b/crates/c-api/tests/continuations/gc.wasm new file mode 100644 index 0000000000000000000000000000000000000000..604ed8aec1f511834337e33aa4cef901c3311157 GIT binary patch literal 351 zcmZXQy>7xl5QJy$_Audu0HcC>NN67oq@9vnN5|!m}+`M22Yv0HzbDY3YEGe>_Cq-l_FO9=JZk9H?s2tZ< zp}OE}+ec>u`xu)KX2I|TFMw*9{Vm@4Pq vIee(}Mc}hVqNR?(Z Date: Tue, 3 Nov 2026 22:32:31 -0800 Subject: [PATCH 16/20] test: audit native unwinding and document missing guest traces --- .github/workflows/shared_continuations.yml | 11 + .../tests/continuations/FEATURE_COMPAT.md | 3 + crates/c-api/tests/continuations/README.md | 3 + .../c-api/tests/continuations/debug/README.md | 115 ++++++++++ .../c-api/tests/continuations/debug/bridge.c | 9 + .../tests/continuations/debug/guest.wasm | Bin 0 -> 152 bytes .../c-api/tests/continuations/debug/guest.wat | 9 + .../c-api/tests/continuations/debug/native.c | 149 +++++++++++++ .../tests/continuations/debug/native.cpp | 10 + .../c-api/tests/continuations/debug/native.rs | 10 + .../tests/continuations/debug/native.swift | 8 + .../tests/continuations/debug/observed.json | 209 ++++++++++++++++++ crates/c-api/tests/continuations/debug/run.py | 136 ++++++++++++ crates/c-api/tests/continuations/run.py | 5 +- 14 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 crates/c-api/tests/continuations/debug/README.md create mode 100644 crates/c-api/tests/continuations/debug/bridge.c create mode 100644 crates/c-api/tests/continuations/debug/guest.wasm create mode 100644 crates/c-api/tests/continuations/debug/guest.wat create mode 100644 crates/c-api/tests/continuations/debug/native.c create mode 100644 crates/c-api/tests/continuations/debug/native.cpp create mode 100644 crates/c-api/tests/continuations/debug/native.rs create mode 100644 crates/c-api/tests/continuations/debug/native.swift create mode 100644 crates/c-api/tests/continuations/debug/observed.json create mode 100644 crates/c-api/tests/continuations/debug/run.py diff --git a/.github/workflows/shared_continuations.yml b/.github/workflows/shared_continuations.yml index b68b981..aab7817 100644 --- a/.github/workflows/shared_continuations.yml +++ b/.github/workflows/shared_continuations.yml @@ -68,3 +68,14 @@ jobs: toolchain: nightly components: miri - run: cargo miri test --locked -p tinywasm-c-api --features external-host-calls --lib + + native-unwind: + name: Native unwind (GDB, C/C++/Rust) + runs-on: ubuntu-24.04 + env: + WASM_EXT_TEST_ONLY: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - run: sudo apt-get update && sudo apt-get install -y gdb + - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build build/native-unwind + - run: python3 crates/c-api/tests/continuations/debug/run.py --runtime tinywasm --include crates/c-api/include --library build/native-unwind/runtime/release/libtinywasm.a --build build/native-unwind/client --debugger gdb diff --git a/crates/c-api/tests/continuations/FEATURE_COMPAT.md b/crates/c-api/tests/continuations/FEATURE_COMPAT.md index f8fdec8..93a58c7 100644 --- a/crates/c-api/tests/continuations/FEATURE_COMPAT.md +++ b/crates/c-api/tests/continuations/FEATURE_COMPAT.md @@ -105,3 +105,6 @@ fairness budget. AOT stack suspension, general reference-valued C calls, shared memory, arbitrary component graphs and watchOS deployment need separate work. All schedulers, component bindings, transport and fixtures remain consumer code intended for later out-of-tree extraction. + +See the [mixed-stack audit](debug/README.md) for native debugger/crash-report +coverage and the remaining guest-frame and async-ancestry requirements. diff --git a/crates/c-api/tests/continuations/README.md b/crates/c-api/tests/continuations/README.md index 543bbb0..caac306 100644 --- a/crates/c-api/tests/continuations/README.md +++ b/crates/c-api/tests/continuations/README.md @@ -91,3 +91,6 @@ to match the runtime. WAMR fast-interpreter SIMD is unavailable on Windows. Regenerate the portable fixtures with `wasm-tools parse gc.wat -o gc.wasm` and `wasm-tools parse simd.wat -o simd.wasm`. See [FEATURE_COMPAT.md](FEATURE_COMPAT.md) for results and remaining boundaries. + +See the [mixed-stack audit](debug/README.md) for native debugger/crash-report +coverage and the remaining guest-frame and async-ancestry requirements. diff --git a/crates/c-api/tests/continuations/debug/README.md b/crates/c-api/tests/continuations/debug/README.md new file mode 100644 index 0000000..3f7eff0 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/README.md @@ -0,0 +1,115 @@ +# Mixed native and guest stack audit + +Full mixed guest/native crash reporting is a readiness requirement for this RFC, +not a capability established by the continuation and HTTP tests. + +The fixture calls Wasm `root -> middle -> leaf -> example:native.call`, then C, +C++, Rust and optionally Swift. It stops inside the native provider, parks the +continuation, and traps after resumption. An early-trap case checks failure +before the first yield. The ordinary-call mode also accepts a WAMR AOT module. +Provider functions use `noinline` so this test can assert native ABI unwind order; +this is not a requirement to disable inlining or tail dispatch in the runtime. + +## Observed behavior + +On macOS arm64, optimized builds recover the native C/C++/Rust/Swift chain in +LLDB and actual Apple `.ips` reports. Matching dSYM UUIDs are checked. After +yield, the native backtrace contains the scheduler/driver: the guest continuation +has no native stack. A physical backtrace must not invent the old frames. + +WAMR previously returned an empty C trap trace for continuation failures even +with `WAMR_BUILD_DUMP_CALL_STACK=1`. The initial error path discarded the trace; +the resumed error path also released frames before capturing them. Both now +reuse the ordinary trap mechanism. Tests require all three guest function +indices for initial, resumed and ordinary failures. This requires the existing +opt-in call-stack feature; the shared C header has not changed. + +TinyWasm still returns an empty `wasm_trap_trace`. Its optimized C adapter invokes +external callbacks after returning from the interpreter, so the native backtrace +does not contain an active interpreter frame either. The fixture records this +limitation, not a successful guest-stack qualification. + +WAMR ordinary AOT calls preserve native unwinding and the opt-in logical trap +trace, but the tested default AOT build shows an unnamed generated-code address +in LLDB. It does not provide named guest source frames. The separate experimental +`WAMR_BUILD_DEBUG_AOT` compiler configuration requires LLDB development libraries, +which were unavailable in the local LLVM 21 installation. Its DWARF registration +path remains unqualified by this audit. + +The fast interpreter's reported offsets are internal compiled-bytecode offsets; +module offsets may be zero. They must not be presented to a Wasm DWARF symbolizer +as original Wasm instruction offsets. Function identity is useful; full source +locations require a retained mapping to the exact original module. + +The [recorded observations](observed.json) include native backtraces, minimal +Apple crash receipts, source hashes and the unchanged disabled artifacts. + +## Required out-of-tree diagnostic contract + +1. Associate each loaded module with its content hash, component/instance identity, + original Wasm/debug sidecar, and native image UUID or build ID. Preserve the + component linker's mapping from lowered core functions to original modules. +2. Provide a bounded, versioned, read-only guest-frame snapshot at host-call and + suspension boundaries, with explicit offset coordinate systems and truncation. + TinyWasm needs a public capture seam; its current `guest-debug` by-index APIs + are insufficient. WAMR's allocating trap-trace API is not a fatal-signal API. +3. Keep async ancestry in the external scheduler: task/call generation, parent, + import identity, and active/pending/completed/cancelled state. Distinguish + concurrent streams and reused slots. Show an explicit async boundary rather + than treating a resumed callback as a continuous native stack. +4. Capture no borrowed guest/native pointers across a yield. Publish immutable + or coherently versioned records for debugger/crash readers; invalidate them on + cancellation and teardown. Do not allocate, acquire runtime locks, invoke + callbacks, format strings or re-enter the engine from a fatal-signal handler. +5. Preserve native unwind metadata and archive matching dSYMs/ELF debug files. + Keep inline/tail-call semantics honest. C++ exceptions, Rust panics and Swift + errors must be caught/translated before crossing the C ABI; stack inspection + does not authorize exception propagation through it. +6. Compile diagnostic state and capture work out of embedded builds. Use existing + runtime frames rather than a second stack updated on every instruction. Bound + enabled memory and measure capture cost at native-call boundaries, including + high-frequency GPU/audio imports. Disabling diagnostics must retain the LTO + binary/RSS/native-call baseline; don't sacrifice tail dispatch for prettier + native traces. + +Native Apple reports alone do not automatically include interpreter/async +records. An out-of-tree reporting integration must attach or reconstruct them. +Apple's [CrashReportExtension](https://developer.apple.com/documentation/crashreportextension) +can inspect a crashed process out of process on its supported platforms; its +documented availability is iOS/iPadOS/macOS 27, not watchOS. It must not become a +runtime dependency or the only diagnostic path. GDB/LLDB readers and an external +crash-context attachment remain necessary for other targets. + +These requirements are intentionally not a new public diagnostics ABI in this +patch. Named guest frames, source mappings and async ancestry remain gates before +claiming a complete `Wasm -> component/module -> native provider` crash stack. + +## Reproduce + +Build the shared runtime fixture with `run.py --runtime wamr --root /path/to/wamr +--build /tmp/runtime --call-stacks`, or the equivalent TinyWasm command without +`--call-stacks`. Use one compiler family for native LTO objects and the final +linker. For WAMR source-level runtime debugging, additionally build its archive +with `RelWithDebInfo` and `-fno-omit-frame-pointer`. The default build flags stay +unchanged. + +Run this directory's `run.py` with `--runtime`, `--include`, `--library`, +`--build` and `--debugger lldb` or `--debugger gdb`. WAMR's include directory must +contain its C API header under the shared name `wasm.h`, as the parent runner +arranges. Add `--swift` on macOS. Without it, a C bridge connects Rust to the +checkpoint; this is the Linux GDB CI configuration. + +`--apple-crash` deliberately traps the standalone fixture, checks a new Apple +`.ips` report for the native frame order and matching dSYM UUID, and symbolicates +its image addresses with `atos`. Run it only when intentionally producing a test +crash. The script records a minimal receipt, not the report's machine identifiers. +`--ordinary --guest /path/to/guest.aot` checks ordinary AOT calls; compile +`guest.wasm` with `--enable-dump-call-stack` and matching GC/bounds-check settings. + +The runner writes `debugger.txt`, `guest_trace.txt`, and `receipt.json`; Apple +crash tests also write `apple_symbolicated.txt`. It explicitly records +`full_mixed_guest_stack_qualified: false`, even when native unwinding passes. + +References: [Apple symbolication](https://developer.apple.com/documentation/xcode/adding-identifiable-symbol-names-to-a-crash-report), +[GDB JIT interface](https://sourceware.org/gdb/current/onlinedocs/gdb.html/JIT-Interface.html), +[LLVM debugging generated code](https://llvm.org/docs/DebuggingJITedCode.html). diff --git a/crates/c-api/tests/continuations/debug/bridge.c b/crates/c-api/tests/continuations/debug/bridge.c new file mode 100644 index 0000000..3df864f --- /dev/null +++ b/crates/c-api/tests/continuations/debug/bridge.c @@ -0,0 +1,9 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +extern int +native_checkpoint(int); +__attribute__((noinline)) int +native_bridge(int value) +{ + return native_checkpoint(value) + 1; +} diff --git a/crates/c-api/tests/continuations/debug/guest.wasm b/crates/c-api/tests/continuations/debug/guest.wasm new file mode 100644 index 0000000000000000000000000000000000000000..1847ada4c00ca8aa8805cc7f6d1887e16e72b9ea GIT binary patch literal 152 zcmXAhK?=e!6b0XJJ}ny2?gUpw@C<@0U3iZAX(B|ERN7SB_3E~CHv_{!`al3Erw5HR zW_#M&pM;OMmng}7EjQvlpjb^=QIP6}2(><`9V*=U3SFVZ0eUrgNjG{el+A=QM+p`i g-6v_@wG753Ix_S!uje_jxAM&L;EeHBao4u#4{}Ez(*OVf literal 0 HcmV?d00001 diff --git a/crates/c-api/tests/continuations/debug/guest.wat b/crates/c-api/tests/continuations/debug/guest.wat new file mode 100644 index 0000000..4a3a5e4 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/guest.wat @@ -0,0 +1,9 @@ +;; Copyright (C) 2026 Matt Hargett. All rights reserved. +;; SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +(module $stack_audit + (import "example:native" "call" (func $native_call (param i32) (result i32))) + (func $leaf (param i32) (result i32) + local.get 0 i32.eqz if unreachable end + local.get 0 call $native_call drop unreachable) + (func $middle (param i32) (result i32) local.get 0 call $leaf i32.const 1 i32.add) + (func $root (export "run") (param i32) (result i32) local.get 0 call $middle i32.const 1 i32.add)) diff --git a/crates/c-api/tests/continuations/debug/native.c b/crates/c-api/tests/continuations/debug/native.c new file mode 100644 index 0000000..b295c4f --- /dev/null +++ b/crates/c-api/tests/continuations/debug/native.c @@ -0,0 +1,149 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#include "wasm.h" +#include "wasm_ext.h" +#include +#include +#include +#ifdef _MSC_VER +#include +#define NOINLINE __declspec(noinline) +#define CRASH() __debugbreak() +#else +#define NOINLINE __attribute__((noinline)) +#define CRASH() __builtin_trap() +#endif +#define CHECK(x) \ + do { \ + if (!(x)) { \ + fprintf(stderr, "failed: %s\n", #x); \ + exit(1); \ + } \ + } while (0) +#ifdef STACK_AUDIT_NATIVE +extern int +native_cpp(int value); +#else +static int +native_cpp(int value) +{ + return value + 4; +} +#endif +static int crash_enabled; +NOINLINE int +native_checkpoint(int value) +{ + if (crash_enabled) + CRASH(); + return value + 1; +} +NOINLINE void +parked_checkpoint(void) +{ + static volatile unsigned visits; + visits++; +} +static NOINLINE wasm_ext_status_t +native_import(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results, + wasm_trap_t **trap) +{ + (void)env; + (void)results; + (void)trap; + CHECK(native_cpp(args->data[0].of.i32) == args->data[0].of.i32 + 4); + return WASM_EXT_PENDING; +} +static NOINLINE wasm_trap_t * +sync_import(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *results) +{ + (void)env; + results->data[0].kind = WASM_I32; + results->data[0].of.i32 = native_cpp(args->data[0].of.i32); + return NULL; +} +int +main(int argc, char **argv) +{ + CHECK(argc >= 2); + crash_enabled = argc > 2 && !strcmp(argv[2], "crash"); + int ordinary = argc > 2 && !strcmp(argv[2], "ordinary"); + int early = argc > 2 && !strcmp(argv[2], "early"); + wasm_engine_t *engine = wasm_engine_new(); + CHECK(engine); + wasm_store_t *store = wasm_store_new(engine); + CHECK(store); + FILE *f = fopen(argv[1], "rb"); + CHECK(f); + CHECK(!fseek(f, 0, SEEK_END)); + long n = ftell(f); + CHECK(n > 0); + rewind(f); + wasm_byte_vec_t bytes; + wasm_byte_vec_new_uninitialized(&bytes, (size_t)n); + CHECK(fread(bytes.data, 1, bytes.size, f) == bytes.size); + fclose(f); + wasm_module_t *module = wasm_module_new(store, &bytes); + CHECK(module); + wasm_byte_vec_delete(&bytes); + wasm_functype_t *type = + wasm_functype_new_1_1(wasm_valtype_new_i32(), wasm_valtype_new_i32()); + wasm_func_t *host = + ordinary ? wasm_func_new_with_env(store, type, sync_import, NULL, NULL) + : wasm_ext_func_new(store, type, native_import, NULL, NULL); + CHECK(host); + wasm_functype_delete(type); + wasm_extern_t *items[] = { wasm_func_as_extern(host) }; + wasm_extern_vec_t imports = WASM_ARRAY_VEC(items); + wasm_trap_t *trap = NULL; + wasm_instance_t *instance = + wasm_instance_new(store, module, &imports, &trap); + CHECK(instance && !trap); + wasm_extern_vec_t exports; + wasm_instance_exports(instance, &exports); + wasm_val_t in[] = { WASM_I32_VAL(20) }, out[] = { WASM_I32_VAL(0) }; + wasm_val_vec_t args = WASM_ARRAY_VEC(in), results = WASM_ARRAY_VEC(out); + wasm_ext_call_t *call = NULL; + if (ordinary) { + trap = wasm_func_call(wasm_extern_as_func(exports.data[0]), &args, + &results); + CHECK(trap); + } + else { + if (early) + in[0].of.i32 = 0; + call = wasm_ext_call_new(wasm_extern_as_func(exports.data[0]), &args); + CHECK(call); + int status = wasm_ext_call_resume(call, NULL, &results, &trap); + if (!early) { + CHECK(status == WASM_EXT_PENDING); + parked_checkpoint(); + status = wasm_ext_call_resume(call, &args, &results, &trap); + } + CHECK(status == WASM_EXT_ERROR && trap); + } + wasm_frame_vec_t frames; + wasm_trap_trace(trap, &frames); + printf("guest_trace_count=%zu\n", frames.size); +#ifdef EXPECT_GUEST_TRACE + CHECK(frames.size == 3); + for (size_t i = 0; i < 3; i++) + CHECK(wasm_frame_func_index(frames.data[i]) == i + 1); +#endif + for (size_t i = 0; i < frames.size; i++) + printf("guest_frame index=%u function_offset=%zu module_offset=%zu\n", + wasm_frame_func_index(frames.data[i]), + wasm_frame_func_offset(frames.data[i]), + wasm_frame_module_offset(frames.data[i])); + wasm_frame_vec_delete(&frames); + wasm_trap_delete(trap); + if (call) + wasm_ext_call_delete(call); + wasm_extern_vec_delete(&exports); + wasm_instance_delete(instance); + wasm_func_delete(host); + wasm_module_delete(module); + wasm_store_delete(store); + wasm_engine_delete(engine); + return 0; +} diff --git a/crates/c-api/tests/continuations/debug/native.cpp b/crates/c-api/tests/continuations/debug/native.cpp new file mode 100644 index 0000000..6966165 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/native.cpp @@ -0,0 +1,10 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +extern "C" int +native_rust(int); +extern "C" __attribute__((noinline)) int +native_cpp(int value) +{ + volatile int result = native_rust(value); + return result + 1; +} diff --git a/crates/c-api/tests/continuations/debug/native.rs b/crates/c-api/tests/continuations/debug/native.rs new file mode 100644 index 0000000..48e87b1 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/native.rs @@ -0,0 +1,10 @@ +// Copyright (C) 2026 Matt Hargett. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +unsafe extern "C" { + fn native_bridge(value: i32) -> i32; +} +#[unsafe(no_mangle)] +#[inline(never)] +pub extern "C" fn native_rust(value: i32) -> i32 { + std::hint::black_box(unsafe { native_bridge(value) }) + 1 +} diff --git a/crates/c-api/tests/continuations/debug/native.swift b/crates/c-api/tests/continuations/debug/native.swift new file mode 100644 index 0000000..5855f2f --- /dev/null +++ b/crates/c-api/tests/continuations/debug/native.swift @@ -0,0 +1,8 @@ +// Copyright (C) 2026 Matt Hargett. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +@_silgen_name("native_checkpoint") func nativeCheckpoint(_ value:Int32) -> Int32 +@_cdecl("native_bridge") +@inline(never) +public func nativeSwift(_ value:Int32) -> Int32 { + return nativeCheckpoint(value)+1 +} diff --git a/crates/c-api/tests/continuations/debug/observed.json b/crates/c-api/tests/continuations/debug/observed.json new file mode 100644 index 0000000..8e61cfa --- /dev/null +++ b/crates/c-api/tests/continuations/debug/observed.json @@ -0,0 +1,209 @@ +{ + "date": "2026-11-03", + "wamr_core_fix": "563b962a", + "tinywasm_runtime": "2ffa27fe2f17528f43eaf97eaca42028c9fe26b7", + "full_mixed_guest_stack_qualified": false, + "wamr_guest_trace_before": 0, + "wamr_guest_trace_after": 3, + "wamr_ctest_cases": 6, + "wamr_asan_trace_modes": [ + "pending", + "early", + "ordinary" + ], + "feature_off_executable_sha256": "783554184715d4d6429534732107893c68b481b21c8d61dca34ae2d2baf950be", + "embedded_http2_elf_sha256": "b8889f994b926dfb91799e00d74a5251b215e01b8a4759e33eb41f719762f495", + "disabled_artifacts_byte_identical": true, + "aot_dwarf_registration": "NOT_RUN: installed LLVM 21 lacks LLDB development library", + "runs": { + "wamr_interpreter": { + "runtime": "wamr", + "debugger": "lldb", + "platform": "macOS-27.2-arm64-arm-64bit-Mach-O", + "swift": true, + "ordinary": false, + "native_frame_order": [ + "native_checkpoint", + "nativeSwift", + "native_rust", + "native_cpp", + "native_import", + "wasm_ext_call_resume" + ], + "guest_trace": "\n#00: 0x0056 - leaf\n#01: 0x0010 - middle\n#02: 0x0010 - root\n\nguest_trace_count=3\nguest_frame index=1 function_offset=86 module_offset=0\nguest_frame index=2 function_offset=16 module_offset=0\nguest_frame index=3 function_offset=16 module_offset=0\n", + "full_mixed_guest_stack_qualified": false, + "apple_crash": { + "exception": { + "codes": "0x0000000000000001, 0x00000001021b0844", + "rawCodes": [ + 1, + 4330293316 + ], + "type": "EXC_BREAKPOINT", + "signal": "SIGTRAP" + }, + "binary_uuid": "B60023B0-264B-3349-A975-9383FF6ABE2E", + "native_symbols": [ + "native_checkpoint", + "nativeSwift(_:)", + "native_rust", + "native_cpp", + "native_import", + "ext_host", + "wasm_runtime_invoke_c_api_native", + "wasm_interp_call_func_native", + "wasm_interp_call_func_bytecode", + "wasm_interp_call_wasm", + "wasm_call_function", + "wasm_ext_call_resume", + "main", + "start" + ], + "dsym_uuid_matches": true + }, + "native_backtrace": [ + "frame #0: 0x0000000100000830 wamr_stack_audit`native_checkpoint(value=20) at native.c:37:9 [opt]", + "* frame #0: 0x0000000100000830 wamr_stack_audit`native_checkpoint(value=20) at native.c:37:9 [opt]", + "frame #1: 0x0000000100000e34 wamr_stack_audit`nativeSwift(value=) at native.swift:7:12 [opt]", + "frame #2: 0x0000000100000e08 wamr_stack_audit`native_rust(value=) at native.rs:9:35 [opt]", + "frame #3: 0x0000000100000de0 wamr_stack_audit`native_cpp(value=) at native.cpp:8:27 [opt]", + "frame #4: 0x0000000100000d64 wamr_stack_audit`native_import(env=, args=0x000000016fdfd760, results=, trap=) at native.c:54:5 [opt]", + "frame #5: 0x000000010000925c wamr_stack_audit`ext_host(env=0x0000000100634a00, args=, results=) at wasm_c_api_continuation.inc:93:14 [opt]", + "frame #6: 0x000000010000d320 wamr_stack_audit`wasm_runtime_invoke_c_api_native(module_inst=0x0000000100634d40, func_ptr=0x00000001000091f8, func_type=0x0000000100634a60, argc=, argv=0x0000007aa082c1e8, with_env=true, wasm_c_api_env=0x0000000100634a00) at wasm_runtime_common.c:7445:16 [opt]", + "frame #7: 0x000000010000dd90 wamr_stack_audit`wasm_interp_call_func_native(module_inst=0x0000000100634d40, exec_env=0x0000007aa082c000, cur_func=, prev_frame=) at wasm_interp_fast.c:1256:15 [opt]", + "frame #8: 0x000000010003a984 wamr_stack_audit`wasm_interp_call_func_bytecode(module=, exec_env=0x0000007aa082c000, cur_func=0x0000000100634f00, prev_frame=0x0000007aa082c174) at wasm_interp_fast.c:7709:17 [opt]", + "frame #9: 0x000000010000db48 wamr_stack_audit`wasm_interp_call_wasm(module_inst=0x0000000100634d40, exec_env=, function=, argc=, argv=0x0000000100635470) at wasm_interp_fast.c:8014:9 [opt]", + "frame #10: 0x00000001000258f0 wamr_stack_audit`wasm_call_function(exec_env=0x0000007aa082c000, function=0x0000000100634f90, argc=1, argv=0x0000000100635470) at wasm_runtime.c:3722:5 [opt]", + "frame #11: 0x0000000100009b08 wamr_stack_audit`wasm_ext_call_resume(call=, completion=, results=0x000000016fdfde10, out_trap=0x000000016fdfdd90) at wasm_c_api_continuation.inc:274:14 [opt]", + "frame #12: 0x0000000100000b44 wamr_stack_audit`main(argc=, argv=) at native.c:117:22 [opt]", + "frame #13: 0x0000000190bacd7c dyld`start + 6652", + "frame #0: 0x0000000100000848 wamr_stack_audit`parked_checkpoint at native.c:45:11 [opt]", + "* frame #0: 0x0000000100000848 wamr_stack_audit`parked_checkpoint at native.c:45:11 [opt]", + "frame #1: 0x0000000100000b54 wamr_stack_audit`main(argc=, argv=) at native.c:120:13 [opt]", + "frame #2: 0x0000000190bacd7c dyld`start + 6652" + ], + "apple_symbolicated": [ + "native_checkpoint (in wamr_stack_audit) (native.c:38)", + "nativeSwift(_:) (in wamr_stack_audit) (native.swift:7)", + "native_rust (in wamr_stack_audit) (native.rs:9)", + "native_cpp (in wamr_stack_audit) (native.cpp:8)", + "native_import (in wamr_stack_audit) (native.c:54)", + "ext_host (in wamr_stack_audit) (wasm_c_api_continuation.inc:94)", + "wasm_runtime_invoke_c_api_native (in wamr_stack_audit) (wasm_runtime_common.c:0)", + "wasm_interp_call_func_native (in wamr_stack_audit) (wasm_interp_fast.c:1260)", + "wasm_interp_call_func_bytecode (in wamr_stack_audit) (wasm_interp_fast.c:0)", + "wasm_interp_call_wasm (in wamr_stack_audit) (wasm_interp_fast.c:0)", + "wasm_call_function (in wamr_stack_audit) (wasm_runtime.c:3723)", + "wasm_ext_call_resume (in wamr_stack_audit) (wasm_c_api_continuation.inc:0)", + "main (in wamr_stack_audit) (native.c:118)" + ] + }, + "tinywasm_interpreter": { + "runtime": "tinywasm", + "debugger": "lldb", + "platform": "macOS-27.2-arm64-arm-64bit-Mach-O", + "swift": true, + "ordinary": false, + "native_frame_order": [ + "native_checkpoint", + "nativeSwift", + "native_rust", + "native_cpp", + "native_import", + "wasm_ext_call_resume" + ], + "guest_trace": "guest_trace_count=0\n", + "full_mixed_guest_stack_qualified": false, + "apple_crash": { + "exception": { + "codes": "0x0000000000000001, 0x0000000104cec914", + "rawCodes": [ + 1, + 4375628052 + ], + "type": "EXC_BREAKPOINT", + "signal": "SIGTRAP" + }, + "binary_uuid": "ABFEB7FE-5220-3A47-94BD-67B69D4CF002", + "native_symbols": [ + "native_checkpoint", + "nativeSwift(_:)", + "native_rust", + "native_cpp", + "native_import", + "wasm_ext_call_resume", + "main", + "start" + ], + "dsym_uuid_matches": true + }, + "native_backtrace": [ + "frame #0: 0x0000000100000900 tinywasm_stack_audit`native_checkpoint(value=20) at native.c:37:9 [opt]", + "* frame #0: 0x0000000100000900 tinywasm_stack_audit`native_checkpoint(value=20) at native.c:37:9 [opt]", + "frame #1: 0x0000000100000e88 tinywasm_stack_audit`nativeSwift(value=) at native.swift:7:12 [opt]", + "frame #2: 0x0000000100000e5c tinywasm_stack_audit`native_rust(value=) at native.rs:9:35 [opt]", + "frame #3: 0x0000000100000e34 tinywasm_stack_audit`native_cpp(value=) at native.cpp:8:27 [opt]", + "frame #4: 0x0000000100000db8 tinywasm_stack_audit`native_import(env=, args=0x000000016fdfdac8, results=, trap=) at native.c:54:5 [opt]", + "frame #5: 0x000000010000c320 tinywasm_stack_audit`wasm_ext_call_resume + 3872", + "frame #6: 0x0000000100000be8 tinywasm_stack_audit`main(argc=, argv=) at native.c:117:22 [opt]", + "frame #7: 0x0000000190bacd7c dyld`start + 6652", + "frame #0: 0x0000000100000918 tinywasm_stack_audit`parked_checkpoint at native.c:45:11 [opt]", + "* frame #0: 0x0000000100000918 tinywasm_stack_audit`parked_checkpoint at native.c:45:11 [opt]", + "frame #1: 0x0000000100000bf8 tinywasm_stack_audit`main(argc=, argv=) at native.c:120:13 [opt]", + "frame #2: 0x0000000190bacd7c dyld`start + 6652" + ], + "apple_symbolicated": [ + "native_checkpoint (in tinywasm_stack_audit) (native.c:38)", + "nativeSwift(_:) (in tinywasm_stack_audit) (native.swift:7)", + "native_rust (in tinywasm_stack_audit) (native.rs:9)", + "native_cpp (in tinywasm_stack_audit) (native.cpp:8)", + "native_import (in tinywasm_stack_audit) (native.c:54)", + "wasm_ext_call_resume (in tinywasm_stack_audit) + 3872", + "main (in tinywasm_stack_audit) (native.c:118)" + ] + }, + "wamr_aot": { + "runtime": "wamr", + "debugger": "lldb", + "platform": "macOS-27.2-arm64-arm-64bit-Mach-O", + "swift": true, + "ordinary": true, + "native_frame_order": [ + "native_checkpoint", + "nativeSwift", + "native_rust", + "native_cpp", + "sync_import", + "wasm_func_call" + ], + "guest_trace": "\n#00: 0x0049 - $f1\n#01: 0x004f - $f2\n#02: 0x0059 - run\n\nguest_trace_count=3\nguest_frame index=1 function_offset=73 module_offset=0\nguest_frame index=2 function_offset=79 module_offset=0\nguest_frame index=3 function_offset=89 module_offset=0\n", + "full_mixed_guest_stack_qualified": false, + "native_backtrace": [ + "frame #0: 0x0000000100000830 wamr_stack_audit`native_checkpoint(value=20) at native.c:37:9 [opt]", + "* frame #0: 0x0000000100000830 wamr_stack_audit`native_checkpoint(value=20) at native.c:37:9 [opt]", + "frame #1: 0x0000000100000e34 wamr_stack_audit`nativeSwift(value=) at native.swift:7:12 [opt]", + "frame #2: 0x0000000100000e08 wamr_stack_audit`native_rust(value=) at native.rs:9:35 [opt]", + "frame #3: 0x0000000100000de0 wamr_stack_audit`native_cpp(value=) at native.cpp:8:27 [opt]", + "frame #4: 0x0000000100000d2c wamr_stack_audit`sync_import(env=, args=, results=0x000000016fdfd7c0) at native.c:62:31 [opt]", + "frame #5: 0x000000010000d320 wamr_stack_audit`wasm_runtime_invoke_c_api_native(module_inst=0x0000000100609210, func_ptr=0x0000000100000d08, func_type=0x0000000100608f30, argc=, argv=0x0000000100609790, with_env=true, wasm_c_api_env=0x0000000000000000) at wasm_runtime_common.c:7445:16 [opt]", + "frame #6: 0x0000000100080364", + "frame #7: 0x000000010002fbb8 wamr_stack_audit`invoke_native_internal(exec_env=, func_ptr=, func_type=, signature=0x0000000000000000, attachment=, argv=, argc=, argv_ret=) at aot_runtime.c:2576:9 [opt]", + "frame #8: 0x000000010002fb98 wamr_stack_audit`aot_call_function(exec_env=, function=, argc=, argv=) at aot_runtime.c:2800:15 [opt]", + "frame #9: 0x000000010000792c wamr_stack_audit`wasm_runtime_call_wasm(exec_env=, function=, argc=1, argv=0x000000016fdfdc80) at wasm_runtime_common.c:2804:15 [opt] [inlined]", + "frame #10: 0x00000001000078d4 wamr_stack_audit`wasm_func_call(func=0x00000078930001c0, params=, results=) at wasm_c_api.c:3471:10 [opt]", + "frame #11: 0x0000000100000b00 wamr_stack_audit`main(argc=, argv=) at native.c:108:16 [opt]", + "frame #12: 0x0000000190bacd7c dyld`start + 6652" + ] + } + }, + "source_sha256": { + "native.rs": "6c8fd7356dbd368e671c88b23433a5dfaa041a51ae310fc1aacc96f68221a4ea", + "run.py": "2e17bdbb3aabdb695a98245d9e09bb911da1b8c5922555523a19e5f530a40f07", + "native.swift": "fcb299ebbb0010434d8d8be1dcc757952b788e41bb09aa6c85e7da3952d04256", + "guest.wasm": "fb2b5328292da4b82d122ff5daa19d839304c5fb82c3682bd09291ebda4aceb2", + "native.cpp": "8386a057ee92ca34a948ae6275e2ebc6400f98e3eac7adfa65ac70ba818527f1", + "guest.wat": "e022873eaeb98e17fec111e442356172cf53f072e616da483fff2434c06c484b", + "native.c": "50542903fe608a6b806fb3d74996f0a464e1c156b3a7a651e278eabe4c3e9615", + "bridge.c": "7f3df406a3746a33ed2075609edcea0dff2d5842aa3230d5a37a5c13a0d02e74" + } +} diff --git a/crates/c-api/tests/continuations/debug/run.py b/crates/c-api/tests/continuations/debug/run.py new file mode 100644 index 0000000..0f105a7 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/run.py @@ -0,0 +1,136 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Exercise native unwinding; guest/async trace limitations are recorded separately.""" +import argparse +import json +import os +from pathlib import Path +import platform +import subprocess +import time + +p = argparse.ArgumentParser() +p.add_argument('--runtime', choices=['wamr', 'tinywasm'], required=True) +p.add_argument('--include', type=Path, required=True) +p.add_argument('--library', type=Path, required=True) +p.add_argument('--build', type=Path, required=True) +p.add_argument('--debugger', choices=['lldb', 'gdb'], required=True) +p.add_argument('--swift', action='store_true') +p.add_argument('--apple-crash', action='store_true', help='intentionally crash the fixture and verify an Apple .ips report') +p.add_argument('--guest', type=Path) +p.add_argument('--ordinary', action='store_true', help='ordinary calls, including WAMR AOT') +a = p.parse_args() +source = Path(__file__).resolve().parent +build = a.build.resolve() +build.mkdir(parents=True, exist_ok=True) +guest = (a.guest or source/'guest.wasm').resolve() +mac = platform.system() == 'Darwin' +if a.swift and not mac: + p.error('this fixture uses the macOS Swift toolchain') +if a.apple_crash and (not mac or a.ordinary): + p.error('--apple-crash requires macOS and the continuation fixture') + +def run(cmd, **kwargs): + print(' '.join(map(str, cmd)), flush=True) + return subprocess.run(list(map(str, cmd)), check=True, timeout=300, **kwargs) + +cc = os.environ.get('CC', 'cc') +cxx = os.environ.get('CXX', 'c++') +flags = ['-g', '-O2', '-fno-omit-frame-pointer', '-Wall', '-Wextra', '-Werror'] +defines = ['-DSTACK_AUDIT_NATIVE'] +if a.runtime == 'wamr': + defines += ['-DEXPECT_GUEST_TRACE'] +run([cc, *flags, *defines, '-I'+str(a.include.resolve()), '-c', source/'native.c', '-o', build/'native.o']) +run([cxx, *flags, '-c', source/'native.cpp', '-o', build/'cpp.o']) +run([os.environ.get('RUSTC', 'rustc'), '--crate-type=lib', '--emit=obj', '--edition=2024', + '-Copt-level=2', '-Cdebuginfo=2', '-Cpanic=abort', '-Cforce-frame-pointers=yes', + source/'native.rs', '-o', build/'rust.o']) +if a.swift: + linker = ['xcrun', 'swiftc', '-module-cache-path', build/'swiftcache'] + run([*linker, '-g', '-O', '-parse-as-library', '-emit-object', source/'native.swift', '-o', build/'bridge.o']) +else: + linker = [cxx] + run([cc, *flags, '-c', source/'bridge.c', '-o', build/'bridge.o']) +exe = build/(a.runtime+'_stack_audit') +libs = ['-framework', 'Security', '-framework', 'CoreFoundation', '-liconv'] if mac else ['-lm', '-lpthread', '-ldl'] +run([*linker, '-g', build/'native.o', build/'cpp.o', build/'rust.o', build/'bridge.o', + a.library.resolve(), '-o', exe, *libs, + *(['-Xlinker', '-object_path_lto', '-Xlinker', build/'linked_lto.o'] if mac else [])]) +if mac: + run(['xcrun', 'dsymutil', exe]) +mode = ['ordinary'] if a.ordinary else [] +normal = run([exe, guest, *mode], capture_output=True, text=True) +(build/'guest_trace.txt').write_text(normal.stdout) +if not a.ordinary: + run([exe, guest, 'early'], capture_output=True, text=True) + +if a.debugger == 'lldb': + commands = ['settings set plugin.jit-loader.gdb.enable on', 'breakpoint set -n native_checkpoint'] + if not a.ordinary: + commands += ['breakpoint set -n parked_checkpoint'] + commands += ['run', 'bt 32', 'continue'] + if not a.ordinary: + commands += ['bt 32', 'continue'] + cmd = ['lldb', '--batch', *[x for c in commands for x in ['-o', c]], '--', exe, guest, *mode] +else: + commands = ['set pagination off', 'break native_checkpoint'] + if not a.ordinary: + commands += ['break parked_checkpoint'] + commands += ['run', 'bt 32', 'continue'] + if not a.ordinary: + commands += ['bt 32', 'continue'] + cmd = ['gdb', '--batch', *[x for c in commands for x in ['-ex', c]], '--args', exe, guest, *mode] +result = run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) +(build/'debugger.txt').write_text(result.stdout) +expected = ['native_checkpoint', 'nativeSwift' if a.swift else 'native_bridge', 'native_rust', 'native_cpp', + 'sync_import' if a.ordinary else 'native_import', 'wasm_func_call' if a.ordinary else 'wasm_ext_call_resume'] +frames = [line for line in result.stdout.splitlines() + if ('frame #' in line or line.startswith('#'))] +text = '\n'.join(frames) +position = 0 +for symbol in expected: + position = text.find(symbol, position) + if position < 0: + raise RuntimeError('missing or misordered native frame: '+symbol+'; see debugger.txt') + position += len(symbol) +if not a.ordinary and 'parked_checkpoint' not in text: + raise RuntimeError('missing suspended checkpoint') +receipt = {'runtime': a.runtime, 'debugger': a.debugger, 'platform': platform.platform(), + 'swift': a.swift, 'ordinary': a.ordinary, 'native_frame_order': expected, + 'guest_trace': normal.stdout, 'full_mixed_guest_stack_qualified': False} +if a.apple_crash: + reports = Path.home()/'Library/Logs/DiagnosticReports' + before = set(reports.glob(exe.name+'*.ips')) + crashed = subprocess.run([str(exe), str(guest), 'crash'], capture_output=True, timeout=30) + if crashed.returncode != -5: + raise RuntimeError('expected SIGTRAP from the deliberate native crash') + deadline = time.monotonic()+30 + while time.monotonic() < deadline: + found = set(reports.glob(exe.name+'*.ips'))-before + if found: + break + time.sleep(0.25) + else: + raise RuntimeError('Apple did not produce an .ips report within 30 seconds') + report = json.loads(max(found, key=lambda x:x.stat().st_mtime).read_text().split('\n', 1)[1]) + frames = next(t for t in report['threads'] if t.get('triggered'))['frames'] + symbols = [f.get('symbol', '') for f in frames] + position = 0 + for symbol in expected: + matches = [i for i in range(position, len(symbols)) if symbol in symbols[i]] + if not matches: + raise RuntimeError('Apple report is missing native frame: '+symbol) + position = matches[0]+1 + image = report['usedImages'][frames[0]['imageIndex']] + dsym = Path(str(exe)+'.dSYM')/'Contents/Resources/DWARF'/exe.name + uuid = subprocess.check_output(['xcrun', 'dwarfdump', '--uuid', str(dsym)], text=True).split()[1] + if uuid.lower() != image['uuid'].lower(): + raise RuntimeError('crash report and dSYM UUID mismatch') + native_frames = [f for f in frames if f['imageIndex'] == frames[0]['imageIndex']] + symbolicated = subprocess.check_output(['xcrun', 'atos', '-o', str(dsym), '-l', hex(image['base']), + *[hex(image['base']+f['imageOffset']) for f in native_frames]], text=True) + (build/'apple_symbolicated.txt').write_text(symbolicated) + receipt['apple_crash'] = {'exception': report['exception'], 'binary_uuid': uuid, + 'native_symbols': symbols, 'dsym_uuid_matches': True} +(build/'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n') +print('PASS native unwind order; full guest/async stack remains a separate requirement') diff --git a/crates/c-api/tests/continuations/run.py b/crates/c-api/tests/continuations/run.py index 9d43524..55d6b2f 100644 --- a/crates/c-api/tests/continuations/run.py +++ b/crates/c-api/tests/continuations/run.py @@ -13,6 +13,7 @@ p.add_argument('--root', type=Path, required=True) p.add_argument('--build', type=Path, required=True) p.add_argument('--feature', choices=['on', 'off'], default='on') +p.add_argument('--call-stacks', action='store_true', help='enable WAMR guest trap trace capture') p.add_argument('--gc', action='store_true', help='exercise guest GC across pending calls') p.add_argument('--simd', action='store_true', help='exercise guest SIMD across pending calls') p.add_argument('--aot', action='store_true', help='build the WAMR AOT backend alongside the interpreter') @@ -35,6 +36,8 @@ p.error('Rust ASan does not support Windows MSVC; --client-asan is explicitly partial coverage') if a.ubsan and (not a.sanitize or win or a.runtime != 'wamr'): p.error('--ubsan requires --sanitize and a Unix WAMR build') +if a.call_stacks and a.runtime != 'wamr': + p.error('--call-stacks currently applies only to WAMR') if a.aot and a.runtime != 'wamr': p.error('--aot applies only to WAMR') if a.aot_compiler and (a.runtime != 'wamr' or not (a.gc or a.simd) or a.feature != 'on'): @@ -130,7 +133,7 @@ def client(source, exe, inc, lib=None, defines=()): lib /= 'release/tinywasm.lib' if win else 'release/libtinywasm.a' else: target = 'windows' if win else 'darwin' if mac else 'linux' - cmd = ['cmake', '-S', root/'product-mini/platforms'/target, '-B', build/'runtime', '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_INTERPROCEDURAL_OPTIMIZATION='+('OFF' if san else 'ON'), '-DWAMR_BUILD_INTERP=1', '-DWAMR_BUILD_FAST_INTERP=1', '-DWAMR_BUILD_AOT='+('1' if a.aot or a.aot_compiler else '0'), '-DWAMR_BUILD_JIT=0', '-DWAMR_BUILD_FAST_JIT=0', '-DWAMR_BUILD_LIBC_WASI=0', '-DWAMR_BUILD_LIBC_BUILTIN=0', '-DWAMR_BUILD_SIMD='+('1' if a.simd else '0'), '-DWAMR_BUILD_GC='+('1' if a.gc else '0'), '-DWAMR_BUILD_STRINGREF=0', '-DWAMR_DISABLE_HW_BOUND_CHECK=1', '-DWAMR_BUILD_C_API_CONTINUATIONS='+('1' if a.feature=='on' else '0')] + cmd = ['cmake', '-S', root/'product-mini/platforms'/target, '-B', build/'runtime', '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_INTERPROCEDURAL_OPTIMIZATION='+('OFF' if san else 'ON'), '-DWAMR_BUILD_INTERP=1', '-DWAMR_BUILD_FAST_INTERP=1', '-DWAMR_BUILD_AOT='+('1' if a.aot or a.aot_compiler else '0'), '-DWAMR_BUILD_JIT=0', '-DWAMR_BUILD_FAST_JIT=0', '-DWAMR_BUILD_LIBC_WASI=0', '-DWAMR_BUILD_LIBC_BUILTIN=0', '-DWAMR_BUILD_SIMD='+('1' if a.simd else '0'), '-DWAMR_BUILD_GC='+('1' if a.gc else '0'), '-DWAMR_BUILD_STRINGREF=0', '-DWAMR_BUILD_DUMP_CALL_STACK='+('1' if a.call_stacks else '0'), '-DWAMR_DISABLE_HW_BOUND_CHECK=1', '-DWAMR_BUILD_C_API_CONTINUATIONS='+('1' if a.feature=='on' else '0')] if san: flags = '/MD /O1 /Zi /fsanitize=address /DNDEBUG' if win else '-O1 -g -DNDEBUG '+' '.join(sanitize_flags) cmd += ['-DCMAKE_C_FLAGS_RELEASE='+flags, '-DCMAKE_CXX_FLAGS_RELEASE='+flags] From 60072ee0382c87b80c32d6583200bffef86f9bce Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Tue, 3 Nov 2026 23:36:19 -0800 Subject: [PATCH 17/20] feat: add optional guest source snapshots and async failure completion --- crates/c-api/Cargo.toml | 2 + crates/c-api/include/wasm_ext.h | 6 + crates/c-api/include/wasm_ext_debug.h | 47 +++++++ crates/c-api/src/continuation.rs | 116 +++++++++++++++++- crates/c-api/src/objects.rs | 5 +- crates/c-api/src/runtime.rs | 14 +++ crates/parser/Cargo.toml | 2 + crates/parser/src/emitter.rs | 51 +++++++- crates/parser/src/visit.rs | 16 +++ crates/tinywasm/Cargo.toml | 2 + crates/tinywasm/src/diagnostics.rs | 71 +++++++++++ crates/tinywasm/src/func/host_execution.rs | 25 +++- crates/tinywasm/src/instance.rs | 11 +- .../src/interpreter/executor/budget.rs | 2 + .../tinywasm/src/interpreter/executor/mod.rs | 8 ++ .../src/interpreter/stack/call_stack.rs | 5 + crates/tinywasm/src/lib.rs | 2 + crates/tinywasm/src/store/function.rs | 11 ++ crates/tinywasm/src/store/mod.rs | 12 +- crates/tinywasm/tests/host_execution.rs | 43 +++++++ crates/types/Cargo.toml | 2 + crates/types/src/lib.rs | 4 + 22 files changed, 443 insertions(+), 14 deletions(-) create mode 100644 crates/c-api/include/wasm_ext_debug.h create mode 100644 crates/tinywasm/src/diagnostics.rs diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml index b02bc0c..3104ca1 100644 --- a/crates/c-api/Cargo.toml +++ b/crates/c-api/Cargo.toml @@ -16,6 +16,8 @@ name = "tinywasm" tinywasm = { workspace = true, features = ["archive", "parser", "std", "validate"] } [features] +# Optional bounded diagnostic snapshots and original Wasm source offsets. +diagnostics = ["external-host-calls", "tinywasm/diagnostics"] external-host-calls = ["tinywasm/external-host-calls"] custom-prefix = [] diff --git a/crates/c-api/include/wasm_ext.h b/crates/c-api/include/wasm_ext.h index 3b3d4e8..74c7d05 100644 --- a/crates/c-api/include/wasm_ext.h +++ b/crates/c-api/include/wasm_ext.h @@ -48,6 +48,12 @@ wasm_ext_call_new(const wasm_func_t *, const wasm_val_vec_t *); WASM_API_EXTERN wasm_ext_status_t wasm_ext_call_resume(wasm_ext_call_t *, const wasm_val_vec_t *completion, wasm_val_vec_t *results, wasm_trap_t **out_trap); +/* Terminal failure of a pending host operation. Message is borrowed; out_trap + * receives an owned trap. Does not invoke cancel: the producer has completed. + * Rejects nonpending/terminal calls without consuming them. This is a trap, + * not guest exception injection. The consumer fences stale generation IDs. */ +WASM_API_EXTERN wasm_ext_status_t +wasm_ext_call_fail(wasm_ext_call_t *, const wasm_name_t *, wasm_trap_t **); WASM_API_EXTERN void wasm_ext_call_cancel(wasm_ext_call_t *); WASM_API_EXTERN void diff --git a/crates/c-api/include/wasm_ext_debug.h b/crates/c-api/include/wasm_ext_debug.h new file mode 100644 index 0000000..1a9f2ac --- /dev/null +++ b/crates/c-api/include/wasm_ext_debug.h @@ -0,0 +1,47 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#ifndef WASM_EXT_DEBUG_H +#define WASM_EXT_DEBUG_H +#include "wasm.h" +#include "wasm_ext.h" +#ifdef __cplusplus +extern "C" { +#endif +/* Experimental v1, separately opt-in. No pointers, locals, or guest memory in + * snapshots. Instances are store-local ordinals (WAMR: 0), joined by the + * consumer to a content-hashed original module. Offsets are ORIGINAL Wasm + * file offsets, never interpreter IR offsets. Zero flags means unmapped. + * Frames are youngest first; tail-eliminated callers are absent. */ +#define WASM_EXT_DEBUG_FRAMES 32 +#define WASM_EXT_DEBUG_MAPPED 1 +#define WASM_EXT_DEBUG_TRUNCATED 1 +#define WASM_EXT_DEBUG_VERSION 1 +typedef struct wasm_ext_debug_frame_t { + uint32_t instance, function, offset, flags; +} wasm_ext_debug_frame_t; +typedef struct wasm_ext_debug_snapshot_t { + uint32_t version, size, count, flags; + wasm_ext_debug_frame_t frames[WASM_EXT_DEBUG_FRAMES]; +} wasm_ext_debug_snapshot_t; +enum { + WASM_EXT_DEBUG_START = 1, WASM_EXT_DEBUG_HOST = 2, + WASM_EXT_DEBUG_PENDING = 3, WASM_EXT_DEBUG_RESUME = 4, + WASM_EXT_DEBUG_TRAP = 5, WASM_EXT_DEBUG_CANCEL = 6, + WASM_EXT_DEBUG_DONE = 7, WASM_EXT_DEBUG_ASYNC_ERROR = 8 +}; +/* Called on the owner thread at safe boundaries, never from a signal handler. + * The snapshot is borrowed only for this callback. Copy it to consumer-owned + * bounded storage. Do not reenter ANY runtime/call API, throw, or longjmp. + * Register once before the first resume; env outlives call. NULL disables. + * No allocations during capture; source-map storage is allocated at load. + * Native crashes inside an import see HOST, while suspended tasks see PENDING. + * RESUME retains the await-site, DONE clears frames; TRAP captures the fault. + * Missing debug information must remain visibly unmapped in the symbolizer. */ +typedef void (*wasm_ext_debug_observer_t)(void *, uint32_t, + const wasm_ext_debug_snapshot_t *); +WASM_API_EXTERN bool +wasm_ext_call_observe(wasm_ext_call_t *, wasm_ext_debug_observer_t, void *); +#ifdef __cplusplus +} +#endif +#endif diff --git a/crates/c-api/src/continuation.rs b/crates/c-api/src/continuation.rs index 9eeb388..dc45c15 100644 --- a/crates/c-api/src/continuation.rs +++ b/crates/c-api/src/continuation.rs @@ -23,6 +23,12 @@ pub struct wasm_ext_call_t { state: Rc, pending: Option>, status: i32, + #[cfg(feature = "diagnostics")] + observer: Option, + #[cfg(feature = "diagnostics")] + debug_env: *mut c_void, + #[cfg(feature = "diagnostics")] + started: bool, } fn numeric(ty: &tinywasm::types::FuncType) -> bool { ty.results().len() <= 1 @@ -75,7 +81,10 @@ export! { pub unsafe extern "C" fn wasm_ext_call_new(func: *const wasm_func_t, a Ok((args, count)) }); let Ok((args, count)) = prepared else { return ptr::null_mut(); }; - let mut call = Box::new(wasm_ext_call_t { execution: None, values: vec![WasmValue::I32(0);count].into_boxed_slice(), result_count: count, state, pending: None, status: 1 }); + let mut call = Box::new(wasm_ext_call_t { execution: None, values: vec![WasmValue::I32(0);count].into_boxed_slice(), result_count: count, state, pending: None, status: 1, + #[cfg(feature = "diagnostics")] observer: None, + #[cfg(feature = "diagnostics")] debug_env: ptr::null_mut(), + #[cfg(feature = "diagnostics")] started: false }); call.state.ext_reserved.set(true); // SAFETY: reservation excludes all idle C access; Rc pins the Store. Box pins results. // The execution is dropped before either backing object and before releasing reservation. @@ -88,7 +97,23 @@ export! { pub unsafe extern "C" fn wasm_ext_call_new(func: *const wasm_func_t, a } }} impl wasm_ext_call_t { + #[cfg(feature = "diagnostics")] + fn debug_event(&self, event: u32) { + if let Some(observer) = self.observer { + let snapshot = if event == 1 || event == 7 { + tinywasm::diagnostics::DebugSnapshot::default() + } else { + self.execution.as_ref().unwrap().debug_snapshot() + }; + unsafe { + observer(self.debug_env, event, &snapshot); + } + } + } + fn finish(&mut self, status: i32) { + #[cfg(feature = "diagnostics")] + self.state.debug_observer.set(None); self.execution.take(); self.state.ext_reserved.set(false); self.status = status; @@ -97,6 +122,8 @@ impl wasm_ext_call_t { if self.execution.is_none() { return; } + #[cfg(feature = "diagnostics")] + self.debug_event(6); self.status = 2; if let Some(host) = self.pending.take() && let Some(cancel) = host.cancel @@ -133,6 +160,8 @@ export! { pub unsafe extern "C" fn wasm_ext_call_resume(call: *mut wasm_ext_call if host_call.complete(&input).is_err() { return -1; } call.pending = None; } else if !completion.is_null() { return -1; } + #[cfg(feature = "diagnostics")] + { call.debug_event(if call.started { 4 } else { 1 }); call.started = true; } let state = call.state.clone(); let mut last = None; loop { @@ -145,6 +174,7 @@ export! { pub unsafe extern "C" fn wasm_ext_call_resume(call: *mut wasm_ext_call match progress { Ok(HostExecProgress::Suspended) => continue, Ok(HostExecProgress::Completed) => { + #[cfg(feature = "diagnostics")] call.debug_event(7); call.execution.take(); // End the exclusive result-buffer borrow before reading it. // Numeric v1 results need no store lookup or reference allocation. for (i,v) in call.values.iter().enumerate() { @@ -153,7 +183,7 @@ export! { pub unsafe extern "C" fn wasm_ext_call_resume(call: *mut wasm_ext_call } call.finish(0); return 0; } - Err(error) => { unsafe { *out_trap = call.state.trap.borrow_mut().take().map_or_else(|| trap(&call.state,error.to_string()), |object| crate::boxed(wasm_ref_t(object))); } call.finish(-1); return -1; } + Err(error) => { #[cfg(feature = "diagnostics")] call.debug_event(5); unsafe { *out_trap = call.state.trap.borrow_mut().take().map_or_else(|| trap(&call.state,error.to_string()), |object| crate::boxed(wasm_ref_t(object))); } call.finish(-1); return -1; } Ok(HostExecProgress::HostCall) => { let mut request = call.execution.as_mut().unwrap().host_call().unwrap(); let reference = request.function().as_func_ref(request.store()).unwrap(); @@ -171,14 +201,19 @@ export! { pub unsafe extern "C" fn wasm_ext_call_resume(call: *mut wasm_ext_call if out.data!=data || out.size!=count { std::process::abort(); } // Callback owns neither vector; modifications to ownership are C contract violations. - if status==1 && trapped.is_null() { call.pending=Some(host); return 1; } - if status!=0 || !trapped.is_null() { unsafe{*out_trap=trapped;} call.finish(-1); return -1; } + if status==1 && trapped.is_null() { call.pending=Some(host); + #[cfg(feature = "diagnostics")] call.debug_event(3); + return 1; } + if status!=0 || !trapped.is_null() { + #[cfg(feature = "diagnostics")] call.debug_event(5); + unsafe{*out_trap=trapped;} call.finish(-1); return -1; } if !unsafe { out.as_slice() }.iter().zip(ty.results()).all(|(v,t)|Some(v.kind)==crate::types::value_kind(*t)) { + #[cfg(feature = "diagnostics")] call.debug_event(5); unsafe { *out_trap = trap(&call.state,"host result type mismatch"); } call.finish(-1); return -1; } let values=request.with_context(|mut ctx|unsafe{out.as_slice()}.iter().map(|v|unsafe{v.to_runtime(&state,ctx.store_mut())}).collect::>>()); - if values.and_then(|v|request.complete(&v)).is_err() { call.finish(-1); return -1; } + if values.and_then(|v|request.complete(&v)).is_err() { #[cfg(feature = "diagnostics")] call.debug_event(5); call.finish(-1); return -1; } } } } @@ -207,6 +242,55 @@ mod tests { let counts = unsafe { &*env.cast::() }; counts.cancels.set(counts.cancels.get() + 1); } + #[cfg(feature = "diagnostics")] + #[test] + fn observer_copies_and_async_failure_release_the_store() { + use std::cell::RefCell; + unsafe extern "C" fn observe( + env: *mut c_void, + event: u32, + snapshot: *const tinywasm::diagnostics::DebugSnapshot, + ) { + let events = unsafe { &*env.cast::>>() }; + events.borrow_mut().push((event, unsafe { *snapshot })); + } + let events = RefCell::new(Vec::<(u32, tinywasm::diagnostics::DebugSnapshot)>::new()); + let counts = Counts { calls: Cell::new(0), cancels: Cell::new(0) }; + unsafe { + let engine = wasm_engine_new(); + let store = wasm_store_new(engine); + let mut params = Vector::from_vec(vec![wasm_valtype_new(0)]); + let mut results = params.clone(); + let ty = wasm_functype_new(&mut params, &mut results); + let host = + wasm_ext_func_new(store, ty, Some(pending), (&counts as *const Counts).cast_mut().cast(), Some(cancel)); + wasm_functype_delete(ty); + let args = Vector::from_vec(vec![wasm_val_t { kind: 0, of: wasm_val_union { i32: 20 } }]); + let mut out = Vector::from_vec(vec![wasm_val_t::default()]); + let call = wasm_ext_call_new(host, &args); + assert!(wasm_ext_call_observe(call, Some(observe), (&events as *const RefCell<_>).cast_mut().cast())); + let mut error = ptr::null_mut(); + assert_eq!(wasm_ext_call_resume(call, ptr::null(), &mut out, &mut error), 1); + assert!(!wasm_ext_call_observe(call, None, ptr::null_mut())); + let message = Vector::from_vec(b"async operation failed".to_vec()); + assert_eq!(wasm_ext_call_fail(call, &message, &mut error), -1); + assert!(!error.is_null()); + wasm_trap_delete(error); + assert_eq!(wasm_ext_call_fail(call, &message, &mut error), -1); + assert!(error.is_null()); + assert!((&(*store).0).debug_observer.get().is_none()); + wasm_ext_call_delete(call); + assert_eq!(counts.cancels.get(), 0); + let events = events.borrow(); + assert_eq!(events.iter().map(|x| x.0).collect::>(), [1, 2, 3, 8]); + assert!( + events.iter().all(|(_, snapshot)| snapshot.version == 1 && snapshot.size == 528 && snapshot.count == 0) + ); + wasm_func_delete(host); + wasm_store_delete(store); + wasm_engine_delete(engine); + } + } #[test] fn owned_execution_releases_borrows_before_results_and_cancel() { let counts = Counts { calls: Cell::new(0), cancels: Cell::new(0) }; @@ -352,3 +436,25 @@ mod tests { } } } + +#[cfg(feature = "diagnostics")] +export! { pub unsafe extern "C" fn wasm_ext_call_observe(call: *mut wasm_ext_call_t, + observer: Option, env: *mut c_void) -> bool { + let Some(call) = (unsafe { call.as_mut() }) else { return false; }; + if call.started || call.execution.is_none() { return false; } + call.observer = observer; call.debug_env = env; + call.state.debug_observer.set(observer.map(|callback| (callback, env))); true +}} +export! { pub unsafe extern "C" fn wasm_ext_call_fail(call: *mut wasm_ext_call_t, + message: *const wasm_byte_vec_t, out_trap: *mut *mut wasm_trap_t) -> i32 { + if out_trap.is_null() { return -1; } + unsafe { *out_trap = ptr::null_mut(); } + let Some(call) = (unsafe { call.as_mut() }) else { return -1; }; + if call.execution.is_none() || call.pending.is_none() || message.is_null() + || unsafe { (*message).size != 0 && (*message).data.is_null() } { return -1; } + // The operation has failed, not been cancelled. Preserve its await-site snapshot. + unsafe { *out_trap = crate::objects::wasm_trap_new( + &mut wasm_store_t(call.state.clone()), message); } + #[cfg(feature = "diagnostics")] call.debug_event(8); + call.pending = None; call.finish(-1); -1 +}} diff --git a/crates/c-api/src/objects.rs b/crates/c-api/src/objects.rs index bb71799..708dfd7 100644 --- a/crates/c-api/src/objects.rs +++ b/crates/c-api/src/objects.rs @@ -310,9 +310,8 @@ export! { pub unsafe extern "C" fn wasm_trap_message(value: *const wasm_trap_t, unsafe { out.write(Vector::from_vec(message.clone())) }; }} -// TinyWasm currently has no public source-frame capture. The standard permits -// an unavailable origin and an empty trace. Frame accessors are provided for ABI -// completeness, but this implementation does not create frame objects yet. +// Standard trap objects remain message-only. Optional bounded source snapshots +// are delivered separately by wasm_ext_call_observe, before stack teardown. #[derive(Clone)] pub struct wasm_frame_t { instance: wasm_instance_t, diff --git a/crates/c-api/src/runtime.rs b/crates/c-api/src/runtime.rs index ad3d656..fad1a63 100644 --- a/crates/c-api/src/runtime.rs +++ b/crates/c-api/src/runtime.rs @@ -13,8 +13,13 @@ pub struct wasm_config_t(pub(crate) tinywasm::engine::Config); pub struct wasm_engine_t(Engine); pub struct wasm_store_t(pub(crate) Rc); +#[cfg(feature = "diagnostics")] +pub(crate) type DebugObserver = + unsafe extern "C" fn(*mut std::ffi::c_void, u32, *const tinywasm::diagnostics::DebugSnapshot); /// Thread-confined store ownership and the dynamic callback borrow stack. pub(crate) struct StoreState { + #[cfg(feature = "diagnostics")] + pub(crate) debug_observer: Cell>, pub(crate) store: UnsafeCell, #[cfg(feature = "external-host-calls")] pub(crate) ext_reserved: Cell, @@ -64,6 +69,8 @@ impl StoreState { } fn new(engine: Engine) -> Self { Self { + #[cfg(feature = "diagnostics")] + debug_observer: Cell::new(None), store: UnsafeCell::new(Store::new(engine)), #[cfg(feature = "external-host-calls")] ext_reserved: Cell::new(false), @@ -106,6 +113,13 @@ impl StoreState { /// Suspends the Rust callback context while C runs and may synchronously reenter. pub(crate) fn callback(&self, context: &mut FuncContext<'_>, action: impl FnOnce() -> T) -> T { + #[cfg(feature = "diagnostics")] + if let Some((observer, env)) = self.debug_observer.get() { + let snapshot = context.store().debug_snapshot(); + unsafe { + observer(env, 2, &snapshot); + } + } struct Restore<'a> { state: &'a StoreState, active: *mut FuncContext<'static>, diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index c630232..a62a3cb 100644 --- a/crates/parser/Cargo.toml +++ b/crates/parser/Cargo.toml @@ -19,6 +19,8 @@ wasmparser = { workspace = true, features = ["simd"] } wat.workspace = true [features] +# Optional bounded diagnostic snapshots and original Wasm source offsets. +diagnostics = ["tinywasm-types/diagnostics"] default = ["log", "parallel", "std", "validate"] # enable integration with the log crate diff --git a/crates/parser/src/emitter.rs b/crates/parser/src/emitter.rs index cb896bd..3279268 100644 --- a/crates/parser/src/emitter.rs +++ b/crates/parser/src/emitter.rs @@ -11,13 +11,24 @@ const LOOKBEHIND: usize = 3; /// Restricted vector view, with no readable or mutable access to the committed prefix. pub(crate) struct PendingTail<'a> { instructions: &'a mut Vec, + #[cfg(feature = "diagnostics")] + source_offsets: &'a mut Vec, start: usize, } impl<'a> PendingTail<'a> { - fn new(instructions: &'a mut Vec, start: usize) -> Self { + fn new( + instructions: &'a mut Vec, + start: usize, + #[cfg(feature = "diagnostics")] source_offsets: &'a mut Vec, + ) -> Self { assert!(start < instructions.len() && instructions.len() - start <= LOOKBEHIND + 1); - Self { instructions, start } + Self { + instructions, + start, + #[cfg(feature = "diagnostics")] + source_offsets, + } } /// Copies at most four pending instructions, including the incoming instruction. @@ -38,6 +49,14 @@ impl<'a> PendingTail<'a> { let start = pending.len().checked_sub(N).expect("replacement exceeds pending tail"); pending[start..start + M].copy_from_slice(&replacements); self.instructions.truncate(self.start + start + M); + #[cfg(feature = "diagnostics")] + { + // A fused instruction is attributed to the final operation (e.g. load), + // not an earlier operand producer. Call boundaries are never fused. + let offset = *self.source_offsets.last().unwrap(); + self.source_offsets.truncate(self.start + start + M); + self.source_offsets[self.start + start..].fill(offset); + } } } @@ -55,6 +74,10 @@ enum PatchSite { /// Owns one instruction vector with an immutable prefix and at most three pending instructions. pub(crate) struct Emitter { + #[cfg(feature = "diagnostics")] + pub(crate) source_offset: u32, + #[cfg(feature = "diagnostics")] + source_offsets: Vec, instructions: Vec, committed: usize, checkpoint_start: usize, @@ -80,6 +103,10 @@ impl Emitter { }; Self { instructions: Vec::with_capacity(body_size.min(1024)), + #[cfg(feature = "diagnostics")] + source_offset: 0, + #[cfg(feature = "diagnostics")] + source_offsets: Vec::new(), committed: 0, checkpoint_start: 0, checkpoint_interval: options.checkpoint_interval(), @@ -113,6 +140,8 @@ impl Emitter { debug_assert!(matches!(self.instructions.last(), Some(Instruction::Jump(_)))); self.pending_jump = None; self.instructions.pop(); + #[cfg(feature = "diagnostics")] + self.source_offsets.pop(); } self.seal_jump(); self.commit(self.instructions.len()); @@ -160,10 +189,14 @@ impl Emitter { // This permits at most LOOKBEHIND extra guest instructions per interval. Labels bound // at this boundary target the checkpoint, and exclusive exception ends exclude it. self.instructions.push(Instruction::Checkpoint); + #[cfg(feature = "diagnostics")] + self.source_offsets.push(self.source_offset); self.commit(self.instructions.len()); self.checkpoint_start = self.instructions.len(); } self.instructions.push(instruction); + #[cfg(feature = "diagnostics")] + self.source_offsets.push(self.source_offset); Ok(()) } @@ -184,7 +217,15 @@ impl Emitter { ) -> Result<()> { self.push(instruction)?; if self.select { - rules(&mut PendingTail::new(&mut self.instructions, self.committed), data)?; + rules( + &mut PendingTail::new( + &mut self.instructions, + self.committed, + #[cfg(feature = "diagnostics")] + &mut self.source_offsets, + ), + data, + )?; } self.commit(self.instructions.len().saturating_sub(LOOKBEHIND).max(self.committed)); Ok(()) @@ -322,6 +363,10 @@ impl Emitter { return Err(ParseError::Other("exception handler range out of bounds".into())); } } + #[cfg(feature = "diagnostics")] + { + data.source_offsets = self.source_offsets; + } Ok(self.instructions) } diff --git a/crates/parser/src/visit.rs b/crates/parser/src/visit.rs index 4799710..b3ba83a 100644 --- a/crates/parser/src/visit.rs +++ b/crates/parser/src/visit.rs @@ -85,6 +85,8 @@ pub(crate) struct ModuleMetadata { } pub(crate) struct FunctionDataBuilder { + #[cfg(feature = "diagnostics")] + pub(crate) source_offsets: Vec, pub(crate) operands64: Vec, pub(crate) operands128: Vec, pub(crate) branch_table_targets: Vec, @@ -96,6 +98,8 @@ pub(crate) struct FunctionDataBuilder { impl FunctionDataBuilder { pub(crate) fn new(deduplicate_operands: bool) -> Self { Self { + #[cfg(feature = "diagnostics")] + source_offsets: Vec::new(), operands64: Vec::new(), operands128: Vec::new(), branch_table_targets: Vec::new(), @@ -165,6 +169,8 @@ impl FunctionDataBuilder { pub(crate) fn finish(self) -> WasmFunctionData { WasmFunctionData { + #[cfg(feature = "diagnostics")] + source_offsets: self.source_offsets.into_boxed_slice(), operands64: self.operands64.into_boxed_slice(), operands128: self.operands128.into_boxed_slice(), branch_table_targets: self.branch_table_targets.into_boxed_slice(), @@ -472,6 +478,11 @@ pub(crate) fn process_operators( while !reader.eof() { let position = reader.original_position(); + #[cfg(feature = "diagnostics")] + { + builder.emitter.source_offset = + u32::try_from(position).map_err(|_| crate::ParseError::Other("source offset exceeds u32".into()))?; + } let res = if options.limits.max_br_table_targets.is_some() || options.limits.max_array_new_fixed_elements.is_some() { let op = reader @@ -531,6 +542,11 @@ pub(crate) fn process_operators_and_validate( while !reader.eof() { let position = reader.original_position(); + #[cfg(feature = "diagnostics")] + { + builder.emitter.source_offset = + u32::try_from(position).map_err(|_| crate::ParseError::Other("source offset exceeds u32".into()))?; + } let mut visitor = ValidateThenVisit { validator: &mut validator, builder: &mut builder, position }; let res = if options.limits.max_br_table_targets.is_some() || options.limits.max_array_new_fixed_elements.is_some() { diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index 44fac88..cf627e0 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -99,6 +99,8 @@ wat.workspace = true autocfg = "1.5" [features] +# Optional bounded diagnostic snapshots and original Wasm source offsets. +diagnostics = ["tinywasm-types/diagnostics", "tinywasm-parser?/diagnostics"] # expose host-call continuations for external schedulers; excluded from default and full external-host-calls = [] default = ["full", "nightly", "simd-intrinsics", "std"] diff --git a/crates/tinywasm/src/diagnostics.rs b/crates/tinywasm/src/diagnostics.rs new file mode 100644 index 0000000..92aaf0a --- /dev/null +++ b/crates/tinywasm/src/diagnostics.rs @@ -0,0 +1,71 @@ +//! Optional bounded logical guest frames. Never use the runtime from a fatal signal handler. +use crate::{ + Store, + interpreter::stack::{CallFrame, CallStack}, + store::Code, +}; + +/// A logical frame with an original Wasm file offset (flags bit 0), not an IR offset. +#[derive(Clone, Copy, Debug, Default)] +#[repr(C)] +pub struct DebugFrame { + /// Store-local instance ordinal; join using a consumer module manifest. + pub instance: u32, + /// Module-local function index. + pub function: u32, + /// Original Wasm file offset when mapped. + pub offset: u32, + /// Bit zero indicates a mapped offset (frame) or truncation (snapshot). + pub flags: u32, +} + +/// Versioned POD snapshot; youngest first, flags bit 0 means truncated. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct DebugSnapshot { + /// Snapshot ABI version, currently one. + pub version: u32, + /// Byte size of this versioned POD record. + pub size: u32, + /// Number of initialized frames, at most 32. + pub count: u32, + /// Bit zero indicates a mapped offset (frame) or truncation (snapshot). + pub flags: u32, + /// Youngest-first logical guest frames; unused slots are zeroed. + pub frames: [DebugFrame; 32], +} +impl Default for DebugSnapshot { + fn default() -> Self { + Self { + version: 1, + size: core::mem::size_of::() as u32, + count: 0, + flags: 0, + frames: [DebugFrame::default(); 32], + } + } +} +impl DebugSnapshot { + pub(crate) fn capture(&mut self, code: &Code, stack: &CallStack, current: CallFrame, ip: usize) { + *self = Self::default(); + for (cf, ip) in + core::iter::once((¤t, ip)).chain(stack.debug_frames().map(|cf| (cf, cf.instr_ptr.saturating_sub(1)))) + { + if self.count == 32 { + self.flags = 1; + break; + } + let func = code.funcs.wasm(cf.func_addr); + let offset = func.func.data.source_offsets.get(ip).copied().unwrap_or(0); + self.frames[self.count as usize] = + DebugFrame { instance: func.owner, function: func.debug_index, offset, flags: u32::from(offset != 0) }; + self.count += 1; + } + } +} +impl Store { + /// Copies the last safe-boundary snapshot; no references to guest memory or runtime internals. + pub fn debug_snapshot(&self) -> DebugSnapshot { + self.debug_snapshot + } +} diff --git a/crates/tinywasm/src/func/host_execution.rs b/crates/tinywasm/src/func/host_execution.rs index bf3bb9b..73774e2 100644 --- a/crates/tinywasm/src/func/host_execution.rs +++ b/crates/tinywasm/src/func/host_execution.rs @@ -27,6 +27,8 @@ pub struct HostExecution<'store> { store: &'store mut Store, results: &'store mut [WasmValue], root_type: TypeAddr, + #[cfg(feature = "diagnostics")] + debug_snapshot: crate::diagnostics::DebugSnapshot, state: State, } @@ -64,6 +66,10 @@ impl Function { let state = { let mut store = store.enter_execution()?; store.call_stack.clear(); + #[cfg(feature = "diagnostics")] + { + store.debug_snapshot = crate::diagnostics::DebugSnapshot::default(); + } store.value_stack.clear(); (|| -> Result { if store.code.funcs.is_host(self.addr()) { @@ -77,11 +83,24 @@ impl Function { })() .inspect_err(|_| store.value_stack.clear())? }; - Ok(HostExecution { store, results, root_type, state }) + Ok(HostExecution { + store, + results, + root_type, + state, + #[cfg(feature = "diagnostics")] + debug_snapshot: crate::diagnostics::DebugSnapshot::default(), + }) } } impl<'store> HostExecution<'store> { + /// Last fault or host-boundary snapshot, copied before stack teardown. + #[cfg(feature = "diagnostics")] + pub fn debug_snapshot(&self) -> crate::diagnostics::DebugSnapshot { + self.debug_snapshot + } + /// Runs until completion, fuel exhaustion, or the next host call. /// /// A pending host call is returned again without executing guest code or invoking a callback. @@ -136,6 +155,10 @@ impl<'store> HostExecution<'store> { if call.tail { executor.return_from_host() } else { executor.run() } })(); } + #[cfg(feature = "diagnostics")] + { + self.debug_snapshot = self.store.debug_snapshot(); + } self.accept_outcome(outcome).inspect_err(|_| self.finish()) } diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index c5acf22..5175f82 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -211,7 +211,16 @@ impl ModuleInstance { let id = store.next_module_instance_id(); let mut addrs = crate::imports::ResolvedImports::new(store, module, &type_addrs, imports)?; let imported_funcs = addrs.funcs.len(); - addrs.funcs.extend(store.init_funcs(&module.funcs, id, &module.func_type_idxs[imported_funcs..], &type_addrs)); + #[cfg(feature = "diagnostics")] + let debug_first_index = imported_funcs as u32; + addrs.funcs.extend(store.init_funcs( + &module.funcs, + id, + #[cfg(feature = "diagnostics")] + debug_first_index, + &module.func_type_idxs[imported_funcs..], + &type_addrs, + )); addrs.tags.extend(store.init_tags(&module.tags, &type_addrs)); let limiter = store.engine.config().resource_limiter.clone(); if !module.skip_local_memory_allocation { diff --git a/crates/tinywasm/src/interpreter/executor/budget.rs b/crates/tinywasm/src/interpreter/executor/budget.rs index 73c3a72..32f234f 100644 --- a/crates/tinywasm/src/interpreter/executor/budget.rs +++ b/crates/tinywasm/src/interpreter/executor/budget.rs @@ -31,6 +31,8 @@ impl Executor<'_> { /// Settles attempted instructions on an ordinary trap, using the dispatcher's existing IP. #[cold] pub(super) fn settle_error(&mut self, instr_ptr: usize, error: ExecError) -> ExecError { + #[cfg(feature = "diagnostics")] + self.debug_snapshot.capture(self.code, self.call_stack, self.cf, instr_ptr); self.charge_instructions(instr_ptr + 1); error } diff --git a/crates/tinywasm/src/interpreter/executor/mod.rs b/crates/tinywasm/src/interpreter/executor/mod.rs index 616eb4e..35e3ca3 100644 --- a/crates/tinywasm/src/interpreter/executor/mod.rs +++ b/crates/tinywasm/src/interpreter/executor/mod.rs @@ -78,6 +78,8 @@ pub(crate) struct HostCall { /// Active Wasm execution, borrowing code and mutable state while owning its value stack. pub(crate) struct Executor<'store> { cf: CallFrame, + #[cfg(feature = "diagnostics")] + debug_snapshot: &'store mut crate::diagnostics::DebugSnapshot, func: &'store WasmFunction, module: &'store ModuleInstance, code: &'store Code, @@ -103,6 +105,8 @@ impl<'store> Executor<'store> { Self { module, cf, + #[cfg(feature = "diagnostics")] + debug_snapshot: &mut store.debug_snapshot, func: &wasm_func.func, code: &store.code, state: &mut store.state, @@ -548,6 +552,8 @@ impl<'store> Executor<'store> { } fn throw_exception(&mut self, exception: ValueRef, protected_ip: usize) -> ExecResult { + #[cfg(feature = "diagnostics")] + self.debug_snapshot.capture(self.code, self.call_stack, self.cf, protected_ip); match self.dispatch_exception(exception, protected_ip)? { Some(flow) => Ok(flow), None => { @@ -632,6 +638,8 @@ impl<'store> Executor<'store> { if !self.code.funcs.is_host(func_addr) { return self.exec_call_wasm::(func_addr, return_instr_ptr); } + #[cfg(feature = "diagnostics")] + self.debug_snapshot.capture(self.code, self.call_stack, self.cf, return_instr_ptr.saturating_sub(1)); let mut caller = self.cf; caller.instr_ptr = return_instr_ptr; self.outcome = ExecOutcome::Host(HostCall { func: func_addr, module: self.module.id(), caller, tail: TAIL }); diff --git a/crates/tinywasm/src/interpreter/stack/call_stack.rs b/crates/tinywasm/src/interpreter/stack/call_stack.rs index 6a8741b..6c2fc57 100644 --- a/crates/tinywasm/src/interpreter/stack/call_stack.rs +++ b/crates/tinywasm/src/interpreter/stack/call_stack.rs @@ -11,6 +11,11 @@ pub(crate) struct CallStack { } impl CallStack { + #[cfg(feature = "diagnostics")] + pub(crate) fn debug_frames(&self) -> impl Iterator { + self.stack.iter().rev() + } + pub(crate) fn new(config: &crate::engine::Config) -> Self { let stack = config.call_stack; Self { stack: Vec::with_capacity(stack.initial_size), max_size: stack.max_size, dynamic: stack.dynamic } diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 3d0fded..3a0a8c9 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -132,6 +132,8 @@ pub use instance::{ExternItem, ModuleInstance}; pub use reference::*; pub use store::*; +#[cfg(feature = "diagnostics")] +pub mod diagnostics; mod func; mod imports; mod instance; diff --git a/crates/tinywasm/src/store/function.rs b/crates/tinywasm/src/store/function.rs index 3eae3ab..8cd8de3 100644 --- a/crates/tinywasm/src/store/function.rs +++ b/crates/tinywasm/src/store/function.rs @@ -15,6 +15,8 @@ pub(crate) struct WasmFunctionInstance { pub(crate) type_addr: TypeAddr, pub(crate) func: Shared, pub(crate) owner: ModuleInstanceId, + #[cfg(feature = "diagnostics")] + pub(crate) debug_index: u32, } #[derive(Default)] @@ -42,12 +44,21 @@ impl Functions { &mut self, funcs: impl ExactSizeIterator)>, owner: ModuleInstanceId, + #[cfg(feature = "diagnostics")] first_index: u32, ) -> Range { let start = self.wasm.len(); let end = start.checked_add(funcs.len()).expect("too many Wasm functions"); assert!(end <= Self::HOST_TAG as usize, "too many Wasm functions"); self.wasm.reserve_exact(funcs.len()); + #[cfg(not(feature = "diagnostics"))] self.wasm.extend(funcs.map(|(type_addr, func)| WasmFunctionInstance { type_addr, func, owner })); + #[cfg(feature = "diagnostics")] + self.wasm.extend(funcs.enumerate().map(|(index, (type_addr, func))| WasmFunctionInstance { + type_addr, + func, + owner, + debug_index: first_index + index as u32, + })); start as FuncAddr..end as FuncAddr } diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index aed7de7..f9ae243 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -163,6 +163,8 @@ pub struct Store { pub(crate) reentrant_call_depth: usize, pub(crate) state: State, pub(crate) call_stack: CallStack, + #[cfg(feature = "diagnostics")] + pub(crate) debug_snapshot: crate::diagnostics::DebugSnapshot, pub(crate) value_stack: ValueStack, value_scratch: Vec, #[cfg(feature = "state")] @@ -256,6 +258,8 @@ impl Store { code: Code::default(), state, call_stack: CallStack::new(engine.config()), + #[cfg(feature = "diagnostics")] + debug_snapshot: crate::diagnostics::DebugSnapshot::default(), value_stack: ValueStack::new(engine.config()), value_scratch: Vec::new(), engine, @@ -582,13 +586,19 @@ impl Store { &mut self, funcs: &[Shared], owner: ModuleInstanceId, + #[cfg(feature = "diagnostics")] first_index: u32, module_type_idxs: &[TypeAddr], type_addrs: &[TypeAddr], ) -> impl ExactSizeIterator { debug_assert_eq!(funcs.len(), module_type_idxs.len()); let funcs = funcs.iter().cloned().zip(module_type_idxs).map(|(func, &type_idx)| (type_addrs[type_idx as usize], func)); - self.code.funcs.extend_wasm(funcs, owner) + self.code.funcs.extend_wasm( + funcs, + owner, + #[cfg(feature = "diagnostics")] + first_index, + ) } /// Add tags to the store, returning their addresses in the store. diff --git a/crates/tinywasm/tests/host_execution.rs b/crates/tinywasm/tests/host_execution.rs index 60b9df5..4107e49 100644 --- a/crates/tinywasm/tests/host_execution.rs +++ b/crates/tinywasm/tests/host_execution.rs @@ -420,3 +420,46 @@ fn unselected_imports_use_the_synchronous_stack_path() -> Result<()> { assert_eq!(calls.load(Ordering::SeqCst), 6); Ok(()) } + +#[cfg(feature = "diagnostics")] +#[test] +fn diagnostics_survive_nested_callbacks_and_guest_exception_unwinding() -> Result<()> { + let mut store = Store::default(); + let bytes = wat::parse_str( + r#"(module + (import "env" "wait" (func $wait (param i32) (result i32))) + (tag $failure) + (func $leaf i32.const 20 call $wait drop throw $failure) + (func $middle call $leaf) + (func (export "run") call $middle) + (func (export "nested") unreachable))"#, + ) + .unwrap(); + let mut imports = Imports::new(); + imports.define("env", "wait", HostFunction::from(|_: FuncContext<'_>, v: i32| Ok(v))); + let module = tinywasm::parse_bytes(&bytes)?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + let function = instance.func_untyped(&store, "run")?; + let mut results = []; + let mut execution = function.call_resumable_with_host_calls(&mut store, &[], &mut results)?; + assert_eq!(execution.resume_with_fuel(100)?, HostExecProgress::HostCall); + let before = execution.debug_snapshot(); + assert_eq!(before.count, 3); + for frame in &before.frames[..3] { + assert_eq!(frame.flags, 1); + assert_eq!(bytes[frame.offset as usize], 0x10); // actual call opcode, not an IR offset + } + execution.host_call().unwrap().with_context(|mut context| { + let nested = context.module().func::<(), ()>(context.store(), "nested")?; + assert!(context.call(&nested, ()).is_err()); + Ok(()) + })?; + assert_eq!(execution.debug_snapshot().frames[0].offset, before.frames[0].offset); + execution.host_call().unwrap().complete(&[WasmValue::I32(1)])?; + assert!(matches!(execution.resume_with_fuel(100), Err(Error::Exception(_)))); + let fault = execution.debug_snapshot(); + assert_eq!(fault.count, 3); + assert_eq!(bytes[fault.frames[0].offset as usize], 0x08); // throw, captured before unwinding + assert_eq!(fault.frames[0].function, 1); + Ok(()) +} diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 2d2bf91..9cef986 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -16,6 +16,8 @@ postcard = { version = "1.1", default-features = false, features = ["alloc"], op serde = { version = "1.0", default-features = false, features = ["alloc"], optional = true } [features] +# Optional bounded diagnostic snapshots and original Wasm source offsets. +diagnostics = [] default = ["archive", "debug", "std"] # support serializing and deserializing twasm archives diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index a6ba7c3..b5ebee2 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -503,6 +503,10 @@ pub struct WasmFunction { #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] pub struct WasmFunctionData { + /// Original Wasm file offsets. Archives deliberately omit build-specific debug metadata. + #[cfg(feature = "diagnostics")] + #[cfg_attr(feature = "archive", serde(skip))] + pub source_offsets: Box<[u32]>, pub operands64: Box<[Operand64]>, pub operands128: Box<[Operand128]>, pub branch_table_targets: Box<[u32]>, From 1be986bf8460e7c3ea6ccdb64a299f46006a761a Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Tue, 3 Nov 2026 23:37:55 -0800 Subject: [PATCH 18/20] test: join native crash traces with guest sources and async ancestry --- .github/workflows/shared_continuations.yml | 49 ++- .../c-api/tests/continuations/debug/README.md | 235 ++++++------ .../tests/continuations/debug/debugger.py | 43 +++ .../tests/continuations/debug/diagnostic.c | 334 ++++++++++++++++++ .../continuations/debug/diagnostic_run.py | 164 +++++++++ .../continuations/debug/disabled_profile.py | 92 +++++ .../c-api/tests/continuations/debug/fixture.c | 5 + .../tests/continuations/debug/native.cpp | 9 + .../c-api/tests/continuations/debug/native.rs | 8 + .../tests/continuations/debug/native.swift | 10 + .../tests/continuations/debug/recorder.c | 157 ++++++++ .../tests/continuations/debug/recorder.h | 71 ++++ .../tests/continuations/debug/source_guest.c | 26 ++ .../continuations/debug/source_guest.wasm | Bin 0 -> 1340 bytes .../tests/continuations/debug/symbolize.py | 86 +++++ .../continuations/debug/test_symbolize.py | 51 +++ crates/c-api/tests/continuations/run.py | 15 + 17 files changed, 1229 insertions(+), 126 deletions(-) create mode 100644 crates/c-api/tests/continuations/debug/debugger.py create mode 100644 crates/c-api/tests/continuations/debug/diagnostic.c create mode 100644 crates/c-api/tests/continuations/debug/diagnostic_run.py create mode 100644 crates/c-api/tests/continuations/debug/disabled_profile.py create mode 100644 crates/c-api/tests/continuations/debug/fixture.c create mode 100644 crates/c-api/tests/continuations/debug/recorder.c create mode 100644 crates/c-api/tests/continuations/debug/recorder.h create mode 100644 crates/c-api/tests/continuations/debug/source_guest.c create mode 100755 crates/c-api/tests/continuations/debug/source_guest.wasm create mode 100644 crates/c-api/tests/continuations/debug/symbolize.py create mode 100644 crates/c-api/tests/continuations/debug/test_symbolize.py diff --git a/.github/workflows/shared_continuations.yml b/.github/workflows/shared_continuations.yml index aab7817..b9913c3 100644 --- a/.github/workflows/shared_continuations.yml +++ b/.github/workflows/shared_continuations.yml @@ -2,7 +2,7 @@ name: Shared C continuations on: workflow_dispatch: pull_request: - paths: ["crates/tinywasm/**", "crates/c-api/**", ".github/workflows/shared_continuations.yml"] + paths: ["crates/parser/**", "crates/types/**", "crates/tinywasm/**", "crates/c-api/**", ".github/workflows/shared_continuations.yml"] permissions: contents: read jobs: @@ -14,18 +14,18 @@ jobs: - os: ubuntu-24.04 rust: stable extra: "" - core_args: "--features external-host-calls" - capi_args: "--features external-host-calls" + core_args: "--features external-host-calls,diagnostics" + capi_args: "--features external-host-calls,diagnostics" - os: windows-2025 rust: stable extra: "" - core_args: "--features external-host-calls" - capi_args: "--features external-host-calls" + core_args: "--features external-host-calls,diagnostics" + capi_args: "--features external-host-calls,diagnostics" - os: windows-2025 rust: nightly-2026-08-28 extra: "--tail-calls" - core_args: "--no-default-features --features full,std,nightly-tail-calls,external-host-calls" - capi_args: "--features external-host-calls,tinywasm/nightly-tail-calls" + core_args: "--no-default-features --features full,std,nightly-tail-calls,external-host-calls,diagnostics" + capi_args: "--features external-host-calls,tinywasm/nightly-tail-calls,diagnostics" runs-on: ${{ matrix.os }} env: WASM_EXT_TEST_ONLY: "1" @@ -58,7 +58,7 @@ jobs: with: toolchain: nightly components: rust-src - - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-full-asan --gc --simd --sanitize + - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build target/shared-c-full-asan --gc --simd --sanitize --diagnostics miri: runs-on: ubuntu-24.04 steps: @@ -67,15 +67,36 @@ jobs: with: toolchain: nightly components: miri - - run: cargo miri test --locked -p tinywasm-c-api --features external-host-calls --lib + - run: cargo miri test --locked -p tinywasm-c-api --features diagnostics --lib native-unwind: - name: Native unwind (GDB, C/C++/Rust) - runs-on: ubuntu-24.04 + name: Guest/native/async diagnostics (${{ matrix.os }}) + strategy: + matrix: + include: + - os: ubuntu-24.04 + debugger: gdb + extra: "" + - os: macos-15 + debugger: lldb + extra: "--swift" + runs-on: ${{ matrix.os }} env: WASM_EXT_TEST_ONLY: "1" + CARGO_PROFILE_RELEASE_DEBUG: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - - run: sudo apt-get update && sudo apt-get install -y gdb - - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build build/native-unwind - - run: python3 crates/c-api/tests/continuations/debug/run.py --runtime tinywasm --include crates/c-api/include --library build/native-unwind/runtime/release/libtinywasm.a --build build/native-unwind/client --debugger gdb + - if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y gdb + - run: cargo install wasm-tools --locked --version 1.247.0 + - run: python3 crates/c-api/tests/continuations/run.py --runtime tinywasm --root . --build build/native-unwind --diagnostics + - run: python3 crates/c-api/tests/continuations/debug/diagnostic_run.py --runtime tinywasm --include crates/c-api/include --library build/native-unwind/runtime/release/libtinywasm.a --build build/native-unwind/client --debugger ${{ matrix.debugger }} ${{ matrix.extra }} + - run: python3 crates/c-api/tests/continuations/debug/test_symbolize.py + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + if: always() + with: + name: diagnostic-traces-${{ matrix.os }} + path: | + build/native-unwind/client/debugger.txt + build/native-unwind/client/receipt.json + build/native-unwind/client/tests.txt diff --git a/crates/c-api/tests/continuations/debug/README.md b/crates/c-api/tests/continuations/debug/README.md index 3f7eff0..f82c7c3 100644 --- a/crates/c-api/tests/continuations/debug/README.md +++ b/crates/c-api/tests/continuations/debug/README.md @@ -1,115 +1,126 @@ -# Mixed native and guest stack audit - -Full mixed guest/native crash reporting is a readiness requirement for this RFC, -not a capability established by the continuation and HTTP tests. - -The fixture calls Wasm `root -> middle -> leaf -> example:native.call`, then C, -C++, Rust and optionally Swift. It stops inside the native provider, parks the -continuation, and traps after resumption. An early-trap case checks failure -before the first yield. The ordinary-call mode also accepts a WAMR AOT module. -Provider functions use `noinline` so this test can assert native ABI unwind order; -this is not a requirement to disable inlining or tail dispatch in the runtime. - -## Observed behavior - -On macOS arm64, optimized builds recover the native C/C++/Rust/Swift chain in -LLDB and actual Apple `.ips` reports. Matching dSYM UUIDs are checked. After -yield, the native backtrace contains the scheduler/driver: the guest continuation -has no native stack. A physical backtrace must not invent the old frames. - -WAMR previously returned an empty C trap trace for continuation failures even -with `WAMR_BUILD_DUMP_CALL_STACK=1`. The initial error path discarded the trace; -the resumed error path also released frames before capturing them. Both now -reuse the ordinary trap mechanism. Tests require all three guest function -indices for initial, resumed and ordinary failures. This requires the existing -opt-in call-stack feature; the shared C header has not changed. - -TinyWasm still returns an empty `wasm_trap_trace`. Its optimized C adapter invokes -external callbacks after returning from the interpreter, so the native backtrace -does not contain an active interpreter frame either. The fixture records this -limitation, not a successful guest-stack qualification. - -WAMR ordinary AOT calls preserve native unwinding and the opt-in logical trap -trace, but the tested default AOT build shows an unnamed generated-code address -in LLDB. It does not provide named guest source frames. The separate experimental -`WAMR_BUILD_DEBUG_AOT` compiler configuration requires LLDB development libraries, -which were unavailable in the local LLVM 21 installation. Its DWARF registration -path remains unqualified by this audit. - -The fast interpreter's reported offsets are internal compiled-bytecode offsets; -module offsets may be zero. They must not be presented to a Wasm DWARF symbolizer -as original Wasm instruction offsets. Function identity is useful; full source -locations require a retained mapping to the exact original module. - -The [recorded observations](observed.json) include native backtraces, minimal -Apple crash receipts, source hashes and the unchanged disabled artifacts. - -## Required out-of-tree diagnostic contract - -1. Associate each loaded module with its content hash, component/instance identity, - original Wasm/debug sidecar, and native image UUID or build ID. Preserve the - component linker's mapping from lowered core functions to original modules. -2. Provide a bounded, versioned, read-only guest-frame snapshot at host-call and - suspension boundaries, with explicit offset coordinate systems and truncation. - TinyWasm needs a public capture seam; its current `guest-debug` by-index APIs - are insufficient. WAMR's allocating trap-trace API is not a fatal-signal API. -3. Keep async ancestry in the external scheduler: task/call generation, parent, - import identity, and active/pending/completed/cancelled state. Distinguish - concurrent streams and reused slots. Show an explicit async boundary rather - than treating a resumed callback as a continuous native stack. -4. Capture no borrowed guest/native pointers across a yield. Publish immutable - or coherently versioned records for debugger/crash readers; invalidate them on - cancellation and teardown. Do not allocate, acquire runtime locks, invoke - callbacks, format strings or re-enter the engine from a fatal-signal handler. -5. Preserve native unwind metadata and archive matching dSYMs/ELF debug files. - Keep inline/tail-call semantics honest. C++ exceptions, Rust panics and Swift - errors must be caught/translated before crossing the C ABI; stack inspection - does not authorize exception propagation through it. -6. Compile diagnostic state and capture work out of embedded builds. Use existing - runtime frames rather than a second stack updated on every instruction. Bound - enabled memory and measure capture cost at native-call boundaries, including - high-frequency GPU/audio imports. Disabling diagnostics must retain the LTO - binary/RSS/native-call baseline; don't sacrifice tail dispatch for prettier - native traces. - -Native Apple reports alone do not automatically include interpreter/async -records. An out-of-tree reporting integration must attach or reconstruct them. -Apple's [CrashReportExtension](https://developer.apple.com/documentation/crashreportextension) -can inspect a crashed process out of process on its supported platforms; its -documented availability is iOS/iPadOS/macOS 27, not watchOS. It must not become a -runtime dependency or the only diagnostic path. GDB/LLDB readers and an external -crash-context attachment remain necessary for other targets. - -These requirements are intentionally not a new public diagnostics ABI in this -patch. Named guest frames, source mappings and async ancestry remain gates before -claiming a complete `Wasm -> component/module -> native provider` crash stack. +# Optional guest and async diagnostics + +The continuation drafts now provide bounded logical guest snapshots, original +Wasm source offsets, terminal asynchronous host failures, and consumer-owned +async ancestry. `diagnostic_run.py` checks these alongside native C → C++ → +Rust → Swift → C unwinding. These are logical Wasm/async segments attached to +a native trace; suspended interpreter frames are not native machine frames. + +Enable `WAMR_BUILD_C_API_DIAGNOSTICS=1` with continuations, or TinyWasm's +`diagnostics` Cargo feature. Both are excluded from default builds. The +experimental `wasm_ext_debug.h` observer ABI is source-compatible between the +runtimes and separate from the standard Wasm C API. It does not promise a +stable cross-runtime binary ABI for the underlying `wasm.h` objects. + +`wasm_ext_call_observe` registers before first resume. Safe-boundary events +cover start, host entry, pending, resume, guest trap, cancellation, completion, +and asynchronous failure. Each snapshot is 528 bytes: a versioned header and +at most 32 youngest-first frames, with explicit truncation and mapping flags. +No locals, memory bytes, runtime pointers, or managed references escape. +An observer must copy the borrowed record, remain on the owner thread, and +never reenter the runtime, throw, or longjmp. Capture allocates no memory. + +`wasm_ext_call_fail` terminates a pending host operation with an owned trap. +Duplicate/late failure is rejected; the producer already completed, so its +cancellation callback is not invoked. C++ and Swift exceptions are caught +inside their bridges; Rust `Result` errors are translated there. Rust panics +under `panic=abort` remain native crashes. This API does not inject a catchable +Wasm exception or unwind a language exception through C. TinyWasm also tests +source capture before an uncaught guest exception unwinds its frames. + +## Consumer pieces to move out of tree + +`recorder.[ch]`, `debugger.py`, `symbolize.py`, the language bridges, source +manifest, and all runners are review/reproduction fixtures, not runtime +policy. The recorder has one writer and two publication banks (12,264 bytes +in total). Readers stop the process in a debugger or inspect it after death. +They never read a concurrently mutating bank. Interrupted publication leaves +the preceding committed bank available. This is a process-crash transport, +not a power-loss journal. + +Each task has a monotonically increasing, non-reused generation ID. Spawn +copies up to four parent segments; propagation copies up to four failure +causes. Parent destruction or slot reuse cannot retarget those records. +Both limits and the 128-byte error-message limit report truncation. The +scheduler must check generations before using a call pointer: these APIs +cannot make a deleted C pointer safe. Cancellation remains terminal core +cancellation, distinct from Component Model cooperative task cancellation. + +The sample loads one original guest module into separate stores. Its immutable +manifest associates store-local instance ordinals with the exact Wasm and +source hashes. A multi-module consumer must maintain that association for +each task's store/module graph; ordinals alone are not global module identity. +A launch nonce and native executable hash identify the manifest. Published +records contain its SHA-256 and process ID. Symbolization rejects a mismatched +manifest, module, or source rather than guessing a plausible line. + +Apple's ordinary `.ips` report supplies native PCs. The runner checks the +crashed process ID and executable/dSYM UUID, symbolicates with `atos`, and +joins the mapped sidecar's guest source/async segments into a combined report. +It installs no signal handler and invokes no runtime code during fatal signal +handling. The standard Apple report itself is not rewritten. Raw machine +crash reports are not committed. `wasm-bt` in GDB/LLDB reads the stopped +process's published POD through debugger memory APIs, without target calls. + +## Source coordinates and costs + +The loaders retain original Wasm **file offsets**, not internal IR offsets. +WAMR maps the fast interpreter's compiled bytes; TinyWasm tracks instruction +selection, fusion, checkpoints and caller call sites. Fused instructions map +to their final operation. Both check source-call lines in a Clang DWARF guest. +Unmapped offsets stay explicit. Completed tail-eliminated callers are not +invented. The guest fixture disables compiler sibling-call optimization to +provide a known three-frame oracle; production builds need not do that. + +Enabled source maps cost four bytes per compiled byte in WAMR and four bytes +per IR instruction in TinyWasm, plus bounded snapshot/observer state. TinyWasm +stores module-local function indices at instantiation, keeping capture work +bounded by stack depth rather than module function count. Diagnostic metadata +is deliberately omitted from TinyWasm archives: a cached archive is unmapped +unless the consumer restores matching debug metadata. Load original Wasm for +this source-qualified configuration. WAMR AOT stack suspension is still outside +this contract; the older `run.py --ordinary` checks ordinary/AOT native +unwinding, not this interpreter source-snapshot API. JIT, arbitrary sampling +of actively executing Wasm, and physical Watch/Pico qualification are not +claimed by these fixtures. + +When diagnostics are compiled out, source maps, snapshot fields and capture +calls are absent. The disabled-build comparison covers both continuation-on +and continuation-off builds with LTO and whole-program dead stripping. See +`diagnostic_receipts.json`, `disabled_profile.json`, and +`qemu_diagnostics_disabled.json` for current evidence, configuration and limits. +Timing/RSS measurements are process samples, not mathematical proof of zero +noise. Code and loadable-section comparisons establish whether instrumentation +or storage was retained. Runtime source compatibility across feature sets and +maintenance cost of the loader hooks remain RFC feedback topics. ## Reproduce -Build the shared runtime fixture with `run.py --runtime wamr --root /path/to/wamr ---build /tmp/runtime --call-stacks`, or the equivalent TinyWasm command without -`--call-stacks`. Use one compiler family for native LTO objects and the final -linker. For WAMR source-level runtime debugging, additionally build its archive -with `RelWithDebInfo` and `-fno-omit-frame-pointer`. The default build flags stay -unchanged. - -Run this directory's `run.py` with `--runtime`, `--include`, `--library`, -`--build` and `--debugger lldb` or `--debugger gdb`. WAMR's include directory must -contain its C API header under the shared name `wasm.h`, as the parent runner -arranges. Add `--swift` on macOS. Without it, a C bridge connects Rust to the -checkpoint; this is the Linux GDB CI configuration. - -`--apple-crash` deliberately traps the standalone fixture, checks a new Apple -`.ips` report for the native frame order and matching dSYM UUID, and symbolicates -its image addresses with `atos`. Run it only when intentionally producing a test -crash. The script records a minimal receipt, not the report's machine identifiers. -`--ordinary --guest /path/to/guest.aot` checks ordinary AOT calls; compile -`guest.wasm` with `--enable-dump-call-stack` and matching GC/bounds-check settings. - -The runner writes `debugger.txt`, `guest_trace.txt`, and `receipt.json`; Apple -crash tests also write `apple_symbolicated.txt`. It explicitly records -`full_mixed_guest_stack_qualified: false`, even when native unwinding passes. - -References: [Apple symbolication](https://developer.apple.com/documentation/xcode/adding-identifiable-symbol-names-to-a-crash-report), -[GDB JIT interface](https://sourceware.org/gdb/current/onlinedocs/gdb.html/JIT-Interface.html), -[LLVM debugging generated code](https://llvm.org/docs/DebuggingJITedCode.html). +Build a runtime with `../run.py --diagnostics` (add `--gc`, `--call-stacks` for +WAMR where desired), then run, with `wasm-tools` 1.247.0 on PATH: + +```sh +python3 diagnostic_run.py --runtime wamr --include BUILD/include \ + --library BUILD/runtime/libiwasm.a --build BUILD/debug \ + --debugger lldb --swift --apple-crash +# TinyWasm: --runtime tinywasm --include ROOT/crates/c-api/include +# --library BUILD/runtime/release/libtinywasm.a +# Linux: --debugger gdb, omit --swift and --apple-crash. +python3 test_symbolize.py +``` + +The runner deliberately crashes child fixture processes for `--apple-crash`. +It checks host-entry, parked, failed-await and parent source locations, native +frame order, and actual Apple report production. Separate runtime/C ASan runs +use `../run.py --diagnostics --sanitize`; `diagnostic_run.py --sanitize +--skip-debugger` also exercises the native bridge/error driver. Windows shared +C CI exercises bounded capture/failure without claiming Swift or Apple support. + +Regenerate the source fixture with Clang targeting wasm32, `-g -O1 +-fno-optimize-sibling-calls -nostdlib -Wl,--no-entry -Wl,--export=guest_root +-Wl,--allow-undefined`, mapping the source directory and compilation directory +to `/guest`. Keep the exact original Wasm, source hash and native dSYM together. +`disabled_profile.py` compares paired macOS baseline/current LTO builds and +records all timing/RSS samples. `observed.json` retains the earlier native-only +audit as historical evidence; its missing-feature conclusions are superseded +by the optional diagnostics implemented here. diff --git a/crates/c-api/tests/continuations/debug/debugger.py b/crates/c-api/tests/continuations/debug/debugger.py new file mode 100644 index 0000000..bd90c12 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/debugger.py @@ -0,0 +1,43 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Load explicitly in GDB or LLDB, then use wasm-bt. No inferior expressions/calls.""" +import importlib.util +from pathlib import Path +spec = importlib.util.spec_from_file_location('wasm_symbolize', Path(__file__).with_name('symbolize.py')) +symbolize = importlib.util.module_from_spec(spec) +spec.loader.exec_module(symbolize) + +def lldb_trace(debugger, command, result, internal_dict): + import lldb + try: + target = debugger.GetSelectedTarget() + pointer = target.FindFirstGlobalVariable('debug_published') + if not pointer.IsValid(): + raise ValueError('missing consumer debug_published symbol') + error = lldb.SBError() + data = target.GetProcess().ReadMemory(pointer.GetValueAsUnsigned(), symbolize.RECORD, error) + if not error.Success(): + raise ValueError(str(error)) + result.AppendMessage(symbolize.render(data)) + except Exception as error: + result.SetError(str(error)) + +def __lldb_init_module(debugger, internal_dict): + debugger.HandleCommand('command script add -f '+__name__+'.lldb_trace wasm-bt') + +try: + import gdb +except ImportError: + pass +else: + class WasmTrace(gdb.Command): + def __init__(self): + super().__init__('wasm-bt', gdb.COMMAND_STACK) + def invoke(self, args, from_tty): + try: + symbol = gdb.lookup_global_symbol('debug_published') + data = bytes(gdb.selected_inferior().read_memory(int(symbol.value()), symbolize.RECORD)) + gdb.write(symbolize.render(data)) + except Exception as error: + raise gdb.GdbError(str(error)) + WasmTrace() diff --git a/crates/c-api/tests/continuations/debug/diagnostic.c b/crates/c-api/tests/continuations/debug/diagnostic.c new file mode 100644 index 0000000..3518f3d --- /dev/null +++ b/crates/c-api/tests/continuations/debug/diagnostic.c @@ -0,0 +1,334 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#include "recorder.h" +#include +#include +#include +#define CHECK(x) \ + do { \ + if (!(x)) { \ + fprintf(stderr, "failed line %d: %s\n", __LINE__, #x); \ + exit(1); \ + } \ + } while (0) +#ifdef _MSC_VER +#define NOINLINE __declspec(noinline) +#define DEBUG_CRASH() __debugbreak() +#else +#define NOINLINE __attribute__((noinline)) +#define DEBUG_CRASH() __builtin_trap() +#endif +#ifdef DIAGNOSTIC_NATIVE +extern int +native_cpp(int); +extern int +native_cpp_error(void); +extern int +native_rust_error(void); +#ifdef DIAGNOSTIC_SWIFT +extern int +native_swift_error(void); +#endif +#else +static int +native_cpp(int v) +{ + return v + 4; +} +static int +native_cpp_error(void) +{ + return -11; +} +static int +native_rust_error(void) +{ + return -12; +} +#endif +static int crashing, sync_mode; +static unsigned cancellations; +NOINLINE int +native_checkpoint(int v) +{ + if (crashing == 1) + DEBUG_CRASH(); + return v + 1; +} +NOINLINE void +parked_checkpoint(void) +{ + static volatile int visits; + visits++; +} +NOINLINE void +failure_checkpoint(void) +{ + if (crashing == 2) + DEBUG_CRASH(); +} +static NOINLINE wasm_ext_status_t +diagnostic_import(void *env, const wasm_val_vec_t *args, + wasm_val_vec_t *results, wasm_trap_t **trap) +{ + (void)env; + (void)results; + (void)trap; + CHECK(debug_published->banks[debug_load(&debug_published->active)] + .current.event + == WASM_EXT_DEBUG_HOST); + CHECK(native_cpp(args->data[0].of.i32) == args->data[0].of.i32 + 4); + return WASM_EXT_PENDING; +} +static NOINLINE wasm_trap_t * +sync_import(void *env, const wasm_val_vec_t *args, wasm_val_vec_t *out) +{ + (void)env; + const debug_task *task = + &debug_published->banks[debug_load(&debug_published->active)]; + CHECK(task->current.event == WASM_EXT_DEBUG_HOST + && task->current.guest.count == 3); + out->data[0].kind = WASM_I32; + out->data[0].of.i32 = native_cpp(args->data[0].of.i32) - 4; + return NULL; +} +static void +cancelled(void *env) +{ + (void)env; + cancellations++; +} +typedef struct invocation { + wasm_store_t *store; + wasm_module_t *module; + wasm_func_t *host; + wasm_instance_t *instance; + wasm_extern_vec_t exports; + wasm_ext_call_t *call; + debug_task task; +} invocation; +static wasm_engine_t *engine; +static wasm_byte_vec_t bytes; +static uint32_t generation; +static wasm_val_t output; +static wasm_val_vec_t results = { .size = 1, .data = &output }; +static void +start(invocation *inv, int value, const debug_task *parent) +{ + memset(inv, 0, sizeof(*inv)); + CHECK(debug_task_start(&inv->task, &generation, parent)); + inv->store = wasm_store_new(engine); + CHECK(inv->store); + inv->module = wasm_module_new(inv->store, &bytes); + CHECK(inv->module); + wasm_functype_t *type = + wasm_functype_new_1_1(wasm_valtype_new_i32(), wasm_valtype_new_i32()); + inv->host = + sync_mode + ? wasm_func_new_with_env(inv->store, type, sync_import, NULL, NULL) + : wasm_ext_func_new(inv->store, type, diagnostic_import, NULL, + cancelled); + CHECK(inv->host); + wasm_functype_delete(type); + wasm_extern_t *items[] = { wasm_func_as_extern(inv->host) }; + wasm_extern_vec_t imports = WASM_ARRAY_VEC(items); + wasm_trap_t *trap = NULL; + inv->instance = wasm_instance_new(inv->store, inv->module, &imports, &trap); + CHECK(inv->instance && !trap); + wasm_instance_exports(inv->instance, &inv->exports); + wasm_func_t *run = NULL; + for (size_t i = 0; i < inv->exports.size; i++) + if (wasm_extern_kind(inv->exports.data[i]) == WASM_EXTERN_FUNC) { + run = wasm_extern_as_func(inv->exports.data[i]); + break; + } + CHECK(run); + wasm_val_t arg = WASM_I32_VAL(value); + wasm_val_vec_t args = { .size = 1, .data = &arg }; + args.size = 1; + args.data = &arg; + inv->call = wasm_ext_call_new(run, &args); + CHECK(inv->call); + CHECK(wasm_ext_call_observe(inv->call, debug_observe, &inv->task)); + if (value == -99) { + wasm_ext_call_cancel(inv->call); + CHECK(inv->task.current.event == WASM_EXT_DEBUG_CANCEL); + CHECK(inv->task.current.guest.version == 1 + && !inv->task.current.guest.count); + CHECK(wasm_ext_call_resume(inv->call, NULL, &results, &trap) + == WASM_EXT_CANCELLED + && !trap); + return; + } + int status = wasm_ext_call_resume(inv->call, NULL, &results, &trap); + CHECK(!wasm_ext_call_observe(inv->call, debug_observe, &inv->task)); + if (sync_mode) { + CHECK(status == WASM_EXT_DONE && !trap && output.of.i32 == 23); + CHECK(inv->task.current.event == WASM_EXT_DEBUG_DONE); + return; + } + CHECK(status == (value == 0 ? WASM_EXT_ERROR : WASM_EXT_PENDING)); + if (trap) + wasm_trap_delete(trap); +} +static void +destroy(invocation *inv) +{ + wasm_ext_call_delete(inv->call); + wasm_extern_vec_delete(&inv->exports); + wasm_instance_delete(inv->instance); + wasm_func_delete(inv->host); + wasm_module_delete(inv->module); + wasm_store_delete(inv->store); +} +static void +failure(invocation *inv, uint32_t cause) +{ + CHECK(debug_task_error(&inv->task, inv->task.current.task, cause, + "native async failure")); + wasm_name_t message; + wasm_name_new_from_string(&message, "native async failure"); + wasm_trap_t *trap = NULL; + wasm_name_t invalid = { .size = 1, .data = NULL }; + CHECK(wasm_ext_call_fail(inv->call, &invalid, &trap) == WASM_EXT_ERROR + && !trap); + CHECK(inv->task.current.event == WASM_EXT_DEBUG_PENDING); + CHECK(wasm_ext_call_fail(inv->call, &message, &trap) == WASM_EXT_ERROR + && trap); + wasm_byte_vec_t text; + wasm_trap_message(trap, &text); + CHECK(text.size >= 20 && !memcmp(text.data, "native async failure", 19)); + wasm_byte_vec_delete(&text); + wasm_trap_delete(trap); + trap = NULL; + CHECK(wasm_ext_call_fail(inv->call, &message, &trap) == WASM_EXT_ERROR + && !trap); + CHECK(!debug_task_error(&inv->task, inv->task.current.task, cause, + "duplicate")); + wasm_byte_vec_delete(&message); +} +int +main(int argc, char **argv) +{ + CHECK(argc >= 2); + crashing = argc > 2 && !strcmp(argv[2], "crash") ? 1 + : argc > 2 && !strcmp(argv[2], "async_crash") ? 2 + : 0; + CHECK(debug_open(getenv("WASM_DEBUG_SIDECAR"))); + engine = wasm_engine_new(); + CHECK(engine); + FILE *f = fopen(argv[1], "rb"); + CHECK(f); + CHECK(!fseek(f, 0, SEEK_END)); + long n = ftell(f); + CHECK(n > 0); + rewind(f); + wasm_byte_vec_new_uninitialized(&bytes, (size_t)n); + CHECK(fread(bytes.data, 1, bytes.size, f) == bytes.size); + fclose(f); + invocation parent, child; + start(&parent, 20, NULL); + CHECK(parent.task.current.guest.count == 3); + for (uint32_t i = 0; i < 3; i++) + CHECK(parent.task.current.guest.frames[i].flags + == WASM_EXT_DEBUG_MAPPED); + parked_checkpoint(); + start(&child, 21, &parent.task); + CHECK(child.task.ancestors == 1 + && child.task.ancestry[0].task == parent.task.current.task); + CHECK(!debug_task_error(&child.task, child.task.current.task - 1, 0, + "stale")); + failure(&child, 0); + CHECK(child.task.current.event == WASM_EXT_DEBUG_ASYNC_ERROR + && child.task.current.guest.count == 3); + uint32_t child_id = child.task.current.task; + CHECK(debug_task_cause(&parent.task, &child.task)); + failure(&parent, child_id); + CHECK(parent.task.causes == 1 && parent.task.failures[0].task == child_id); + /* Retain the child report as the failing async segment, including ancestry. + */ + debug_observe(&child.task, child.task.current.event, + &child.task.current.guest); + failure_checkpoint(); + destroy(&child); + destroy(&parent); + CHECK(!cancellations); + /* These errors are caught/converted before returning through the C ABI, + * then delivered later as pending-operation failures. Rust uses Result, + * not catch_unwind: panic=abort remains a fatal native crash. */ + int (*errors[])(void) = { + native_cpp_error, + native_rust_error, +#ifdef DIAGNOSTIC_SWIFT + native_swift_error, +#endif + }; + for (size_t i = 0; i < sizeof(errors) / sizeof(errors[0]); i++) { + start(&parent, 20, NULL); + CHECK(errors[i]() == -11 - (int)i); + failure(&parent, 0); + destroy(&parent); + } + start(&parent, -99, NULL); + destroy(&parent); + CHECK(!cancellations); + start(&parent, 0, NULL); + CHECK(parent.task.current.event == WASM_EXT_DEBUG_TRAP + && parent.task.current.guest.count == 3); + CHECK(parent.task.current.guest.frames[0].flags == WASM_EXT_DEBUG_MAPPED); + CHECK(parent.task.current.guest.frames[0].offset < bytes.size); + CHECK(bytes.data[parent.task.current.guest.frames[0].offset] + == 0); /* original unreachable opcode */ + destroy(&parent); + start(&parent, 140, NULL); + CHECK(parent.task.current.guest.count == 32 + && parent.task.current.guest.flags == 1); + wasm_ext_call_cancel(parent.call); + CHECK(parent.task.current.event == WASM_EXT_DEBUG_CANCEL); + destroy(&parent); + CHECK(cancellations == 1); + start(&parent, 20, NULL); + wasm_trap_t *trap = NULL; + wasm_val_t value = WASM_I32_VAL(-1); + wasm_val_vec_t completion = { .size = 1, .data = &value }; + CHECK(wasm_ext_call_resume(parent.call, &completion, &results, &trap) + == WASM_EXT_ERROR + && trap); + CHECK(parent.task.current.event == WASM_EXT_DEBUG_TRAP + && parent.task.current.guest.count == 3); + wasm_trap_delete(trap); + destroy(&parent); + start(&parent, 20, NULL); + value.of.i32 = 20; + CHECK(wasm_ext_call_resume(parent.call, &completion, &results, &trap) + == WASM_EXT_DONE + && !trap); + CHECK(output.of.i32 == 23 + && parent.task.current.event == WASM_EXT_DEBUG_DONE + && !parent.task.current.guest.count); + destroy(&parent); + sync_mode = 1; + start(&parent, 20, NULL); + destroy(&parent); + sync_mode = 0; + debug_task a, b; + CHECK(debug_task_start(&a, &generation, NULL)); + for (int i = 0; i < 7; i++) { + CHECK(debug_task_start(&b, &generation, &a)); + a = b; + } + CHECK(a.ancestors == 4 && a.truncated); + char long_error[200]; + memset(long_error, 'x', sizeof(long_error) - 1); + long_error[199] = 0; + a.current.event = WASM_EXT_DEBUG_PENDING; + CHECK(debug_task_error(&a, a.current.task, 0, long_error)); + CHECK(a.current.error_flags == 1 && a.current.error[127] == 0); + generation = UINT32_MAX; + CHECK(!debug_task_start(&a, &generation, NULL)); + wasm_byte_vec_delete(&bytes); + wasm_engine_delete(engine); + debug_close(); + puts("PASS bounded frames, source offsets, async failure, ancestry, " + "truncation, cancellation, stale generations"); +} diff --git a/crates/c-api/tests/continuations/debug/diagnostic_run.py b/crates/c-api/tests/continuations/debug/diagnostic_run.py new file mode 100644 index 0000000..5c6e8cb --- /dev/null +++ b/crates/c-api/tests/continuations/debug/diagnostic_run.py @@ -0,0 +1,164 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Verify mixed native/guest/async traces in debuggers and Apple reports.""" +import argparse +import hashlib +import shutil +import sys +import uuid +import struct +import json +import os +from pathlib import Path +import platform +import subprocess +import time + +p = argparse.ArgumentParser() +p.add_argument('--runtime', choices=['wamr', 'tinywasm'], required=True) +p.add_argument('--include', type=Path, required=True) +p.add_argument('--library', type=Path, required=True) +p.add_argument('--build', type=Path, required=True) +p.add_argument('--debugger', choices=['lldb', 'gdb'], required=True) +p.add_argument('--swift', action='store_true') +p.add_argument('--apple-crash', action='store_true', help='intentionally crash the fixture and verify an Apple .ips report') +p.add_argument('--guest', type=Path) +p.add_argument('--sanitize', action='store_true') +p.add_argument('--skip-debugger', action='store_true') + +a = p.parse_args() +source = Path(__file__).resolve().parent +build = a.build.resolve() +build.mkdir(parents=True, exist_ok=True) +guest = (a.guest or source/'source_guest.wasm').resolve() +mac = platform.system() == 'Darwin' +if a.swift and not mac: + p.error('this fixture uses the macOS Swift toolchain') +if a.apple_crash and (not mac or a.sanitize): + p.error('--apple-crash requires macOS and the continuation fixture') + +def run(cmd, **kwargs): + print(' '.join(map(str, cmd)), flush=True) + return subprocess.run(list(map(str, cmd)), check=True, timeout=300, **kwargs) + +cc = os.environ.get('CC', 'cc') +cxx = os.environ.get('CXX', 'c++') +flags = ['-g', '-O2', '-fno-omit-frame-pointer', '-Wall', '-Wextra', '-Werror'] +defines = ['-DDIAGNOSTIC_NATIVE'] + (['-DDIAGNOSTIC_SWIFT'] if a.swift else []) +if a.sanitize: + flags += ['-fsanitize=address'] +run([cc, *flags, *defines, '-I'+str(a.include.resolve()), '-c', source/'diagnostic.c', '-o', build/'native.o']) +run([cc, *flags, '-I'+str(a.include.resolve()), '-c', source/'recorder.c', '-o', build/'recorder.o']) +run([cxx, *flags, '-DDIAGNOSTIC_ERRORS', '-c', source/'native.cpp', '-o', build/'cpp.o']) +run([os.environ.get('RUSTC', 'rustc'), '--cfg=diagnostic_errors', '--crate-type=lib', '--emit=obj', '--edition=2024', + '-Copt-level=2', '-Cdebuginfo=2', '-Cpanic=abort', '-Cforce-frame-pointers=yes', + source/'native.rs', '-o', build/'rust.o']) +if a.swift: + linker = ['xcrun', 'swiftc', '-module-cache-path', build/'swiftcache'] + run([*linker, '-D', 'DIAGNOSTIC_ERRORS', '-g', '-O', '-parse-as-library', '-emit-object', source/'native.swift', '-o', build/'bridge.o']) +else: + linker = [cxx] + run([cc, *flags, '-c', source/'bridge.c', '-o', build/'bridge.o']) +exe = build/(a.runtime+'_diagnostic') +libs = ['-framework', 'Security', '-framework', 'CoreFoundation', '-liconv'] if mac else ['-lm', '-lpthread', '-ldl'] +run([*linker, '-g', build/'native.o', build/'recorder.o', build/'cpp.o', build/'rust.o', build/'bridge.o', + a.library.resolve(), '-o', exe, *libs, *(['-lc++'] if a.swift else []), *(['-fsanitize=address'] if a.sanitize else []), + *(['-Xlinker', '-object_path_lto', '-Xlinker', build/'linked_lto.o'] if mac else [])]) +if mac: + run(['xcrun', 'dsymutil', exe]) +wasm_tools = shutil.which('wasm-tools') +if not wasm_tools: + raise RuntimeError('wasm-tools is required for original DWARF source mappings') +manifest = {'session': str(uuid.uuid4()), 'executable_sha256': hashlib.sha256(exe.read_bytes()).hexdigest(), 'modules': {'0': {'path': str(guest), 'sha256': hashlib.sha256(guest.read_bytes()).hexdigest(), + 'sources': [{'path': str(source/'source_guest.c'), 'sha256': hashlib.sha256((source/'source_guest.c').read_bytes()).hexdigest()}]}}, + 'wasm_tools': wasm_tools} +manifest_path = build/'manifest.json' +manifest_path.write_text(json.dumps(manifest, indent=2)+'\n') +os.environ['WASM_DEBUG_MANIFEST'] = str(manifest_path) +os.environ['WASM_DEBUG_MANIFEST_SHA256'] = hashlib.sha256(manifest_path.read_bytes()).hexdigest() +source_lines = (source/'source_guest.c').read_text().splitlines() +expected_locations = ['source_guest.c:'+str(next(i for i,line in enumerate(source_lines, 1) if text in line))+':' + for text in ['int result = native_call(value)', 'return guest_leaf(value) + 1', 'return guest_middle(value) + 1']] +normal = run([exe, guest], capture_output=True, text=True) +(build/'tests.txt').write_text(normal.stdout) +expected = ['native_checkpoint', 'nativeSwift' if a.swift else 'native_bridge', 'native_rust', 'native_cpp', + 'diagnostic_import', 'wasm_ext_call_resume'] +receipt = {'runtime': a.runtime, 'debugger': a.debugger, 'platform': platform.platform(), + 'swift': a.swift, 'guest_sha256': manifest['modules']['0']['sha256'], 'tests': normal.stdout} +if not a.skip_debugger: + if a.debugger == 'lldb': + commands = ['command script import '+str(source/'debugger.py'), + 'breakpoint set -n native_checkpoint', 'breakpoint set -n parked_checkpoint', + 'breakpoint set -n failure_checkpoint', 'run', 'bt 32', 'wasm-bt', + 'breakpoint disable 1', 'continue', 'bt 32', 'wasm-bt', 'continue', 'bt 32', 'wasm-bt', 'continue'] + cmd = ['lldb', '--batch', *[x for c in commands for x in ['-o', c]], '--', exe, guest] + else: + commands = ['set pagination off', 'source '+str(source/'debugger.py'), + 'break native_checkpoint', 'break parked_checkpoint', 'break failure_checkpoint', + 'run', 'bt 32', 'wasm-bt', 'disable 1', 'continue', 'bt 32', 'wasm-bt', 'continue', 'bt 32', 'wasm-bt', 'continue'] + cmd = ['gdb', '--batch', *[x for c in commands for x in ['-ex', c]], '--args', exe, guest] + result = run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + (build/'debugger.txt').write_text(result.stdout) + frames = '\n'.join(line for line in result.stdout.splitlines() if ('frame #' in line or line.startswith('#'))) + position = 0 + for symbol in expected: + position = frames.find(symbol, position) + if position < 0: + raise RuntimeError('missing or misordered native frame: '+symbol) + position += len(symbol) + for text in ['guest_leaf', 'guest_middle', 'guest_root', *expected_locations, + 'state=host', 'state=pending', 'state=async-error', 'async parent task=1', 'exception: native async failure']: + if text not in result.stdout: + raise RuntimeError('missing mixed trace evidence: '+text+'; see debugger.txt') + receipt['mixed_debugger_trace'] = True + +if a.apple_crash: + import symbolize + reports = Path.home()/'Library/Logs/DiagnosticReports' + receipt['apple_crashes'] = [] + for mode in ['crash', 'async_crash']: + sidecar = build/(mode+'.snapshot') + sidecar.unlink(missing_ok=True) + before = set(reports.glob(exe.name+'*.ips')) + env = os.environ | {'WASM_DEBUG_SIDECAR': str(sidecar)} + crashed = subprocess.run([str(exe), str(guest), mode], env=env, capture_output=True, timeout=30) + if crashed.returncode != -5: + raise RuntimeError('expected SIGTRAP from the deliberate native crash: '+repr(crashed.stderr)) + deadline = time.monotonic()+30 + while time.monotonic() < deadline: + found = set(reports.glob(exe.name+'*.ips'))-before + if found: + break + time.sleep(0.25) + else: + raise RuntimeError('Apple did not produce an .ips report within 30 seconds') + report = json.loads(max(found, key=lambda x:x.stat().st_mtime).read_text().split('\n', 1)[1]) + frames = next(t for t in report['threads'] if t.get('triggered'))['frames'] + symbols = [f.get('symbol', '') for f in frames] + position = 0 + for symbol in expected if mode == 'crash' else ['failure_checkpoint', 'main']: + matches = [i for i in range(position, len(symbols)) if symbol in symbols[i]] + if not matches: + raise RuntimeError('Apple report is missing native frame: '+symbol) + position = matches[0]+1 + image = report['usedImages'][frames[0]['imageIndex']] + dsym = Path(str(exe)+'.dSYM')/'Contents/Resources/DWARF'/exe.name + uuid = subprocess.check_output(['xcrun', 'dwarfdump', '--uuid', str(dsym)], text=True).split()[1] + if uuid.lower() != image['uuid'].lower(): + raise RuntimeError('crash report and dSYM UUID mismatch') + native_frames = [f for f in frames if f['imageIndex'] == frames[0]['imageIndex']] + native = subprocess.check_output(['xcrun', 'atos', '-o', str(dsym), '-l', hex(image['base']), + *[hex(image['base']+f['imageOffset']) for f in native_frames]], text=True) + if struct.unpack_from(' +extern "C" __attribute__((noinline)) int native_cpp_error(void) +{ + try { throw std::runtime_error("native C++ operation failed"); } + catch (const std::exception &) { return -11; } +} +#endif diff --git a/crates/c-api/tests/continuations/debug/native.rs b/crates/c-api/tests/continuations/debug/native.rs index 48e87b1..dd0f197 100644 --- a/crates/c-api/tests/continuations/debug/native.rs +++ b/crates/c-api/tests/continuations/debug/native.rs @@ -8,3 +8,11 @@ unsafe extern "C" { pub extern "C" fn native_rust(value: i32) -> i32 { std::hint::black_box(unsafe { native_bridge(value) }) + 1 } + +#[cfg(diagnostic_errors)] +#[unsafe(no_mangle)] +#[inline(never)] +pub extern "C" fn native_rust_error() -> i32 { + let result: Result<(), i32> = Err(-12); + result.err().unwrap_or(0) +} diff --git a/crates/c-api/tests/continuations/debug/native.swift b/crates/c-api/tests/continuations/debug/native.swift index 5855f2f..c93d187 100644 --- a/crates/c-api/tests/continuations/debug/native.swift +++ b/crates/c-api/tests/continuations/debug/native.swift @@ -6,3 +6,13 @@ public func nativeSwift(_ value:Int32) -> Int32 { return nativeCheckpoint(value)+1 } + +#if DIAGNOSTIC_ERRORS +private enum NativeFailure: Error { case failed } +@_cdecl("native_swift_error") +@inline(never) +public func nativeSwiftError() -> Int32 { + do { throw NativeFailure.failed } + catch { return -13 } +} +#endif diff --git a/crates/c-api/tests/continuations/debug/recorder.c b/crates/c-api/tests/continuations/debug/recorder.c new file mode 100644 index 0000000..14ab277 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/recorder.c @@ -0,0 +1,157 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#include "recorder.h" +#include +#include +#include +#ifdef _WIN32 +#include +#endif +#include +#ifndef _WIN32 +#include +#include +#include +#endif +_Static_assert(sizeof(wasm_ext_debug_snapshot_t) == 528, "snapshot ABI"); +_Static_assert(sizeof(debug_segment) == 676, "segment format"); +_Static_assert(sizeof(debug_record) == 12264, "record format"); +static debug_record fallback; +debug_record *debug_published = &fallback; +#ifndef _WIN32 +static int mapping_fd = -1; +#endif +static void +publish(const debug_task *task) +{ + uint32_t bank = 1 - debug_load(&debug_published->active); + debug_published->banks[bank] = *task; + debug_store(&debug_published->active, bank); +} +bool +debug_open(const char *path) +{ + const uint32_t endian = 1; + if (*(const unsigned char *)&endian != 1) + return false; /* v1 file transport is explicitly little endian. */ +#ifndef _WIN32 + if (path) { + mapping_fd = open(path, O_CREAT | O_EXCL | O_RDWR, 0600); + if (mapping_fd < 0) + return false; + if (ftruncate(mapping_fd, sizeof(debug_record))) { + close(mapping_fd); + mapping_fd = -1; + return false; + } + void *mapping = mmap(NULL, sizeof(debug_record), PROT_READ | PROT_WRITE, + MAP_SHARED, mapping_fd, 0); + if (mapping == MAP_FAILED) { + close(mapping_fd); + mapping_fd = -1; + return false; + } + debug_published = mapping; + } +#else + if (path) + return false; /* debugger memory works; Windows file transport is + external */ +#endif + memset(debug_published, 0, sizeof(*debug_published)); + debug_published->magic = DEBUG_MAGIC; + debug_published->version = 1; + debug_published->size = sizeof(*debug_published); + debug_store(&debug_published->active, 0); +#ifdef _WIN32 + debug_published->pid = (uint32_t)_getpid(); +#else + debug_published->pid = (uint32_t)getpid(); +#endif + const char *identity = getenv("WASM_DEBUG_MANIFEST_SHA256"); + if (identity) { + if (strlen(identity) != 64) + return false; + for (size_t i = 0; i < 32; i++) { + unsigned byte; + if (sscanf(identity + 2 * i, "%2x", &byte) != 1) + return false; + debug_published->manifest_sha256[i] = (unsigned char)byte; + } + } + return true; +} +void +debug_close(void) +{ +#ifndef _WIN32 + if (mapping_fd >= 0) { + munmap(debug_published, sizeof(*debug_published)); + close(mapping_fd); + mapping_fd = -1; + debug_published = &fallback; + } +#endif +} +bool +debug_task_start(debug_task *task, uint32_t *generation, + const debug_task *parent) +{ + if (!task || !generation || *generation == UINT32_MAX || parent == task) + return false; + memset(task, 0, sizeof(*task)); + task->current.task = ++*generation; + if (parent) { + task->current.parent = parent->current.task; + task->ancestry[0] = parent->current; + task->ancestors = 1; + for (uint32_t i = 0; + i < parent->ancestors && task->ancestors < DEBUG_ANCESTORS; i++) + task->ancestry[task->ancestors++] = parent->ancestry[i]; + task->truncated = + parent->truncated || parent->ancestors == DEBUG_ANCESTORS; + } + return true; +} +bool +debug_task_error(debug_task *task, uint32_t generation, uint32_t cause, + const char *message) +{ + if (task->current.task != generation + || task->current.event != WASM_EXT_DEBUG_PENDING) + return false; + task->current.cause = cause; + int length = snprintf(task->current.error, sizeof(task->current.error), + "%s", message); + task->current.error_flags = + length < 0 || (size_t)length >= sizeof(task->current.error); + return true; +} +bool +debug_task_cause(debug_task *task, const debug_task *cause) +{ + if (task == cause || task->current.event != WASM_EXT_DEBUG_PENDING + || cause->current.event != WASM_EXT_DEBUG_ASYNC_ERROR) + return false; + task->current.cause = cause->current.task; + task->failures[0] = cause->current; + task->causes = 1; + for (uint32_t i = 0; i < cause->causes && task->causes < DEBUG_ANCESTORS; + i++) + task->failures[task->causes++] = cause->failures[i]; + task->causes_truncated = + cause->causes_truncated || cause->causes == DEBUG_ANCESTORS; + return true; +} +void +debug_observe(void *env, uint32_t event, + const wasm_ext_debug_snapshot_t *snapshot) +{ + debug_task *task = env; + if (snapshot->version != 1 || snapshot->size != sizeof(*snapshot) + || snapshot->count > WASM_EXT_DEBUG_FRAMES) + return; + task->current.guest = *snapshot; + task->current.event = event; + publish(task); +} diff --git a/crates/c-api/tests/continuations/debug/recorder.h b/crates/c-api/tests/continuations/debug/recorder.h new file mode 100644 index 0000000..9e9d199 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/recorder.h @@ -0,0 +1,71 @@ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#ifndef DIAGNOSTIC_RECORDER_H +#define DIAGNOSTIC_RECORDER_H +#include "wasm_ext_debug.h" +#ifdef _MSC_VER +#include +typedef volatile long debug_atomic; +static inline uint32_t +debug_load(debug_atomic *value) +{ + return (uint32_t)_InterlockedCompareExchange(value, 0, 0); +} +static inline void +debug_store(debug_atomic *value, uint32_t bank) +{ + _InterlockedExchange(value, (long)bank); +} +#else +#include +typedef _Atomic uint32_t debug_atomic; +static inline uint32_t +debug_load(debug_atomic *value) +{ + return atomic_load_explicit(value, memory_order_acquire); +} +static inline void +debug_store(debug_atomic *value, uint32_t bank) +{ + atomic_store_explicit(value, bank, memory_order_release); +} +#endif +/* Consumer policy, not a runtime ABI. Single writer; readers stop the writer + * (debuggers) or inspect the mapping after process death. Never read a live + * mutating bank without synchronization. No process pointers in this format. */ +#define DEBUG_ANCESTORS 4 +#define DEBUG_MAGIC 0x57444247u +typedef struct debug_segment { + uint32_t task, parent, event, cause, error_flags; + wasm_ext_debug_snapshot_t guest; + char error[128]; +} debug_segment; +typedef struct debug_task { + debug_segment current; + uint32_t ancestors, truncated, causes, causes_truncated; + debug_segment ancestry[DEBUG_ANCESTORS]; + debug_segment failures[DEBUG_ANCESTORS]; +} debug_task; +typedef struct debug_record { + uint32_t magic, version, size; + debug_atomic active; + uint32_t pid; + unsigned char manifest_sha256[32], reserved[12]; + debug_task banks[2]; +} debug_record; +extern debug_record *debug_published; +/* Generation IDs are never reused; zero and wrap are rejected. Ancestors are + * copied at spawn, so parent destruction/slot reuse cannot retarget a trace. */ +bool +debug_task_start(debug_task *, uint32_t *, const debug_task *); +bool +debug_task_error(debug_task *, uint32_t, uint32_t, const char *); +bool +debug_task_cause(debug_task *, const debug_task *); +void +debug_observe(void *, uint32_t, const wasm_ext_debug_snapshot_t *); +bool +debug_open(const char *); +void +debug_close(void); +#endif diff --git a/crates/c-api/tests/continuations/debug/source_guest.c b/crates/c-api/tests/continuations/debug/source_guest.c new file mode 100644 index 0000000..b7c4ab5 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/source_guest.c @@ -0,0 +1,26 @@ +/* Copyright (C) 2026 Matt Hargett. SPDX-License-Identifier: Apache-2.0 WITH + * LLVM-exception */ +__attribute__((import_module("example:native"), import_name("call"))) int +native_call(int); +__attribute__((noinline)) int +guest_leaf(int value) +{ + if (value == 0) + __builtin_trap(); + if (value > 100) + return guest_leaf(value - 1) + 1; + int result = native_call(value); + if (result < 0) + __builtin_trap(); + return result + 1; +} +__attribute__((noinline)) int +guest_middle(int value) +{ + return guest_leaf(value) + 1; +} +__attribute__((export_name("run"))) int +guest_root(int value) +{ + return guest_middle(value) + 1; +} diff --git a/crates/c-api/tests/continuations/debug/source_guest.wasm b/crates/c-api/tests/continuations/debug/source_guest.wasm new file mode 100755 index 0000000000000000000000000000000000000000..d6c6f63b21084c4700299a395c08c44f0676c897 GIT binary patch literal 1340 zcmZ8hOOM<{5Uz5&J+tF^?TnP!5lZCc5rjr|7}yI-6n0q!5`>fkti%OPdOSUrgI}`S zlk90Xi-ZJcB*baOPvFe&;g|zw4p42+?5m|JSM^t2uKucQ5JijtfEGT-Q#>_B*p;tD zoJDd!5h`3s(-%<$z)VJACdUjBIYYok->_mCr}>EiEH4vaEy0eEavV7DxC6$4HV*%W zCmo>Y>5KmBv$HeUg*Ms_^vOPhw{aI7JUsonqxTN+Mb8FXhu5*RQG4e)SbK2X@)mMl z2D2#j|D$D4ReF6wV!|7Mi^?UnK|QiRtWg6lw6%%F%UJjwJL|k+KF{R}xADF1kh5-o z^j3Epk4FH8N1FrdVCT2b$E>?=e`>!!emeYYOx$oVM)lUyA(gRr!{tApBTR?e2w-`5 zb@Jg@pKWl^XPWN<>prq=8(@pF?ObI8cx~A0+QMX+{!?9r6qxuu>AUcyc5W?0^KwMe zc*8Ui7ax#~){mrhA7Ss(HSdvmN4sr}^82LT(MC>6vjx^&QfO+lp%pX-q_}B(L5~i} z<{kTF<3NXOnYt6zODez0`iuZDz9n`2P=kCN-8xkH{1<}s6=`{!_T(Z7WT9{Om_%3b z4T(-FNG{h;xS5keet@mK6A zkq1)EmQtvaI@g@o^D_Fut$#q1-ZjTfGc~!R`{{L zmrBed=>}0c7f~@8CTXH_k!4{LxXVnXG$xZ?E|)Twi7#DslF4GyEW#jh 1: + raise ValueError('snapshot header mismatch') + start = 64 + active * TASK + def segment(offset): + task, parent, event, cause, error_flags = struct.unpack_from('<5I', data, offset) + v, size, count, flags = struct.unpack_from('<4I', data, offset+20) + if (v, size) != (1, 528) or count > 32 or flags & ~1: + raise ValueError('invalid guest snapshot') + frames = [struct.unpack_from('<4I', data, offset+36+i*16) for i in range(count)] + error = data[offset+548:offset+676].split(b'\0', 1)[0].decode('utf-8', errors='replace') + return dict(task=task, parent=parent, event=EVENTS.get(event, 'unknown'), cause=cause, + frames=frames, truncated=bool(flags), error=error, error_truncated=bool(error_flags)) + ancestors, truncated, causes, causes_truncated = struct.unpack_from('<4I', data, start+SEGMENT) + if ancestors > 4 or causes > 4: + raise ValueError('invalid bounded ancestry count') + return dict(current=segment(start), + ancestry=[segment(start+SEGMENT+16+i*SEGMENT) for i in range(ancestors)], + failures=[segment(start+SEGMENT+16+(4+i)*SEGMENT) for i in range(causes)], + truncated=bool(truncated), causes_truncated=bool(causes_truncated)) + +def render(data, manifest_path=None): + record = decode(data) + manifest_bytes = Path(manifest_path or os.environ['WASM_DEBUG_MANIFEST']).read_bytes() + if hashlib.sha256(manifest_bytes).digest() != data[20:52]: + raise ValueError('snapshot belongs to a different build/session manifest') + manifest = json.loads(manifest_bytes) + modules = manifest['modules'] + # Refuse stale modules instead of resolving a plausible but incorrect source line. + for module in modules.values(): + if hashlib.sha256(Path(module['path']).read_bytes()).hexdigest() != module['sha256']: + raise ValueError('original Wasm content hash mismatch') + for source in module.get('sources', []): + if hashlib.sha256(Path(source['path']).read_bytes()).hexdigest() != source['sha256']: + raise ValueError('guest source content hash mismatch') + lines = [] + def show(segment, label): + lines.append(f"{label} task={segment['task']} parent={segment['parent']} state={segment['event']} cause={segment['cause']}") + if segment['error']: + lines.append(' exception: '+segment['error']+(' [message truncated]' if segment['error_truncated'] else '')) + for instance, function, offset, flags in segment['frames']: + module = modules.get(str(instance)) + prefix = f' Wasm instance={instance} function={function} ' + if flags != 1 or not module: + lines.append(prefix+'') + continue + if offset >= Path(module['path']).stat().st_size: + raise ValueError('source offset outside original module') + resolved = subprocess.check_output([manifest['wasm_tools'], 'addr2line', module['path'], hex(offset)], text=True, timeout=15).strip() + for source in module.get('sources', []): + resolved = resolved.replace('/guest/'+Path(source['path']).name, source['path']) + lines.append(prefix+resolved) + if segment['truncated']: + lines.append(' [guest stack truncated at 32 frames]') + show(record['current'], 'current') + for segment in record['ancestry']: + show(segment, 'async parent') + for segment in record['failures']: + show(segment, 'caused by') + if record['truncated']: + lines.append('[async ancestry truncated at 4 parents]') + if record['causes_truncated']: + lines.append('[exception causes truncated at 4]') + return '\n'.join(lines)+'\n' + +if __name__ == '__main__': + import sys + print(render(Path(sys.argv[1]).read_bytes(), sys.argv[2]), end='') diff --git a/crates/c-api/tests/continuations/debug/test_symbolize.py b/crates/c-api/tests/continuations/debug/test_symbolize.py new file mode 100644 index 0000000..b52e4d4 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/test_symbolize.py @@ -0,0 +1,51 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +import hashlib +import json +from pathlib import Path +import struct +import tempfile +import unittest +import symbolize + + +class DecodeTest(unittest.TestCase): + def record(self): + data = bytearray(symbolize.RECORD) + struct.pack_into('<4I', data, 0, 0x57444247, 1, len(data), 0) + struct.pack_into('<4I', data, 64, 7, 0, 3, 0) + struct.pack_into('<4I', data, 84, 1, 528, 0, 0) + return data + + def test_bounded_corrupt_and_interrupted_publication(self): + data = self.record() + # Inactive bank can be half-written; active committed bank stays readable. + data[64+symbolize.TASK:] = b'\xff'*symbolize.TASK + self.assertEqual(symbolize.decode(data)['current']['task'], 7) + for offset, value in [(12,2), (92,33), (64+symbolize.SEGMENT,5), (64+symbolize.SEGMENT+8,5)]: + broken = data.copy() + struct.pack_into(' Date: Tue, 3 Nov 2026 23:44:41 -0800 Subject: [PATCH 19/20] test: make the crash recorder portable to strict C11 builds --- crates/c-api/tests/continuations/debug/recorder.c | 3 +++ .../c-api/tests/continuations/debug/source_guest.c | 4 ++-- .../tests/continuations/debug/source_guest.wasm | Bin 3 files changed, 5 insertions(+), 2 deletions(-) mode change 100755 => 100644 crates/c-api/tests/continuations/debug/source_guest.wasm diff --git a/crates/c-api/tests/continuations/debug/recorder.c b/crates/c-api/tests/continuations/debug/recorder.c index 14ab277..18cae38 100644 --- a/crates/c-api/tests/continuations/debug/recorder.c +++ b/crates/c-api/tests/continuations/debug/recorder.c @@ -1,5 +1,8 @@ /* Copyright (C) 2026 Matt Hargett. All rights reserved. * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif #include "recorder.h" #include #include diff --git a/crates/c-api/tests/continuations/debug/source_guest.c b/crates/c-api/tests/continuations/debug/source_guest.c index b7c4ab5..b233ac6 100644 --- a/crates/c-api/tests/continuations/debug/source_guest.c +++ b/crates/c-api/tests/continuations/debug/source_guest.c @@ -1,5 +1,5 @@ -/* Copyright (C) 2026 Matt Hargett. SPDX-License-Identifier: Apache-2.0 WITH - * LLVM-exception */ +/* Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ __attribute__((import_module("example:native"), import_name("call"))) int native_call(int); __attribute__((noinline)) int diff --git a/crates/c-api/tests/continuations/debug/source_guest.wasm b/crates/c-api/tests/continuations/debug/source_guest.wasm old mode 100755 new mode 100644 From 5194cfb22157f31f0b0e0312675143be3cc12a93 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 4 Nov 2026 00:07:06 -0800 Subject: [PATCH 20/20] test: retain diagnostic qualification and disabled-build receipts --- .../c-api/tests/continuations/debug/README.md | 35 +- .../debug/diagnostic_receipts.json | 216 +++ .../continuations/debug/disabled_profile.json | 1242 +++++++++++++++++ .../continuations/debug/disabled_profile.py | 3 + .../tests/continuations/debug/heap_profile.py | 72 + .../tests/continuations/debug/native.cpp | 10 +- .../debug/qemu_diagnostics_disabled.json | 31 + .../tests/continuations/debug/symbolize.py | 6 +- .../continuations/debug/test_symbolize.py | 15 + 9 files changed, 1625 insertions(+), 5 deletions(-) create mode 100644 crates/c-api/tests/continuations/debug/diagnostic_receipts.json create mode 100644 crates/c-api/tests/continuations/debug/disabled_profile.json create mode 100644 crates/c-api/tests/continuations/debug/heap_profile.py create mode 100644 crates/c-api/tests/continuations/debug/qemu_diagnostics_disabled.json diff --git a/crates/c-api/tests/continuations/debug/README.md b/crates/c-api/tests/continuations/debug/README.md index f82c7c3..dd8719d 100644 --- a/crates/c-api/tests/continuations/debug/README.md +++ b/crates/c-api/tests/continuations/debug/README.md @@ -94,6 +94,37 @@ noise. Code and loadable-section comparisons establish whether instrumentation or storage was retained. Runtime source compatibility across feature sets and maintenance cost of the loader hooks remain RFC feedback topics. +## Disabled-build measurements (2026-11-04) + +Both continuation-on and continuation-off builds keep diagnostics disabled. +LTO, whole-program dead stripping, executable sizes and every loadable-section +size match the pre-diagnostics baseline. The complete ordinary-native-call and +continuation-resume disassembly hashes match. Both WAMR executables are +byte-identical; TinyWasm has metadata/cold-layout differences, so no whole-file +identity is claimed there. + +| Runtime | Continuations | Executable bytes, before = after | Native call ns, median before → after | +| --- | --- | ---: | ---: | +| WAMR | on | 228,104 | 14.741 → 14.748 | +| WAMR | off | 210,456 | 14.371 → 14.422 | +| TinyWasm | on | 2,318,552 | 81.360 → 77.725 | +| TinyWasm | off | 2,277,256 | 71.229 → 70.676 | + +Nine paired, alternating process samples have overlapping RSS ranges. WAMR's +RSS medians match; TinyWasm's move by +48 KiB (continuations on) and +16 KiB +(off). These are not evidence of exact zero process-RSS variation or a speedup. +A separate instrumented fixture samples macOS allocator state after the short +semantic workload, before teardown, using the same executable path for both +builds. All five before/after samples have identical live block counts, live +heap bytes and allocator-reserved bytes in all four configurations. This is a +live-heap check, not a peak-heap measurement. Together with the absent compiled +fields/hooks and matching code/section sizes, it checks that diagnostics add no +retained storage or native dispatch work in these disabled configurations. + +The rebuilt WAMR HTTP/2 Cortex-M33 image remains byte-identical and reserves +339,104 of 532,480 bytes (including the existing network allowance). This is +scripted QEMU readiness, not a physical Pico or a diagnostics-enabled image. + ## Reproduce Build a runtime with `../run.py --diagnostics` (add `--gc`, `--call-stacks` for @@ -121,6 +152,8 @@ Regenerate the source fixture with Clang targeting wasm32, `-g -O1 -Wl,--allow-undefined`, mapping the source directory and compilation directory to `/guest`. Keep the exact original Wasm, source hash and native dSYM together. `disabled_profile.py` compares paired macOS baseline/current LTO builds and -records all timing/RSS samples. `observed.json` retains the earlier native-only +records all timing/RSS samples. `heap_profile.py BUILDS --fixture +../fixture.c --tiny-include TINY_ROOT/crates/c-api/include --output heap.json` +rebuilds separate allocator probes from those libraries. `observed.json` retains the earlier native-only audit as historical evidence; its missing-feature conclusions are superseded by the optional diagnostics implemented here. diff --git a/crates/c-api/tests/continuations/debug/diagnostic_receipts.json b/crates/c-api/tests/continuations/debug/diagnostic_receipts.json new file mode 100644 index 0000000..b77f0bc --- /dev/null +++ b/crates/c-api/tests/continuations/debug/diagnostic_receipts.json @@ -0,0 +1,216 @@ +{ + "date_utc": "2026-11-04", + "implementation_heads": { + "wamr": "30d3007f72887ae7003316511e30dadd09548322", + "tinywasm": "cf17b595a806b1759e285513d5546e235b906062" + }, + "scope": "Optional interpreter safe-boundary snapshots plus external native/guest/source/async report joining. Final receipt commit changes fixtures and documentation only.", + "bounds": { + "snapshot_bytes": 528, + "guest_frames": 32, + "recorder_bytes": 12264, + "parent_segments": 4, + "failure_causes": 4, + "error_bytes": 128 + }, + "local_toolchain": { + "clang": "Apple clang 21.0.0 (clang-2100.3.34.1)", + "swift": "6.4.2 (swiftlang-6.4.2.1.9)", + "lldb": "2103.0.34.407", + "rust": "1.100.0-nightly (e457a7b0d 2026-08-27)", + "wasm_tools": "1.247.0" + }, + "local_configuration": { + "wamr": "fast interpreter; diagnostics, continuations, GC and guest call stacks enabled; AOT/JIT/SIMD/libc off; software bounds; -O2 -g with frame pointers and LTO", + "tinywasm": "external-host-calls,diagnostics; release fat LTO, one codegen unit, debug info and panic=abort", + "native_bridges": "optimized C/C++/Rust/Swift with debug info; exceptions caught in their own languages before C ABI return" + }, + "local_apple_and_lldb": { + "wamr": { + "runtime": "wamr", + "debugger": "lldb", + "platform": "macOS-27.2-arm64-arm-64bit-Mach-O", + "swift": true, + "guest_sha256": "69ced9ca122a3b4d6b9d9f88996ea0192f9ec0a55bec3d0021b03edf1d364cdc", + "tests": "\n#00: 0x00ed - $f1\n#01: 0x0010 - $f2\n#02: 0x0010 - run\n\n\n#00: 0x00ed - $f1\n#01: 0x0010 - $f2\n#02: 0x0010 - run\n\nPASS bounded frames, source offsets, async failure, ancestry, truncation, cancellation, stale generations\n", + "mixed_debugger_trace": true, + "apple_crashes": [ + { + "mode": "crash", + "exception": { + "type": "EXC_BREAKPOINT", + "signal": "SIGTRAP" + }, + "binary_uuid": "3056A0A7-0295-38C3-B013-416DC98C8BAA", + "native_symbols": [ + "native_checkpoint", + "nativeSwift(_:)", + "native_rust", + "native_cpp", + "diagnostic_import", + "ext_host", + "wasm_runtime_invoke_c_api_native", + "wasm_interp_call_func_native", + "wasm_interp_call_func_bytecode", + "wasm_interp_call_wasm", + "wasm_call_function", + "wasm_ext_call_resume", + "start", + "main", + "start" + ], + "dsym_uuid_matches": true, + "guest_async_trace": "current task=1 parent=0 state=host cause=0\n Wasm instance=0 function=1 0x79: guest_leaf /source_guest.c:12:18\n Wasm instance=0 function=2 0x98: guest_middle /source_guest.c:20:12\n Wasm instance=0 function=3 0xa6: guest_root /source_guest.c:25:12\n" + }, + { + "mode": "async_crash", + "exception": { + "type": "EXC_BREAKPOINT", + "signal": "SIGTRAP" + }, + "binary_uuid": "3056A0A7-0295-38C3-B013-416DC98C8BAA", + "native_symbols": [ + "failure_checkpoint", + "main", + "start" + ], + "dsym_uuid_matches": true, + "guest_async_trace": "current task=2 parent=1 state=async-error cause=0\n exception: native async failure\n Wasm instance=0 function=1 0x79: guest_leaf /source_guest.c:12:18\n Wasm instance=0 function=2 0x98: guest_middle /source_guest.c:20:12\n Wasm instance=0 function=3 0xa6: guest_root /source_guest.c:25:12\nasync parent task=1 parent=0 state=pending cause=0\n Wasm instance=0 function=1 0x79: guest_leaf /source_guest.c:12:18\n Wasm instance=0 function=2 0x98: guest_middle /source_guest.c:20:12\n Wasm instance=0 function=3 0xa6: guest_root /source_guest.c:25:12\n" + } + ] + }, + "tinywasm": { + "runtime": "tinywasm", + "debugger": "lldb", + "platform": "macOS-27.2-arm64-arm-64bit-Mach-O", + "swift": true, + "guest_sha256": "69ced9ca122a3b4d6b9d9f88996ea0192f9ec0a55bec3d0021b03edf1d364cdc", + "tests": "PASS bounded frames, source offsets, async failure, ancestry, truncation, cancellation, stale generations\n", + "mixed_debugger_trace": true, + "apple_crashes": [ + { + "mode": "crash", + "exception": { + "type": "EXC_BREAKPOINT", + "signal": "SIGTRAP" + }, + "binary_uuid": "D2089CC9-68CC-306D-A628-19A1AAC1B5BB", + "native_symbols": [ + "native_checkpoint", + "nativeSwift(_:)", + "native_rust", + "native_cpp", + "diagnostic_import", + "_RNCNCNvNtCsjE0la0rHT8c_8tinywasm12continuation20wasm_ext_call_resumes7_00B7_", + "_RINvMs_NtCsjE0la0rHT8c_8tinywasm7runtimeNtB5_10StoreState8callbacklNCNCNvNtB7_12continuation20wasm_ext_call_resumes7_00EB7_", + "_RNCNvNtCsjE0la0rHT8c_8tinywasm12continuation20wasm_ext_call_resumes7_0B5_", + "_RINvMs1_NtNtCs9e8SWNSpun4_8tinywasm4func14host_executionNtB6_15PendingHostCall12with_contextlNCNvNtCsjE0la0rHT8c_8tinywasm12continuation20wasm_ext_call_resumes7_0EB1z_", + "wasm_ext_call_resume", + "start", + "main", + "start" + ], + "dsym_uuid_matches": true, + "guest_async_trace": "current task=1 parent=0 state=host cause=0\n Wasm instance=0 function=1 0x79: guest_leaf /source_guest.c:12:18\n Wasm instance=0 function=2 0x98: guest_middle /source_guest.c:20:12\n Wasm instance=0 function=3 0xa6: guest_root /source_guest.c:25:12\n" + }, + { + "mode": "async_crash", + "exception": { + "type": "EXC_BREAKPOINT", + "signal": "SIGTRAP" + }, + "binary_uuid": "D2089CC9-68CC-306D-A628-19A1AAC1B5BB", + "native_symbols": [ + "failure_checkpoint", + "main", + "start" + ], + "dsym_uuid_matches": true, + "guest_async_trace": "current task=2 parent=1 state=async-error cause=0\n exception: native async failure\n Wasm instance=0 function=1 0x79: guest_leaf /source_guest.c:12:18\n Wasm instance=0 function=2 0x98: guest_middle /source_guest.c:20:12\n Wasm instance=0 function=3 0xa6: guest_root /source_guest.c:25:12\nasync parent task=1 parent=0 state=pending cause=0\n Wasm instance=0 function=1 0x79: guest_leaf /source_guest.c:12:18\n Wasm instance=0 function=2 0x98: guest_middle /source_guest.c:20:12\n Wasm instance=0 function=3 0xa6: guest_root /source_guest.c:25:12\n" + } + ] + } + }, + "hosted": { + "wamr": { + "run": "https://github.com/wasm-micro-runtime/wasm-micro-runtime/actions/runs/41794545393", + "head": "30d3007f72887ae7003316511e30dadd09548322", + "result": "success", + "checks": [ + "macOS LLDB with Swift", + "Linux GDB", + "Linux C runtime and client ASan", + "Windows x64 C runtime and client ASan" + ], + "gdb_receipt": { + "runtime": "wamr", + "debugger": "gdb", + "platform": "Linux-7.0.0-1017-azure-x86_64-with-glibc2.39", + "swift": false, + "guest_sha256": "69ced9ca122a3b4d6b9d9f88996ea0192f9ec0a55bec3d0021b03edf1d364cdc", + "tests": "\n#00: 0x00ed - $f1\n#01: 0x0010 - $f2\n#02: 0x0010 - run\n\n\n#00: 0x00ed - $f1\n#01: 0x0010 - $f2\n#02: 0x0010 - run\n\nPASS bounded frames, source offsets, async failure, ancestry, truncation, cancellation, stale generations\n", + "mixed_debugger_trace": true + }, + "gdb_artifact": { + "id": 13304767474, + "sha256": "f712b214c6a0b11d8d818803b96235c0419d61ea96ec44c6848b137f610a1947" + } + }, + "tinywasm": { + "run": "https://github.com/explodingcamera/tinywasm/actions/runs/41794400536", + "head": "cf17b595a806b1759e285513d5546e235b906062", + "result": "success", + "checks": [ + "macOS LLDB with Swift", + "Linux GDB", + "Linux Rust runtime, rebuilt std and C ASan", + "Miri with diagnostics", + "Windows x64 stable and nightly tail ABI; C-client ASan only, Rust uninstrumented" + ], + "gdb_receipt": { + "runtime": "tinywasm", + "debugger": "gdb", + "platform": "Linux-7.0.0-1017-azure-x86_64-with-glibc2.39", + "swift": false, + "guest_sha256": "69ced9ca122a3b4d6b9d9f88996ea0192f9ec0a55bec3d0021b03edf1d364cdc", + "tests": "PASS bounded frames, source offsets, async failure, ancestry, truncation, cancellation, stale generations\n", + "mixed_debugger_trace": true + }, + "gdb_artifact": { + "id": 13305246645, + "sha256": "17b59827423b509b55c268c5fe508a18c6cc3d7ad29c894d10da57d041b3d663" + } + } + }, + "local_checks": { + "wamr_ctest": "7 passed", + "tinywasm_parser_and_core": "42 passed", + "tinywasm_host_execution": "8 passed", + "tinywasm_c_api": "7 passed", + "symbolizer": "3 passed per copy, including interrupted publication, mismatched identities and missing DWARF", + "asan": "Both runtime/C ASan drivers passed, including canary, GC fixture and native language error translation (Swift excluded from ASan).", + "static_analysis": "Required WAMR pre-commit checks passed. Optional diagnostic-enabled clang-tidy also reported two possible null dereferences in unchanged select-rewrite code in the loader; these were not reproduced at runtime under ASan and are not counted as a clean static-analysis result." + }, + "qualified_scenarios": [ + "ordinary and async native-import snapshots", + "pending/resume/done and cancellation before first resume", + "guest traps before and after suspension", + "async native failure with await site and copied parent ancestry", + "native C++/Rust/Swift errors translated after suspension", + "duplicate and malformed failure rejected", + "stale task generations and ID overflow rejected", + "frame, ancestry, cause and message bounds/truncation", + "source function and file/line checks against original DWARF guest", + "TinyWasm guest exception snapshot captured before unwind", + "nested guest trap cannot overwrite pending await snapshot" + ], + "limits": [ + "Apple .ips remains a native report; a manifest/PID/UUID-checked external sidecar adds logical guest and async segments. No fatal-signal handler or live runtime inspection.", + "Host failure completion returns a trap; it does not inject a catchable guest exception or unwind native exceptions through C.", + "No arbitrary live-Wasm sampling, JIT, or WAMR AOT suspension/source-snapshot qualification.", + "TinyWasm cached archives omit debug metadata; use original Wasm for source-qualified snapshots.", + "Physical Windows host 192.168.7.20 timed out; Windows evidence above is hosted CI.", + "No physical Watch/Pico evidence. QEMU is a Cortex-M33 model with scripted network readiness.", + "Process RSS and timing vary. Disabled-build section and dispatch comparisons accompany nine paired serial samples, rather than claiming exact equality of noisy samples." + ] +} diff --git a/crates/c-api/tests/continuations/debug/disabled_profile.json b/crates/c-api/tests/continuations/debug/disabled_profile.json new file mode 100644 index 0000000..13f3897 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/disabled_profile.json @@ -0,0 +1,1242 @@ +{ + "date_utc": "2026-11-04", + "diagnostics": false, + "continuations": "Both on and off; suffix names refer to continuations, not diagnostics.", + "before_heads": { + "wamr": "12a743d38df90d718d150dc5ce672f2a6f451669", + "tinywasm": "62307911eca22dd200dfa0781b69e4429ab479a7" + }, + "after_heads": { + "wamr": "30d3007f72887ae7003316511e30dadd09548322", + "tinywasm": "cf17b595a806b1759e285513d5546e235b906062" + }, + "configuration": "macOS arm64; Apple clang 21 -O3 -flto -fvisibility=hidden; dead_strip; Rust 1.100.0-nightly 2026-08-27 fat LTO, one codegen unit, panic=abort; WAMR fast interpreter without GC/SIMD/AOT/JIT/libc; nine paired alternating serial process samples.", + "interpretation": "All executable and loadable-section sizes match. WAMR binaries are byte-identical. TinyWasm continuation-off text is byte-identical; continuation-on has cold constructor cleanup/layout differences but both ordinary native-call and continuation-resume dispatch code hashes match. TinyWasm metadata differs; no binary-identity claim. RSS/timing samples fluctuate; no retained diagnostic storage or hot-path instructions in these configurations.", + "results": { + "wamr_on": { + "binary_bytes": { + "base": 228104, + "after": 228104 + }, + "sha256": { + "base": "79da7f654915c948f716e48876f109240293332eb9e1a1fbde19518a97c15898", + "after": "79da7f654915c948f716e48876f109240293332eb9e1a1fbde19518a97c15898" + }, + "section_sizes_identical": true, + "text_identical": true, + "sections": { + "base": { + "__TEXT/__text": { + "size": 157216, + "sha256": "1ec99f7690541c673bc954e31d666e8f23626f0a267be859bebfd07b126266cf" + }, + "__TEXT/__stubs": { + "size": 540, + "sha256": "de7f4eda2143c13c92afed042b3915e87607d0393e147a22045f01d5b0cda54f" + }, + "__TEXT/__const": { + "size": 976, + "sha256": "1245e9f2029432dbd488e047d44cdb717c145c3d09211579257bd5953cf57398" + }, + "__TEXT/__cstring": { + "size": 11886, + "sha256": "fd948bbcbbf7b4f1b4259803a4509888f557a6b64cc3733f436c1aa7aa6aacb0" + }, + "__TEXT/__unwind_info": { + "size": 688, + "sha256": "17f1087cdb393305739f76e347ddd6505299ed08713b43a9e48d2aa58846ff36" + }, + "__DATA_CONST/__const": { + "size": 40, + "sha256": "dfd1d02785ce8a5bba2b0dbae50c263de8f3d300fdacbf62a70735736d369f04" + }, + "__DATA_CONST/__got": { + "size": 376, + "sha256": "f5abd62524d4be027d9ca70d003cf280b0109d1539efde1004966d60181603f4" + }, + "__DATA/__data": { + "size": 2232, + "sha256": "d0168d0cb6d83292acf218a3f37e95f9dbf1a5223e412dd7aad652abbccd154f" + }, + "__DATA/__thread_vars": { + "size": 72, + "sha256": "a824209443a7b949b0b3b1eee0dc226ae5fed74c1ec5b85cd180aedc7e83bb43" + }, + "__DATA/__thread_bss": { + "size": 24, + "sha256": null + }, + "__DATA/__bss": { + "size": 24, + "sha256": null + }, + "__DATA/__common": { + "size": 128, + "sha256": null + } + }, + "after": { + "__TEXT/__text": { + "size": 157216, + "sha256": "1ec99f7690541c673bc954e31d666e8f23626f0a267be859bebfd07b126266cf" + }, + "__TEXT/__stubs": { + "size": 540, + "sha256": "de7f4eda2143c13c92afed042b3915e87607d0393e147a22045f01d5b0cda54f" + }, + "__TEXT/__const": { + "size": 976, + "sha256": "1245e9f2029432dbd488e047d44cdb717c145c3d09211579257bd5953cf57398" + }, + "__TEXT/__cstring": { + "size": 11886, + "sha256": "fd948bbcbbf7b4f1b4259803a4509888f557a6b64cc3733f436c1aa7aa6aacb0" + }, + "__TEXT/__unwind_info": { + "size": 688, + "sha256": "17f1087cdb393305739f76e347ddd6505299ed08713b43a9e48d2aa58846ff36" + }, + "__DATA_CONST/__const": { + "size": 40, + "sha256": "dfd1d02785ce8a5bba2b0dbae50c263de8f3d300fdacbf62a70735736d369f04" + }, + "__DATA_CONST/__got": { + "size": 376, + "sha256": "f5abd62524d4be027d9ca70d003cf280b0109d1539efde1004966d60181603f4" + }, + "__DATA/__data": { + "size": 2232, + "sha256": "d0168d0cb6d83292acf218a3f37e95f9dbf1a5223e412dd7aad652abbccd154f" + }, + "__DATA/__thread_vars": { + "size": 72, + "sha256": "a824209443a7b949b0b3b1eee0dc226ae5fed74c1ec5b85cd180aedc7e83bb43" + }, + "__DATA/__thread_bss": { + "size": 24, + "sha256": null + }, + "__DATA/__bss": { + "size": 24, + "sha256": null + }, + "__DATA/__common": { + "size": 128, + "sha256": null + } + } + }, + "hot_function_disassembly": { + "base": { + "_wasm_func_call": "c14fc8e5e4b359c3eae372cbc047ebfde55eb5aef7d8ea9d9d17d79787c44ffc", + "_wasm_ext_call_resume": "2061039cc3b71f299a6916688fe9c4f604ed9d564e215a1c4d25fc32b87f83f1" + }, + "after": { + "_wasm_func_call": "c14fc8e5e4b359c3eae372cbc047ebfde55eb5aef7d8ea9d9d17d79787c44ffc", + "_wasm_ext_call_resume": "2061039cc3b71f299a6916688fe9c4f604ed9d564e215a1c4d25fc32b87f83f1" + } + }, + "samples": { + "base": [ + { + "compute_ns_per_iteration": 2.6101, + "native_ns_per_iteration": 15.117, + "optin_native_ns_per_iteration": 15.1175, + "peak_rss_bytes": 6340608.0 + }, + { + "compute_ns_per_iteration": 2.2417, + "native_ns_per_iteration": 14.8995, + "optin_native_ns_per_iteration": 14.69, + "peak_rss_bytes": 6324224.0 + }, + { + "compute_ns_per_iteration": 2.2827, + "native_ns_per_iteration": 14.7408, + "optin_native_ns_per_iteration": 14.6385, + "peak_rss_bytes": 6324224.0 + }, + { + "compute_ns_per_iteration": 2.4833, + "native_ns_per_iteration": 14.7357, + "optin_native_ns_per_iteration": 14.497, + "peak_rss_bytes": 6324224.0 + }, + { + "compute_ns_per_iteration": 2.6348, + "native_ns_per_iteration": 14.9502, + "optin_native_ns_per_iteration": 14.942, + "peak_rss_bytes": 6340608.0 + }, + { + "compute_ns_per_iteration": 2.168, + "native_ns_per_iteration": 14.6448, + "optin_native_ns_per_iteration": 14.479, + "peak_rss_bytes": 6324224.0 + }, + { + "compute_ns_per_iteration": 2.7666, + "native_ns_per_iteration": 14.6766, + "optin_native_ns_per_iteration": 14.538, + "peak_rss_bytes": 6307840.0 + }, + { + "compute_ns_per_iteration": 2.2168, + "native_ns_per_iteration": 14.9131, + "optin_native_ns_per_iteration": 14.962, + "peak_rss_bytes": 6356992.0 + }, + { + "compute_ns_per_iteration": 2.6807, + "native_ns_per_iteration": 14.6511, + "optin_native_ns_per_iteration": 14.394, + "peak_rss_bytes": 6324224.0 + } + ], + "after": [ + { + "compute_ns_per_iteration": 2.3092, + "native_ns_per_iteration": 15.2744, + "optin_native_ns_per_iteration": 15.737, + "peak_rss_bytes": 6340608.0 + }, + { + "compute_ns_per_iteration": 2.4332, + "native_ns_per_iteration": 14.8223, + "optin_native_ns_per_iteration": 14.6135, + "peak_rss_bytes": 6373376.0 + }, + { + "compute_ns_per_iteration": 2.175, + "native_ns_per_iteration": 14.8899, + "optin_native_ns_per_iteration": 14.9905, + "peak_rss_bytes": 6307840.0 + }, + { + "compute_ns_per_iteration": 2.8506, + "native_ns_per_iteration": 14.5399, + "optin_native_ns_per_iteration": 14.724, + "peak_rss_bytes": 6307840.0 + }, + { + "compute_ns_per_iteration": 2.3524, + "native_ns_per_iteration": 14.7478, + "optin_native_ns_per_iteration": 14.5295, + "peak_rss_bytes": 6324224.0 + }, + { + "compute_ns_per_iteration": 2.7069, + "native_ns_per_iteration": 14.6864, + "optin_native_ns_per_iteration": 14.625, + "peak_rss_bytes": 6324224.0 + }, + { + "compute_ns_per_iteration": 2.147, + "native_ns_per_iteration": 14.9869, + "optin_native_ns_per_iteration": 14.9265, + "peak_rss_bytes": 6307840.0 + }, + { + "compute_ns_per_iteration": 2.6632, + "native_ns_per_iteration": 14.6438, + "optin_native_ns_per_iteration": 14.6755, + "peak_rss_bytes": 6373376.0 + }, + { + "compute_ns_per_iteration": 2.3269, + "native_ns_per_iteration": 14.6349, + "optin_native_ns_per_iteration": 14.5555, + "peak_rss_bytes": 6324224.0 + } + ] + }, + "medians": { + "base": { + "compute_ns_per_iteration": 2.4833, + "native_ns_per_iteration": 14.7408, + "optin_native_ns_per_iteration": 14.6385, + "peak_rss_bytes": 6324224.0 + }, + "after": { + "compute_ns_per_iteration": 2.3524, + "native_ns_per_iteration": 14.7478, + "optin_native_ns_per_iteration": 14.6755, + "peak_rss_bytes": 6324224.0 + } + } + }, + "wamr_off": { + "binary_bytes": { + "base": 210456, + "after": 210456 + }, + "sha256": { + "base": "add658b02c7326bc430fadcafe303a59753767e66fa1eabbb07d4d25b5b26392", + "after": "add658b02c7326bc430fadcafe303a59753767e66fa1eabbb07d4d25b5b26392" + }, + "section_sizes_identical": true, + "text_identical": true, + "sections": { + "base": { + "__TEXT/__text": { + "size": 147256, + "sha256": "440cb747b2b5e88b65396115320f31f98839667b0a48bbaf6026060dc115c988" + }, + "__TEXT/__stubs": { + "size": 516, + "sha256": "bcea69167a080d794d64186a392fb8ea24d14b410cf9466bba6f51995378fb25" + }, + "__TEXT/__cstring": { + "size": 9761, + "sha256": "f07c13b5c08bca830647367aea9d8a0a8fd0d947bcee973882e69b44602f1b6d" + }, + "__TEXT/__const": { + "size": 768, + "sha256": "f9a268477e38c498cb316a112f14f1b78b14575b028660720d85b7e4301693fa" + }, + "__TEXT/__unwind_info": { + "size": 624, + "sha256": "7596ceab582a81dd98a12d8429cc5e1912b6331dff31dd38d6f79b14844f6744" + }, + "__DATA_CONST/__const": { + "size": 40, + "sha256": "0ea711f4c1753db409264130b6774392908be4b86f863015d3ac570fb07d1c24" + }, + "__DATA_CONST/__got": { + "size": 360, + "sha256": "4556bc72cad1258e17e4d614cfff9c01e13e1369949d02c2f7a7184cde4924f3" + }, + "__DATA/__data": { + "size": 2232, + "sha256": "4d4eccbf31cabf71adf6279744cacf136ad98aa8789952146c25e0b07c61f413" + }, + "__DATA/__thread_vars": { + "size": 48, + "sha256": "501a6180e5660e83732938f859c53dee583a04942418e24fda154ffbe14629df" + }, + "__DATA/__thread_bss": { + "size": 16, + "sha256": null + }, + "__DATA/__bss": { + "size": 16, + "sha256": null + }, + "__DATA/__common": { + "size": 128, + "sha256": null + } + }, + "after": { + "__TEXT/__text": { + "size": 147256, + "sha256": "440cb747b2b5e88b65396115320f31f98839667b0a48bbaf6026060dc115c988" + }, + "__TEXT/__stubs": { + "size": 516, + "sha256": "bcea69167a080d794d64186a392fb8ea24d14b410cf9466bba6f51995378fb25" + }, + "__TEXT/__cstring": { + "size": 9761, + "sha256": "f07c13b5c08bca830647367aea9d8a0a8fd0d947bcee973882e69b44602f1b6d" + }, + "__TEXT/__const": { + "size": 768, + "sha256": "f9a268477e38c498cb316a112f14f1b78b14575b028660720d85b7e4301693fa" + }, + "__TEXT/__unwind_info": { + "size": 624, + "sha256": "7596ceab582a81dd98a12d8429cc5e1912b6331dff31dd38d6f79b14844f6744" + }, + "__DATA_CONST/__const": { + "size": 40, + "sha256": "0ea711f4c1753db409264130b6774392908be4b86f863015d3ac570fb07d1c24" + }, + "__DATA_CONST/__got": { + "size": 360, + "sha256": "4556bc72cad1258e17e4d614cfff9c01e13e1369949d02c2f7a7184cde4924f3" + }, + "__DATA/__data": { + "size": 2232, + "sha256": "4d4eccbf31cabf71adf6279744cacf136ad98aa8789952146c25e0b07c61f413" + }, + "__DATA/__thread_vars": { + "size": 48, + "sha256": "501a6180e5660e83732938f859c53dee583a04942418e24fda154ffbe14629df" + }, + "__DATA/__thread_bss": { + "size": 16, + "sha256": null + }, + "__DATA/__bss": { + "size": 16, + "sha256": null + }, + "__DATA/__common": { + "size": 128, + "sha256": null + } + } + }, + "hot_function_disassembly": { + "base": { + "_wasm_func_call": "9808204961e4b325d846d278934b3961739e5cd2a5a0ec76b18e2800f883eca0" + }, + "after": { + "_wasm_func_call": "9808204961e4b325d846d278934b3961739e5cd2a5a0ec76b18e2800f883eca0" + } + }, + "samples": { + "base": [ + { + "compute_ns_per_iteration": 2.8751, + "native_ns_per_iteration": 14.3457, + "peak_rss_bytes": 6111232.0 + }, + { + "compute_ns_per_iteration": 2.9642, + "native_ns_per_iteration": 14.3872, + "peak_rss_bytes": 6094848.0 + }, + { + "compute_ns_per_iteration": 3.0237, + "native_ns_per_iteration": 14.2337, + "peak_rss_bytes": 6144000.0 + }, + { + "compute_ns_per_iteration": 2.9851, + "native_ns_per_iteration": 14.345, + "peak_rss_bytes": 6094848.0 + }, + { + "compute_ns_per_iteration": 3.1183, + "native_ns_per_iteration": 14.5759, + "peak_rss_bytes": 6144000.0 + }, + { + "compute_ns_per_iteration": 2.8989, + "native_ns_per_iteration": 14.3706, + "peak_rss_bytes": 6127616.0 + }, + { + "compute_ns_per_iteration": 3.1103, + "native_ns_per_iteration": 14.7779, + "peak_rss_bytes": 6127616.0 + }, + { + "compute_ns_per_iteration": 3.0621, + "native_ns_per_iteration": 14.3166, + "peak_rss_bytes": 6127616.0 + }, + { + "compute_ns_per_iteration": 3.1381, + "native_ns_per_iteration": 14.7906, + "peak_rss_bytes": 6144000.0 + } + ], + "after": [ + { + "compute_ns_per_iteration": 2.9784, + "native_ns_per_iteration": 14.4933, + "peak_rss_bytes": 6111232.0 + }, + { + "compute_ns_per_iteration": 2.9298, + "native_ns_per_iteration": 14.2639, + "peak_rss_bytes": 6111232.0 + }, + { + "compute_ns_per_iteration": 2.8881, + "native_ns_per_iteration": 14.312, + "peak_rss_bytes": 6127616.0 + }, + { + "compute_ns_per_iteration": 2.9179, + "native_ns_per_iteration": 14.4632, + "peak_rss_bytes": 6144000.0 + }, + { + "compute_ns_per_iteration": 2.8899, + "native_ns_per_iteration": 14.2214, + "peak_rss_bytes": 6111232.0 + }, + { + "compute_ns_per_iteration": 2.9321, + "native_ns_per_iteration": 14.2436, + "peak_rss_bytes": 6127616.0 + }, + { + "compute_ns_per_iteration": 3.142, + "native_ns_per_iteration": 14.824, + "peak_rss_bytes": 6144000.0 + }, + { + "compute_ns_per_iteration": 2.9047, + "native_ns_per_iteration": 14.4224, + "peak_rss_bytes": 6144000.0 + }, + { + "compute_ns_per_iteration": 2.9337, + "native_ns_per_iteration": 14.5567, + "peak_rss_bytes": 6144000.0 + } + ] + }, + "medians": { + "base": { + "compute_ns_per_iteration": 3.0237, + "native_ns_per_iteration": 14.3706, + "peak_rss_bytes": 6127616.0 + }, + "after": { + "compute_ns_per_iteration": 2.9298, + "native_ns_per_iteration": 14.4224, + "peak_rss_bytes": 6127616.0 + } + } + }, + "tinywasm_on": { + "binary_bytes": { + "base": 2318552, + "after": 2318552 + }, + "sha256": { + "base": "b85e30e43556a79d6865f9a6c4986b15b126d0912c137981ab83f91ebeeafe3f", + "after": "ea5130aa8ad95564b7765250ace80b0a6c345daa9484ad8578d77605bc6193a6" + }, + "section_sizes_identical": true, + "text_identical": false, + "sections": { + "base": { + "__TEXT/__text": { + "size": 1449768, + "sha256": "df5e276c8a6ece1ceb78efcc87a75bf1cdaac257605837e95102f9671051c6bc" + }, + "__TEXT/__stubs": { + "size": 888, + "sha256": "48949c071ae183ff4658f469de5864fdb0e3fbb8fc6a4657a391046177320a21" + }, + "__TEXT/__const": { + "size": 73264, + "sha256": "6a3ee293879b0c463810d466c6547ad004197283e6231a403c260c4c72ec98b0" + }, + "__TEXT/__gcc_except_tab": { + "size": 3228, + "sha256": "e75cd6da042a736152a2f8d52356ed9a809bba5f885ac462aee724416447812b" + }, + "__TEXT/__cstring": { + "size": 17579, + "sha256": "b9688c6fbe583876cffd487764b1197d64b3d20e57fff2c769ac92c7daa4deb1" + }, + "__TEXT/__unwind_info": { + "size": 7472, + "sha256": "00f42d85c1c6eab9a477fef8295426be17cb022d14b342a2949d89ec30bfd857" + }, + "__TEXT/__eh_frame": { + "size": 20536, + "sha256": "6e814d8e37fc872dca9f2a400b66d1bcb4b536d6d356f48f806325bcd6a39206" + }, + "__DATA_CONST/__const": { + "size": 28632, + "sha256": "57131b07246421757f4b3d1d0a2876ee2cfb701da2d28784000bcbc5cefd37a3" + }, + "__DATA_CONST/__got": { + "size": 616, + "sha256": "4a5707c0b00bd04643e937e8e069e8198500b697fe1e3d0b1cf69fe7435cea48" + }, + "__DATA/__data": { + "size": 2576, + "sha256": "109c6a5b262cfc7ec7dba14416097f7d5a27318663b88b1fd523c00065693689" + }, + "__DATA/__thread_vars": { + "size": 168, + "sha256": "0b04ae6eb2fd34e4ebffde457fd90e676f887376a85135e39cb68938dcc21681" + }, + "__DATA/__thread_data": { + "size": 72, + "sha256": "ac5d32dc98f1a65e0b86688dd74d8c0dc15cd3c1ba0525aac9c6212bbbc94159" + }, + "__DATA/__thread_bss": { + "size": 64, + "sha256": null + }, + "__DATA/__bss": { + "size": 104, + "sha256": null + } + }, + "after": { + "__TEXT/__text": { + "size": 1449768, + "sha256": "cf2e48cda592e77b6e3434b8f74e0bec367cbbfb066f35d2e4408fd1df2b6985" + }, + "__TEXT/__stubs": { + "size": 888, + "sha256": "48949c071ae183ff4658f469de5864fdb0e3fbb8fc6a4657a391046177320a21" + }, + "__TEXT/__const": { + "size": 73264, + "sha256": "6a3ee293879b0c463810d466c6547ad004197283e6231a403c260c4c72ec98b0" + }, + "__TEXT/__gcc_except_tab": { + "size": 3228, + "sha256": "e75cd6da042a736152a2f8d52356ed9a809bba5f885ac462aee724416447812b" + }, + "__TEXT/__cstring": { + "size": 17579, + "sha256": "b9688c6fbe583876cffd487764b1197d64b3d20e57fff2c769ac92c7daa4deb1" + }, + "__TEXT/__unwind_info": { + "size": 7472, + "sha256": "ef54ff08f3639cbb50ba9b3e051f71bd82979cd3b3bdcaca2408b558651d0bfc" + }, + "__TEXT/__eh_frame": { + "size": 20536, + "sha256": "6e814d8e37fc872dca9f2a400b66d1bcb4b536d6d356f48f806325bcd6a39206" + }, + "__DATA_CONST/__const": { + "size": 28632, + "sha256": "e0f8a60f050d32ced38cdfdb95cdac55bb85fb2624b8762fc18c4ec0bef2b53f" + }, + "__DATA_CONST/__got": { + "size": 616, + "sha256": "4a5707c0b00bd04643e937e8e069e8198500b697fe1e3d0b1cf69fe7435cea48" + }, + "__DATA/__data": { + "size": 2576, + "sha256": "109c6a5b262cfc7ec7dba14416097f7d5a27318663b88b1fd523c00065693689" + }, + "__DATA/__thread_vars": { + "size": 168, + "sha256": "0b04ae6eb2fd34e4ebffde457fd90e676f887376a85135e39cb68938dcc21681" + }, + "__DATA/__thread_data": { + "size": 72, + "sha256": "ac5d32dc98f1a65e0b86688dd74d8c0dc15cd3c1ba0525aac9c6212bbbc94159" + }, + "__DATA/__thread_bss": { + "size": 64, + "sha256": null + }, + "__DATA/__bss": { + "size": 104, + "sha256": null + } + } + }, + "hot_function_disassembly": { + "base": { + "_wasm_func_call": "51458ff0e08853d4d29917ab02448b1799bae5117903e1f1af23d38436d59477", + "_wasm_ext_call_resume": "09834552c2bca3efcdffa286bdc1f8e1caf0a9d8e0f69c3b905f1e7550445e94" + }, + "after": { + "_wasm_func_call": "51458ff0e08853d4d29917ab02448b1799bae5117903e1f1af23d38436d59477", + "_wasm_ext_call_resume": "09834552c2bca3efcdffa286bdc1f8e1caf0a9d8e0f69c3b905f1e7550445e94" + } + }, + "samples": { + "base": [ + { + "compute_ns_per_iteration": 3.4445, + "native_ns_per_iteration": 80.3782, + "optin_native_ns_per_iteration": 82.5645, + "peak_rss_bytes": 7012352.0 + }, + { + "compute_ns_per_iteration": 3.478, + "native_ns_per_iteration": 84.6224, + "optin_native_ns_per_iteration": 89.8685, + "peak_rss_bytes": 6995968.0 + }, + { + "compute_ns_per_iteration": 3.465, + "native_ns_per_iteration": 81.36, + "optin_native_ns_per_iteration": 83.006, + "peak_rss_bytes": 6995968.0 + }, + { + "compute_ns_per_iteration": 3.5798, + "native_ns_per_iteration": 81.2699, + "optin_native_ns_per_iteration": 83.659, + "peak_rss_bytes": 6979584.0 + }, + { + "compute_ns_per_iteration": 3.4596, + "native_ns_per_iteration": 80.9736, + "optin_native_ns_per_iteration": 83.169, + "peak_rss_bytes": 6979584.0 + }, + { + "compute_ns_per_iteration": 3.7028, + "native_ns_per_iteration": 81.6964, + "optin_native_ns_per_iteration": 83.656, + "peak_rss_bytes": 6995968.0 + }, + { + "compute_ns_per_iteration": 3.5874, + "native_ns_per_iteration": 85.8935, + "optin_native_ns_per_iteration": 89.0425, + "peak_rss_bytes": 6995968.0 + }, + { + "compute_ns_per_iteration": 3.7037, + "native_ns_per_iteration": 80.9374, + "optin_native_ns_per_iteration": 83.251, + "peak_rss_bytes": 7012352.0 + }, + { + "compute_ns_per_iteration": 3.4975, + "native_ns_per_iteration": 82.144, + "optin_native_ns_per_iteration": 85.207, + "peak_rss_bytes": 7012352.0 + } + ], + "after": [ + { + "compute_ns_per_iteration": 3.6744, + "native_ns_per_iteration": 77.2653, + "optin_native_ns_per_iteration": 80.6365, + "peak_rss_bytes": 7028736.0 + }, + { + "compute_ns_per_iteration": 3.4265, + "native_ns_per_iteration": 77.5066, + "optin_native_ns_per_iteration": 83.344, + "peak_rss_bytes": 7028736.0 + }, + { + "compute_ns_per_iteration": 3.644, + "native_ns_per_iteration": 81.1142, + "optin_native_ns_per_iteration": 83.194, + "peak_rss_bytes": 7061504.0 + }, + { + "compute_ns_per_iteration": 3.4593, + "native_ns_per_iteration": 77.5701, + "optin_native_ns_per_iteration": 80.7845, + "peak_rss_bytes": 7077888.0 + }, + { + "compute_ns_per_iteration": 3.4434, + "native_ns_per_iteration": 80.8701, + "optin_native_ns_per_iteration": 83.614, + "peak_rss_bytes": 7045120.0 + }, + { + "compute_ns_per_iteration": 3.4474, + "native_ns_per_iteration": 77.7136, + "optin_native_ns_per_iteration": 80.8925, + "peak_rss_bytes": 7045120.0 + }, + { + "compute_ns_per_iteration": 3.5952, + "native_ns_per_iteration": 78.5437, + "optin_native_ns_per_iteration": 83.8495, + "peak_rss_bytes": 6995968.0 + }, + { + "compute_ns_per_iteration": 3.4784, + "native_ns_per_iteration": 78.5523, + "optin_native_ns_per_iteration": 83.655, + "peak_rss_bytes": 6995968.0 + }, + { + "compute_ns_per_iteration": 3.6759, + "native_ns_per_iteration": 77.7246, + "optin_native_ns_per_iteration": 81.1365, + "peak_rss_bytes": 7061504.0 + } + ] + }, + "medians": { + "base": { + "compute_ns_per_iteration": 3.4975, + "native_ns_per_iteration": 81.36, + "optin_native_ns_per_iteration": 83.656, + "peak_rss_bytes": 6995968.0 + }, + "after": { + "compute_ns_per_iteration": 3.4784, + "native_ns_per_iteration": 77.7246, + "optin_native_ns_per_iteration": 83.194, + "peak_rss_bytes": 7045120.0 + } + } + }, + "tinywasm_off": { + "binary_bytes": { + "base": 2277256, + "after": 2277256 + }, + "sha256": { + "base": "d0e5d80e7fe388a32f5b393cb1e77ff2643544c12fc7a1030fcb0898eb55530c", + "after": "42499c82f19b35377241d36a47316da4330ced278178f41348989fe468d21908" + }, + "section_sizes_identical": true, + "text_identical": true, + "sections": { + "base": { + "__TEXT/__text": { + "size": 1419688, + "sha256": "d84519a01114cc73de451f2d99bb4fe398792d1cf1e3a9f94988a35ecc44726c" + }, + "__TEXT/__stubs": { + "size": 864, + "sha256": "704d839fda17269caa84d4e2b712477cda745a1c34981ec7f109af7e9e6ee45f" + }, + "__TEXT/__const": { + "size": 72772, + "sha256": "48a3c7353c2a45b188ca3224e90e4382618eb02d9b6e9eac5710fba0c6079097" + }, + "__TEXT/__gcc_except_tab": { + "size": 3228, + "sha256": "a0b2254c77feff000cebc2526fc3052812e24f9bb67504283cb4fcbaa6411810" + }, + "__TEXT/__cstring": { + "size": 15347, + "sha256": "8e42c82463c4e449071d628135ebd292e03789859da83ce9005a93351003982a" + }, + "__TEXT/__unwind_info": { + "size": 7352, + "sha256": "be4b22997536a2567ede48de551fd249cd5fa51b771a2a66f55acf6f985434e3" + }, + "__TEXT/__eh_frame": { + "size": 20488, + "sha256": "30c18a00f4216f7f5c1d079a61bb7c89019683be9e10a148fda60c457a5e61b0" + }, + "__DATA_CONST/__const": { + "size": 27952, + "sha256": "3e778de0461ee4fb3f884e14534dfcf0f574f689f6ecbc9eb8c8bfa68ee07957" + }, + "__DATA_CONST/__got": { + "size": 600, + "sha256": "a9ced5fca14ea71a9a16aab787c969dfb3b26385366a5f0d7b6407f68050fc28" + }, + "__DATA/__data": { + "size": 2576, + "sha256": "59bb0b629c107d46878a7948ad4d2559248b0b6e7a8a1312f30168bcd0ad7933" + }, + "__DATA/__thread_vars": { + "size": 144, + "sha256": "6e4cfb2257b0cd8a36ef6bff65457e6a161b985122ff6a10f0b072746e22ee9e" + }, + "__DATA/__thread_data": { + "size": 72, + "sha256": "ac5d32dc98f1a65e0b86688dd74d8c0dc15cd3c1ba0525aac9c6212bbbc94159" + }, + "__DATA/__thread_bss": { + "size": 40, + "sha256": null + }, + "__DATA/__bss": { + "size": 96, + "sha256": null + } + }, + "after": { + "__TEXT/__text": { + "size": 1419688, + "sha256": "d84519a01114cc73de451f2d99bb4fe398792d1cf1e3a9f94988a35ecc44726c" + }, + "__TEXT/__stubs": { + "size": 864, + "sha256": "704d839fda17269caa84d4e2b712477cda745a1c34981ec7f109af7e9e6ee45f" + }, + "__TEXT/__const": { + "size": 72772, + "sha256": "48a3c7353c2a45b188ca3224e90e4382618eb02d9b6e9eac5710fba0c6079097" + }, + "__TEXT/__gcc_except_tab": { + "size": 3228, + "sha256": "a0b2254c77feff000cebc2526fc3052812e24f9bb67504283cb4fcbaa6411810" + }, + "__TEXT/__cstring": { + "size": 15347, + "sha256": "8e42c82463c4e449071d628135ebd292e03789859da83ce9005a93351003982a" + }, + "__TEXT/__unwind_info": { + "size": 7352, + "sha256": "be4b22997536a2567ede48de551fd249cd5fa51b771a2a66f55acf6f985434e3" + }, + "__TEXT/__eh_frame": { + "size": 20488, + "sha256": "30c18a00f4216f7f5c1d079a61bb7c89019683be9e10a148fda60c457a5e61b0" + }, + "__DATA_CONST/__const": { + "size": 27952, + "sha256": "c12ccbe066cfa8e904d3940fdadd05c4591c1b86b830cde1063d9bc08d43eda2" + }, + "__DATA_CONST/__got": { + "size": 600, + "sha256": "a9ced5fca14ea71a9a16aab787c969dfb3b26385366a5f0d7b6407f68050fc28" + }, + "__DATA/__data": { + "size": 2576, + "sha256": "59bb0b629c107d46878a7948ad4d2559248b0b6e7a8a1312f30168bcd0ad7933" + }, + "__DATA/__thread_vars": { + "size": 144, + "sha256": "6e4cfb2257b0cd8a36ef6bff65457e6a161b985122ff6a10f0b072746e22ee9e" + }, + "__DATA/__thread_data": { + "size": 72, + "sha256": "ac5d32dc98f1a65e0b86688dd74d8c0dc15cd3c1ba0525aac9c6212bbbc94159" + }, + "__DATA/__thread_bss": { + "size": 40, + "sha256": null + }, + "__DATA/__bss": { + "size": 96, + "sha256": null + } + } + }, + "hot_function_disassembly": { + "base": { + "_wasm_func_call": "1b78b55e786966437153b016bb33ef43676e7eaa65cb61b99851957a00525130" + }, + "after": { + "_wasm_func_call": "1b78b55e786966437153b016bb33ef43676e7eaa65cb61b99851957a00525130" + } + }, + "samples": { + "base": [ + { + "compute_ns_per_iteration": 3.4962, + "native_ns_per_iteration": 72.3662, + "peak_rss_bytes": 6881280.0 + }, + { + "compute_ns_per_iteration": 3.46, + "native_ns_per_iteration": 68.0671, + "peak_rss_bytes": 6864896.0 + }, + { + "compute_ns_per_iteration": 3.5237, + "native_ns_per_iteration": 71.2285, + "peak_rss_bytes": 6881280.0 + }, + { + "compute_ns_per_iteration": 3.5482, + "native_ns_per_iteration": 70.8358, + "peak_rss_bytes": 6864896.0 + }, + { + "compute_ns_per_iteration": 3.4813, + "native_ns_per_iteration": 71.8066, + "peak_rss_bytes": 6848512.0 + }, + { + "compute_ns_per_iteration": 3.4752, + "native_ns_per_iteration": 70.6255, + "peak_rss_bytes": 6881280.0 + }, + { + "compute_ns_per_iteration": 3.5149, + "native_ns_per_iteration": 72.013, + "peak_rss_bytes": 6832128.0 + }, + { + "compute_ns_per_iteration": 3.5004, + "native_ns_per_iteration": 70.5183, + "peak_rss_bytes": 6881280.0 + }, + { + "compute_ns_per_iteration": 3.4318, + "native_ns_per_iteration": 71.4248, + "peak_rss_bytes": 6864896.0 + } + ], + "after": [ + { + "compute_ns_per_iteration": 3.4824, + "native_ns_per_iteration": 70.1211, + "peak_rss_bytes": 6864896.0 + }, + { + "compute_ns_per_iteration": 3.5184, + "native_ns_per_iteration": 70.6756, + "peak_rss_bytes": 6832128.0 + }, + { + "compute_ns_per_iteration": 3.4556, + "native_ns_per_iteration": 70.3697, + "peak_rss_bytes": 6881280.0 + }, + { + "compute_ns_per_iteration": 3.5318, + "native_ns_per_iteration": 70.4548, + "peak_rss_bytes": 6897664.0 + }, + { + "compute_ns_per_iteration": 3.4878, + "native_ns_per_iteration": 70.7302, + "peak_rss_bytes": 6897664.0 + }, + { + "compute_ns_per_iteration": 3.4567, + "native_ns_per_iteration": 70.7418, + "peak_rss_bytes": 6881280.0 + }, + { + "compute_ns_per_iteration": 3.6126, + "native_ns_per_iteration": 71.0895, + "peak_rss_bytes": 6897664.0 + }, + { + "compute_ns_per_iteration": 3.4648, + "native_ns_per_iteration": 75.5747, + "peak_rss_bytes": 6897664.0 + }, + { + "compute_ns_per_iteration": 3.4778, + "native_ns_per_iteration": 70.2816, + "peak_rss_bytes": 6848512.0 + } + ] + }, + "medians": { + "base": { + "compute_ns_per_iteration": 3.4962, + "native_ns_per_iteration": 71.2285, + "peak_rss_bytes": 6864896.0 + }, + "after": { + "compute_ns_per_iteration": 3.4824, + "native_ns_per_iteration": 70.6756, + "peak_rss_bytes": 6881280.0 + } + } + } + }, + "live_heap_check": { + "method": "Separate instrumented fixture, same executable path, WASM_EXT_TEST_ONLY=1; malloc_zone_statistics(NULL) before runtime teardown; five process samples. Live heap and allocator reservation, not peak RSS/heap.", + "results": { + "wamr_on": { + "base": [ + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + } + ], + "after": [ + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1135, + "size_in_use": 211472, + "size_allocated": 20971520 + } + ] + }, + "wamr_off": { + "base": [ + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + } + ], + "after": [ + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1079, + "size_in_use": 159392, + "size_allocated": 20971520 + } + ] + }, + "tinywasm_on": { + "base": [ + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + } + ], + "after": [ + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 1017, + "size_in_use": 615104, + "size_allocated": 20971520 + } + ] + }, + "tinywasm_off": { + "base": [ + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + } + ], + "after": [ + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + }, + { + "blocks_in_use": 993, + "size_in_use": 613248, + "size_allocated": 20971520 + } + ] + } + } + } +} diff --git a/crates/c-api/tests/continuations/debug/disabled_profile.py b/crates/c-api/tests/continuations/debug/disabled_profile.py index d4d5e8f..d82dd81 100644 --- a/crates/c-api/tests/continuations/debug/disabled_profile.py +++ b/crates/c-api/tests/continuations/debug/disabled_profile.py @@ -70,6 +70,9 @@ def main(): 'section_sizes_identical': {k:v['size'] for k,v in sec['base'].items()} == {k:v['size'] for k,v in sec['after'].items()}, 'text_identical': sec['base']['__TEXT/__text'] == sec['after']['__TEXT/__text'], 'sections': sec, 'hot_function_disassembly': {phase: hot_functions(path) for phase,path in paths.items()}, 'samples': {'base': [], 'after': []}} + expected = {'_wasm_func_call'} | ({'_wasm_ext_call_resume'} if feature == 'on' else set()) + if any(set(functions) != expected for functions in entry['hot_function_disassembly'].values()): + raise RuntimeError('missing or unexpected dispatch symbols: '+key) if not entry['section_sizes_identical'] or len(set(entry['binary_bytes'].values())) != 1 or entry['hot_function_disassembly']['base'] != entry['hot_function_disassembly']['after']: raise RuntimeError('disabled build changed code or memory footprint: '+key) for i in range(a.samples): diff --git a/crates/c-api/tests/continuations/debug/heap_profile.py b/crates/c-api/tests/continuations/debug/heap_profile.py new file mode 100644 index 0000000..bc0c812 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/heap_profile.py @@ -0,0 +1,72 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Compare live macOS allocator state in separate, instrumented fixture builds.""" +import argparse +import json +import os +from pathlib import Path +import platform +import shutil +import subprocess +import tempfile + + +def main(): + p = argparse.ArgumentParser() + p.add_argument('builds', type=Path) + p.add_argument('--fixture', type=Path, required=True, help='shared fixture.c') + p.add_argument('--tiny-include', type=Path, required=True) + p.add_argument('--output', type=Path, required=True) + p.add_argument('--samples', type=int, default=5) + a = p.parse_args() + if platform.system() != 'Darwin' or a.samples < 1: + p.error('requires macOS and at least one sample') + fixture = a.fixture.resolve() + source = fixture.read_text() + marker = ' wasm_func_delete(run);' + if source.count(marker) != 1: + raise RuntimeError('fixture teardown changed; review the sampling boundary') + # Stop before teardown, after the same short semantic workload in both + # builds. This is live heap, not peak RSS or allocator high-water usage. + source = '#include \n' + source.replace(marker, ''' malloc_statistics_t stats; + malloc_zone_statistics(NULL, &stats); + printf("heap %u %zu %zu\\n", stats.blocks_in_use, stats.size_in_use, stats.size_allocated); +''' + marker) + env = os.environ.copy() + env['WASM_EXT_TEST_ONLY'] = '1' + cc = env.get('CC', 'clang') + result = {} + with tempfile.TemporaryDirectory(dir=a.builds.resolve(), prefix='heap-') as directory: + work = Path(directory) + c = work/'fixture.c' + c.write_text(source) + # A common executable path avoids counting different dyld path-string + # allocation sizes as a runtime difference. + measured = work/'measure' + for runtime in ['wamr', 'tinywasm']: + for feature in ['on', 'off']: + key = runtime+'_'+feature + result[key] = {} + for phase in ['base', 'after']: + build = a.builds.resolve()/(phase+'_'+key) + inc = build/'include' if runtime == 'wamr' else a.tiny_include.resolve() + lib = build/('runtime/libiwasm.a' if runtime == 'wamr' else 'runtime/release/libtinywasm.a') + obj, exe = work/'fixture.o', work/'linked' + cmd = [cc, '-std=c11', '-O3', '-flto', '-fvisibility=hidden', '-I'+str(inc), '-I'+str(fixture.parent), '-c', str(c), '-o', str(obj)] + if feature == 'on': + cmd += ['-DTEST_CONTINUATIONS'] + subprocess.run(cmd, check=True) + subprocess.run([cc, '-O3', '-flto', str(obj), str(lib), '-o', str(exe), '-Wl,-dead_strip', '-framework', 'Security', '-framework', 'CoreFoundation', '-liconv'], check=True) + shutil.copy2(exe, measured) + samples = [] + for _ in range(a.samples): + output = subprocess.check_output([str(measured), str(fixture.with_suffix('.wasm'))], env=env, text=True) + values = next(line.split()[1:] for line in output.splitlines() if line.startswith('heap ')) + samples.append(dict(zip(['blocks_in_use', 'size_in_use', 'size_allocated'], map(int, values)))) + result[key][phase] = samples + print(key, result[key], flush=True) + a.output.write_text(json.dumps(result, indent=2)+'\n') + + +if __name__ == '__main__': + main() diff --git a/crates/c-api/tests/continuations/debug/native.cpp b/crates/c-api/tests/continuations/debug/native.cpp index a20ef51..52d1016 100644 --- a/crates/c-api/tests/continuations/debug/native.cpp +++ b/crates/c-api/tests/continuations/debug/native.cpp @@ -11,9 +11,13 @@ native_cpp(int value) #ifdef DIAGNOSTIC_ERRORS #include -extern "C" __attribute__((noinline)) int native_cpp_error(void) +extern "C" __attribute__((noinline)) int +native_cpp_error(void) { - try { throw std::runtime_error("native C++ operation failed"); } - catch (const std::exception &) { return -11; } + try { + throw std::runtime_error("native C++ operation failed"); + } catch (const std::exception &) { + return -11; + } } #endif diff --git a/crates/c-api/tests/continuations/debug/qemu_diagnostics_disabled.json b/crates/c-api/tests/continuations/debug/qemu_diagnostics_disabled.json new file mode 100644 index 0000000..d75fcd6 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/qemu_diagnostics_disabled.json @@ -0,0 +1,31 @@ +{ + "qemu": "QEMU emulator version 11.1.3", + "machine": "mps2-an505", + "elf_sha256": "b8889f994b926dfb91799e00d74a5251b215e01b8a4759e33eb41f719762f495", + "memory": { + "sram_reserved_bytes": 339104, + "sram_limit_bytes": 532480, + "sram_sections": { + ".data": 3528, + ".bss": 196312, + ".net_reserve": 65536, + ".heap": 65536, + ".stack": 8192 + } + }, + "workload": { + "completed": 4, + "peak_streams": 4, + "peak_handles": 41, + "peak_resources": 9, + "input_peak": 2048, + "h2_heap_peak": 40463, + "libc_peak": 45912, + "pool_peak": 111712, + "server_bytes": 65080 + }, + "runtime": "wamr", + "implementation_head": "30d3007f72887ae7003316511e30dadd09548322", + "diagnostics": false, + "scope": "Stackless HTTP/2 fixture with scripted readiness. Rebuilt on the implementation head; ELF is byte-identical to the previous baseline. This does not run continuation source snapshots or a physical Pico/radio." +} diff --git a/crates/c-api/tests/continuations/debug/symbolize.py b/crates/c-api/tests/continuations/debug/symbolize.py index b8f7788..47acecb 100644 --- a/crates/c-api/tests/continuations/debug/symbolize.py +++ b/crates/c-api/tests/continuations/debug/symbolize.py @@ -64,7 +64,11 @@ def show(segment, label): continue if offset >= Path(module['path']).stat().st_size: raise ValueError('source offset outside original module') - resolved = subprocess.check_output([manifest['wasm_tools'], 'addr2line', module['path'], hex(offset)], text=True, timeout=15).strip() + try: + resolved = subprocess.check_output([manifest['wasm_tools'], 'addr2line', module['path'], hex(offset)], text=True, stderr=subprocess.PIPE, timeout=15).strip() + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError): + lines.append(prefix+hex(offset)+' ') + continue for source in module.get('sources', []): resolved = resolved.replace('/guest/'+Path(source['path']).name, source['path']) lines.append(prefix+resolved) diff --git a/crates/c-api/tests/continuations/debug/test_symbolize.py b/crates/c-api/tests/continuations/debug/test_symbolize.py index b52e4d4..0537978 100644 --- a/crates/c-api/tests/continuations/debug/test_symbolize.py +++ b/crates/c-api/tests/continuations/debug/test_symbolize.py @@ -6,6 +6,8 @@ import struct import tempfile import unittest +from unittest.mock import patch +import subprocess import symbolize @@ -30,6 +32,19 @@ def test_bounded_corrupt_and_interrupted_publication(self): with self.assertRaises(ValueError): symbolize.decode(data[:-1]) + def test_missing_dwarf_keeps_the_logical_frame(self): + data = self.record() + struct.pack_into('', symbolize.render(data, manifest)) + def test_build_identity_and_original_module_hash(self): data = self.record() with tempfile.TemporaryDirectory() as directory: