diff --git a/.github/workflows/shared_continuations.yml b/.github/workflows/shared_continuations.yml new file mode 100644 index 00000000..b9913c31 --- /dev/null +++ b/.github/workflows/shared_continuations.yml @@ -0,0 +1,102 @@ +name: Shared C continuations +on: + workflow_dispatch: + pull_request: + paths: ["crates/parser/**", "crates/types/**", "crates/tinywasm/**", "crates/c-api/**", ".github/workflows/shared_continuations.yml"] +permissions: + contents: read +jobs: + c-fixture: + name: C fixture (${{ matrix.os }}, ${{ matrix.rust }}) + strategy: + matrix: + include: + - os: ubuntu-24.04 + rust: stable + extra: "" + 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,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,diagnostics" + capi_args: "--features external-host-calls,tinywasm/nightly-tail-calls,diagnostics" + 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: ${{ 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 --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 --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 --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: + 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 --gc --simd --sanitize --diagnostics + 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 diagnostics --lib + + native-unwind: + 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 + - 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/.github/workflows/stackless_http.yml b/.github/workflows/stackless_http.yml new file mode 100644 index 00000000..5701c4e9 --- /dev/null +++ b/.github/workflows/stackless_http.yml @@ -0,0 +1,47 @@ +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: + fail-fast: false + 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/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6be4ae55..3f3d66ca 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 c0d12a37..f88c1415 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ 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, 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. The shared fixtures also exercise guest GC + and SIMD across suspension, cancellation and indirect host calls. + ## [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 4a370045..82248fbb 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,27 @@ assert_eq!(result, 3); See the [examples](./examples) directory and [documentation](https://docs.rs/tinywasm) for more information. +## 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. +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/c-api/Cargo.toml b/crates/c-api/Cargo.toml index 63a1c476..3104ca1a 100644 --- a/crates/c-api/Cargo.toml +++ b/crates/c-api/Cargo.toml @@ -16,6 +16,9 @@ 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 = [] [lints] diff --git a/crates/c-api/Makefile b/crates/c-api/Makefile index dfe2b607..77438b02 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 c885ede7..19c945e0 100644 --- a/crates/c-api/README.md +++ b/crates/c-api/README.md @@ -35,7 +35,45 @@ 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. `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. +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 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. + +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 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 - 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/c-api/examples/stackless-http/CMakeLists.txt b/crates/c-api/examples/stackless-http/CMakeLists.txt new file mode 100644 index 00000000..3ab22d22 --- /dev/null +++ b/crates/c-api/examples/stackless-http/CMakeLists.txt @@ -0,0 +1,69 @@ +# 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) +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) +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_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) +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}) + 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/HTTP2.md b/crates/c-api/examples/stackless-http/HTTP2.md new file mode 100644 index 00000000..5fd2efa7 --- /dev/null +++ b/crates/c-api/examples/stackless-http/HTTP2.md @@ -0,0 +1,137 @@ +# 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. + +## 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/README.md b/crates/c-api/examples/stackless-http/README.md new file mode 100644 index 00000000..9f130300 --- /dev/null +++ b/crates/c-api/examples/stackless-http/README.md @@ -0,0 +1,153 @@ +# 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. + +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/RESULTS.md b/crates/c-api/examples/stackless-http/RESULTS.md new file mode 100644 index 00000000..4d50aa78 --- /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 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 +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/WASI_HTTP.md b/crates/c-api/examples/stackless-http/WASI_HTTP.md new file mode 100644 index 00000000..8ad6e95e --- /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_guest.py b/crates/c-api/examples/stackless-http/build_guest.py new file mode 100644 index 00000000..d65e2996 --- /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/build_wasi_http_guest.py b/crates/c-api/examples/stackless-http/build_wasi_http_guest.py new file mode 100644 index 00000000..f1c630c5 --- /dev/null +++ b/crates/c-api/examples/stackless-http/build_wasi_http_guest.py @@ -0,0 +1,57 @@ +#!/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('--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() + 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', + 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) + (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/check.c b/crates/c-api/examples/stackless-http/check.c new file mode 100644 index 00000000..dd4b3ab4 --- /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 00000000..c51111ab --- /dev/null +++ b/crates/c-api/examples/stackless-http/component.py @@ -0,0 +1,250 @@ +#!/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 + + +_TOKEN = re.compile(r'[^\s()]+') + + +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 = _TOKEN.match(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 00000000..5dd89216 --- /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 00000000..44a44e32 --- /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 00000000..75de8aa4 --- /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/h2.c b/crates/c-api/examples/stackless-http/h2.c new file mode 100644 index 00000000..fdfeb138 --- /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 00000000..5af68488 --- /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 00000000..86756487 --- /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/handler.component.wasm b/crates/c-api/examples/stackless-http/handler.component.wasm new file mode 100644 index 00000000..9cbd9327 Binary files /dev/null and b/crates/c-api/examples/stackless-http/handler.component.wasm differ diff --git a/crates/c-api/examples/stackless-http/handler.linked.wasm b/crates/c-api/examples/stackless-http/handler.linked.wasm new file mode 100644 index 00000000..ecc3f586 Binary files /dev/null and b/crates/c-api/examples/stackless-http/handler.linked.wasm differ diff --git a/crates/c-api/examples/stackless-http/handler.wit b/crates/c-api/examples/stackless-http/handler.wit new file mode 100644 index 00000000..1e10ed8f --- /dev/null +++ b/crates/c-api/examples/stackless-http/handler.wit @@ -0,0 +1,7 @@ +package example:stackless-http@0.1.0; + +world handler { + // The native transport supplies request bytes. The resolved call returns the + // response byte stream and a future that completes when production ends. + export handle: async func(slot: u32, request: stream) -> 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 00000000..d87be34e --- /dev/null +++ b/crates/c-api/examples/stackless-http/http.c @@ -0,0 +1,148 @@ +/* + * 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; +} + +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 new file mode 100644 index 00000000..3319a734 --- /dev/null +++ b/crates/c-api/examples/stackless-http/http.h @@ -0,0 +1,56 @@ +/* + * 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" +#ifdef P3_WASI_HTTP +#include "wasi_http.h" +#endif +#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; +#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 +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/http2-receipts.json b/crates/c-api/examples/stackless-http/http2-receipts.json new file mode 100644 index 00000000..8bd203e4 --- /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 00000000..010c90e3 --- /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 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 00000000..4e536e86 --- /dev/null +++ b/crates/c-api/examples/stackless-http/p3.c @@ -0,0 +1,500 @@ +/* + * 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; +} +#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_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); + 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) +{ + 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; + 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); + 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; + /* 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); + 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 = 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; + 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 00000000..6cb63474 --- /dev/null +++ b/crates/c-api/examples/stackless-http/p3.h @@ -0,0 +1,99 @@ +/* + * 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 +#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 { + 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); +#ifdef P3_WASI_HTTP +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); +#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. */ +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 00000000..9940314e --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/CMakeLists.txt @@ -0,0 +1,58 @@ +# 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) + +if(P3_WASI_HTTP) + set(http_sources ../wasi_http.c ../wasi_http_io.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=${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 +# 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 00000000..7efccb40 --- /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 00000000..5f068536 --- /dev/null +++ b/crates/c-api/examples/stackless-http/pico/pico_server.c @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * 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" +#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 = http_input_capacity(&server, i); + 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); +#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"); + 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 00000000..220a44a2 --- /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 00000000..320a4afb --- /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 00000000..ad3f8530 --- /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 00000000..c0b01983 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/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) +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) +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_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=${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) +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/h2_inputs.py b/crates/c-api/examples/stackless-http/qemu/h2_inputs.py new file mode 100644 index 00000000..bbed4e54 --- /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 00000000..5f5dec94 --- /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 new file mode 100644 index 00000000..039dee5d --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/m33.ld @@ -0,0 +1,23 @@ +/* + * 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) +PROVIDE(__p3_stack_size = 4K); +PROVIDE(__p3_heap_size = 16K); +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__ = .; . += __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 00000000..1e92bbb8 --- /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 new file mode 100644 index 00000000..a4ccd9d2 --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/run.py @@ -0,0 +1,121 @@ +#!/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, stack=4096, heap=16384): + 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 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} + + +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.split('_')[0] 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 or not counts['native']: + 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('--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()] + 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', + '-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) + 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, 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, 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)) + + +if __name__ == '__main__': + main() 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 00000000..e02116d4 --- /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/qemu/sim.c b/crates/c-api/examples/stackless-http/qemu/sim.c new file mode 100644 index 00000000..854bb0af --- /dev/null +++ b/crates/c-api/examples/stackless-http/qemu/sim.c @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * 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 +#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(); +} +#ifdef P3_WASI_HTTP +#include "wasi_http_sim.h" +#else +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); +} +#endif +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))); +#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); + 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); +#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)); + printf( + "M33 PASS completed=%lu cancelled=%lu disconnected=%lu live=%lu " + "peak_handles=%lu " + "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, + (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/qemu/startup.c b/crates/c-api/examples/stackless-http/qemu/startup.c new file mode 100644 index 00000000..6095d6ab --- /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/qemu/wasi_http_sim.h b/crates/c-api/examples/stackless-http/qemu/wasi_http_sim.h new file mode 100644 index 00000000..70f5d9ab --- /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/receipts.json b/crates/c-api/examples/stackless-http/receipts.json new file mode 100644 index 00000000..ebe53008 --- /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": "b436dc0623b894e36349597af3f2355f065c215b31e572caf63fe291805726e0", + "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 00000000..ef65139b --- /dev/null +++ b/crates/c-api/examples/stackless-http/run.py @@ -0,0 +1,123 @@ +#!/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' + pinned = subprocess.check_output(['git', '-C', component_tree, 'show', f'{PIN}:{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] + 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 00000000..9d626f9b --- /dev/null +++ b/crates/c-api/examples/stackless-http/server.c @@ -0,0 +1,259 @@ +/* + * 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" +#ifdef P3_WASI_HTTP +#include "standard_handler.h" +#else +#include "handler.h" +#endif +#include +#include +#include +#include +#include "socket_loop.h" +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) +{ + 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); +#ifdef P3_WASI_HTTP + struct sigaction action = { 0 }; + action.sa_handler = interrupt; + sigemptyset(&action.sa_mask); + sigaction(SIGINT, &action, NULL); +#endif +#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)) + 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, 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 }; + 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 (http_input_capacity(&server, i)) + 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)) { + ++rejected; + 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 = http_input_capacity(&server, i); + 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; + } +#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 " + "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/socket_loop.h b/crates/c-api/examples/stackless-http/socket_loop.h new file mode 100644 index 00000000..897cb85b --- /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/task.c b/crates/c-api/examples/stackless-http/task.c new file mode 100644 index 00000000..460b1f28 --- /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 00000000..613f03f0 --- /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/test_wasi_h2.py b/crates/c-api/examples/stackless-http/test_wasi_h2.py new file mode 100644 index 00000000..7fff90c3 --- /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 new file mode 100644 index 00000000..24fa4c78 --- /dev/null +++ b/crates/c-api/examples/stackless-http/test_wasi_http_load.py @@ -0,0 +1,150 @@ +#!/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 ConnectionAbortedError: + return phase + ':abort' + 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']) + 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\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(request_line + headers) + 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/tools/Cargo.lock b/crates/c-api/examples/stackless-http/tools/Cargo.lock new file mode 100644 index 00000000..d95d43bc --- /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 00000000..a046578f --- /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 00000000..0694601d --- /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 00000000..095af0f1 --- /dev/null +++ b/crates/c-api/examples/stackless-http/vm.c @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ +#include "vm.h" +#ifdef P3_WASI_HTTP +#include "wasi_http.h" +#endif +#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); +} +#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) +{ + 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") +#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)) { + 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])); + 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, callback, 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 00000000..fe72bfe3 --- /dev/null +++ b/crates/c-api/examples/stackless-http/vm.h @@ -0,0 +1,91 @@ +/* + * 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 +#ifndef P3_IMPORTS +#define P3_IMPORTS 48 +#endif +#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_; +#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; + 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; +#ifdef P3_WASI_HTTP + struct wasi_http *http; +#endif +}; +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; +#ifdef P3_WASI_HTTP + uint32_t http_result; +#endif +}; +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/examples/stackless-http/wasi-http-receipts.json b/crates/c-api/examples/stackless-http/wasi-http-receipts.json new file mode 100644 index 00000000..de650f40 --- /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 00000000..a1248a86 --- /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 00000000..23459922 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http.h @@ -0,0 +1,87 @@ +/* + * 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 +#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 { + 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 00000000..f06444e8 --- /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 00000000..357083d1 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_guest.c @@ -0,0 +1,326 @@ +/* + * 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 +#ifndef CONNECTIONS +#define CONNECTIONS 4 +#endif + +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_io.c b/crates/c-api/examples/stackless-http/wasi_http_io.c new file mode 100644 index 00000000..cb3b4192 --- /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 00000000..db338415 --- /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_link.py b/crates/c-api/examples/stackless-http/wasi_http_link.py new file mode 100644 index 00000000..36906127 --- /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 00000000..ab1fbfe9 --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_server.c @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2026 Matt Hargett. All rights reserved. + * 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) + +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; + 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; + return http_start_task(http, &c->task, request, &c->input, &c->trailers_tx); +} +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 http_close_end(&server->http, &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 http_close_end(&server->http, &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 && !http_close_end(&server->http, &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 (!http_send_future(&server->http, &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 && !http_close_end(&server->http, &c->input)) + return false; + if ((!c->input || 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; + 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) + || !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)) + 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 (!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, + 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 00000000..734f8fda --- /dev/null +++ b/crates/c-api/examples/stackless-http/wasi_http_tests.py @@ -0,0 +1,125 @@ +#!/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') + 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) + 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 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') + 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()) diff --git a/crates/c-api/include/tinywasm-prefix.h b/crates/c-api/include/tinywasm-prefix.h index ba967725..43df532a 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 00000000..74c7d05f --- /dev/null +++ b/crates/c-api/include/wasm_ext.h @@ -0,0 +1,64 @@ +/* + * 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. + * 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 +#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); +/* 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 +wasm_ext_call_delete(wasm_ext_call_t *); +#ifdef __cplusplus +} +#endif +#endif 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 00000000..1a9f2ac0 --- /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 new file mode 100644 index 00000000..dc45c15a --- /dev/null +++ b/crates/c-api/src/continuation.rs @@ -0,0 +1,460 @@ +//! 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, + #[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 + && 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, + #[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. + 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 { + #[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; + } + fn cancel(&mut self) { + 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 + { + 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; } + #[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 { + 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) => { + #[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() { + 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) => { #[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(); + 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); + #[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() { #[cfg(feature = "diagnostics")] call.debug_event(5); 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); + } + #[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) }; + 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); + } + } + #[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); + } + } +} + +#[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/function.rs b/crates/c-api/src/function.rs index e6cd58a2..00b4fe44 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 484de1b5..b036de41 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/objects.rs b/crates/c-api/src/objects.rs index bb717997..708dfd72 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 8d064055..fad1a636 100644 --- a/crates/c-api/src/runtime.rs +++ b/crates/c-api/src/runtime.rs @@ -13,9 +13,18 @@ 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 { - store: UnsafeCell, + #[cfg(feature = "diagnostics")] + pub(crate) debug_observer: Cell>, + 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 +63,19 @@ 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 { + #[cfg(feature = "diagnostics")] + debug_observer: Cell::new(None), 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 +85,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())); } @@ -90,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/c-api/tests/continuations/FEATURE_COMPAT.md b/crates/c-api/tests/continuations/FEATURE_COMPAT.md new file mode 100644 index 00000000..93a58c7a --- /dev/null +++ b/crates/c-api/tests/continuations/FEATURE_COMPAT.md @@ -0,0 +1,110 @@ +# 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. + +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/READINESS.md b/crates/c-api/tests/continuations/READINESS.md new file mode 100644 index 00000000..e1428483 --- /dev/null +++ b/crates/c-api/tests/continuations/READINESS.md @@ -0,0 +1,133 @@ +# 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. + +## 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 new file mode 100644 index 00000000..caac3061 --- /dev/null +++ b/crates/c-api/tests/continuations/README.md @@ -0,0 +1,96 @@ +# 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) 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 +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, +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 +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. + +## 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. + +## 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. + +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/VALIDATION.md b/crates/c-api/tests/continuations/VALIDATION.md new file mode 100644 index 00000000..5212da12 --- /dev/null +++ b/crates/c-api/tests/continuations/VALIDATION.md @@ -0,0 +1,171 @@ +# 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. + +## 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. 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. + +`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 +`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 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 + +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/audit_receipts.csv b/crates/c-api/tests/continuations/audit_receipts.csv new file mode 100644 index 00000000..336f2bd4 --- /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,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 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 00000000..e82b74bf --- /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 `/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. + +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 | `d8e0dda609af249242065e5e747054230e39826d4db26583115c3d15ee3c2d2e` | +| 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 00000000..c0689884 --- /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/debug/README.md b/crates/c-api/tests/continuations/debug/README.md new file mode 100644 index 00000000..dd8719d0 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/README.md @@ -0,0 +1,159 @@ +# 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. + +## 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 +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. `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/bridge.c b/crates/c-api/tests/continuations/debug/bridge.c new file mode 100644 index 00000000..3df864f4 --- /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/debugger.py b/crates/c-api/tests/continuations/debug/debugger.py new file mode 100644 index 00000000..bd90c126 --- /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 00000000..3518f3d0 --- /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_receipts.json b/crates/c-api/tests/continuations/debug/diagnostic_receipts.json new file mode 100644 index 00000000..b77f0bcc --- /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/diagnostic_run.py b/crates/c-api/tests/continuations/debug/diagnostic_run.py new file mode 100644 index 00000000..5c6e8cbd --- /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(' +#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 00000000..52d1016b --- /dev/null +++ b/crates/c-api/tests/continuations/debug/native.cpp @@ -0,0 +1,23 @@ +/* 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; +} + +#ifdef DIAGNOSTIC_ERRORS +#include +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 new file mode 100644 index 00000000..dd0f1972 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/native.rs @@ -0,0 +1,18 @@ +// 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 +} + +#[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 new file mode 100644 index 00000000..c93d1870 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/native.swift @@ -0,0 +1,18 @@ +// 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 +} + +#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/observed.json b/crates/c-api/tests/continuations/debug/observed.json new file mode 100644 index 00000000..8e61cfa7 --- /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/qemu_diagnostics_disabled.json b/crates/c-api/tests/continuations/debug/qemu_diagnostics_disabled.json new file mode 100644 index 00000000..d75fcd61 --- /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/recorder.c b/crates/c-api/tests/continuations/debug/recorder.c new file mode 100644 index 00000000..18cae383 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/recorder.c @@ -0,0 +1,160 @@ +/* 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 +#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 00000000..9e9d1992 --- /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/run.py b/crates/c-api/tests/continuations/debug/run.py new file mode 100644 index 00000000..0f105a73 --- /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/debug/source_guest.c b/crates/c-api/tests/continuations/debug/source_guest.c new file mode 100644 index 00000000..b233ac68 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/source_guest.c @@ -0,0 +1,26 @@ +/* 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 +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 100644 index 00000000..d6c6f63b Binary files /dev/null and b/crates/c-api/tests/continuations/debug/source_guest.wasm differ diff --git a/crates/c-api/tests/continuations/debug/symbolize.py b/crates/c-api/tests/continuations/debug/symbolize.py new file mode 100644 index 00000000..47acecbf --- /dev/null +++ b/crates/c-api/tests/continuations/debug/symbolize.py @@ -0,0 +1,90 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Consumer-side logical stack decoding; never executes code in a crashed target.""" +import hashlib +import json +import os +from pathlib import Path +import struct +import subprocess + +SEGMENT = 676 +TASK = SEGMENT * 9 + 16 +RECORD = 64 + TASK * 2 +EVENTS = {1: 'start', 2: 'host', 3: 'pending', 4: 'resume', 5: 'trap', 6: 'cancel', 7: 'done', 8: 'async-error'} + +def decode(data): + if len(data) != RECORD: + raise ValueError('snapshot size mismatch') + magic, version, size, active = struct.unpack_from('<4I', data) + if (magic, version, size) != (0x57444247, 1, RECORD) or active > 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') + 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) + 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 00000000..05379781 --- /dev/null +++ b/crates/c-api/tests/continuations/debug/test_symbolize.py @@ -0,0 +1,66 @@ +# 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 +from unittest.mock import patch +import subprocess +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('', symbolize.render(data, manifest)) + + def test_build_identity_and_original_module_hash(self): + data = self.record() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + module = root/'guest.wasm' + module.write_bytes(b'original') + manifest = root/'manifest.json' + manifest.write_text(json.dumps({'modules': {'0': {'path':str(module), 'sha256':hashlib.sha256(module.read_bytes()).hexdigest()}}})) + data[20:52] = hashlib.sha256(manifest.read_bytes()).digest() + self.assertIn('state=pending', symbolize.render(data,manifest)) + module.write_bytes(b'different build') + with self.assertRaisesRegex(ValueError,'content hash'): + symbolize.render(data,manifest) + manifest.write_text('{}') + with self.assertRaisesRegex(ValueError,'build/session'): + symbolize.render(data,manifest) + +if __name__ == '__main__': + unittest.main() diff --git a/crates/c-api/tests/continuations/edges.c b/crates/c-api/tests/continuations/edges.c new file mode 100644 index 00000000..ec64547a --- /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 00000000..808b8f23 --- /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 00000000..afa477c2 Binary files /dev/null and b/crates/c-api/tests/continuations/edges.wasm differ diff --git a/crates/c-api/tests/continuations/edges.wat b/crates/c-api/tests/continuations/edges.wat new file mode 100644 index 00000000..6a8a90bf --- /dev/null +++ b/crates/c-api/tests/continuations/edges.wat @@ -0,0 +1,12 @@ +(module + (import "h" "pause" (func $pause (param i32))) + (memory (export "memory") 1 3) + (func (export "run") (param $seed i32) (result i32) + i32.const 0 local.get $seed i32.store + i32.const 0 call $pause + i32.const 1 memory.grow drop + i32.const 65536 local.get $seed i32.const 1 i32.add i32.store + i32.const 1 call $pause + i32.const 0 i32.load i32.const 65536 i32.load i32.add) + (func (export "void") i32.const 9 call $pause) + (func (export "trap") i32.const 8 call $pause unreachable)) 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 00000000..3b785ee8 --- /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 00000000..e8938dba --- /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 00000000..cb875afe --- /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 00000000..567afa6c --- /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 00000000..aeab1c41 --- /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/fixture.c b/crates/c-api/tests/continuations/fixture.c new file mode 100644 index 00000000..99e8c752 --- /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 00000000..55b57a51 Binary files /dev/null and b/crates/c-api/tests/continuations/fixture.wasm differ diff --git a/crates/c-api/tests/continuations/fixture.wat b/crates/c-api/tests/continuations/fixture.wat new file mode 100644 index 00000000..e740be3b --- /dev/null +++ b/crates/c-api/tests/continuations/fixture.wat @@ -0,0 +1,22 @@ +(module + (type $i2i (func (param i32) (result i32))) + (import "host" "wait" (func $wait (param i32) (result i32))) + (import "host" "inc" (func $inc (param i32) (result i32))) + (table 1 funcref) (elem (i32.const 0) func $wait) + (func (export "indirect") (param i32) (result i32) local.get 0 i32.const 0 call_indirect (type $i2i)) + (func (export "twice") (param i32) (result i32) local.get 0 call $wait call $wait) + (func (export "trap") unreachable) + (global $visits (mut i32) (i32.const 0)) + (func $leaf (param i32) (result i32) local.get 0 call $wait i32.const 2 i32.mul) + (func (export "run") (param i32) (result i32) + global.get $visits i32.const 1 i32.add global.set $visits + local.get 0 call $leaf i32.const 1 i32.add) + (func (export "visits") (result i32) global.get $visits) + (func (export "native") (param $n i32) (result i32) (local $sum i32) + (loop $again + local.get $sum local.get $n call $inc i32.add local.set $sum + local.get $n i32.const 1 i32.sub local.tee $n br_if $again) + local.get $sum) + (func (export "compute") (param $n i32) (result i32) (local $sum i32) + (loop $again local.get $sum local.get $n i32.add local.set $sum + local.get $n i32.const 1 i32.sub local.tee $n br_if $again) local.get $sum)) diff --git a/crates/c-api/tests/continuations/gc.wasm b/crates/c-api/tests/continuations/gc.wasm new file mode 100644 index 00000000..604ed8ae Binary files /dev/null and b/crates/c-api/tests/continuations/gc.wasm differ diff --git a/crates/c-api/tests/continuations/gc.wat b/crates/c-api/tests/continuations/gc.wat new file mode 100644 index 00000000..fb05768f --- /dev/null +++ b/crates/c-api/tests/continuations/gc.wat @@ -0,0 +1,39 @@ +;; Copyright (C) 2026 Matt Hargett. All rights reserved. +;; SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +(module + (type $waittype (func (param i32) (result i32))) + (type $box (struct (field i32))) + (type $garbage (array i32)) + (import "host" "wait" (func $wait (param i32) (result i32))) + (table 1 funcref) + (elem (i32.const 0) func $wait) + ;; Allocate 2 MiB per round, exceeding both runtimes' default GC thresholds. + (func $churn (local $n i32) + i32.const 128 local.set $n + (loop $again + i32.const 4096 array.new_default $garbage drop + local.get $n i32.const 1 i32.sub local.tee $n br_if $again)) + (func $leaf (param $seed i32) (result i32) (local $keep (ref null $box)) + local.get $seed struct.new $box local.set $keep + local.get $seed call $wait drop + ;; Reading a local must not remove it from the suspended frame's root map. + local.get $keep struct.get $box 0 drop + call $churn + local.get $keep struct.get $box 0 i32.const 0 call_indirect (type $waittype) drop + local.get $keep struct.get $box 0 drop + call $churn + local.get $keep struct.get $box 0) + (func (export "run") (param $seed i32) (result i32) + (local $parent (ref null $box)) + local.get $seed i32.const 1 i32.add struct.new $box local.set $parent + local.get $seed call $leaf + local.get $parent struct.get $box 0 i32.add) + (func (export "fail") (param i32) (result i32) + local.get 0 call $wait drop unreachable) + (func $deep (export "deep") (param $n i32) (result i32) + local.get $n i32.eqz + if (result i32) + i32.const 0 call $wait + else + local.get $n i32.const 1 i32.sub call $deep i32.const 1 i32.add + end)) 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 00000000..6b1cadee --- /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 new file mode 100644 index 00000000..4fd048c3 --- /dev/null +++ b/crates/c-api/tests/continuations/measure.py @@ -0,0 +1,62 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Alternate named fixture executables and preserve every timing/memory sample as CSV.""" +import argparse +import csv +import ctypes +import os +from pathlib import Path +import re +import subprocess +import sys + +p = argparse.ArgumentParser() +p.add_argument('--fixture', type=Path, required=True) +p.add_argument('--rounds', type=int, default=7) +p.add_argument('--cpu', type=int, help='logical CPU index; Windows/Linux only') +p.add_argument('executables', nargs='+', help='name=path, e.g. baseline=build-base/fixture') +a = p.parse_args() +if a.rounds < 1 or (a.cpu is not None and a.cpu < 0): + p.error('rounds must be positive and cpu nonnegative') +if a.cpu is not None and os.name != 'nt' and not hasattr(os, 'sched_setaffinity'): + p.error('CPU affinity is unavailable on this platform') +variants = [(name, str(Path(path).resolve())) for name, path in (x.split('=', 1) for x in a.executables)] +if os.name == 'nt' and a.cpu is not None: + k32 = ctypes.WinDLL('kernel32', use_last_error=True) + k32.OpenProcess.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_uint32] + 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, 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)): + child = subprocess.Popen([path, str(a.fixture.resolve())], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + try: + if a.cpu is not None: + if os.name == 'nt': + handle = k32.OpenProcess(0x200, False, child.pid) # PROCESS_SET_INFORMATION + if not handle: + raise ctypes.WinError(ctypes.get_last_error()) + try: + if not k32.SetProcessAffinityMask(handle, 1 << a.cpu): + raise ctypes.WinError(ctypes.get_last_error()) + finally: + k32.CloseHandle(handle) + else: + os.sched_setaffinity(child.pid, {a.cpu}) + out, err = child.communicate(timeout=120) + if child.returncode: + raise RuntimeError(f'{name} failed: {out}\n{err}') + values = [] + for key in ['compute_ns_per_iteration', 'native_ns_per_iteration', 'peak_rss_bytes', 'peak_commit_bytes', 'compute_cycles_per_iteration', 'native_cycles_per_iteration']: + m = re.search(r'(?m)^' + key + r' ([0-9.]+)', out) if key != 'peak_commit_bytes' else re.search(key + r' ([0-9.]+)', out) + if m is None and key not in ('peak_commit_bytes', 'compute_cycles_per_iteration', 'native_cycles_per_iteration'): + raise RuntimeError(f'{name} omitted {key}: {out}') + values.append(m[1] if m else '') + writer.writerow([name, i, *values, a.cpu if a.cpu is not None else '']) + sys.stdout.flush() + finally: + if child.poll() is None: + child.kill() + child.communicate() diff --git a/crates/c-api/tests/continuations/measurements.csv b/crates/c-api/tests/continuations/measurements.csv new file mode 100644 index 00000000..f777eb0f --- /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/run.py b/crates/c-api/tests/continuations/run.py new file mode 100644 index 00000000..e27ea6fa --- /dev/null +++ b/crates/c-api/tests/continuations/run.py @@ -0,0 +1,189 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Build shared fixtures; sanitizer builds are separate from LTO measurements.""" +import argparse +import os +from pathlib import Path +import platform +import shutil +import subprocess + +p = argparse.ArgumentParser() +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('--feature', choices=['on', 'off'], default='on') +p.add_argument('--diagnostics', action='store_true', help='enable optional bounded guest source snapshots') +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') +p.add_argument('--aot-compiler', type=Path, help='enable WAMR AOT coexistence and test compiled feature guests') +p.add_argument('--tail-calls', action='store_true', help='exercise TinyWasm nightly tail ABI and verify it was enabled') +p.add_argument('--sanitize', action='store_true', help='instrument the runtime and C clients with ASan') +p.add_argument('--client-asan', action='store_true', help='instrument C clients only; Rust remains uninstrumented') +p.add_argument('--ubsan', action='store_true', help='also enable UBSan for a Unix WAMR build') +a = p.parse_args() +win = os.name == 'nt' +mac = platform.system() == 'Darwin' +san = a.sanitize or a.client_asan +if a.diagnostics and a.feature != 'on': + p.error('--diagnostics requires --feature on') +if a.sanitize and a.client_asan: + p.error('choose full-runtime or client-only instrumentation') +if a.tail_calls and a.runtime != 'tinywasm': + p.error('--tail-calls applies only to TinyWasm') +if a.client_asan and a.runtime != 'tinywasm': + p.error('use --sanitize for the WAMR C runtime') +if a.sanitize and a.runtime == 'tinywasm' and win: + 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'): + p.error('--aot-compiler requires WAMR continuations and --gc or --simd') +if a.simd and a.runtime == 'wamr' and win: + p.error('WAMR fast-interpreter SIMD is not supported on Windows') +root = a.root.resolve() +build = a.build.resolve() +build.mkdir(parents=True, exist_ok=True) +fixture = Path(__file__).resolve().parent +env = os.environ.copy() +if win and env.get('VSINSTALLDIR'): + cmake_tools = Path(env['VSINSTALLDIR']) / 'Common7/IDE/CommonExtensions/Microsoft/CMake' + env['PATH'] = os.pathsep.join([str(cmake_tools/'CMake/bin'), str(cmake_tools/'Ninja'), env['PATH']]) +if san: + env['ASAN_OPTIONS'] = 'halt_on_error=1' + ('' if win else ':detect_stack_use_after_return=1') + print('Instrumentation: ' + ('C client only; Rust runtime UNINSTRUMENTED' if a.client_asan else 'runtime and C clients; TinyWasm includes rebuilt Rust std'), flush=True) + +def run(cmd): + print(' '.join(map(str, cmd)), flush=True) + subprocess.run(list(map(str, cmd)), check=True, env=env, timeout=900) + +sanitize_flags = ['-fsanitize=address,undefined' if a.ubsan else '-fsanitize=address', '-fno-omit-frame-pointer'] +cc = env.get('CC', 'cl' if win else 'cc') +def client(source, exe, inc, lib=None, defines=()): + if win: + 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: + cmd += [lib] + cmd += ['/link', '/INCREMENTAL:NO', '/OPT:REF', '/OPT:ICF'] + if not san: + cmd += ['/LTCG', '/Brepro'] + cmd += ['ws2_32.lib', 'userenv.lib', 'bcrypt.lib', 'ntdll.lib', 'advapi32.lib', 'kernel32.lib'] + else: + obj = exe.with_suffix('.o') + cmd = [cc, '-std=c11', '-O1' if san else '-O3', '-g' if san else '-flto', '-Wall', '-Wextra', '-Werror', '-fvisibility=hidden', '-I'+str(inc), '-c', source, '-o', obj] + cmd += ['-D'+x for x in defines] + if san: + cmd += sanitize_flags + run(cmd) + cmd = [cc, '-O1' if san else '-O3', '-g' if san else '-flto', obj] + if lib: + cmd += [lib] + cmd += ['-o', exe] + if san: + cmd += sanitize_flags + cmd += ['-Wl,-dead_strip', '-framework', 'Security', '-framework', 'CoreFoundation', '-liconv', '-lSystem'] if mac else ['-Wl,--gc-sections', '-lm', '-lpthread', '-ldl'] + run(cmd) + +if san: + # An expected failure verifies that the C instrumentation/runtime is active. + canary = build/'asan_canary.c' + canary.write_text('#include \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.diagnostics: + features += ['diagnostics'] + 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='+('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 a.diagnostics: + cmd += ['-DWAMR_BUILD_C_API_DIAGNOSTICS=1'] + 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] + 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') + if a.diagnostics: + shutil.copy2(root/'core/iwasm/include/wasm_ext_debug.h', inc/'wasm_ext_debug.h') + shutil.copy2(root/'core/iwasm/include/wasm_c_api.h', inc/'wasm_c_api.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']) + +if a.feature == 'on' and (a.gc or a.simd): + exe = build/('features.exe' if win else 'features') + client(fixture/'features.c', exe, inc, lib, ['TEST_WAMR'] if a.runtime == 'wamr' else []) + for name, enabled in [('gc', a.gc), ('simd', a.simd)]: + if not enabled: + continue + run([exe, fixture/(name+'.wasm')]) + if a.aot_compiler: + output = build/(name+'.aot') + run([a.aot_compiler.resolve(), '--bounds-checks=1', '--stack-bounds-checks=1', + *(['--enable-gc'] if a.gc else []), + '-o', output, fixture/(name+'.wasm')]) + run([exe, output, 'aot']) + +if a.diagnostics: + exe = build/('diagnostics.exe' if win else 'diagnostics') + client(fixture/'debug/fixture.c', exe, inc, lib) + run([exe, fixture/'debug/source_guest.wasm']) diff --git a/crates/c-api/tests/continuations/sections.py b/crates/c-api/tests/continuations/sections.py new file mode 100644 index 00000000..e83ad65e --- /dev/null +++ b/crates/c-api/tests/continuations/sections.py @@ -0,0 +1,47 @@ +# Copyright (C) 2026 Matt Hargett. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Print linked Mach-O64 or PE image section sizes and hashes as CSV.""" +import argparse +import csv +import hashlib +from pathlib import Path +import struct +import sys +p = argparse.ArgumentParser() +p.add_argument('executables', nargs='+', help='variant=path') +a = p.parse_args() +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) + b = Path(path).read_bytes() + w.writerow([name, '', '', len(b), hashlib.sha256(b).hexdigest()]) + if b[:4] == b'\xcf\xfa\xed\xfe': + commands = struct.unpack_from(',,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 diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index c6302326..a62a3cb7 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 cb896bd7..32792685 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 47997101..b3ba83a1 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 bca9371d..cf627e0b 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,10 @@ 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"] full = ["archive", "debug", "parallel-parser", "parser", "state", "validate"] diff --git a/crates/tinywasm/src/diagnostics.rs b/crates/tinywasm/src/diagnostics.rs new file mode 100644 index 00000000..92aaf0a0 --- /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 new file mode 100644 index 00000000..73774e2e --- /dev/null +++ b/crates/tinywasm/src/func/host_execution.rs @@ -0,0 +1,301 @@ +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, + #[cfg(feature = "diagnostics")] + debug_snapshot: crate::diagnostics::DebugSnapshot, + 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(); + #[cfg(feature = "diagnostics")] + { + store.debug_snapshot = crate::diagnostics::DebugSnapshot::default(); + } + 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, + #[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. + /// 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 => { + 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 mut 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!(), + } + }; + 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() } + })(); + } + #[cfg(feature = "diagnostics")] + { + self.debug_snapshot = self.store.debug_snapshot(); + } + 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 869e2a70..1ff9fb3a 100644 --- a/crates/tinywasm/src/func/mod.rs +++ b/crates/tinywasm/src/func/mod.rs @@ -9,12 +9,16 @@ use crate::{FuncRef, WasmValue}; mod context; mod host; +#[cfg(feature = "external-host-calls")] +mod host_execution; mod resume; mod values; 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/instance.rs b/crates/tinywasm/src/instance.rs index c5acf22e..5175f820 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 73c3a72e..32f234fa 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 616eb4ec..35e3ca32 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 6a8741b0..6c2fc57d 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 6aeb6454..3a0a8c95 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -125,11 +125,15 @@ pub use func::{ ExecProgress, FromWasmValues, FuncContext, FuncExecution, FuncExecutionTyped, Function, FunctionTyped, 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::*; 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 3eae3ab0..8cd8de3c 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 aed7de75..f9ae243b 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 new file mode 100644 index 00000000..4107e49e --- /dev/null +++ b/crates/tinywasm/tests/host_execution.rs @@ -0,0 +1,465 @@ +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(()) +} + +#[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(()) +} + +#[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 2d2bf918..9cef9861 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 a6ba7c30..b5ebee22 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]>,