From c84fc86e6cdfe254a59d57b909f647e6c254ef76 Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 13 Jun 2026 16:19:52 +0000 Subject: [PATCH] W6 --- WASM-PROPOSAL.md | 91 ++- .../native-baseline-2026-06-12.json | 0 .../native-baseline-2026-06-12.md | 0 .../site-static-audit-2026-06-12.md | 0 .../wasm-toolchain-findings.md | 0 scripts/wasm/run_w5.sh | 6 +- spikes/wasm-phase0/README.md | 30 - spikes/wasm-phase0/build_loader.sh | 17 - spikes/wasm-phase0/build_modules.sh | 51 -- spikes/wasm-phase0/core.cpp | 74 --- spikes/wasm-phase0/loader.cpp | 446 -------------- spikes/wasm-phase0/realunit-report.md | 165 ------ spikes/wasm-phase0/unit.cpp | 73 --- spikes/wasm-phase0/wasm_inspect.py | 95 --- spikes/wasm-phase2/README.md | 51 -- spikes/wasm-phase2/build_loader.sh | 16 - spikes/wasm-phase2/build_modules.sh | 21 - spikes/wasm-phase2/core.cpp | 142 ----- spikes/wasm-phase2/hostcalls.syms | 2 - spikes/wasm-phase2/loader.cpp | 280 --------- spikes/wasm-phase2/page.uce | 12 - spikes/wasm-phase3/README.md | 63 -- spikes/wasm-phase3/build_loader.sh | 16 - spikes/wasm-phase3/build_modules.sh | 70 --- spikes/wasm-phase3/core.cpp | 188 ------ spikes/wasm-phase3/generated/helper.cpp | 6 - spikes/wasm-phase3/generated/page.uce.cpp | 72 --- spikes/wasm-phase3/hostcalls.syms | 2 - spikes/wasm-phase3/loader.cpp | 554 ------------------ spikes/wasm-phase3/page.uce | 12 - spikes/wasm-phase4/README.md | 77 --- spikes/wasm-phase4/build_runner.sh | 16 - spikes/wasm-phase4/runner.cpp | 298 ---------- spikes/wasm-phase5/README.md | 62 -- spikes/wasm-phase5/run_phase5.sh | 62 -- src/lib/_scratchpad.cpp | 90 --- src/lib/sys.cpp | 92 ++- src/wasm/backend.cpp | 11 +- src/wasm/core_hostcalls.syms | 4 + src/wasm/worker.cpp | 175 ++++-- .../benchmark.py => tests/wasm_benchmark.py | 0 .../wasm_site_audit.py | 2 +- 42 files changed, 307 insertions(+), 3137 deletions(-) rename {spikes/wasm-phase5/reports => docs/wasm-baselines}/native-baseline-2026-06-12.json (100%) rename {spikes/wasm-phase5/reports => docs/wasm-baselines}/native-baseline-2026-06-12.md (100%) rename {spikes/wasm-phase5/reports => docs/wasm-baselines}/site-static-audit-2026-06-12.md (100%) rename spikes/wasm-phase0/FINDINGS.md => docs/wasm-toolchain-findings.md (100%) delete mode 100644 spikes/wasm-phase0/README.md delete mode 100644 spikes/wasm-phase0/build_loader.sh delete mode 100644 spikes/wasm-phase0/build_modules.sh delete mode 100644 spikes/wasm-phase0/core.cpp delete mode 100644 spikes/wasm-phase0/loader.cpp delete mode 100644 spikes/wasm-phase0/realunit-report.md delete mode 100644 spikes/wasm-phase0/unit.cpp delete mode 100644 spikes/wasm-phase0/wasm_inspect.py delete mode 100644 spikes/wasm-phase2/README.md delete mode 100755 spikes/wasm-phase2/build_loader.sh delete mode 100755 spikes/wasm-phase2/build_modules.sh delete mode 100644 spikes/wasm-phase2/core.cpp delete mode 100644 spikes/wasm-phase2/hostcalls.syms delete mode 100644 spikes/wasm-phase2/loader.cpp delete mode 100644 spikes/wasm-phase2/page.uce delete mode 100644 spikes/wasm-phase3/README.md delete mode 100755 spikes/wasm-phase3/build_loader.sh delete mode 100755 spikes/wasm-phase3/build_modules.sh delete mode 100644 spikes/wasm-phase3/core.cpp delete mode 100644 spikes/wasm-phase3/generated/helper.cpp delete mode 100644 spikes/wasm-phase3/generated/page.uce.cpp delete mode 100644 spikes/wasm-phase3/hostcalls.syms delete mode 100644 spikes/wasm-phase3/loader.cpp delete mode 100644 spikes/wasm-phase3/page.uce delete mode 100644 spikes/wasm-phase4/README.md delete mode 100755 spikes/wasm-phase4/build_runner.sh delete mode 100644 spikes/wasm-phase4/runner.cpp delete mode 100644 spikes/wasm-phase5/README.md delete mode 100755 spikes/wasm-phase5/run_phase5.sh delete mode 100644 src/lib/_scratchpad.cpp rename spikes/wasm-phase5/benchmark.py => tests/wasm_benchmark.py (100%) rename spikes/wasm-phase5/audit_site_statics.py => tests/wasm_site_audit.py (99%) diff --git a/WASM-PROPOSAL.md b/WASM-PROPOSAL.md index f7b9b4b..0055008 100644 --- a/WASM-PROPOSAL.md +++ b/WASM-PROPOSAL.md @@ -6,9 +6,16 @@ `src/wasm/backend.cpp`) are **done**: with `WASM_BACKEND_ENABLED=1` the real nginx → fastcgi → wasm path serves the starter app (parity 14/14) and produces clean error pages from an unharmed worker on real kill pages. - Native is still the default backend (full suite 83/83). Current phase: - **W5 — full parity, performance, cutover** (un-stub regex/xml/yaml, add the - remaining membrane hostcalls, then flip the default). + W5 cutover is done: WASM is the default page-render backend with native kept + as the config-selectable reference/fallback; the membrane now covers regex, + xml/yaml, markdown, filesystem read/write, sqlite, background tasks, sleep, + and time formatting. W6 spike cleanup is done (prototypes deleted, gates + re-homed to `tests/`/`docs/`); the native-machinery retirements stay gated + while native still serves the remaining fallback surfaces (zip, sockets, + memcache, mysql, `unit_call`, compiler-introspection). Open items: the + sqlite membrane is ~6–7× native (per-query double hostcall + UCEB1 + marshalling — §11.4 cursor/bulk decision), and CoW snapshot birth for the + workspace-birth budget. - **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` → `dlopen`) with per-unit WebAssembly modules executed in a per-request, runtime-linked workspace, exposing the same API surface to page code, @@ -428,7 +435,7 @@ stub) hello-world linked at runtime by a minimal loader, in the chosen vendored runtime. > **Status: DONE (2026-06-12).** Exit criterion passed on k-uce; see -> `spikes/wasm-phase0/FINDINGS.md`. Runtime selected: **Wasmtime v45.0.1** +> `docs/wasm-toolchain-findings.md`. Runtime selected: **Wasmtime v45.0.1** > — WAMR is blocked on the load-bearing requirement (its wasm-c-api ignores > imported memories/tables; host-side table growth unsupported). wasi-sdk-33 > PIC validated on stubs **and** on real generated units @@ -521,7 +528,7 @@ Exit: the uce-starter renders end-to-end with components loading lazily; > wrong values and wrote into core memory at low addresses; both spike loaders > are fixed and the Phase 3 exit gate now asserts every GOT-derived output > marker (`self-got`/`callback`/`each`/`map`) so a regression cannot pass. -> Details in the `spikes/wasm-phase0/FINDINGS.md` erratum. +> Details in the `docs/wasm-toolchain-findings.md` erratum. > > **Production Phase 3 work plan** (ordered by dependency/risk; spike-proven > mechanics not repeated here): @@ -618,7 +625,7 @@ call cost. Exit: numbers published in this document, all tests and reviews pass. > measured gate to absorb cold unit-cache timeouts. Informational native medians > from the 2026-06-12 baseline are: template-heavy doc `313.8 ms`, sqlite page > `3.4 ms`, starter dashboard `41.1 ms`. A durable snapshot lives in -> `spikes/wasm-phase5/reports/native-baseline-2026-06-12.md`; paired wasm/native +> `docs/wasm-baselines/native-baseline-2026-06-12.md`; paired wasm/native > gate runs still recompute the native medians for the actual ≤2× comparison. > True Phase 5 completion still requires passing the same harness against a real > wasm worker URL and adding worker-internal probes for workspace birth and @@ -846,8 +853,8 @@ reference. > **Status: DONE (2026-06-13).** W5 flips the page-render default to the WASM > backend while retaining explicit native fallback for the surfaces still -> genuinely host-owned: zip, sqlite, background tasks, filesystem writes, -> sockets/sleep, and `unit_call`/compiler-introspection (the last need the +> genuinely host-owned or not yet membraned: zip, sockets/custom servers, +> memcache/mysql, and `unit_call`/compiler-introspection (the last need the > native toolchain). The fallback is selected before workspace creation by > scanning the entry source for a small native-only token set, so unsupported > pages neither fail through wasm nor hide as trap regressions. @@ -855,12 +862,27 @@ reference. > **Promoted onto wasm during W5** (no longer fallback): **regex** runs through > a single UCEB1-marshalled `uce_host_regex` hostcall against the host's PCRE2; > **markdown** is compiled into the core (pure compute); **xml/yaml** were -> already in-core; **filesystem writes** (`file_put_contents`/`file_append`) go -> through a policy-gated `uce_host_file_write` hostcall, and the read membrane -> was widened to the same allowlist (site tree + scratch roots: `/tmp`, +> already in-core; **filesystem writes** (`file_put_contents`/`file_append`/ +> `file_unlink`) go through policy-gated hostcalls, and the read membrane was +> widened to the same allowlist (site tree + scratch roots: `/tmp`, > `BIN_DIRECTORY`, `SESSION_PATH`, `TMP_UPLOAD_PATH`) so a page can read back -> what it writes (e.g. the `io.uce` /tmp round-trip). The `/doc/*` blanket -> fallback is gone — doc pages render on wasm. Two wasm-stack/ABI issues surfaced and were fixed: the core stack was +> what it writes (e.g. the `io.uce` /tmp round-trip); **sqlite** runs through a +> `uce_host_sqlite` hostcall against the host's real connector, with +> connections held in a per-workspace handle table that is closed on workspace +> drop — the first concrete realization of the §3.1 "handle-table drop = +> resource cleanup" model; **background tasks** now use a host-managed process +> membrane (`uce_host_task_spawn`/`pid`/`kill`) with a guest callback trampoline +> (`uce_wasm_task_run`) in the forked child, so `task()`, `task_repeat()`, +> `task_pid()`, and `task_kill()` run under the wasm backend; **sleep/usleep** +> use a host sleep call that renews the epoch deadline after blocking. The +> `/doc/*` blanket fallback is gone — doc pages render on wasm. +> +> One subtle bug fixed here: the sized-hostcall convention has the guest call +> twice (buf=0 to learn the length, then to fetch). That re-executed the op — +> harmless for idempotent regex/file-read, but for sqlite it ran every +> `INSERT`/`CREATE` twice and the second pass failed on its own side effects. +> The host now stages the encoded result on the first call (keyed on the exact +> input) and replays it on the fetch, so a side-effecting op runs once. Two wasm-stack/ABI issues surfaced and were fixed: the core stack was > raised to 8 MB so recursive `ucb_decode_node` reaches the 1024 depth limit > like native (the core.uce depth-bomb negative test overflowed the small > default stack), and `wasm_trace`'s demangler is host-only (wasi-libc++ has no @@ -884,8 +906,8 @@ reference. > starter subset + the native-vs-wasm benchmark, optionally leave the backend > enabled (`UCE_W5_KEEP_BACKEND=1`). Final k-uce gate: native reference suite > **83/83**; wasm/default full suite **83/83** warm (starter, docs, core tests, -> markdown, regex all on wasm; zip/sqlite/tasks correctly on native fallback); -> starter subset **14/14**; kill pages (trap/loop/recurse) clean with the worker +> markdown, regex, sqlite, and tasks on wasm; zip and native-only service/ +> compiler surfaces routed to native fallback); starter subset **14/14**; kill pages (trap/loop/recurse) clean with the worker > unharmed. Benchmark medians passed the ≤2× budget: doc singlepage native > ~324ms vs wasm ~318ms, sqlite 3.7ms vs 3.8ms, starter dashboard 44ms vs wasm > ~6ms. Worker-internal probe headers (`X-UCE-Backend`, @@ -896,13 +918,14 @@ reference. > cutover budget). Live `/etc/uce/settings.cfg` left with `WASM_BACKEND_ENABLED=1`; > native remains in-tree and config-selectable. > -> **Still genuinely deferred (not W5-blocking):** sqlite/tasks/zip membrane -> hostcalls (host-owned, native fallback works today; zip additionally can't be -> a wasm side module while its `.uce` source uses try/catch), `unit_call` and -> compiler/unit-introspection pages (need the native toolchain — candidates for -> host-serviced hostcalls if native is ever to be fully retired), CoW snapshot -> birth + placement memoization for the workspace-birth budget, and the §3.2 -> statics audit follow-through. +> **Remaining native fallback** (the only pages still routed to native): **zip** +> (its `.uce` source uses try/catch, so it can't be a `-fno-exceptions` wasm +> side module regardless of a hostcall), socket/custom-server/memcache/mysql +> service surfaces, and `unit_call` + compiler/unit-introspection +> (`unit-info`/`unit-browser`/`sharedunit` — need the native toolchain; +> candidates for host-serviced hostcalls if native is to be fully retired). +> **Still deferred, not blocking:** CoW snapshot birth + placement memoization +> for the workspace-birth budget, and the §3.2 statics-audit follow-through. **W6 — cleanup.** Cutover checklist gate 4, verbatim: re-home the spike-hosted gates @@ -910,6 +933,30 @@ Cutover checklist gate 4, verbatim: re-home the spike-hosted gates `docs/`), delete `spikes/wasm-phase*`, retire the SIGSEGV recovery, the tracking `operator new`, and `cleanup_*_connections()`. +> **Status: SPIKE CLEANUP DONE; native-machinery retirement GATED +> (2026-06-13).** Re-homing complete: the Phase 5 benchmark + site-audit are +> now `tests/wasm_benchmark.py` / `tests/wasm_site_audit.py` (with +> `scripts/wasm/run_w5.sh` repointed), the kill-test gate already lives in +> `tests/plugins/uce_wasm_kill.py`, the Phase 0 toolchain findings + GOT +> erratum are `docs/wasm-toolchain-findings.md`, and the pinned native +> baselines are `docs/wasm-baselines/`. **All `spikes/wasm-phase*` deleted** +> (prototypes superseded by `src/wasm/`), and the dead 2022 +> `src/lib/_scratchpad.cpp` arena experiment is removed. Build + full suite +> stay green (83/83). The phase-0–4 status blocks above keep their narrative; +> their `spikes/...` paths are now historical — the artifacts that mattered +> were re-homed, the rest was throwaway scaffolding by design. +> +> **The three native-machinery retirements remain BLOCKED, deliberately:** +> native is still the live backend for pages touching zip, sockets / custom +> HTTP servers, memcache, mysql, `unit_call`, and compiler/unit-introspection +> — everything still in `native_only_tokens`. While *any* page renders on +> native, retiring its SIGSEGV/`sigsetjmp` recovery, the tracking +> `operator new`, or `cleanup_*_connections()` would strip crash protection +> and resource cleanup from live traffic. They retire only once those surfaces +> move behind membrane hostcalls (so nothing falls back) or the native backend +> is formally decommissioned. This is the honest end-state of W6 given the +> current fallback set, not an oversight. + --- ## 10. Risks & mitigations diff --git a/spikes/wasm-phase5/reports/native-baseline-2026-06-12.json b/docs/wasm-baselines/native-baseline-2026-06-12.json similarity index 100% rename from spikes/wasm-phase5/reports/native-baseline-2026-06-12.json rename to docs/wasm-baselines/native-baseline-2026-06-12.json diff --git a/spikes/wasm-phase5/reports/native-baseline-2026-06-12.md b/docs/wasm-baselines/native-baseline-2026-06-12.md similarity index 100% rename from spikes/wasm-phase5/reports/native-baseline-2026-06-12.md rename to docs/wasm-baselines/native-baseline-2026-06-12.md diff --git a/spikes/wasm-phase5/reports/site-static-audit-2026-06-12.md b/docs/wasm-baselines/site-static-audit-2026-06-12.md similarity index 100% rename from spikes/wasm-phase5/reports/site-static-audit-2026-06-12.md rename to docs/wasm-baselines/site-static-audit-2026-06-12.md diff --git a/spikes/wasm-phase0/FINDINGS.md b/docs/wasm-toolchain-findings.md similarity index 100% rename from spikes/wasm-phase0/FINDINGS.md rename to docs/wasm-toolchain-findings.md diff --git a/scripts/wasm/run_w5.sh b/scripts/wasm/run_w5.sh index 78b1d11..7b4f3ea 100755 --- a/scripts/wasm/run_w5.sh +++ b/scripts/wasm/run_w5.sh @@ -68,20 +68,20 @@ PY set_backend 0 python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/native-warmup.json" >/dev/null || true python3 tests/run_network_tests.py --include-internal --json-report "$OUT/native-network.json" -python3 spikes/wasm-phase5/benchmark.py --out-dir "$OUT/native-benchmark" --samples "${UCE_W5_BENCH_SAMPLES:-20}" --timeout 30 >/dev/null +python3 tests/wasm_benchmark.py --out-dir "$OUT/native-benchmark" --samples "${UCE_W5_BENCH_SAMPLES:-20}" --timeout 30 >/dev/null # WASM default backend with W5 native fallbacks for host-owned surfaces. set_backend 1 UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/wasm-warmup.json" >/dev/null || true UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --json-report "$OUT/wasm-network.json" python3 tests/run_network_tests.py --include-internal --match starter --json-report "$OUT/wasm-starter.json" -python3 spikes/wasm-phase5/benchmark.py \ +python3 tests/wasm_benchmark.py \ --out-dir "$OUT/benchmark" \ --backend-label wasm \ --compare-native-json "$OUT/native-benchmark/benchmark.json" \ --samples "${UCE_W5_BENCH_SAMPLES:-20}" \ --timeout 30 -python3 spikes/wasm-phase5/audit_site_statics.py --out-dir "$OUT" >/dev/null +python3 tests/wasm_site_audit.py --out-dir "$OUT" >/dev/null python3 - "$OUT" <<'PY' import json, sys diff --git a/spikes/wasm-phase0/README.md b/spikes/wasm-phase0/README.md deleted file mode 100644 index 9e89603..0000000 --- a/spikes/wasm-phase0/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# spikes/wasm-phase0 — WASM-PROPOSAL Phase 0 (toolchain & runtime spike) - -Validates the dynamic-linking foundation for the WASM unit runtime: -wasi-sdk PIC side modules + a host loader linking a core module and a unit -module at runtime with shared memory/table. **Exit criterion passed; runtime -selected: Wasmtime.** See `FINDINGS.md` for results and `realunit-report.md` -for the real-generated-unit compile log. - -Everything builds and runs **on k-uce** (toolchains under `/opt`, artifacts -under `/tmp/uce/wasm-phase0`): - -``` -bash spikes/wasm-phase0/build_modules.sh # core.wasm + unit.wasm (wasi-sdk) -bash spikes/wasm-phase0/build_loader.sh # loader (Wasmtime C API) -/tmp/uce/wasm-phase0/loader # expect: PHASE0 EXIT CRITERION: PASS -``` - -Files: - -- `core.cpp` — core-module stand-in: owns memory/allocator/libc++, exports - everything; includes the guest-side GOT.func resolver pattern -- `unit.cpp` — unit stand-in: PIC, imports allocator/runtime, exercises GOT, - cross-module C++ objects, function pointers, lambdas -- `loader.cpp` — minimal runtime linker (standard wasm-c-api): dylink.0 - parsing, base allocation, GOT resolution, init sequencing -- `wasm_inspect.py` — dumps imports/exports/dylink.0 of a module -- `FINDINGS.md` — toolchain pins, WAMR rejection evidence, flag recipe, - Phase 2/3 implications -- `realunit-report.md` — `collections.uce.cpp`/`hello.uce.cpp` compiled as - side modules (worker-agent log) diff --git a/spikes/wasm-phase0/build_loader.sh b/spikes/wasm-phase0/build_loader.sh deleted file mode 100644 index a1ae08e..0000000 --- a/spikes/wasm-phase0/build_loader.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# Phase 0: build the minimal loader against the WAMR build at /opt/wamr. -# Runs on k-uce. -set -e -cd "$(dirname "$0")" - -# Runtime: Wasmtime C API. (WAMR was evaluated first per WASM-PROPOSAL §9 but -# its wasm-c-api ignores imported memories/tables — see FINDINGS.md.) -WASMTIME=/opt/wasmtime -OUT=/tmp/uce/wasm-phase0 - -clang++ -std=c++17 -O1 -g loader.cpp -o "$OUT/loader" \ - -I"$WASMTIME/include" \ - -L"$WASMTIME/lib" -lwasmtime -Wl,-rpath,"$WASMTIME/lib" \ - -lpthread -lm -ldl - -echo built: "$OUT/loader" diff --git a/spikes/wasm-phase0/build_modules.sh b/spikes/wasm-phase0/build_modules.sh deleted file mode 100644 index cc25a30..0000000 --- a/spikes/wasm-phase0/build_modules.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -# Phase 0: build core (non-PIC reactor, libc statically linked, all-exported) -# and unit (-fPIC, wasm-ld -shared, dylink.0) with wasi-sdk. -# Runs on k-uce. Artifacts go to /tmp/uce/wasm-phase0. -set -e -cd "$(dirname "$0")" - -SDK=/opt/wasi-sdk -OUT=/tmp/uce/wasm-phase0 -mkdir -p "$OUT" - -# Core module: reactor (no main, exports _initialize), exceptions off per -# WASM-PROPOSAL §11.1 (error codes at unit boundaries). -# --export-all so unit modules can import libc/libc++/core symbols from it; -# --import-table so the host creates the shared funcref table (sized with -# headroom up front — WAMR cannot grow or write tables from the host side); -# __stack_pointer exported because PIC side modules import it (dylink ABI). -"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \ - -O1 -fno-exceptions \ - core.cpp -o "$OUT/core.wasm" \ - -Wl,--export-all \ - -Wl,--import-table \ - -Wl,--export=__stack_pointer \ - -Wl,--export=__heap_base \ - -Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE - -# Unit module: PIC object, linked -shared with no libc/libc++ of its own — -# every undefined symbol must become an import (resolved from core by the -# loader). Inline/template code (std::string etc.) instantiates locally, -# which is fine; the allocator must NOT be defined here (§3.2). -# -fvisibility-inlines-hidden: vague-linkage code (templates, lambdas) -# binds locally instead of being interposable — without it, one libc++ -# tree-emplace lambda became a self-import the loader cannot satisfy. -"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \ - -fvisibility-inlines-hidden \ - -O1 -fno-exceptions \ - -c unit.cpp -o "$OUT/unit.o" - -# --Bsymbolic: bind weak/vague-linkage symbols the unit defines itself -# (template instantiations, lambdas) locally instead of emitting -# self-imports that the loader would have to lazy-bind. -"$SDK/bin/wasm-ld" -shared --experimental-pic \ - --unresolved-symbols=import-dynamic \ - --Bsymbolic \ - "$OUT/unit.o" -o "$OUT/unit.wasm" \ - --export=uce_unit_render - -echo "--- core.wasm ---" -ls -la "$OUT/core.wasm" -echo "--- unit.wasm ---" -ls -la "$OUT/unit.wasm" diff --git a/spikes/wasm-phase0/core.cpp b/spikes/wasm-phase0/core.cpp deleted file mode 100644 index d72b58b..0000000 --- a/spikes/wasm-phase0/core.cpp +++ /dev/null @@ -1,74 +0,0 @@ -// WASM-PROPOSAL Phase 0 — core module stub. -// -// Stands in for the future "core module" (uce_lib + libc compiled to wasm): -// owns linear memory, the allocator, and libc/libc++ (statically linked, -// per the §10 mitigation: avoid shared wasi-libc entirely). Built as a -// non-PIC wasm32-wasi reactor with everything exported so unit modules can -// import from it. -// -// Deliberately avoids WASI I/O (no printf-to-fd): output accumulates in a -// buffer the host reads back, so the module can be instantiated through the -// plain wasm-c-api without a WASI context if need be. - -#include -#include -#include -#include - -static std::string g_output; - -extern "C" { - -// data symbol referenced from the unit module → must arrive there as a -// GOT.mem import -int core_counter = 7; - -void uce_print(const char* s, size_t len) -{ - g_output.append(s, len); -} - -// heap C++ object created in core, handed to the unit by pointer — -// validates one-heap/one-allocator pointer semantics (§3.4) -std::string* core_make_string(const char* s) -{ - return(new std::string(s)); -} - -void core_append_string(std::string* str, const char* s) -{ - str->append(s); -} - -// plain function pointer crossing: unit passes its own function, core calls -// it back — validates the shared funcref table -void core_invoke_callback(void (*cb)(int), int arg) -{ - cb(arg); -} - -// std::function allocated by unit code, executed here — validates fat -// callable objects (lambdas) across module boundaries -void core_invoke_function(std::function* f, int arg) -{ - char buf[64]; - snprintf(buf, sizeof(buf), "[fn:%d]", (*f)(arg)); - g_output.append(buf); -} - -// GOT.func resolution helper: taking the address here forces uce_print into -// core's elem segment at link time, and on wasm a function pointer IS its -// table index — so the loader can resolve GOT.func.uce_print with a plain -// call instead of host-side funcref injection (which WAMR does not allow). -// The production core will generalize this into a name → funcptr registry. -intptr_t core_table_index_of_uce_print() -{ - return(reinterpret_cast(uce_print)); -} - -// host reads the result out of linear memory via these -const char* core_output_data() { return(g_output.data()); } -size_t core_output_size() { return(g_output.size()); } -void core_output_clear() { g_output.clear(); } - -} diff --git a/spikes/wasm-phase0/loader.cpp b/spikes/wasm-phase0/loader.cpp deleted file mode 100644 index f5e358c..0000000 --- a/spikes/wasm-phase0/loader.cpp +++ /dev/null @@ -1,446 +0,0 @@ -// WASM-PROPOSAL Phase 0 — minimal runtime loader (exit criterion). -// -// Embeds a wasm runtime through the standard wasm-c-api and links two -// modules at runtime, the way the production loader (§6) will: -// -// 1. instantiate core.wasm (owns memory/table/allocator/libc), stubbing -// its WASI imports with named trap functions (the core stub does no -// real I/O; any stub that actually gets called names itself) -// 2. parse unit.wasm's dylink.0 → data size/align, table slots needed -// 3. allocate __memory_base by calling core's exported malloc, and -// __table_base by growing core's exported funcref table -// 4. build the unit's import vector: env.memory / env.__indirect_function_table / -// env.__stack_pointer straight from core's exports; env.* functions from -// core's exports; GOT.mem.* as mutable i32 globals holding resolved -// addresses (self-resolved from the unit's own exports after -// instantiation when core lacks the symbol — weak data case); -// GOT.func.* as mutable i32 globals holding freshly grown table slots -// pointing at core export funcrefs -// 5. instantiate unit, patch deferred GOT entries, run -// __wasm_apply_data_relocs then __wasm_call_ctors -// 6. call uce_unit_render and read the output back out of linear memory -// -// Everything here is deliberately validation-grade: fail loudly, never guess. - -#include - -#include -#include -#include -#include -#include -#include - -#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0) -#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0) - -static std::vector read_file(const char* fn) -{ - FILE* f = fopen(fn, "rb"); - CHECK(f, "cannot open %s", fn); - fseek(f, 0, SEEK_END); - long n = ftell(f); - fseek(f, 0, SEEK_SET); - std::vector buf(n); - CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn); - fclose(f); - return buf; -} - -// ---- minimal dylink.0 parser ------------------------------------------- - -struct DylinkInfo -{ - uint32_t mem_size = 0; - uint32_t mem_align = 0; // power of 2 - uint32_t table_size = 0; - uint32_t table_align = 0; - bool found = false; -}; - -static uint64_t read_uleb(const uint8_t* buf, size_t& pos) -{ - uint64_t result = 0; - int shift = 0; - while(true) - { - uint8_t b = buf[pos++]; - result |= (uint64_t)(b & 0x7f) << shift; - if(!(b & 0x80)) - return result; - shift += 7; - } -} - -static DylinkInfo parse_dylink(const std::vector& wasm) -{ - DylinkInfo info; - CHECK(wasm.size() > 8 && !memcmp(wasm.data(), "\0asm", 4), "not a wasm module"); - size_t pos = 8; - while(pos < wasm.size()) - { - uint8_t sec_id = wasm[pos++]; - uint64_t size = read_uleb(wasm.data(), pos); - size_t end = pos + size; - if(sec_id == 0) - { - uint64_t name_len = read_uleb(wasm.data(), pos); - std::string name((const char*)wasm.data() + pos, name_len); - pos += name_len; - if(name == "dylink.0") - { - while(pos < end) - { - uint8_t sub = wasm[pos++]; - uint64_t sub_len = read_uleb(wasm.data(), pos); - size_t sub_end = pos + sub_len; - if(sub == 1) // WASM_DYLINK_MEM_INFO - { - info.mem_size = read_uleb(wasm.data(), pos); - info.mem_align = read_uleb(wasm.data(), pos); - info.table_size = read_uleb(wasm.data(), pos); - info.table_align = read_uleb(wasm.data(), pos); - info.found = true; - } - pos = sub_end; - } - } - } - pos = end; - } - return info; -} - -// ---- named trap stubs for unsatisfied (WASI) imports -------------------- - -static wasm_store_t* g_store_for_stubs = nullptr; - -static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) -{ - (void)args; (void)results; - fprintf(stderr, "[stub called: %s]\n", (const char*)env); - char msg[256]; - snprintf(msg, sizeof(msg), "unimplemented host import called: %s", (const char*)env); - wasm_message_t message; - wasm_byte_vec_new(&message, strlen(msg) + 1, msg); - wasm_trap_t* trap = wasm_trap_new(g_store_for_stubs, &message); - wasm_byte_vec_delete(&message); - return trap; -} - -// ---- instance wrapper: name → extern map -------------------------------- - -struct Instance -{ - wasm_module_t* module = nullptr; - wasm_instance_t* instance = nullptr; - wasm_extern_vec_t exports = WASM_EMPTY_VEC; - std::map by_name; - - void index_exports() - { - wasm_exporttype_vec_t types = WASM_EMPTY_VEC; - wasm_module_exports(module, &types); - wasm_instance_exports(instance, &exports); - CHECK(types.size == exports.size, "export type/extern count mismatch"); - for(size_t i = 0; i < types.size; i++) - { - const wasm_name_t* nm = wasm_exporttype_name(types.data[i]); - std::string key(nm->data, nm->size); - // some wasm-c-api impls include the trailing NUL in name size - while(!key.empty() && key.back() == '\0') - key.pop_back(); - by_name[key] = exports.data[i]; - } - wasm_exporttype_vec_delete(&types); - } - - wasm_func_t* func(const char* name) - { - auto it = by_name.find(name); - return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second); - } - wasm_global_t* global(const char* name) - { - auto it = by_name.find(name); - return it == by_name.end() ? nullptr : wasm_extern_as_global(it->second); - } -}; - -static wasm_store_t* g_store = nullptr; - -static void report_trap(wasm_trap_t* trap, const char* what) -{ - if(!trap) - return; - wasm_message_t msg; - wasm_trap_message(trap, &msg); - FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data); -} - -static int32_t call_i32(Instance& inst, const char* name, std::vector argv = {}) -{ - wasm_func_t* f = inst.func(name); - CHECK(f, "missing export func %s", name); - wasm_val_t args_buf[4]; - for(size_t i = 0; i < argv.size(); i++) - args_buf[i] = WASM_I32_VAL(argv[i]); - wasm_val_t results_buf[1] = { WASM_INIT_VAL }; - wasm_val_vec_t args = { argv.size(), args_buf }; - wasm_val_vec_t results = { 1, results_buf }; - size_t result_arity = wasm_func_result_arity(f); - wasm_val_vec_t no_results = WASM_EMPTY_VEC; - wasm_trap_t* trap = wasm_func_call(f, &args, result_arity ? &results : &no_results); - report_trap(trap, name); - return result_arity ? results_buf[0].of.i32 : 0; -} - -static wasm_global_t* make_i32_global(int32_t value, wasm_mutability_t mut) -{ - wasm_globaltype_t* gt = wasm_globaltype_new(wasm_valtype_new(WASM_I32), mut); - wasm_val_t val = WASM_I32_VAL(value); - wasm_global_t* g = wasm_global_new(g_store, gt, &val); - CHECK(g, "wasm_global_new failed (host-created globals unsupported?)"); - wasm_globaltype_delete(gt); - return g; -} - -int main(int argc, char** argv) -{ - const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase0/core.wasm"; - const char* unit_path = argc > 2 ? argv[2] : "/tmp/uce/wasm-phase0/unit.wasm"; - - wasm_engine_t* engine = wasm_engine_new(); - CHECK(engine, "engine"); - g_store = wasm_store_new(engine); - CHECK(g_store, "store"); - g_store_for_stubs = g_store; - - // ---- 1. core module --------------------------------------------------- - std::vector core_bytes = read_file(core_path); - wasm_byte_vec_t core_bv; - wasm_byte_vec_new(&core_bv, core_bytes.size(), (const char*)core_bytes.data()); - Instance core; - core.module = wasm_module_new(g_store, &core_bv); - wasm_byte_vec_delete(&core_bv); - CHECK(core.module, "core module load failed"); - - // the shared funcref table is host-created (core links with --import-table) - // because WAMR cannot grow a table from the host: size it up front as - // core's declared minimum (= its own elem needs) plus headroom for unit - // module table regions. __table_base allocation bumps from the minimum. - wasm_table_t* table = nullptr; - uint32_t table_next_free = 0; - const uint32_t TABLE_HEADROOM = 2048; - - wasm_importtype_vec_t core_imports = WASM_EMPTY_VEC; - wasm_module_imports(core.module, &core_imports); - std::vector core_import_externs(core_imports.size); - for(size_t i = 0; i < core_imports.size; i++) - { - const wasm_name_t* mod = wasm_importtype_module(core_imports.data[i]); - const wasm_name_t* nm = wasm_importtype_name(core_imports.data[i]); - const wasm_externtype_t* et = wasm_importtype_type(core_imports.data[i]); - std::string name(nm->data, nm->size); - if(wasm_externtype_kind(et) == WASM_EXTERN_TABLE) - { - CHECK(name.rfind("__indirect_function_table", 0) == 0, - "unexpected core table import %s", name.c_str()); - const wasm_tabletype_t* tt = wasm_externtype_as_tabletype_const(et); - uint32_t core_min = wasm_tabletype_limits(tt)->min; - wasm_limits_t limits = { core_min + TABLE_HEADROOM, core_min + TABLE_HEADROOM }; - wasm_tabletype_t* host_tt = wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &limits); - table = wasm_table_new(g_store, host_tt, nullptr); - CHECK(table, "wasm_table_new failed (host-created tables unsupported?)"); - wasm_tabletype_delete(host_tt); - table_next_free = core_min; - printf("host table created: size=%u, core region=[0,%u)\n", core_min + TABLE_HEADROOM, core_min); - core_import_externs[i] = wasm_table_as_extern(table); - continue; - } - CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, - "core has unexpected non-func import %.*s.%s", - (int)mod->size, mod->data, name.c_str()); - // named trap stub; leaks the name string, fine for a spike - char* label = strdup((std::string(mod->data, mod->size) + "." + name).c_str()); - const wasm_functype_t* ft = wasm_externtype_as_functype_const(et); - wasm_func_t* stub = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr); - CHECK(stub, "stub func creation failed for %s", label); - core_import_externs[i] = wasm_func_as_extern(stub); - } - CHECK(table, "core does not import __indirect_function_table — rebuild with --import-table"); - wasm_extern_vec_t core_iv = { core_import_externs.size(), core_import_externs.data() }; - wasm_trap_t* trap = nullptr; - core.instance = wasm_instance_new(g_store, core.module, &core_iv, &trap); - report_trap(trap, "core instantiation"); - CHECK(core.instance, "core instantiation failed"); - core.index_exports(); - printf("core instantiated: %zu exports\n", core.by_name.size()); - - // reactor init (runs ctors). Wasmtime does not auto-run _initialize; - // WAMR does — and calling it twice trips wasi-libc's double-init guard - // (__builtin_trap → "unreachable"), which cost us a debugging round. - if(core.func("_initialize")) - call_i32(core, "_initialize"); - - wasm_memory_t* memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr); - CHECK(memory, "core does not export memory"); - - // ---- 2./3. dylink + base allocation ------------------------------------ - std::vector unit_bytes = read_file(unit_path); - DylinkInfo dl = parse_dylink(unit_bytes); - CHECK(dl.found, "unit has no dylink.0 mem_info"); - printf("dylink.0: memsize=%u memalign=2^%u tablesize=%u\n", dl.mem_size, dl.mem_align, dl.table_size); - - uint32_t align = 1u << dl.mem_align; - int32_t raw = call_i32(core, "malloc", { (int32_t)(dl.mem_size + align) }); - CHECK(raw, "core malloc returned 0"); - int32_t memory_base = (raw + (align - 1)) & ~(int32_t)(align - 1); - - uint32_t table_base = table_next_free; - table_next_free += dl.table_size; - CHECK(table_next_free <= wasm_table_size(table), "table headroom exhausted"); - printf("bases: __memory_base=%d __table_base=%u (table size %u)\n", - memory_base, table_base, wasm_table_size(table)); - - // ---- 4. unit import resolution ----------------------------------------- - wasm_byte_vec_t unit_bv; - wasm_byte_vec_new(&unit_bv, unit_bytes.size(), (const char*)unit_bytes.data()); - Instance unit; - unit.module = wasm_module_new(g_store, &unit_bv); - wasm_byte_vec_delete(&unit_bv); - CHECK(unit.module, "unit module load failed"); - - wasm_importtype_vec_t unit_imports = WASM_EMPTY_VEC; - wasm_module_imports(unit.module, &unit_imports); - std::vector unit_import_externs(unit_imports.size); - // GOT.mem entries that must be self-resolved from the unit's own exports - // after instantiation (weak data the core does not define) - std::vector> deferred_got; - - for(size_t i = 0; i < unit_imports.size; i++) - { - const wasm_name_t* mod_n = wasm_importtype_module(unit_imports.data[i]); - const wasm_name_t* nm_n = wasm_importtype_name(unit_imports.data[i]); - std::string mod(mod_n->data, mod_n->size); - std::string nm(nm_n->data, nm_n->size); - const wasm_externtype_t* et = wasm_importtype_type(unit_imports.data[i]); - wasm_externkind_t kind = wasm_externtype_kind(et); - wasm_extern_t* resolved = nullptr; - - if(mod == "env" && nm == "memory") - resolved = wasm_memory_as_extern(memory); - else if(mod == "env" && nm == "__indirect_function_table") - resolved = wasm_table_as_extern(table); - else if(mod == "env" && nm == "__stack_pointer") - { - CHECK(core.global("__stack_pointer"), "core does not export __stack_pointer"); - resolved = core.by_name["__stack_pointer"]; - } - else if(mod == "env" && nm == "__memory_base") - resolved = wasm_global_as_extern(make_i32_global(memory_base, WASM_CONST)); - else if(mod == "env" && nm == "__table_base") - resolved = wasm_global_as_extern(make_i32_global((int32_t)table_base, WASM_CONST)); - else if(mod == "env" && kind == WASM_EXTERN_FUNC) - { - auto it = core.by_name.find(nm); - CHECK(it != core.by_name.end(), "unresolved unit func import env.%s", nm.c_str()); - resolved = it->second; - } - else if(mod == "GOT.mem") - { - wasm_global_t* src = core.global(nm.c_str()); - if(src) - { - wasm_val_t v; - wasm_global_get(src, &v); - resolved = wasm_global_as_extern(make_i32_global(v.of.i32, WASM_VAR)); - } - else - { - // provisional 0; patched from the unit's own export post-instantiation - wasm_global_t* g = make_i32_global(0, WASM_VAR); - deferred_got.push_back({ nm, g }); - resolved = wasm_global_as_extern(g); - } - } - else if(mod == "GOT.func") - { - // resolved guest-side: the core exports a helper returning - // (intptr_t)&func, which on wasm is the function's table index — - // no host-side funcref injection needed (WAMR forbids it anyway) - std::string helper = "core_table_index_of_" + nm; - CHECK(core.func(helper.c_str()), "no GOT.func resolver %s in core", helper.c_str()); - int32_t slot = call_i32(core, helper.c_str()); - CHECK(slot > 0, "GOT.func resolver %s returned %d", helper.c_str(), slot); - resolved = wasm_global_as_extern(make_i32_global(slot, WASM_VAR)); - } - CHECK(resolved, "unhandled unit import %s.%s (kind %d)", mod.c_str(), nm.c_str(), (int)kind); - unit_import_externs[i] = resolved; - } - - // ---- 5. instantiate unit, patch GOT, run init --------------------------- - wasm_extern_vec_t unit_iv = { unit_import_externs.size(), unit_import_externs.data() }; - trap = nullptr; - unit.instance = wasm_instance_new(g_store, unit.module, &unit_iv, &trap); - report_trap(trap, "unit instantiation"); - CHECK(unit.instance, "unit instantiation failed"); - unit.index_exports(); - printf("unit instantiated: %zu exports\n", unit.by_name.size()); - - for(auto& [nm, got] : deferred_got) - { - wasm_global_t* own = unit.global(nm.c_str()); - CHECK(own, "GOT.mem.%s defined neither by core nor by unit", nm.c_str()); - wasm_val_t v; - wasm_global_get(own, &v); - // dylink ABI: a PIC module's exported data symbols are offsets relative - // to its __memory_base; the linker must add the base when resolving - wasm_val_t nv = WASM_I32_VAL(memory_base + v.of.i32); - wasm_global_set(got, &nv); - printf("self-resolved GOT.mem.%s = %d (offset %d)\n", nm.c_str(), memory_base + v.of.i32, v.of.i32); - } - - if(unit.func("__wasm_apply_data_relocs")) - call_i32(unit, "__wasm_apply_data_relocs"); - if(unit.func("__wasm_call_ctors")) - call_i32(unit, "__wasm_call_ctors"); - - // ---- 6. render and read back -------------------------------------------- - int32_t counter_addr = 0; - { - wasm_global_t* cc = core.global("core_counter"); - CHECK(cc, "core_counter not exported"); - wasm_val_t v; - wasm_global_get(cc, &v); - counter_addr = v.of.i32; - } - byte_t* mem = wasm_memory_data(memory); - int32_t counter_before; - memcpy(&counter_before, mem + counter_addr, 4); - - call_i32(unit, "uce_unit_render"); - - int32_t out_ptr = call_i32(core, "core_output_data"); - int32_t out_len = call_i32(core, "core_output_size"); - mem = wasm_memory_data(memory); // may have moved if memory grew - int32_t counter_after; - memcpy(&counter_after, mem + counter_addr, 4); - - printf("---- unit output (%d bytes) ----\n%.*s\n--------------------------------\n", - out_len, out_len, mem + out_ptr); - printf("core_counter (in linear memory): before=%d after=%d\n", counter_before, counter_after); - - bool ok = out_len > 0 && - memmem(mem + out_ptr, out_len, "unit-data-segment-ok", 20) && - memmem(mem + out_ptr, out_len, "counter=7", 9) && - memmem(mem + out_ptr, out_len, "mapsum=3", 8) && - memmem(mem + out_ptr, out_len, "core-string+unit", 16) && - memmem(mem + out_ptr, out_len, "[cb:42]", 7) && - memmem(mem + out_ptr, out_len, "[got-func-ok]", 13) && - memmem(mem + out_ptr, out_len, "[fn:42]", 7) && - counter_before == 7 && counter_after == 8; - - printf(ok ? "PHASE0 EXIT CRITERION: PASS\n" : "PHASE0 EXIT CRITERION: FAIL (see output above)\n"); - return ok ? 0 : 1; -} diff --git a/spikes/wasm-phase0/realunit-report.md b/spikes/wasm-phase0/realunit-report.md deleted file mode 100644 index 265f493..0000000 --- a/spikes/wasm-phase0/realunit-report.md +++ /dev/null @@ -1,165 +0,0 @@ -# UCE real generated unit -> WASM PIC side module report - -Remote host: `k-uce` -Working directory/artifacts left in place: `/tmp/uce/wasm-phase0/realunit/` -Repository `/Code/uce.openfu.com/uce` was treated read-only; patched copies/shims only under `/tmp/uce/wasm-phase0/realunit/`. - -## Result - -Both real generated units compiled and linked as WASM PIC side modules with `dylink.0`: - -- `collections.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/collections.wasm` - - `dylink.0`: `memsize=1208 memalign=2^2 tablesize=3 tablealign=2^0` - - imports: 52 - - exports: 23 - - allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`). -- `hello.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/hello.wasm` - - `dylink.0`: `memsize=5544 memalign=2^2 tablesize=0 tablealign=2^0` - - imports: 38 - - exports: 24 - - allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`). - -Inspector outputs are saved remotely as: - -- `/tmp/uce/wasm-phase0/realunit/collections.inspect.txt` -- `/tmp/uce/wasm-phase0/realunit/hello.inspect.txt` - -## Final command lines - -Run on `k-uce` from `/tmp/uce/wasm-phase0/realunit`. - -`collections`: - -```sh -/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \ - -I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \ - -c collections.uce.cpp -o collections.o - -/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \ - collections.o -o collections.wasm --export=__uce_render - -python3 /Code/uce.openfu.com/uce/spikes/wasm-phase0/wasm_inspect.py collections.wasm -``` - -`hello`: - -```sh -/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \ - -I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \ - -c hello.uce.cpp -o hello.o - -/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \ - hello.o -o hello.wasm --export=__uce_render -``` - -Note: the stub-spike command exported `uce_unit_render`, but these generated units define/export `__uce_render` from the repo `RENDER` macro, so the real-unit link exports `__uce_render`. - -## Ordered friction points and fixes - -1. **WASI sysroot rejects `` by default.** - - First compile failed via `/Code/uce.openfu.com/uce/src/lib/sys.h`: - - ```txt - /share/wasi-sysroot/include/wasm32-wasip1/signal.h:2:2: error: - "wasm lacks signal support; to enable minimal signal emulation, compile with -D_WASI_EMULATED_SIGNAL ..." - ``` - - Fix: added `/tmp/uce/wasm-phase0/realunit/shim/signal.h` before repo/system includes. Key lines: - - ```c - typedef int sig_atomic_t; - typedef void (*sighandler_t)(int); - #define SIGTERM 15 - int raise(int); - sighandler_t signal(int, sighandler_t); - ``` - - This is only enough for declarations in `sys.h`; real signal behavior has no WASI equivalent. - -2. **Generated `collections.uce.cpp` includes missing local `demo_guard.h`.** - - First compile also failed: - - ```txt - fatal error: 'demo_guard.h' file not found - ``` - - Fix: added empty include guard shim `/tmp/uce/wasm-phase0/realunit/shim/demo_guard.h`: - - ```c - #ifndef UCE_WASM_SHIM_DEMO_GUARD_H - #define UCE_WASM_SHIM_DEMO_GUARD_H - #endif - ``` - -3. **Generated source uses an absolute repo include, preventing header interposition.** - - Original generated line: - - ```c++ - #include "/Code/uce.openfu.com/uce/src/lib/uce_lib.h" - ``` - - To use patched shim headers without touching `/Code`, copied the generated units into the workdir and changed only that line to: - - ```c++ - #include "uce_lib.h" - ``` - - Copies are `/tmp/uce/wasm-phase0/realunit/collections.uce.cpp` and `hello.uce.cpp`. - -4. **`types.h` defines global `operator new/delete` in every unit.** - - A straight build linked, but exported allocator definitions (`_Znwm`, `_ZdlPv`), violating the side-module allocator rule. The repo code in `types.h` contains: - - ```c++ - void * operator new(decltype(sizeof(0)) n) noexcept(false) { ... malloc(n) ... } - void operator delete(void * p) throw() { free(p); } - ``` - - Fix: copied `uce_lib.h` and `types.h` to the shim tree, then patched only the copied `shim/types.h` allocator block to declarations: - - ```c++ - // WASM side-module shim: allocator must be provided by the host/core module, - // not defined in each generated unit. Keep declarations only. - void * operator new(decltype(sizeof(0)) n) noexcept(false); - void operator delete(void * p) throw(); - ``` - - Verification from `llvm-nm -C collections.o`: - - ```txt - U operator delete(void*, unsigned long) - U operator new(unsigned long) - ``` - -## Notable imports / GOT entries - -`collections.wasm` notable imports: - -- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.__stack_pointer`, `env.memory`, `env.__indirect_function_table` -- UCE/runtime functions: `DValue::set`, `DValue::operator[]`, `DValue::push`, `dv_filter`, `dv_map`, `dv_group_by`, `list_unique`, `list_sort`, `replace`, `to_upper`, `html_escape`, `json_encode`, `var_dump`, `time` -- MySQL wrappers are imported even for this unit because included header inline functions reference `MySQL` methods. -- GOT.mem globals: - - `GOT.mem.context` - - `GOT.mem._ZNSt3__25ctypeIcE2idE` - - `GOT.mem._ZTVN10__cxxabiv117__class_type_infoE` - -`hello.wasm` notable imports: - -- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.memcmp` -- UCE/runtime functions: `time`, `html_escape`, `html_escapey`, `var_dump` -- GOT.mem globals: - - `GOT.mem.context` - - `GOT.mem._ZNSt3__219piecewise_constructE` - - `GOT.mem._ZNSt3__25ctypeIcE2idE` - -## Phase 2 land mines - -- `src/lib/sys.h` includes `` and declares signals/tasks/socket/process/file-lock APIs. WASI has no normal POSIX signals, fork/exec, traditional sockets, or process model. These need real repo-level `#ifdef __wasm__` / runtime boundary design, not local shims. -- `types.h` defines global allocator operators in a header. For side modules this must move to the core/runtime or be gated out under WASM side-module builds; otherwise each unit defines its own allocator entry points. -- Generated units include `/Code/.../uce_lib.h` by absolute path. That makes cross-target header selection brittle. The generator should emit a logical include (`"uce_lib.h"`) and the build should supply target-specific include order. -- Header-defined helper functions cause many exports from each unit (`to_string`, MySQL wrapper functions, vector slow-path/lambda helpers, globals `parent_pid`, `my_pid`, `context`). For a clean side-module ABI, these should probably be hidden, moved out of headers, marked inline/static where appropriate, or provided by the core module. -- MySQL inline wrappers are pulled into every unit by `uce_lib.h` even when the unit does not use MySQL directly. This is why `MySQL::connect/disconnect/query/error` imports appear in both outputs. -- Kept `-fno-exceptions`; no try/catch blocker was hit for these two generated units. diff --git a/spikes/wasm-phase0/unit.cpp b/spikes/wasm-phase0/unit.cpp deleted file mode 100644 index e33edd4..0000000 --- a/spikes/wasm-phase0/unit.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// WASM-PROPOSAL Phase 0 — unit module stub. -// -// Stands in for a generated UCE unit: compiled with -fPIC and linked with -// wasm-ld -shared into a PIC module carrying a dylink.0 section. Defines no -// allocator and links no libc — operator new/delete, memcpy, and the core -// API all arrive as imports resolved by the loader against the core module -// (§5.4 unit module contract). -// -// Exercises, in one render call: -// - unit-local data segment with relocation (__memory_base placement) -// - GOT.mem read+write of a core-defined global (core_counter) -// - C++ containers (std::string/std::map) on the core's heap -// - heap object created in core, mutated and read from the unit -// - function pointer from unit through core and back (shared table) -// - std::function/lambda handed across the module boundary - -#include -#include -#include - -extern "C" { -void uce_print(const char* s, size_t len); -extern int core_counter; -std::string* core_make_string(const char* s); -void core_append_string(std::string* str, const char* s); -void core_invoke_callback(void (*cb)(int), int arg); -void core_invoke_function(std::function* f, int arg); -} - -static const char* unit_static_message = "unit-data-segment-ok"; -static int unit_state = 41; - -static void my_callback(int x) -{ - std::string s = "[cb:" + std::to_string(x + unit_state) + "]"; - uce_print(s.data(), s.size()); -} - -extern "C" void uce_unit_render() -{ - std::string out = "hello from unit; "; - out += unit_static_message; - out += "; counter=" + std::to_string(core_counter); - uce_print(out.data(), out.size()); - - std::map m; - m["a"] = 1; - m["b"] = 2; - int sum = 0; - for(auto& kv : m) - sum += kv.second; - std::string s2 = "; mapsum=" + std::to_string(sum); - uce_print(s2.data(), s2.size()); - - std::string* cs = core_make_string("; core-string"); - core_append_string(cs, "+unit"); - uce_print(cs->data(), cs->size()); - delete cs; - - core_invoke_callback(my_callback, 1); - - // address of a core-defined function taken in the unit → GOT.func import; - // the loader must ensure the core function has a funcref table entry. - // volatile so the optimizer cannot fold it back into a direct call. - void (*volatile print_ptr)(const char*, size_t) = uce_print; - print_ptr("[got-func-ok]", 13); - - auto* fn = new std::function([](int v) { return(v * 3); }); - core_invoke_function(fn, 14); - delete fn; - - core_counter++; -} diff --git a/spikes/wasm-phase0/wasm_inspect.py b/spikes/wasm-phase0/wasm_inspect.py deleted file mode 100644 index efa344d..0000000 --- a/spikes/wasm-phase0/wasm_inspect.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 0 helper: dump a wasm module's imports, exports, and dylink.0 -custom section (memory/table size requirements). Doubles as the reference -for the loader's dylink.0 parsing (§6 step 4).""" -import sys, struct - -def uleb(buf, pos): - result = 0 - shift = 0 - while True: - b = buf[pos] - pos += 1 - result |= (b & 0x7f) << shift - if not (b & 0x80): - return result, pos - shift += 7 - -def name(buf, pos): - n, pos = uleb(buf, pos) - return buf[pos:pos+n].decode("utf-8", "replace"), pos + n - -def limits(buf, pos): - flags = buf[pos]; pos += 1 - mn, pos = uleb(buf, pos) - mx = None - if flags & 1: - mx, pos = uleb(buf, pos) - return (mn, mx), pos - -KIND = {0: "func", 1: "table", 2: "memory", 3: "global"} -VALTYPE = {0x7f: "i32", 0x7e: "i64", 0x7d: "f32", 0x7c: "f64", 0x70: "funcref", 0x6f: "externref"} - -def main(fn): - buf = open(fn, "rb").read() - assert buf[:8] == b"\0asm\x01\0\0\0", "not a wasm module" - pos = 8 - while pos < len(buf): - sec_id = buf[pos]; pos += 1 - size, pos = uleb(buf, pos) - end = pos + size - if sec_id == 0: - sname, p = name(buf, pos) - if sname == "dylink.0": - print("== dylink.0 ==") - while p < end: - sub = buf[p]; p += 1 - sublen, p = uleb(buf, p) - subend = p + sublen - if sub == 1: # WASM_DYLINK_MEM_INFO - memsize, p2 = uleb(buf, p) - memalign, p2 = uleb(buf, p2) - tabsize, p2 = uleb(buf, p2) - tabalign, p2 = uleb(buf, p2) - print(f" mem_info: memsize={memsize} memalign=2^{memalign} tablesize={tabsize} tablealign=2^{tabalign}") - else: - print(f" subsection type={sub} len={sublen}") - p = subend - elif sname in ("uce.abi",): - print(f"== custom section {sname} ({end - p} bytes) ==") - elif sec_id == 2: - count, p = uleb(buf, pos) - print(f"== imports ({count}) ==") - for _ in range(count): - mod, p = name(buf, p) - nm, p = name(buf, p) - kind = buf[p]; p += 1 - detail = "" - if kind == 0: - _, p = uleb(buf, p) - elif kind == 1: - et = buf[p]; p += 1 - lim, p = limits(buf, p) - detail = f" {VALTYPE.get(et, hex(et))} {lim}" - elif kind == 2: - lim, p = limits(buf, p) - detail = f" {lim}" - elif kind == 3: - vt = buf[p]; p += 1 - mut = buf[p]; p += 1 - detail = f" {VALTYPE.get(vt, hex(vt))}{' mut' if mut else ''}" - print(f" {KIND.get(kind, kind):6} {mod}.{nm}{detail}") - elif sec_id == 7: - count, p = uleb(buf, pos) - print(f"== exports ({count}) ==") - for _ in range(count): - nm, p = name(buf, p) - kind = buf[p]; p += 1 - _, p = uleb(buf, p) - print(f" {KIND.get(kind, kind):6} {nm}") - pos = end - -if __name__ == "__main__": - for fn in sys.argv[1:]: - print(f"### {fn}") - main(fn) diff --git a/spikes/wasm-phase2/README.md b/spikes/wasm-phase2/README.md deleted file mode 100644 index 387ed03..0000000 --- a/spikes/wasm-phase2/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# spikes/wasm-phase2 — core module + membrane scaffold - -Phase 2 validates the next WASM step without the Phase 3 dynamic loader. The -scaffold compiles a native UCE core subset (`Request`, `DValue`, UCEB1, print -buffering) plus one statically linked real `.uce` page into a WASM reactor. -A small Wasmtime C-API host provides the first membrane hostcalls, sends a UCEB1 -request context into the guest, invokes render, and reads the response from -linear memory. `uce_host_ctx_read(ptr, cap)` follows a length-query contract: -`ptr == 0` or `cap == 0` returns the required length; a short non-zero buffer -also returns the required length without a partial copy; an adequately sized -buffer receives the full context and returns the copied length. - -This is intentionally not the final worker. It proves the Phase 2 membrane path: -core-owned DValue/UCEB1 code runs in WASM and a `.uce` render entry sees a -host-provided request context through the membrane. The checked-in page uses the -same `RENDER(Request&)` entry shape as generated units, but it is included -directly by the scaffold rather than emitted by the UCE preprocessor. - -Run on `k-uce`: - -```bash -bash spikes/wasm-phase2/build_modules.sh -bash spikes/wasm-phase2/build_loader.sh -/tmp/uce/wasm-phase2/loader -``` - -Expected final line: - -```text -PHASE2 EXIT CRITERION: PASS -``` - -Files: - -- `core.cpp` — WASM reactor core subset and membrane decode/render entry. -- `page.uce` — real UCE page statically linked for Phase 2 only. -- `loader.cpp` — host runner with `uce_host_ctx_read` and `uce_host_log`. - Its small host-side UCEB1 encoder is spike-only; the production UCE server - host should use the native `ucb_encode()` implementation to avoid format drift. -- `build_modules.sh` — wasi-sdk build for `/tmp/uce/wasm-phase2/core.wasm`. -- `build_loader.sh` — Wasmtime C-API build for `/tmp/uce/wasm-phase2/loader`. - -Deferred to Phase 3: - -- PIC side modules and dylink loader integration. -- Lazy unit/component loading. -- Full `uce_lib` hostcall surface beyond the minimal context/log membrane. -- Generator-emitted units (`.uce` preprocessor output with literal markup, - `__uce_set_current_request`, and generated includes) rendering through this - membrane; Phase 0 only proved those generated units can compile as side - modules. diff --git a/spikes/wasm-phase2/build_loader.sh b/spikes/wasm-phase2/build_loader.sh deleted file mode 100755 index 269363b..0000000 --- a/spikes/wasm-phase2/build_loader.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# Phase 2: build the minimal host/membrane runner against Wasmtime's wasm-c-api. -set -e -cd "$(dirname "$0")" - -OUT=/tmp/uce/wasm-phase2 -mkdir -p "$OUT" - -c++ -std=c++17 -O2 loader.cpp \ - -I/opt/wasmtime/include \ - -L/opt/wasmtime/lib \ - -Wl,-rpath,/opt/wasmtime/lib \ - -lwasmtime \ - -o "$OUT/loader" - -echo "built: $OUT/loader" diff --git a/spikes/wasm-phase2/build_modules.sh b/spikes/wasm-phase2/build_modules.sh deleted file mode 100755 index f201939..0000000 --- a/spikes/wasm-phase2/build_modules.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Phase 2: build the native DValue/UCEB1 core subset plus one statically -# linked real .uce page as a WASM reactor. Runs on k-uce; artifacts go under -# /tmp/uce/wasm-phase2. -set -e -cd "$(dirname "$0")" - -SDK=/opt/wasi-sdk -OUT=/tmp/uce/wasm-phase2 -mkdir -p "$OUT" - -"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \ - -O1 -fno-exceptions \ - -I../.. -I../../src/lib \ - core.cpp -o "$OUT/core.wasm" \ - -Wl,--export-all \ - -Wl,--export=__heap_base \ - -Wl,--allow-undefined-file=hostcalls.syms - -echo "--- phase2 core.wasm ---" -ls -la "$OUT/core.wasm" diff --git a/spikes/wasm-phase2/core.cpp b/spikes/wasm-phase2/core.cpp deleted file mode 100644 index ada5f58..0000000 --- a/spikes/wasm-phase2/core.cpp +++ /dev/null @@ -1,142 +0,0 @@ -// WASM-PROPOSAL Phase 2 — native UCE core subset compiled to WASM. -// -// This scaffold validates the Phase 2 membrane without the Phase 3 dynamic -// loader: the core owns memory/libc++/DValue/UCEB1, imports a tiny hostcall -// surface, decodes a host-provided UCEB1 request context, and invokes one -// statically linked real .uce page. - -#include -#include - -#include "../../src/lib/types.h" -#include "../../src/lib/dvalue.cpp" - -extern "C" { -size_t uce_host_ctx_read(char* buf, size_t cap); -void uce_host_log(int level, const char* buf, size_t len); -} - -#define RENDER(X) extern "C" void __uce_render(X) -#include "page.uce" - -static Request g_request; -static ByteStream g_ob; -static String g_output; - -SharedUnit::~SharedUnit() {} - -String nibble(String div, String& haystack) -{ - auto pos = haystack.find(div); - if(pos == String::npos) - { - auto result = haystack; - haystack.clear(); - return(result); - } - auto result = haystack.substr(0, pos); - haystack.erase(0, pos + div.length()); - return(result); -} - -void Request::ob_start() -{ - ob_stack.push_back(new ByteStream()); - ob = ob_stack.back(); -} - -void Request::set_status(s32 code, String reason) -{ - if(reason == "") - reason = code == 200 ? "OK" : "Status"; - response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason; -} - -Request::~Request() -{ - for(auto* stream : ob_stack) - delete stream; - ob_stack.clear(); -} - -static void phase2_clear_ob_stack() -{ - for(auto* stream : g_request.ob_stack) - delete stream; - g_request.ob_stack.clear(); -} - -static void phase2_apply_context(DValue& root) -{ - g_request.call = root; - g_request.params.clear(); - DValue* params = root.key("params"); - if(params) - { - params->each([&](const DValue& item, String key) { - g_request.params[key] = item.to_string(); - }); - } -} - -extern "C" { - -void* uce_alloc(size_t len) -{ - return(malloc(len)); -} - -void uce_free(void* ptr) -{ - free(ptr); -} - -int uce_phase2_render() -{ - context = &g_request; - phase2_clear_ob_stack(); - g_ob.str(""); - g_ob.clear(); - g_request.ob = &g_ob; - g_request.out = ""; - g_output = ""; - - size_t ctx_required = uce_host_ctx_read(0, 0); - if(ctx_required == 0) - return(10); - char* ctx_buf = (char*)malloc(ctx_required); - if(ctx_buf == 0) - return(11); - size_t ctx_len = uce_host_ctx_read(ctx_buf, ctx_required); - if(ctx_len != ctx_required) - { - free(ctx_buf); - return(12); - } - DValue decoded; - String error; - bool ok = ucb_decode(String(ctx_buf, ctx_len), decoded, &error); - free(ctx_buf); - if(!ok) - { - uce_host_log(3, error.data(), error.size()); - return(20); - } - - phase2_apply_context(decoded); - __uce_render(g_request); - g_output = g_ob.str(); - return(0); -} - -const char* uce_phase2_output_data() -{ - return(g_output.data()); -} - -size_t uce_phase2_output_size() -{ - return(g_output.size()); -} - -} diff --git a/spikes/wasm-phase2/hostcalls.syms b/spikes/wasm-phase2/hostcalls.syms deleted file mode 100644 index 6a18b04..0000000 --- a/spikes/wasm-phase2/hostcalls.syms +++ /dev/null @@ -1,2 +0,0 @@ -uce_host_ctx_read -uce_host_log diff --git a/spikes/wasm-phase2/loader.cpp b/spikes/wasm-phase2/loader.cpp deleted file mode 100644 index b58e540..0000000 --- a/spikes/wasm-phase2/loader.cpp +++ /dev/null @@ -1,280 +0,0 @@ -// WASM-PROPOSAL Phase 2 — minimal membrane host. -// Instantiates the statically linked core/page module, serves a UCEB1 request -// context via hostcall, invokes render, and reads the response from guest -// memory. Dynamic linking is intentionally Phase 3 work. - -#include - -#include -#include -#include -#include -#include -#include - -#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0) -#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0) - -static wasm_store_t* g_store = nullptr; -static wasm_memory_t* g_memory = nullptr; -static std::string g_context; - -static std::vector read_file(const char* fn) -{ - FILE* f = fopen(fn, "rb"); - CHECK(f, "cannot open %s", fn); - fseek(f, 0, SEEK_END); - long n = ftell(f); - fseek(f, 0, SEEK_SET); - std::vector buf(n); - CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn); - fclose(f); - return buf; -} - -static void append_varuint(std::string& out, uint64_t value) -{ - while(value >= 0x80) - { - out.push_back((char)((value & 0x7f) | 0x80)); - value >>= 7; - } - out.push_back((char)value); -} - -struct Node -{ - std::string scalar; - bool is_list = false; - std::vector> children; -}; - -static Node scalar(const char* value) -{ - Node n; - n.scalar = value; - return n; -} - -static void encode_node(std::string& out, const Node& node) -{ - out.push_back(node.is_list ? 1 : 0); - append_varuint(out, node.scalar.size()); - out.append(node.scalar); - append_varuint(out, node.children.size()); - for(const auto& child : node.children) - { - append_varuint(out, child.first.size()); - out.append(child.first); - encode_node(out, child.second); - } -} - -static std::string make_context() -{ - Node params; - params.children.push_back({"HTTP_HOST", scalar("phase2.example.test")}); - params.children.push_back({"SCRIPT_URL", scalar("/spikes/wasm-phase2/page.uce")}); - - Node nested; - nested.children.push_back({"answer", scalar("42")}); - - Node root; - root.children.push_back({"params", params}); - root.children.push_back({"route", scalar("/spikes/wasm-phase2/page.uce")}); - root.children.push_back({"nested", nested}); - - std::string out = "UCEB"; - out.push_back((char)1); - encode_node(out, root); - return out; -} - -static wasm_trap_t* host_ctx_read(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) -{ - (void)env; - uint32_t ptr = args->data[0].of.i32; - uint32_t cap = args->data[1].of.i32; - if(ptr == 0 || cap == 0 || cap < g_context.size()) - { - results->data[0] = WASM_I32_VAL((int32_t)g_context.size()); - return nullptr; - } - CHECK(g_memory, "host ctx_read called before memory export was captured"); - uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory); - size_t mem_size = wasm_memory_data_size(g_memory); - CHECK((size_t)ptr <= mem_size, "ctx_read pointer outside memory"); - CHECK((size_t)ptr + g_context.size() <= mem_size, "ctx_read buffer outside memory"); - memcpy(mem + ptr, g_context.data(), g_context.size()); - results->data[0] = WASM_I32_VAL((int32_t)g_context.size()); - return nullptr; -} - -static wasm_trap_t* host_log(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) -{ - (void)env; (void)results; - int level = args->data[0].of.i32; - uint32_t ptr = args->data[1].of.i32; - uint32_t len = args->data[2].of.i32; - uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory); - size_t mem_size = wasm_memory_data_size(g_memory); - if((size_t)ptr + len <= mem_size) - fprintf(stderr, "[guest log %d] %.*s\n", level, (int)len, (const char*)mem + ptr); - return nullptr; -} - -static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) -{ - (void)args; (void)results; - char msg[256]; - snprintf(msg, sizeof(msg), "unimplemented import called: %s", (const char*)env); - wasm_message_t message; - wasm_byte_vec_new(&message, strlen(msg) + 1, msg); - wasm_trap_t* trap = wasm_trap_new(g_store, &message); - wasm_byte_vec_delete(&message); - return trap; -} - -static wasm_func_t* make_func(wasm_store_t* store, std::vector params, std::vector results, wasm_func_callback_with_env_t cb, void* env = nullptr) -{ - wasm_valtype_vec_t ps; - wasm_valtype_vec_new_uninitialized(&ps, params.size()); - for(size_t i = 0; i < params.size(); i++) - ps.data[i] = wasm_valtype_new(params[i]); - wasm_valtype_vec_t rs; - wasm_valtype_vec_new_uninitialized(&rs, results.size()); - for(size_t i = 0; i < results.size(); i++) - rs.data[i] = wasm_valtype_new(results[i]); - wasm_functype_t* ft = wasm_functype_new(&ps, &rs); - wasm_func_t* fn = wasm_func_new_with_env(store, ft, cb, env, nullptr); - wasm_functype_delete(ft); - return fn; -} - -struct Instance -{ - wasm_module_t* module = nullptr; - wasm_instance_t* instance = nullptr; - wasm_extern_vec_t exports = WASM_EMPTY_VEC; - std::map by_name; - - void index_exports() - { - wasm_exporttype_vec_t types = WASM_EMPTY_VEC; - wasm_module_exports(module, &types); - wasm_instance_exports(instance, &exports); - for(size_t i = 0; i < types.size; i++) - { - const wasm_name_t* nm = wasm_exporttype_name(types.data[i]); - std::string key(nm->data, nm->size); - while(!key.empty() && key.back() == '\0') key.pop_back(); - by_name[key] = exports.data[i]; - } - wasm_exporttype_vec_delete(&types); - } - - wasm_func_t* func(const char* name) - { - auto it = by_name.find(name); - return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second); - } -}; - -static int32_t call_i32(Instance& inst, const char* name) -{ - wasm_func_t* f = inst.func(name); - CHECK(f, "missing export func %s", name); - wasm_val_t result_buf[1] = { WASM_INIT_VAL }; - wasm_val_vec_t args = WASM_EMPTY_VEC; - wasm_val_vec_t results = { wasm_func_result_arity(f), result_buf }; - wasm_val_vec_t no_results = WASM_EMPTY_VEC; - wasm_trap_t* trap = wasm_func_call(f, &args, results.size ? &results : &no_results); - if(trap) - { - wasm_message_t msg; - wasm_trap_message(trap, &msg); - FAIL("trap during %s: %.*s", name, (int)msg.size, msg.data); - } - return results.size ? result_buf[0].of.i32 : 0; -} - -int main(int argc, char** argv) -{ - const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase2/core.wasm"; - g_context = make_context(); - - wasm_engine_t* engine = wasm_engine_new(); - CHECK(engine, "engine"); - g_store = wasm_store_new(engine); - CHECK(g_store, "store"); - - std::vector bytes = read_file(core_path); - wasm_byte_vec_t bv; - wasm_byte_vec_new(&bv, bytes.size(), (const char*)bytes.data()); - Instance core; - core.module = wasm_module_new(g_store, &bv); - wasm_byte_vec_delete(&bv); - CHECK(core.module, "core module load failed"); - - wasm_importtype_vec_t imports = WASM_EMPTY_VEC; - wasm_module_imports(core.module, &imports); - std::vector externs(imports.size); - std::vector owned_funcs; - for(size_t i = 0; i < imports.size; i++) - { - const wasm_name_t* mod_n = wasm_importtype_module(imports.data[i]); - const wasm_name_t* name_n = wasm_importtype_name(imports.data[i]); - std::string mod(mod_n->data, mod_n->size); - std::string name(name_n->data, name_n->size); - while(!mod.empty() && mod.back() == '\0') mod.pop_back(); - while(!name.empty() && name.back() == '\0') name.pop_back(); - const wasm_externtype_t* et = wasm_importtype_type(imports.data[i]); - CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, "unexpected non-func import %s.%s", mod.c_str(), name.c_str()); - - wasm_func_t* fn = nullptr; - if(mod == "env" && name == "uce_host_ctx_read") - fn = make_func(g_store, {WASM_I32, WASM_I32}, {WASM_I32}, host_ctx_read); - else if(mod == "env" && name == "uce_host_log") - fn = make_func(g_store, {WASM_I32, WASM_I32, WASM_I32}, {}, host_log); - else - { - char* label = strdup((mod + "." + name).c_str()); - const wasm_functype_t* ft = wasm_externtype_as_functype_const(et); - fn = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr); - } - CHECK(fn, "failed to create import %s.%s", mod.c_str(), name.c_str()); - owned_funcs.push_back(fn); - externs[i] = wasm_func_as_extern(fn); - } - - wasm_extern_vec_t iv = { externs.size(), externs.data() }; - wasm_trap_t* trap = nullptr; - core.instance = wasm_instance_new(g_store, core.module, &iv, &trap); - if(trap) - { - wasm_message_t msg; - wasm_trap_message(trap, &msg); - FAIL("trap during instantiation: %.*s", (int)msg.size, msg.data); - } - CHECK(core.instance, "instantiation failed"); - core.index_exports(); - g_memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr); - CHECK(g_memory, "core does not export memory"); - if(core.func("_initialize")) - call_i32(core, "_initialize"); - - int rc = call_i32(core, "uce_phase2_render"); - CHECK(rc == 0, "render returned %d", rc); - int32_t data = call_i32(core, "uce_phase2_output_data"); - int32_t size = call_i32(core, "uce_phase2_output_size"); - uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory); - CHECK((size_t)data + (size_t)size <= wasm_memory_data_size(g_memory), "output outside memory"); - std::string output((const char*)mem + data, size); - printf("---- phase2 output (%d bytes) ----\n%s", size, output.c_str()); - printf("----------------------------------\n"); - CHECK(output.find("PHASE2 PAGE OK") != std::string::npos, "missing page marker"); - CHECK(output.find("host=phase2.example.test") != std::string::npos, "missing context param"); - CHECK(output.find("answer=42") != std::string::npos, "missing nested context value"); - printf("PHASE2 EXIT CRITERION: PASS\n"); - return 0; -} diff --git a/spikes/wasm-phase2/page.uce b/spikes/wasm-phase2/page.uce deleted file mode 100644 index cf3e5f5..0000000 --- a/spikes/wasm-phase2/page.uce +++ /dev/null @@ -1,12 +0,0 @@ -// WASM-PROPOSAL Phase 2 scaffold page. -// This is a real .uce source file with a normal RENDER entry point. The -// Phase 2 core statically links it to validate the host-context membrane -// before the dynamic loader is introduced in Phase 3. - -RENDER(Request& context) -{ - print("PHASE2 PAGE OK\n"); - print("host=", context.params["HTTP_HOST"], "\n"); - print("route=", context.call["route"].to_string(), "\n"); - print("answer=", context.call["nested"]["answer"].to_string(), "\n"); -} diff --git a/spikes/wasm-phase3/README.md b/spikes/wasm-phase3/README.md deleted file mode 100644 index cec7324..0000000 --- a/spikes/wasm-phase3/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# spikes/wasm-phase3 — dynamic loader + workspace scaffold - -Phase 3 combines the Phase 0 dynamic PIC-module loader with the Phase 2 UCEB1 -request-context membrane. It is still a spike, not the production wasm worker, -but it validates the load-bearing pieces together: - -1. build a core WASM reactor that owns memory, allocator, `Request`, `DValue`, - UCEB1, and output buffering; -2. build a separate PIC side module with `dylink.0` from a generated-shape UCE - page C++ file; -3. parse `dylink.0`, allocate `__memory_base`/`__table_base`, resolve - `env.*`, `GOT.mem.*`, and `GOT.func.*` imports against the core/workspace; -4. instantiate the unit, run relocations/constructors, set its Request pointer, - call `__uce_render`, and read rendered output from core-owned memory. - -The fixture intentionally exercises the loader branches that are easiest to -accidentally leave dark: non-zero `table_size` / `__table_base`, `GOT.func` -resolution (`phase3_core_print`), and deferred self-resolution of unit-owned -`GOT.mem` entries. The loader output prints those resolutions before the final -pass marker. Its generated-shape expressions call `html_escape(...)` so the -side-module import surface includes the first ordinary UCE helper dependency -rather than only `types.h`/`DValue`. - -Run on `k-uce`: - -```bash -bash spikes/wasm-phase3/build_modules.sh -bash spikes/wasm-phase3/build_loader.sh -/tmp/uce/wasm-phase3/loader -``` - -Expected final line: - -```text -PHASE3 EXIT CRITERION: PASS -``` - -Files: - -- `core.cpp` — core/workspace scaffold and UCEB1 membrane decode. -- `page.uce` — source page for the generated-shape fixture. -- `generated/page.uce.cpp` — checked-in UCE-preprocessor-shaped side-module - fixture: `uce_lib.h` include, `__uce_set_current_request`, `__uce_render`, - literal markup lowered to `print(R"(...)")`, expression prints wrapped in - `html_escape(...)`, callback/lambda/table exercises, and self-GOT data. -- `loader.cpp` — dynamic linker/runner adapted from Phase 0. -- `hostcalls.syms` — explicit core hostcall allowlist. - -Still deferred to production Phase 3/4 work: - -- running the actual UCE preprocessor inside this build script rather than using - a checked-in generated-shape fixture; -- lazy component/path dispatch for a full site tree; -- uce-starter end-to-end under a wasm FastCGI worker; -- productionizing the side-module allocator gate as a real `#ifdef` in - `types.h` instead of this spike's copied-header text patch; -- parser hardening beyond the spike's basic magic/version/bounds/alignment - checks, including fuzzing malformed wasm/dylink sections before accepting - cache or third-party units; -- import-policy hardening beyond this spike's fail-fast resolver; -- workspace birth/drop integration with the real server lifecycle, including - retaining unaligned allocation pointers needed to unload units or drop a - workspace cleanly. diff --git a/spikes/wasm-phase3/build_loader.sh b/spikes/wasm-phase3/build_loader.sh deleted file mode 100755 index 8f698af..0000000 --- a/spikes/wasm-phase3/build_loader.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# Phase 3: build the dynamic-loader/membrane runner against Wasmtime's wasm-c-api. -set -e -cd "$(dirname "$0")" - -OUT=/tmp/uce/wasm-phase3 -mkdir -p "$OUT" - -c++ -std=c++17 -O2 loader.cpp \ - -I/opt/wasmtime/include \ - -L/opt/wasmtime/lib \ - -Wl,-rpath,/opt/wasmtime/lib \ - -lwasmtime \ - -o "$OUT/loader" - -echo "built: $OUT/loader" diff --git a/spikes/wasm-phase3/build_modules.sh b/spikes/wasm-phase3/build_modules.sh deleted file mode 100755 index 8b7fc20..0000000 --- a/spikes/wasm-phase3/build_modules.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -# Phase 3: build a core reactor plus one generated-shape PIC side module. -# Runs on k-uce; artifacts go under /tmp/uce/wasm-phase3. -set -e -cd "$(dirname "$0")" - -SDK=/opt/wasi-sdk -ROOT=$(cd ../.. && pwd) -OUT=/tmp/uce/wasm-phase3 -SHIM="$OUT/shim" -mkdir -p "$OUT" "$SHIM" - -cat > "$SHIM/uce_lib.h" <<'EOF' -#pragma once -#include "types.h" -String html_escape(String s); -EOF -cp "$ROOT/src/lib/types.h" "$SHIM/types.h" -python3 - <<'PY' -from pathlib import Path -p = Path('/tmp/uce/wasm-phase3/shim/types.h') -s = p.read_text() -start = s.index('void * operator new(decltype(sizeof(0)) n) noexcept(false)') -end = s.index('void operator delete(void * p) throw()') -end = s.index('\n}', end) + 3 -replacement = '''// WASM side-module shim: allocator is owned by the core module.\nvoid * operator new(decltype(sizeof(0)) n) noexcept(false);\nvoid operator delete(void * p) throw();\nvoid operator delete(void * p, decltype(sizeof(0)) n) noexcept;\n''' -p.write_text(s[:start] + replacement + s[end:]) -PY -cat > "$SHIM/signal.h" <<'EOF' -#pragma once -typedef int sig_atomic_t; -typedef void (*sighandler_t)(int); -#define SIGTERM 15 -int raise(int); -sighandler_t signal(int, sighandler_t); -EOF - -"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \ - -O1 -fno-exceptions \ - -I../.. -I../../src/lib \ - core.cpp -o "$OUT/core.wasm" \ - -Wl,--export-all \ - -Wl,--import-table \ - -Wl,--export=__stack_pointer \ - -Wl,--export=__heap_base \ - -Wl,--allow-undefined-file=hostcalls.syms \ - -Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE - -"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \ - -fvisibility-inlines-hidden \ - -O1 -fno-exceptions \ - -I"$SHIM" -I"$ROOT/src/lib" \ - -c generated/page.uce.cpp -o "$OUT/page.o" - -"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \ - -fvisibility-inlines-hidden \ - -O1 -fno-exceptions \ - -I"$SHIM" -I"$ROOT/src/lib" \ - -c generated/helper.cpp -o "$OUT/helper.o" - -"$SDK/bin/wasm-ld" -shared --experimental-pic \ - --unresolved-symbols=import-dynamic \ - "$OUT/page.o" "$OUT/helper.o" -o "$OUT/page.wasm" \ - --export=__uce_set_current_request \ - --export=__uce_render - -echo "--- phase3 core.wasm ---" -ls -la "$OUT/core.wasm" -echo "--- phase3 page.wasm ---" -ls -la "$OUT/page.wasm" diff --git a/spikes/wasm-phase3/core.cpp b/spikes/wasm-phase3/core.cpp deleted file mode 100644 index 69685cf..0000000 --- a/spikes/wasm-phase3/core.cpp +++ /dev/null @@ -1,188 +0,0 @@ -// WASM-PROPOSAL Phase 3 — dynamic-loader core scaffold. -// -// This core owns the UCE heap, Request, DValue/UCEB1, and output buffer. The -// Phase 3 loader dynamically links a PIC side module generated in the same -// shape as UCE preprocessor output, sets its Request* context, and invokes its -// __uce_render entry. - -#include -#include -#include - -#include "../../src/lib/types.h" -#include "../../src/lib/dvalue.cpp" - -extern "C" { -size_t uce_host_ctx_read(char* buf, size_t cap); -void uce_host_log(int level, const char* buf, size_t len); -} - -extern "C" void phase3_core_print(const char* data, size_t len) -{ - if(context && context->ob) - context->ob->write(data, len); -} - -extern "C" void phase3_core_invoke_callback(void (*cb)(int), int value) -{ - cb(value); -} - -extern "C" void phase3_core_invoke_function(std::function* f, int value) -{ - String out = "

fn=" + std::to_string((*f)(value)) + "

\n"; - phase3_core_print(out.data(), out.size()); -} - -extern "C" intptr_t core_table_index_of_phase3_core_print() -{ - return(reinterpret_cast(phase3_core_print)); -} - -static Request g_request; -static ByteStream g_ob; -static String g_output; - -SharedUnit::~SharedUnit() {} - -String html_escape(String s) -{ - String result; - for(char c : s) - { - switch(c) - { - case '&': result += "&"; break; - case '<': result += "<"; break; - case '>': result += ">"; break; - case '"': result += """; break; - case '\'': result += "'"; break; - default: result.push_back(c); break; - } - } - return(result); -} - -String nibble(String div, String& haystack) -{ - auto pos = haystack.find(div); - if(pos == String::npos) - { - auto result = haystack; - haystack.clear(); - return(result); - } - auto result = haystack.substr(0, pos); - haystack.erase(0, pos + div.length()); - return(result); -} - -void Request::ob_start() -{ - ob_stack.push_back(new ByteStream()); - ob = ob_stack.back(); -} - -void Request::set_status(s32 code, String reason) -{ - if(reason == "") - reason = code == 200 ? "OK" : "Status"; - response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason; -} - -Request::~Request() -{ - for(auto* stream : ob_stack) - delete stream; - ob_stack.clear(); -} - -static void phase3_clear_ob_stack() -{ - for(auto* stream : g_request.ob_stack) - delete stream; - g_request.ob_stack.clear(); -} - -static void phase3_apply_context(DValue& root) -{ - g_request.call = root; - g_request.params.clear(); - DValue* params = root.key("params"); - if(params) - { - params->each([&](const DValue& item, String key) { - g_request.params[key] = item.to_string(); - }); - } -} - -extern "C" { - -void* uce_alloc(size_t len) -{ - return(malloc(len)); -} - -void uce_free(void* ptr) -{ - free(ptr); -} - -int uce_phase3_prepare() -{ - context = &g_request; - phase3_clear_ob_stack(); - g_ob.str(""); - g_ob.clear(); - g_request.ob = &g_ob; - g_request.out = ""; - g_output = ""; - - size_t ctx_required = uce_host_ctx_read(0, 0); - if(ctx_required == 0) - return(10); - char* ctx_buf = (char*)malloc(ctx_required); - if(ctx_buf == 0) - return(11); - size_t ctx_len = uce_host_ctx_read(ctx_buf, ctx_required); - if(ctx_len != ctx_required) - { - free(ctx_buf); - return(12); - } - - DValue decoded; - String error; - bool ok = ucb_decode(String(ctx_buf, ctx_len), decoded, &error); - free(ctx_buf); - if(!ok) - { - uce_host_log(3, error.data(), error.size()); - return(20); - } - phase3_apply_context(decoded); - return(0); -} - -Request* uce_phase3_request() -{ - return(&g_request); -} - -void uce_phase3_finish() -{ - g_output = g_ob.str(); -} - -const char* uce_phase3_output_data() -{ - return(g_output.data()); -} - -size_t uce_phase3_output_size() -{ - return(g_output.size()); -} - -} diff --git a/spikes/wasm-phase3/generated/helper.cpp b/spikes/wasm-phase3/generated/helper.cpp deleted file mode 100644 index fba26fe..0000000 --- a/spikes/wasm-phase3/generated/helper.cpp +++ /dev/null @@ -1,6 +0,0 @@ -extern int phase3_unit_state; - -extern "C" int phase3_helper_state_plus(int value) -{ - return(phase3_unit_state + value); -} diff --git a/spikes/wasm-phase3/generated/page.uce.cpp b/spikes/wasm-phase3/generated/page.uce.cpp deleted file mode 100644 index 3084365..0000000 --- a/spikes/wasm-phase3/generated/page.uce.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include "uce_lib.h" -#include -#include -#include - -extern "C" void phase3_core_print(const char* data, size_t len); -extern "C" void phase3_core_invoke_callback(void (*cb)(int), int value); -extern "C" void phase3_core_invoke_function(std::function* f, int value); -extern "C" int phase3_helper_state_plus(int value); - -int phase3_unit_state = 40; -int phase3_weak_data __attribute__((weak)) = 0; - -static void phase3_unit_callback(int value) -{ - String out = "

callback=" + std::to_string(value + phase3_unit_state) + "

\n"; - print(out); -} - -#ifndef UCE_SET_CURRENT_REQUEST_DEFINED -#define UCE_SET_CURRENT_REQUEST_DEFINED - -/*load_declarations*/ - -extern "C" void __uce_set_current_request(Request* _request) -{ - context = _request; - /*load_units*/ -} - -#endif -#line 4 "spikes/wasm-phase3/page.uce" -extern "C" void __uce_render(Request& context) -{ - print(R"(
-

PHASE3 PAGE OK

-

host=)"); - print(html_escape(context.params["HTTP_HOST"])); - print(R"(

-

route=)"); - print(html_escape(context.call["route"].to_string())); - print(R"(

-

answer=)"); - print(html_escape(context.call["nested"]["answer"].to_string())); - print(R"(

-)"); - context.call["nested"].each([&](const DValue& value, String key) { - print(R"(

each=)"); - print(html_escape(key + ":" + value.to_string())); - print(R"(

-)"); - }); - print(R"(

self-got=)"); - phase3_weak_data = 7; - print(std::to_string(phase3_helper_state_plus(2) + phase3_weak_data - 7)); - print(R"(

-)"); - std::map phase3_map; - phase3_map.emplace(std::piecewise_construct, std::forward_as_tuple("piece"), std::forward_as_tuple("wise")); - print(R"(

map=)"); - print(html_escape(phase3_map["piece"])); - print(R"(

-)"); - phase3_core_invoke_callback(phase3_unit_callback, 2); - void (*volatile print_ptr)(const char*, size_t) = phase3_core_print; - print_ptr("

got-func-ok

\n", 19); - auto* fn = new std::function([](int value) { return(value * 3); }); - phase3_core_invoke_function(fn, 14); - delete fn; - print(R"(
-)"); -} diff --git a/spikes/wasm-phase3/hostcalls.syms b/spikes/wasm-phase3/hostcalls.syms deleted file mode 100644 index 6a18b04..0000000 --- a/spikes/wasm-phase3/hostcalls.syms +++ /dev/null @@ -1,2 +0,0 @@ -uce_host_ctx_read -uce_host_log diff --git a/spikes/wasm-phase3/loader.cpp b/spikes/wasm-phase3/loader.cpp deleted file mode 100644 index 7e4fd2a..0000000 --- a/spikes/wasm-phase3/loader.cpp +++ /dev/null @@ -1,554 +0,0 @@ -// WASM-PROPOSAL Phase 3 — dynamic loader + membrane scaffold. -// -// Embeds a wasm runtime through the standard wasm-c-api and links a core -// workspace module with a generated-shape PIC UCE side module: -// -// 1. instantiate core.wasm (owns memory/table/allocator/Request/DValue) -// 2. parse page.wasm's dylink.0 → data size/align, table slots needed -// 3. allocate __memory_base by calling core malloc, and __table_base by -// appending to the shared funcref table -// 4. build the unit's import vector: env.memory / env.__indirect_function_table / -// env.__stack_pointer from core; env.* functions from core exports; -// GOT.mem.* and GOT.func.* through the same rules as Phase 0 -// 5. instantiate unit, patch deferred GOT entries, run relocations/ctors -// 6. prepare a UCEB1 request context in core, set the unit's Request*, call -// __uce_render, then read output back out of core-owned linear memory -// -// Everything here is deliberately validation-grade: fail loudly, never guess. - -#include - -#include -#include -#include -#include -#include -#include - -#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0) -#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0) - -static std::vector read_file(const char* fn) -{ - FILE* f = fopen(fn, "rb"); - CHECK(f, "cannot open %s", fn); - fseek(f, 0, SEEK_END); - long n = ftell(f); - fseek(f, 0, SEEK_SET); - std::vector buf(n); - CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn); - fclose(f); - return buf; -} - -// ---- minimal dylink.0 parser ------------------------------------------- - -struct DylinkInfo -{ - uint32_t mem_size = 0; - uint32_t mem_align = 0; // power of 2 - uint32_t table_size = 0; - uint32_t table_align = 0; - bool found = false; -}; - -static uint64_t read_uleb(const uint8_t* buf, size_t& pos, size_t end) -{ - uint64_t result = 0; - int shift = 0; - while(true) - { - CHECK(pos < end, "truncated uleb in wasm custom section"); - CHECK(shift < 64, "oversized uleb in wasm custom section"); - uint8_t b = buf[pos++]; - result |= (uint64_t)(b & 0x7f) << shift; - if(!(b & 0x80)) - return result; - shift += 7; - } -} - -static DylinkInfo parse_dylink(const std::vector& wasm) -{ - DylinkInfo info; - CHECK(wasm.size() >= 8 && !memcmp(wasm.data(), "\0asm", 4), "not a wasm module"); - CHECK(wasm[4] == 1 && wasm[5] == 0 && wasm[6] == 0 && wasm[7] == 0, "unsupported wasm binary version"); - size_t pos = 8; - while(pos < wasm.size()) - { - uint8_t sec_id = wasm[pos++]; - uint64_t size = read_uleb(wasm.data(), pos, wasm.size()); - CHECK(size <= wasm.size() - pos, "wasm section exceeds file size"); - size_t end = pos + size; - if(sec_id == 0) - { - uint64_t name_len = read_uleb(wasm.data(), pos, end); - CHECK(name_len <= end - pos, "custom section name exceeds section size"); - std::string name((const char*)wasm.data() + pos, name_len); - pos += name_len; - if(name == "dylink.0") - { - while(pos < end) - { - uint8_t sub = wasm[pos++]; - uint64_t sub_len = read_uleb(wasm.data(), pos, end); - CHECK(sub_len <= end - pos, "dylink subsection exceeds section size"); - size_t sub_end = pos + sub_len; - if(sub == 1) // WASM_DYLINK_MEM_INFO - { - info.mem_size = read_uleb(wasm.data(), pos, sub_end); - info.mem_align = read_uleb(wasm.data(), pos, sub_end); - info.table_size = read_uleb(wasm.data(), pos, sub_end); - info.table_align = read_uleb(wasm.data(), pos, sub_end); - CHECK(!info.found, "duplicate dylink.0 mem_info subsection"); - CHECK(info.mem_align < 31 && info.table_align < 31, "unsupported dylink alignment"); - info.found = true; - } - pos = sub_end; - } - } - } - pos = end; - } - return info; -} - -// ---- tiny UCEB1 request-context encoder --------------------------------- - -static void append_varuint(std::string& out, uint64_t value) -{ - while(value >= 0x80) - { - out.push_back((char)((value & 0x7f) | 0x80)); - value >>= 7; - } - out.push_back((char)value); -} - -struct Node -{ - std::string scalar; - std::vector> children; -}; - -static Node scalar(const char* value) -{ - Node n; - n.scalar = value; - return n; -} - -static void encode_node(std::string& out, const Node& node) -{ - out.push_back(0); - append_varuint(out, node.scalar.size()); - out.append(node.scalar); - append_varuint(out, node.children.size()); - for(const auto& child : node.children) - { - append_varuint(out, child.first.size()); - out.append(child.first); - encode_node(out, child.second); - } -} - -static std::string make_context() -{ - Node params; - params.children.push_back({"HTTP_HOST", scalar("phase3.example.test")}); - params.children.push_back({"SCRIPT_URL", scalar("/spikes/wasm-phase3/page.uce")}); - Node nested; - nested.children.push_back({"answer", scalar("42")}); - Node root; - root.children.push_back({"params", params}); - root.children.push_back({"route", scalar("/spikes/wasm-phase3/page.uce")}); - root.children.push_back({"nested", nested}); - std::string out = "UCEB"; - out.push_back((char)1); - encode_node(out, root); - return out; -} - -// ---- host imports -------------------------------------------------------- - -static wasm_memory_t* g_memory = nullptr; -static std::string g_context; - -static void set_i32_result(wasm_val_t* result, int32_t value) -{ - result->kind = WASM_I32; - result->of.i32 = value; -} - -static wasm_trap_t* host_ctx_read(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) -{ - (void)env; - uint32_t ptr = args->data[0].of.i32; - uint32_t cap = args->data[1].of.i32; - if(ptr == 0 || cap == 0 || cap < g_context.size()) - { - set_i32_result(&results->data[0], (int32_t)g_context.size()); - return nullptr; - } - CHECK(g_memory, "host ctx_read called before memory export was captured"); - uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory); - size_t mem_size = wasm_memory_data_size(g_memory); - CHECK((size_t)ptr + g_context.size() <= mem_size, "ctx_read buffer outside memory"); - memcpy(mem + ptr, g_context.data(), g_context.size()); - set_i32_result(&results->data[0], (int32_t)g_context.size()); - return nullptr; -} - -static wasm_trap_t* host_log(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) -{ - (void)env; (void)results; - int level = args->data[0].of.i32; - uint32_t ptr = args->data[1].of.i32; - uint32_t len = args->data[2].of.i32; - uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory); - size_t mem_size = wasm_memory_data_size(g_memory); - if((size_t)ptr + len <= mem_size) - fprintf(stderr, "[guest log %d] %.*s\n", level, (int)len, (const char*)mem + ptr); - return nullptr; -} - -// ---- named trap stubs for unsatisfied (WASI) imports -------------------- - -static wasm_store_t* g_store_for_stubs = nullptr; - -static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) -{ - (void)args; (void)results; - fprintf(stderr, "[stub called: %s]\n", (const char*)env); - char msg[256]; - snprintf(msg, sizeof(msg), "unimplemented host import called: %s", (const char*)env); - wasm_message_t message; - wasm_byte_vec_new(&message, strlen(msg) + 1, msg); - wasm_trap_t* trap = wasm_trap_new(g_store_for_stubs, &message); - wasm_byte_vec_delete(&message); - return trap; -} - -// ---- instance wrapper: name → extern map -------------------------------- - -struct Instance -{ - wasm_module_t* module = nullptr; - wasm_instance_t* instance = nullptr; - wasm_extern_vec_t exports = WASM_EMPTY_VEC; - std::map by_name; - - void index_exports() - { - wasm_exporttype_vec_t types = WASM_EMPTY_VEC; - wasm_module_exports(module, &types); - wasm_instance_exports(instance, &exports); - CHECK(types.size == exports.size, "export type/extern count mismatch"); - for(size_t i = 0; i < types.size; i++) - { - const wasm_name_t* nm = wasm_exporttype_name(types.data[i]); - std::string key(nm->data, nm->size); - // some wasm-c-api impls include the trailing NUL in name size - while(!key.empty() && key.back() == '\0') - key.pop_back(); - by_name[key] = exports.data[i]; - } - wasm_exporttype_vec_delete(&types); - } - - wasm_func_t* func(const char* name) - { - auto it = by_name.find(name); - return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second); - } - wasm_global_t* global(const char* name) - { - auto it = by_name.find(name); - return it == by_name.end() ? nullptr : wasm_extern_as_global(it->second); - } -}; - -static wasm_store_t* g_store = nullptr; - -static void report_trap(wasm_trap_t* trap, const char* what) -{ - if(!trap) - return; - wasm_message_t msg; - wasm_trap_message(trap, &msg); - FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data); -} - -static int32_t call_i32(Instance& inst, const char* name, std::vector argv = {}) -{ - wasm_func_t* f = inst.func(name); - CHECK(f, "missing export func %s", name); - wasm_val_t args_buf[4]; - CHECK(argv.size() <= 4, "call_i32 argv overflow for %s", name); - for(size_t i = 0; i < argv.size(); i++) - args_buf[i] = WASM_I32_VAL(argv[i]); - wasm_val_t results_buf[1] = { WASM_INIT_VAL }; - wasm_val_vec_t args = { argv.size(), args_buf }; - wasm_val_vec_t results = { 1, results_buf }; - size_t result_arity = wasm_func_result_arity(f); - wasm_val_vec_t no_results = WASM_EMPTY_VEC; - wasm_trap_t* trap = wasm_func_call(f, &args, result_arity ? &results : &no_results); - report_trap(trap, name); - return result_arity ? results_buf[0].of.i32 : 0; -} - -static wasm_global_t* make_i32_global(int32_t value, wasm_mutability_t mut) -{ - wasm_globaltype_t* gt = wasm_globaltype_new(wasm_valtype_new(WASM_I32), mut); - wasm_val_t val = WASM_I32_VAL(value); - wasm_global_t* g = wasm_global_new(g_store, gt, &val); - CHECK(g, "wasm_global_new failed (host-created globals unsupported?)"); - wasm_globaltype_delete(gt); - return g; -} - -int main(int argc, char** argv) -{ - const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase3/core.wasm"; - const char* unit_path = argc > 2 ? argv[2] : "/tmp/uce/wasm-phase3/page.wasm"; - g_context = make_context(); - - wasm_engine_t* engine = wasm_engine_new(); - CHECK(engine, "engine"); - g_store = wasm_store_new(engine); - CHECK(g_store, "store"); - g_store_for_stubs = g_store; - - // ---- 1. core module --------------------------------------------------- - std::vector core_bytes = read_file(core_path); - wasm_byte_vec_t core_bv; - wasm_byte_vec_new(&core_bv, core_bytes.size(), (const char*)core_bytes.data()); - Instance core; - core.module = wasm_module_new(g_store, &core_bv); - wasm_byte_vec_delete(&core_bv); - CHECK(core.module, "core module load failed"); - - // the shared funcref table is host-created (core links with --import-table) - // because WAMR cannot grow a table from the host: size it up front as - // core's declared minimum (= its own elem needs) plus headroom for unit - // module table regions. __table_base allocation bumps from the minimum. - wasm_table_t* table = nullptr; - uint32_t table_next_free = 0; - const uint32_t TABLE_HEADROOM = 2048; - - wasm_importtype_vec_t core_imports = WASM_EMPTY_VEC; - wasm_module_imports(core.module, &core_imports); - std::vector core_import_externs(core_imports.size); - for(size_t i = 0; i < core_imports.size; i++) - { - const wasm_name_t* mod = wasm_importtype_module(core_imports.data[i]); - const wasm_name_t* nm = wasm_importtype_name(core_imports.data[i]); - const wasm_externtype_t* et = wasm_importtype_type(core_imports.data[i]); - std::string name(nm->data, nm->size); - if(wasm_externtype_kind(et) == WASM_EXTERN_TABLE) - { - CHECK(name.rfind("__indirect_function_table", 0) == 0, - "unexpected core table import %s", name.c_str()); - const wasm_tabletype_t* tt = wasm_externtype_as_tabletype_const(et); - uint32_t core_min = wasm_tabletype_limits(tt)->min; - wasm_limits_t limits = { core_min + TABLE_HEADROOM, core_min + TABLE_HEADROOM }; - wasm_tabletype_t* host_tt = wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &limits); - table = wasm_table_new(g_store, host_tt, nullptr); - CHECK(table, "wasm_table_new failed (host-created tables unsupported?)"); - wasm_tabletype_delete(host_tt); - table_next_free = core_min; - printf("host table created: size=%u, core region=[0,%u)\n", core_min + TABLE_HEADROOM, core_min); - core_import_externs[i] = wasm_table_as_extern(table); - continue; - } - CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, - "core has unexpected non-func import %.*s.%s", - (int)mod->size, mod->data, name.c_str()); - const wasm_functype_t* ft = wasm_externtype_as_functype_const(et); - wasm_func_t* import_func = nullptr; - std::string mod_name(mod->data, mod->size); - while(!mod_name.empty() && mod_name.back() == '\0') mod_name.pop_back(); - while(!name.empty() && name.back() == '\0') name.pop_back(); - if(mod_name == "env" && name == "uce_host_ctx_read") - import_func = wasm_func_new_with_env(g_store, ft, host_ctx_read, nullptr, nullptr); - else if(mod_name == "env" && name == "uce_host_log") - import_func = wasm_func_new_with_env(g_store, ft, host_log, nullptr, nullptr); - else - { - // named trap stub; leaks the name string, fine for a spike - char* label = strdup((mod_name + "." + name).c_str()); - import_func = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr); - } - CHECK(import_func, "stub func creation failed for %s", name.c_str()); - core_import_externs[i] = wasm_func_as_extern(import_func); - } - CHECK(table, "core does not import __indirect_function_table — rebuild with --import-table"); - wasm_extern_vec_t core_iv = { core_import_externs.size(), core_import_externs.data() }; - wasm_trap_t* trap = nullptr; - core.instance = wasm_instance_new(g_store, core.module, &core_iv, &trap); - report_trap(trap, "core instantiation"); - CHECK(core.instance, "core instantiation failed"); - core.index_exports(); - printf("core instantiated: %zu exports\n", core.by_name.size()); - - // reactor init (runs ctors). Wasmtime does not auto-run _initialize; - // WAMR does — and calling it twice trips wasi-libc's double-init guard - // (__builtin_trap → "unreachable"), which cost us a debugging round. - if(core.func("_initialize")) - call_i32(core, "_initialize"); - - wasm_memory_t* memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr); - CHECK(memory, "core does not export memory"); - g_memory = memory; - - // ---- 2./3. dylink + base allocation ------------------------------------ - std::vector unit_bytes = read_file(unit_path); - DylinkInfo dl = parse_dylink(unit_bytes); - CHECK(dl.found, "unit has no dylink.0 mem_info"); - printf("dylink.0: memsize=%u memalign=2^%u tablesize=%u\n", dl.mem_size, dl.mem_align, dl.table_size); - - uint32_t align = 1u << dl.mem_align; - uint32_t raw = (uint32_t)call_i32(core, "malloc", { (int32_t)(dl.mem_size + align) }); - CHECK(raw, "core malloc returned 0"); - uint32_t memory_base = (raw + (align - 1)) & ~(align - 1); - - uint32_t table_base = table_next_free; - table_next_free += dl.table_size; - CHECK(table_next_free <= wasm_table_size(table), "table headroom exhausted"); - printf("bases: __memory_base=%u __table_base=%u (table size %u)\n", - memory_base, table_base, wasm_table_size(table)); - - // ---- 4. unit import resolution ----------------------------------------- - wasm_byte_vec_t unit_bv; - wasm_byte_vec_new(&unit_bv, unit_bytes.size(), (const char*)unit_bytes.data()); - Instance unit; - unit.module = wasm_module_new(g_store, &unit_bv); - wasm_byte_vec_delete(&unit_bv); - CHECK(unit.module, "unit module load failed"); - - wasm_importtype_vec_t unit_imports = WASM_EMPTY_VEC; - wasm_module_imports(unit.module, &unit_imports); - std::vector unit_import_externs(unit_imports.size); - // GOT.mem entries that must be self-resolved from the unit's own exports - // after instantiation (weak data the core does not define) - std::vector> deferred_got; - - for(size_t i = 0; i < unit_imports.size; i++) - { - const wasm_name_t* mod_n = wasm_importtype_module(unit_imports.data[i]); - const wasm_name_t* nm_n = wasm_importtype_name(unit_imports.data[i]); - std::string mod(mod_n->data, mod_n->size); - std::string nm(nm_n->data, nm_n->size); - const wasm_externtype_t* et = wasm_importtype_type(unit_imports.data[i]); - wasm_externkind_t kind = wasm_externtype_kind(et); - wasm_extern_t* resolved = nullptr; - - if(mod == "env" && nm == "memory") - resolved = wasm_memory_as_extern(memory); - else if(mod == "env" && nm == "__indirect_function_table") - resolved = wasm_table_as_extern(table); - else if(mod == "env" && nm == "__stack_pointer") - { - CHECK(core.global("__stack_pointer"), "core does not export __stack_pointer"); - resolved = core.by_name["__stack_pointer"]; - } - else if(mod == "env" && nm == "__memory_base") - resolved = wasm_global_as_extern(make_i32_global((int32_t)memory_base, WASM_CONST)); - else if(mod == "env" && nm == "__table_base") - resolved = wasm_global_as_extern(make_i32_global((int32_t)table_base, WASM_CONST)); - else if(mod == "env" && kind == WASM_EXTERN_FUNC) - { - auto it = core.by_name.find(nm); - CHECK(it != core.by_name.end(), "unresolved unit func import env.%s", nm.c_str()); - resolved = it->second; - } - else if(mod == "GOT.mem") - { - wasm_global_t* src = core.global(nm.c_str()); - if(src) - { - wasm_val_t v; - wasm_global_get(src, &v); - resolved = wasm_global_as_extern(make_i32_global(v.of.i32, WASM_VAR)); - } - else - { - // provisional 0; patched from the unit's own export post-instantiation - wasm_global_t* g = make_i32_global(0, WASM_VAR); - deferred_got.push_back({ nm, g }); - resolved = wasm_global_as_extern(g); - } - } - else if(mod == "GOT.func") - { - // resolved guest-side: the core exports a helper returning - // (intptr_t)&func, which on wasm is the function's table index — - // no host-side funcref injection needed (WAMR forbids it anyway) - std::string helper = "core_table_index_of_" + nm; - CHECK(core.func(helper.c_str()), "no GOT.func resolver %s in core", helper.c_str()); - int32_t slot = call_i32(core, helper.c_str()); - CHECK(slot > 0, "GOT.func resolver %s returned %d", helper.c_str(), slot); - printf("resolved GOT.func.%s = table[%d]\n", nm.c_str(), slot); - resolved = wasm_global_as_extern(make_i32_global(slot, WASM_VAR)); - } - CHECK(resolved, "unhandled unit import %s.%s (kind %d)", mod.c_str(), nm.c_str(), (int)kind); - unit_import_externs[i] = resolved; - } - - // ---- 5. instantiate unit, patch GOT, run init --------------------------- - wasm_extern_vec_t unit_iv = { unit_import_externs.size(), unit_import_externs.data() }; - trap = nullptr; - unit.instance = wasm_instance_new(g_store, unit.module, &unit_iv, &trap); - report_trap(trap, "unit instantiation"); - CHECK(unit.instance, "unit instantiation failed"); - unit.index_exports(); - printf("unit instantiated: %zu exports\n", unit.by_name.size()); - - for(auto& [nm, got] : deferred_got) - { - wasm_global_t* own = unit.global(nm.c_str()); - CHECK(own, "GOT.mem.%s defined neither by core nor by unit", nm.c_str()); - wasm_val_t v; - wasm_global_get(own, &v); - // dylink ABI: a PIC module's exported data symbols are offsets relative - // to its __memory_base; the linker must add the base when resolving - wasm_val_t nv = WASM_I32_VAL((int32_t)(memory_base + (uint32_t)v.of.i32)); - wasm_global_set(got, &nv); - printf("self-resolved GOT.mem.%s = %u (offset %d)\n", nm.c_str(), memory_base + (uint32_t)v.of.i32, v.of.i32); - } - - if(unit.func("__wasm_apply_data_relocs")) - call_i32(unit, "__wasm_apply_data_relocs"); - if(unit.func("__wasm_call_ctors")) - call_i32(unit, "__wasm_call_ctors"); - - // ---- 6. membrane prepare, render and read back --------------------------- - int32_t rc = call_i32(core, "uce_phase3_prepare"); - CHECK(rc == 0, "uce_phase3_prepare returned %d", rc); - int32_t request_ptr = call_i32(core, "uce_phase3_request"); - CHECK(request_ptr != 0, "core returned null Request*"); - call_i32(unit, "__uce_set_current_request", { request_ptr }); - call_i32(unit, "__uce_render", { request_ptr }); - call_i32(core, "uce_phase3_finish"); - - int32_t out_ptr = call_i32(core, "uce_phase3_output_data"); - int32_t out_len = call_i32(core, "uce_phase3_output_size"); - byte_t* mem = wasm_memory_data(memory); // may have moved if memory grew - - printf("---- phase3 unit output (%d bytes) ----\n%.*s\n--------------------------------------\n", - out_len, out_len, mem + out_ptr); - - bool ok = out_len > 0 && - memmem(mem + out_ptr, out_len, "PHASE3 PAGE OK", 14) && - memmem(mem + out_ptr, out_len, "host=phase3.example.test", 24) && - memmem(mem + out_ptr, out_len, "answer=42", 9) && - memmem(mem + out_ptr, out_len, "each=answer:42", 14) && - memmem(mem + out_ptr, out_len, "self-got=42", 11) && - memmem(mem + out_ptr, out_len, "map=wise", 8) && - memmem(mem + out_ptr, out_len, "callback=42", 11) && - memmem(mem + out_ptr, out_len, "got-func-ok", 11) && - memmem(mem + out_ptr, out_len, "fn=42", 5); - - printf(ok ? "PHASE3 EXIT CRITERION: PASS\n" : "PHASE3 EXIT CRITERION: FAIL (see output above)\n"); - return ok ? 0 : 1; -} diff --git a/spikes/wasm-phase3/page.uce b/spikes/wasm-phase3/page.uce deleted file mode 100644 index 457da9e..0000000 --- a/spikes/wasm-phase3/page.uce +++ /dev/null @@ -1,12 +0,0 @@ -// Phase 3 generated-unit fixture source. The checked-in generated/page.uce.cpp -// is the UCE-preprocessor-shaped output used by the wasm side-module build. - -RENDER(Request& context) -{ - <>
-

PHASE3 PAGE OK

-

host=

-

route=

-

answer=

-
-} diff --git a/spikes/wasm-phase4/README.md b/spikes/wasm-phase4/README.md deleted file mode 100644 index 1ce9511..0000000 --- a/spikes/wasm-phase4/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# spikes/wasm-phase4 — production mechanics kill-test spike - -Phase 4 validates the operational mechanics needed before a WASM worker can be -trusted in production. This spike is intentionally smaller than the future UCE -worker integration: it uses tiny WAT modules instead of generated UCE units, but -it exercises the Wasmtime controls and workspace cleanup paths the worker will -use. It is also deliberately the first spike on the Wasmtime-specific C++ API -(`wasmtime.hh`) — fuel, epochs, store limiters, and snapshots do not exist in -the portable `wasm.h` surface, so runtime-agnosticism ends here by design. - -Run on `k-uce`: - -```bash -bash spikes/wasm-phase4/build_runner.sh -/tmp/uce/wasm-phase4/runner -``` - -Expected final line: - -```text -PHASE4 EXIT CRITERION: PASS -``` - -What this proves: - -- **Reusable compiled artifact / snapshot proxy:** modules are compiled once and - then instantiated in fresh stores. Not OS CoW yet, but it validates the shape - of "shared core artifact + per-request workspace birth". -- **Unharmed worker:** after all six kill cases, the same engine serves a - healthy request that completes with the expected value. This — not merely - "didn't crash" — is the worker half of the Phase 4 exit criterion. -- **CPU limits, both mechanisms:** fuel (deterministic, per-instruction cost) - and epoch interruption (near-zero overhead, ticker thread, traps as - `interrupt`). Production default is **epoch** per the Phase 0 findings; fuel - is validated as the deterministic fallback. Note: the engine epoch only - advances, so every store must set its own deadline. -- **Memory limit, actually load-bearing:** the OOM fixture grows by 10 pages — - within its declared max (100) — so only the store limiter (2 pages) can deny - the growth. The denial is observed by the guest (`memory.grow` → -1) and - converted to a trap. -- **Trap-to-error-page data path:** every kill case's gate asserts the captured - message contains a `wasm backtrace` and the expected cause. Fixture frames - show `` — readable production traces require units to keep their - name section (or a symbolication side-file in the artifact cache). -- **Trace summarizer (`src/lib/wasm_trace.h`):** trap messages are rendered - through the production collapse facility (repeated frames → `×N` lines, - mangled symbols demangled, cause/detail split out). The stack-exhaustion - case gates it on live trap output here; `site/tests/core.uce` gates the - parsing on canned messages in the native suite. -- **Handle cleanup with falsifiable checks:** closers must run exactly once per - handle and *while the store is still alive* (production closers may flush - guest-resident state); destructor-only cleanup fails the ordering check. - -Kill fixtures (all genuinely trap): - -- `unreachable`: `__builtin_trap` analog. -- `oob-access`: wild pointer; load at 128 KiB from a 64 KiB memory. -- `stack-exhaustion`: runaway recursion → `call stack exhausted`. -- `infinite-loop-fuel` / `infinite-loop-epoch`: same loop, both CPU limits. -- `oom-limiter`: limiter-denied `memory.grow` → guest converts -1 to a trap. - -**A literal C++ null-pointer dereference is deliberately absent:** address 0 is -valid wasm linear memory, so `*(int*)nullptr` does not trap — it silently -writes inside the workspace, which is then dropped at request end (still -strictly better than a native SIGSEGV). See WASM-PROPOSAL §10 for the recorded -risk/policy. - -Still deferred to production Phase 4: - -- real core snapshot + OS-level CoW birth; -- wiring `wasm_trace.h` summaries into the UCE error-page UI (the summarizer - itself is done and gated); name-section policy for unit artifacts; -- wiring these traps into `linux_fastcgi.cpp` / the WASM worker backend; -- real runtime handle-table cleanup for sqlite/mysql/sockets/tasks; -- artifact ABI versioning end-to-end; -- memory-limit policy for guest allocators that return failure instead of - trapping (bump allocator vs dlmalloc behavior under the limiter). diff --git a/spikes/wasm-phase4/build_runner.sh b/spikes/wasm-phase4/build_runner.sh deleted file mode 100755 index 5f41037..0000000 --- a/spikes/wasm-phase4/build_runner.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# Phase 4: build the Wasmtime kill-test runner. -set -e -cd "$(dirname "$0")" - -OUT=/tmp/uce/wasm-phase4 -mkdir -p "$OUT" - -c++ -std=c++17 -O2 -pthread runner.cpp \ - -I/opt/wasmtime/include \ - -L/opt/wasmtime/lib \ - -Wl,-rpath,/opt/wasmtime/lib \ - -lwasmtime \ - -o "$OUT/runner" - -echo "built: $OUT/runner" diff --git a/spikes/wasm-phase4/runner.cpp b/spikes/wasm-phase4/runner.cpp deleted file mode 100644 index 5ef0ca0..0000000 --- a/spikes/wasm-phase4/runner.cpp +++ /dev/null @@ -1,298 +0,0 @@ -// WASM-PROPOSAL Phase 4 — production-mechanics kill-test spike. -// -// Uses Wasmtime's C++ API directly (this spike is deliberately where the -// portable wasm.h surface ends: fuel, epochs, store limiters, and snapshots -// are Wasmtime-specific by nature; the runtime was selected in Phase 0). -// -// Validates, per kill case, that: the guest trap is captured with a wasm -// backtrace, the workspace handle closers run exactly once and while the -// store is still alive, and — after all kills — the same engine still serves -// a healthy request (the actual meaning of "unharmed worker"). -// -// Note: a literal C++ null-pointer dereference does NOT trap in wasm — -// address 0 is valid linear memory; the damage stays inside the dropped -// workspace (see WASM-PROPOSAL §10). The kill fixtures below therefore use -// faults that genuinely trap: unreachable (__builtin_trap analog), an -// out-of-bounds access (wild pointer), stack exhaustion (runaway recursion), -// fuel/epoch exhaustion (infinite loop), and a limiter-denied memory.grow. - -#include - -#include -#include -#include -#include -#include - -#include "../../src/lib/wasm_trace.h" - -using namespace wasmtime; - -// Handle-table proxy. The checks here are deliberately falsifiable: closers -// must run exactly once per handle, and they must run while the store (and -// thus guest memory) is still alive — production closers may need to flush -// guest-resident state. A destructor-only cleanup fails the ordering check. -struct Workspace -{ - int handles_open = 0; - int handles_closed = 0; - bool cleanup_ran = false; - bool closed_while_store_alive = false; - - void open_handles(int count) - { - handles_open = count; - } - - void cleanup(bool store_alive) - { - if(cleanup_ran) - return; - cleanup_ran = true; - closed_while_store_alive = store_alive; - for(int i = 0; i < handles_open; i++) - handles_closed++; - } - - ~Workspace() - { - cleanup(false); - } - - bool ok() const - { - return(cleanup_ran && handles_closed == handles_open && closed_while_store_alive); - } -}; - -static Module compile_wat(Engine& engine, const std::string& wat) -{ - auto module = Module::compile(engine, wat); - if(!module) - { - std::cerr << "compile failed: " << module.err_ref().message() << "\n"; - exit(1); - } - return module.ok(); -} - -struct RunResult -{ - bool trapped = false; - std::string message; - bool cleanup_ok = false; - bool has_value = false; - int32_t value = 0; -}; - -static RunResult run_case(Engine& engine, const Module& module, uint64_t fuel, int64_t memory_limit, bool epoch_ticker = false) -{ - RunResult result; - Workspace workspace; - workspace.open_handles(2); - { - Store store(engine); - store.limiter(memory_limit, -1, -1, -1, -1); - Store::Context cx(store); - // epoch_interruption is enabled engine-wide and the engine epoch only - // advances; every store needs a deadline beyond the current epoch or - // it traps immediately. The epoch kill case gets a deadline of 1 tick - // and a ticker thread; everything else gets effectively-unbounded. - cx.set_epoch_deadline(epoch_ticker ? 1 : 1'000'000'000); - auto fuel_result = cx.set_fuel(fuel); - if(!fuel_result) - { - result.trapped = true; - result.message = fuel_result.err_ref().message(); - workspace.cleanup(true); - result.cleanup_ok = workspace.ok(); - return result; - } - - auto instance = Instance::create(cx, module, {}); - if(!instance) - { - result.trapped = true; - result.message = instance.err_ref().message(); - workspace.cleanup(true); - result.cleanup_ok = workspace.ok(); - return result; - } - - auto run_export = instance.ok_ref().get(cx, "run"); - if(!run_export || !std::get_if(&*run_export)) - { - result.trapped = true; - result.message = "missing run export"; - workspace.cleanup(true); - result.cleanup_ok = workspace.ok(); - return result; - } - - std::thread ticker; - if(epoch_ticker) - ticker = std::thread([&engine] { - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - engine.increment_epoch(); - }); - - Func run = *std::get_if(&*run_export); - auto call = run.call(cx, std::vector{}); - if(ticker.joinable()) - ticker.join(); - if(!call) - { - result.trapped = true; - result.message = call.err_ref().message(); - } - else - { - result.message = "completed without trap"; - auto values = call.ok(); - if(!values.empty()) - { - result.has_value = true; - result.value = values[0].i32(); - } - } - workspace.cleanup(true); - result.cleanup_ok = workspace.ok(); - } - return result; -} - -static void print_case(const char* label, const RunResult& result) -{ - std::cout << "--- " << label << " ---\n"; - std::cout << "trapped=" << (result.trapped ? "yes" : "no") << "\n"; - if(result.trapped) - std::cout << "summary:\n" << wasm_trace_collapse(result.message) << "\n"; - else - std::cout << "message=" << result.message << "\n"; - std::cout << "cleanup=" << (result.cleanup_ok ? "ok" : "failed") << "\n"; -} - -static void check_kill(const char* label, const RunResult& result, const char* expect_in_message) -{ - print_case(label, result); - bool has_trace = result.message.find("wasm backtrace") != std::string::npos; - bool has_cause = result.message.find(expect_in_message) != std::string::npos; - if(!result.trapped || !result.cleanup_ok || !has_trace || !has_cause) - { - std::cerr << "PHASE4 EXIT CRITERION: FAIL at " << label - << (result.trapped ? "" : " (no trap)") - << (result.cleanup_ok ? "" : " (cleanup)") - << (has_trace ? "" : " (no backtrace)") - << (has_cause ? "" : " (wrong cause)") << "\n"; - exit(1); - } -} - -static void check_healthy(const char* label, const RunResult& result) -{ - print_case(label, result); - if(result.trapped || !result.cleanup_ok || !result.has_value || result.value != 42) - { - std::cerr << "PHASE4 EXIT CRITERION: FAIL at " << label << " (worker harmed)\n"; - exit(1); - } -} - -int main() -{ - Config config; - config.consume_fuel(true); - config.epoch_interruption(true); - Engine engine(std::move(config)); - - // Compiled modules are reused across stores: a deliberately small proxy - // for the future core snapshot — per-request workspaces (stores) are - // born fresh while compilation work is shared. - - // __builtin_trap analog; a real C++ *nullptr does not trap (see header) - Module unreachable_module = compile_wat(engine, R"wat( - (module - (func (export "run") - unreachable)) - )wat"); - - // wild/OOB pointer: load at 128 KiB from a 64 KiB memory - Module oob_module = compile_wat(engine, R"wat( - (module - (memory 1 1) - (func (export "run") - (drop (i32.load (i32.const 131072))))) - )wat"); - - // runaway recursion → call stack exhausted - Module stack_module = compile_wat(engine, R"wat( - (module - (func $f (export "run") - (call $f))) - )wat"); - - Module loop_module = compile_wat(engine, R"wat( - (module - (func (export "run") - (loop br 0))) - )wat"); - - // grows by 10 pages: within the module's own declared max (100), so only - // the store limiter (2 pages) can deny it — this proves the limiter is - // load-bearing, not the declared max - Module oom_module = compile_wat(engine, R"wat( - (module - (memory 1 100) - (func (export "run") - i32.const 10 - memory.grow - i32.const -1 - i32.eq - (if (then unreachable)))) - )wat"); - - Module healthy_module = compile_wat(engine, R"wat( - (module - (func (export "run") (result i32) - i32.const 42)) - )wat"); - - const int64_t MEM_LIMIT = 2 * 65536; - const uint64_t FUEL = 10'000; - const uint64_t FUEL_PLENTY = 1'000'000'000'000; - - check_kill("unreachable", run_case(engine, unreachable_module, FUEL, MEM_LIMIT), "unreachable"); - check_kill("oob-access", run_case(engine, oob_module, FUEL, MEM_LIMIT), "out of bounds"); - - RunResult stack_result = run_case(engine, stack_module, FUEL_PLENTY, MEM_LIMIT); - check_kill("stack-exhaustion", stack_result, "call stack exhausted"); - - // the trace formatter (src/lib/wasm_trace.h) is part of the gate: the - // recursion frames (Wasmtime caps the displayed backtrace at 20) must - // collapse to a bounded summary - WasmTraceSummary stack_summary = wasm_trace_summarize(stack_result.message); - if(!stack_summary.parsed || stack_summary.total_frames < 10 - || stack_summary.frames.size() > 12 - || stack_summary.cause.find("call stack exhausted") == std::string::npos - || wasm_trace_format(stack_summary).find("×") == std::string::npos) - { - std::cerr << "PHASE4 EXIT CRITERION: FAIL at trace-collapse (parsed=" - << stack_summary.parsed << " total=" << stack_summary.total_frames - << " lines=" << stack_summary.frames.size() - << " cause=" << stack_summary.cause << ")\n"; - exit(1); - } - std::cout << "--- trace-collapse ---\n" << stack_summary.total_frames - << " raw frames -> " << stack_summary.frames.size() << " summary lines\n"; - check_kill("infinite-loop-fuel", run_case(engine, loop_module, FUEL, MEM_LIMIT), "fuel"); - // Wasmtime reports epoch-deadline traps as "interrupt" - check_kill("infinite-loop-epoch", run_case(engine, loop_module, FUEL_PLENTY, MEM_LIMIT, true), "interrupt"); - check_kill("oom-limiter", run_case(engine, oom_module, FUEL, MEM_LIMIT), "unreachable"); - - // after every kill above, the same engine must still serve a request — - // this is the "unharmed worker" half of the Phase 4 exit criterion - check_healthy("healthy-after-kills", run_case(engine, healthy_module, FUEL, MEM_LIMIT)); - - std::cout << "PHASE4 EXIT CRITERION: PASS\n"; - return 0; -} diff --git a/spikes/wasm-phase5/README.md b/spikes/wasm-phase5/README.md deleted file mode 100644 index 3574731..0000000 --- a/spikes/wasm-phase5/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# spikes/wasm-phase5 — parity, audit, and performance harness - -Phase 5's production exit requires the full network suite to pass on the WASM -worker and performance numbers to be published. The production worker is not in -this branch yet, so this spike builds the Phase 5 harness and records the native -baseline that the WASM worker must match. - -Run on `k-uce`: - -```bash -bash spikes/wasm-phase5/run_phase5.sh -``` - -Expected final line: - -```text -PHASE5 HARNESS: PASS -``` - -The harness first runs a throwaway warmup pass to populate the unit cache after -binary rebuilds. The warmup excludes the stateful `site tests tasks` case so it -cannot perturb the measured task-lifecycle check. The measured full-suite and -starter-subset passes then become the gate. - -Artifacts are written under `/tmp/uce/wasm-phase5/`: - -- `native-network-warmup.json` — throwaway warmup result; allowed to fail on cold - compile timeouts. -- `native-network.json` — measured full native network suite result. The harness - fails if fewer than 80 cases run, so an empty or broken suite cannot pass. -- `native-starter.json` — measured starter-focused parity subset. The harness - fails if fewer than 10 cases run, which protects against a vacuous - `--match starter` exit gate. -- `site-static-audit.{json,md}` — candidate cross-request/static-state risks in - `site/` for the §3.2 semantic change audit. By default it scans code-like - `.uce` and `.h` files only; pass `--include-doc-text` to include `.txt` docs - as `documentation` severity findings. -- `benchmark.{json,md}` — warmed native baseline for the three Phase 5 budget - pages. The default is 20 samples per target; use a higher `--samples` value for - noisy shared-host gate runs if needed: - - `template-heavy-doc`: `/doc/singlepage.uce` - - `sqlite-page`: `/demo/sqlite.uce` - - `component-heavy-starter`: `/examples/uce-starter/?dashboard` - -`benchmark.py` also accepts `--wasm-base-url` when native and WASM are served -from separate endpoints. For the W5 in-place cutover, use -`--backend-label wasm --compare-native-json ` after -switching `/etc/uce/settings.cfg` to the WASM backend; this compares WASM -medians against the Phase 5 budget of ≤2× native page latency. W5 worker builds -emit `X-UCE-Wasm-Workspace-Birth-Us` and component resolve probe headers on -WASM-served responses. - -A durable informational baseline snapshot is kept in `reports/`. Paired -native/WASM runs still recompute native medians for the actual budget decision. - -Current scope: - -- Native parity and baseline collection are automated. -- WASM parity/performance comparison is ready but blocked on the production WASM - worker endpoint. -- The static-state audit is heuristic and intentionally conservative; findings - must be reviewed by a human before migration work is scheduled. diff --git a/spikes/wasm-phase5/run_phase5.sh b/spikes/wasm-phase5/run_phase5.sh deleted file mode 100755 index 0b8432a..0000000 --- a/spikes/wasm-phase5/run_phase5.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -# Phase 5 harness: native parity, site static audit, and native perf baseline. -# Run on k-uce from repo root. -set -euo pipefail -cd "$(dirname "$0")/../.." - -OUT=/tmp/uce/wasm-phase5 -mkdir -p "$OUT" - -# The network suite can show transient cold-compile timeouts immediately after a -# binary rebuild. Run a throwaway pass first so the measured gate is a warmed -# parity signal rather than a compiler-cache signal. The warmup is allowed to -# fail; the measured pass below is not. -python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/native-network-warmup.json" || true - -network_status=0 -starter_status=0 -benchmark_status=0 -python3 tests/run_network_tests.py --include-internal --json-report "$OUT/native-network.json" || network_status=$? -python3 tests/run_network_tests.py --include-internal --match starter --json-report "$OUT/native-starter.json" || starter_status=$? -python3 spikes/wasm-phase5/audit_site_statics.py --out-dir "$OUT" -python3 spikes/wasm-phase5/benchmark.py --out-dir "$OUT" || benchmark_status=$? - -NETWORK_STATUS=$network_status STARTER_STATUS=$starter_status BENCHMARK_STATUS=$benchmark_status python3 - <<'PY' -import json -import os -from pathlib import Path - -MIN_NETWORK_CASES = 80 -MIN_STARTER_CASES = 10 -MIN_BENCHMARKS = 3 -out = Path('/tmp/uce/wasm-phase5') -network = json.loads((out / 'native-network.json').read_text()) -starter = json.loads((out / 'native-starter.json').read_text()) -bench = json.loads((out / 'benchmark.json').read_text()) -failures = [r for r in network if not r['ok']] + [r for r in starter if not r['ok']] + [r for r in bench if not r['ok']] -structural_failures = [] -if len(network) < MIN_NETWORK_CASES: - structural_failures.append(f"network suite ran {len(network)} cases, expected at least {MIN_NETWORK_CASES}") -if len(starter) < MIN_STARTER_CASES: - structural_failures.append(f"starter subset ran {len(starter)} cases, expected at least {MIN_STARTER_CASES}") -if len(bench) < MIN_BENCHMARKS: - structural_failures.append(f"benchmark ran {len(bench)} rows, expected at least {MIN_BENCHMARKS}") -nonzero = { - 'network_status': int(os.environ['NETWORK_STATUS']), - 'starter_status': int(os.environ['STARTER_STATUS']), - 'benchmark_status': int(os.environ['BENCHMARK_STATUS']), -} -for name, status in nonzero.items(): - if status != 0: - structural_failures.append(f"{name} exited {status}") -if failures or structural_failures: - print('PHASE5 HARNESS: FAIL') - for failure in structural_failures: - print(failure) - for failure in failures: - print(failure) - raise SystemExit(1) -print('PHASE5 HARNESS: PASS') -print(f"network_cases={len(network)} starter_cases={len(starter)} benchmarks={len(bench)}") -print(f"reports={out}") -PY diff --git a/src/lib/_scratchpad.cpp b/src/lib/_scratchpad.cpp deleted file mode 100644 index a7dfd15..0000000 --- a/src/lib/_scratchpad.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#define NO_GLOBAL_ARENA_ALLOCATOR - -struct MemoryArena { - - u8* data; - u64 size = 0; - u64 capacity = 0; - String name = "unnamed"; - - MemoryArena(u64 cap, String _name = "unnamed") - { - name = _name; - capacity = cap; - printf("(i) memory arena '%s' created with capacity of %llu bytes\n", name.c_str(), capacity); - data = (u8*)malloc(cap); - } - - ~MemoryArena() - { - free(data); - } - - void clear() - { - #ifdef DEBUG_MEMORY - printf("(i) memory arena '%s' cleared after high mark of %llu bytes\n", name.c_str(), size); - #endif - size = 0; - } - - void* get(u64 size_needed) - { - u64 size_aligned = 8 + (8 * ((size_needed) / 8)); - u8* result = data + size; - if(size_aligned + size >= capacity) - { - printf("(!) memory arena '%s' capacity (%llu) exceeded %llu/%llu + %llu >= %llu\n", - name.c_str(), capacity, size_needed, size_aligned, size, capacity); - return(0); - } - size += size_aligned; - #ifdef DEBUG_MEMORY_DETAILED - printf("(i) memory arena '%s' [+%llu]:%p alloc %llu/%llu bytes\n", name.c_str(), size, result, size_needed, size_aligned); - #endif - return(result); - } - -}; - -MemoryArena* current_memory_arena = 0; - -void switch_to_system_alloc() -{ -#ifdef GLOBAL_ARENA_ALLOCATOR - current_memory_arena = 0; -#endif -} - -void switch_to_arena(MemoryArena* a) -{ -#ifdef GLOBAL_ARENA_ALLOCATOR - current_memory_arena = a; -#endif -} - -#ifdef GLOBAL_ARENA_ALLOCATOR -void * operator new(decltype(sizeof(0)) n) noexcept(false) -{ - if(current_memory_arena) - { - return(current_memory_arena->get(n)); - } - else - { - return(malloc(n)); - } -} - -void operator delete(void * p) throw() -{ - if(current_memory_arena) - { - - } - else - { - free(p); - } -} -#endif diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index 880a632..77a9bac 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -1,4 +1,5 @@ #ifdef __UCE_WASM_CORE__ +#include #include "types.h" #include "functionlib.h" #include "sys.h" @@ -16,6 +17,10 @@ int uce_host_file_exists(const char* path, size_t path_len, const char* current, size_t uce_host_file_read(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap); int uce_host_file_write(const char* path, size_t path_len, const char* current, size_t current_len, const char* content, size_t content_len, int append); void uce_host_file_unlink(const char* path, size_t path_len, const char* current, size_t current_len); +int uce_host_task_spawn(const char* key, size_t key_len, uint64_t callback_id, double interval, uint64_t timeout, int repeat); +int uce_host_task_pid(const char* key, size_t key_len); +int uce_host_task_kill(int pid, int sig); +unsigned int uce_host_sleep_us(uint64_t usec); } static String wasm_current_unit_file() @@ -82,9 +87,55 @@ f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(conte bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); } f64 time_precise() { return(uce_host_time_precise()); } u64 time() { return(uce_host_time()); } -String time_format_local(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); } -String time_format_utc(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); } -String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) { (void)format_very_recent; (void)medium_recency_seconds; (void)format_medium_recent; (void)not_recent_seconds; (void)format_not_recent; return(std::to_string(timestamp)); } +// The native build shells out to `date`; the wasm core has no shell, so it +// formats with wasi-libc strftime (no TZ data → local == UTC, acceptable). +static String wasm_time_strftime(String format, u64 timestamp, bool utc) +{ + if(timestamp == 0) + timestamp = time(); + time_t t = (time_t)timestamp; + struct tm tmv; + if(utc) + gmtime_r(&t, &tmv); + else + localtime_r(&t, &tmv); + char buffer[512]; + size_t n = strftime(buffer, sizeof(buffer), format.c_str(), &tmv); + return(String(buffer, n)); +} +String time_format_local(String format, u64 timestamp) { return(wasm_time_strftime(format, timestamp, false)); } +String time_format_utc(String format, u64 timestamp) +{ + if(format == "RFC1123") + format = "%a, %d %b %Y %T GMT"; + return(wasm_time_strftime(format, timestamp, true)); +} +static String wasm_time_expand_delta(String format, u64 timestamp, u64 now_timestamp) +{ + u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0; + format = replace(format, "%deltaY", std::to_string(delta_seconds / (60 * 60 * 24 * 365))); + format = replace(format, "%deltam", std::to_string(delta_seconds / (60 * 60 * 24 * 30))); + format = replace(format, "%deltad", std::to_string(delta_seconds / (60 * 60 * 24))); + format = replace(format, "%deltaH", std::to_string(delta_seconds / (60 * 60))); + format = replace(format, "%deltaM", std::to_string(delta_seconds / 60)); + format = replace(format, "%deltaS", std::to_string(delta_seconds)); + return(format); +} +String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) +{ + u64 now_timestamp = time(); + u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0; + format_very_recent = first(format_very_recent, "just now"); + medium_recency_seconds = medium_recency_seconds > 0 ? medium_recency_seconds : 90; + format_medium_recent = first(format_medium_recent, "%deltaM minutes ago"); + not_recent_seconds = not_recent_seconds > 0 ? not_recent_seconds : 90 * 60; + format_not_recent = first(format_not_recent, "%deltaH hours ago"); + if(delta_seconds < medium_recency_seconds) + return(wasm_time_expand_delta(format_very_recent, timestamp, now_timestamp)); + if(delta_seconds < not_recent_seconds) + return(wasm_time_expand_delta(format_medium_recent, timestamp, now_timestamp)); + return(wasm_time_expand_delta(format_not_recent, timestamp, now_timestamp)); +} u64 time_parse(String time_String) { char* end = 0; unsigned long long v = strtoull(time_String.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : 0); } u64 socket_connect(String host, short port) { (void)host; (void)port; return(0); } void socket_close(u64 sockfd) { (void)sockfd; } @@ -112,11 +163,38 @@ bool memcache_delete(u64 connection, String key) { (void)connection; (void)key; String memcache_get(u64 connection, String key, String default_value) { (void)connection; (void)key; return(default_value); } StringMap memcache_get_multiple(u64 connection, StringList keys) { (void)connection; (void)keys; return(StringMap()); } void on_segfault(int sig) { (void)sig; } -int task_kill(pid_t pid, int sig) { (void)pid; (void)sig; return(-1); } + +static u64 wasm_next_task_callback_id = 1; +static std::map> wasm_task_callbacks; + +extern "C" int uce_wasm_task_run(uint64_t callback_id) +{ + auto it = wasm_task_callbacks.find(callback_id); + if(it == wasm_task_callbacks.end()) + return(1); + it->second(); + return(0); +} + +int task_kill(pid_t pid, int sig) { return(uce_host_task_kill(pid, sig)); } String runtime_safe_key(String key, String label) { (void)label; return(key); } -pid_t task(String key, std::function exec_after_spawn, u64 timeout) { (void)key; (void)exec_after_spawn; (void)timeout; return(0); } -pid_t task_repeat(String key, f64 interval, std::function exec_after_spawn, u64 timeout) { (void)key; (void)interval; (void)exec_after_spawn; (void)timeout; return(0); } -pid_t task_pid(String key) { (void)key; return(0); } +pid_t task(String key, std::function exec_after_spawn, u64 timeout) +{ + u64 id = wasm_next_task_callback_id++; + wasm_task_callbacks[id] = exec_after_spawn; + return((pid_t)uce_host_task_spawn(key.data(), key.size(), id, 0.0, timeout, 0)); +} +pid_t task_repeat(String key, f64 interval, std::function exec_after_spawn, u64 timeout) +{ + if(!(interval > 0) || !std::isfinite(interval)) + return(0); + u64 id = wasm_next_task_callback_id++; + wasm_task_callbacks[id] = exec_after_spawn; + return((pid_t)uce_host_task_spawn(key.data(), key.size(), id, interval, timeout, 1)); +} +pid_t task_pid(String key) { return((pid_t)uce_host_task_pid(key.data(), key.size())); } +extern "C" unsigned int sleep(unsigned int seconds) { return(uce_host_sleep_us((uint64_t)seconds * 1000000ull)); } +extern "C" int usleep(unsigned int usec) { uce_host_sleep_us(usec); return(0); } pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_filename, String call_function) { (void)key; (void)socket_fn_or_port; (void)call_uce_filename; (void)call_function; return(0); } bool server_stop(String key) { (void)key; return(false); } StringMap default_config() diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index 7253d1a..12dca80 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -117,13 +117,14 @@ static bool wasm_backend_native_fallback_uncached(Request* context, const String // - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in), // - unit_render()/component() (host resolver). // What remains is genuinely host-owned / native-only for now: - // - zip, sqlite, background tasks, filesystem writes, sleeps; + // - zip, sockets/custom servers, memcache, mysql; // - unit_call + compiler/unit introspection (need the native toolchain). + // Background tasks and sleep/usleep are host-owned but now have membrane + // hostcalls, so they intentionally do not fallback here. StringList native_only_tokens = { - "zip_", "task(", "task_repeat(", - "task_pid(", "task_kill(", "unit_call(", - "unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(", - "usleep(", "sleep(" + "zip_", "socket_", "server_start_http(", "server_stop(", + "memcache_", "mysql_", "unit_call(", + "unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(" }; for(auto& token : native_only_tokens) if(source.find(token) != String::npos) diff --git a/src/wasm/core_hostcalls.syms b/src/wasm/core_hostcalls.syms index 1c9d579..2540d2c 100644 --- a/src/wasm/core_hostcalls.syms +++ b/src/wasm/core_hostcalls.syms @@ -3,6 +3,10 @@ uce_host_time_precise uce_host_env uce_host_log uce_host_random +uce_host_task_spawn +uce_host_task_pid +uce_host_task_kill +uce_host_sleep_us uce_host_component_resolve uce_host_file_exists uce_host_file_read diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 3001ed6..0b07f50 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -352,6 +353,27 @@ public: } #endif + // The guest calls a sized hostcall twice (buf=0 to learn the length, then + // to fetch). For side-effecting ops (sqlite) re-executing on the fetch is + // wrong, so the result is staged on the first call (keyed on the exact + // input bytes) and replayed on the second without re-running the op. + String staged_hostcall_input; + String staged_hostcall_result; + bool hostcall_staged(const String& input, String& out) + { + if(!staged_hostcall_input.empty() && input == staged_hostcall_input) + { + out = staged_hostcall_result; + return(true); + } + return(false); + } + void hostcall_stage(const String& input, const String& out) + { + staged_hostcall_input = input; + staged_hostcall_result = out; + } + // resolve-kind values shared with the guest core (src/wasm/core.cpp) // must match WasmResolveKind in src/wasm/core.cpp enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2, RESOLVE_ONCE = 3 }; @@ -1054,6 +1076,17 @@ private: return((int32_t)slot); } + String run_task_callback(u64 callback_id) + { + auto runner = core_func("uce_wasm_task_run"); + if(!runner) + return("core does not export uce_wasm_task_run"); + auto result = runner->call(ctx(), { wasmtime::Val((int64_t)callback_id) }); + if(!result) + return(trap_text(result.err())); + return(""); + } + // ---- host imports for the core ----------------------------------------- wasmtime::Extern make_host_import(wasmtime::Store::Context cx, const String& mod, const String& name, const wasmtime::FuncType& func_type) @@ -1168,52 +1201,58 @@ private: // handle table (handle = 1-based index). String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); - DValue request, response, decode_err_unused; - String decode_error; - if(ucb_decode(encoded, request, &decode_error)) + String out; + // run the op once across the length-query + fetch pair + if(!self->hostcall_staged(encoded, out)) { - String op = request["op"].to_string(); - if(op == "connect") + DValue request, response; + String decode_error; + if(ucb_decode(encoded, request, &decode_error)) { - SQLite* db = new SQLite(); - db->connect(request["path"].to_string()); - u64 handle = 0; - if(db->connection) + String op = request["op"].to_string(); + if(op == "connect") { - self->sqlite_handles.push_back(db); - handle = self->sqlite_handles.size(); - } - response["handle"] = (f64)handle; - response["error_code"] = (f64)db->error_code; - response["statement_info"] = db->error(); - if(handle == 0) - delete db; - } - else - { - u64 handle = request["handle"].to_u64(); - SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size()) - ? self->sqlite_handles[(size_t)handle - 1] : 0; - if(op == "query" && db) - { - StringMap params; - DValue* p = request.key("params"); - if(p) - p->each([&](const DValue& value, String key) { params[key] = value.to_string(); }); - response["result"] = db->query(request["query"].to_string(), params); - response["insert_id"] = (f64)db->insert_id; - response["affected"] = (f64)db->affected_rows; + SQLite* db = new SQLite(); + db->connect(request["path"].to_string()); + u64 handle = 0; + if(db->connection) + { + self->sqlite_handles.push_back(db); + handle = self->sqlite_handles.size(); + } + response["handle"] = (f64)handle; response["error_code"] = (f64)db->error_code; response["statement_info"] = db->error(); + if(handle == 0) + delete db; } - else if(op == "disconnect" && db) + else { - delete db; - self->sqlite_handles[(size_t)handle - 1] = 0; + u64 handle = request["handle"].to_u64(); + SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size()) + ? self->sqlite_handles[(size_t)handle - 1] : 0; + if(op == "query" && db) + { + StringMap params; + DValue* p = request.key("params"); + if(p) + p->each([&](const DValue& value, String key) { params[key] = value.to_string(); }); + response["result"] = db->query(request["query"].to_string(), params); + response["insert_id"] = (f64)db->insert_id; + response["affected"] = (f64)db->affected_rows; + response["error_code"] = (f64)db->error_code; + response["statement_info"] = db->error(); + } + else if(op == "disconnect" && db) + { + delete db; + self->sqlite_handles[(size_t)handle - 1] = 0; + } } } + out = ucb_encode(response); + self->hostcall_stage(encoded, out); } - String out = ucb_encode(response); u32 cap = (u32)args[3].i32(); int32_t buf = args[2].i32(); if(buf != 0 && cap >= out.size()) @@ -1232,6 +1271,70 @@ private: ::unlink(resolved.c_str()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_task_spawn") + return(add([self](Caller, Span args, Span results) -> Result { + String key; + self->hostcall_read(args[0].i32(), args[1].i32(), key); + u64 callback_id = (u64)args[2].i64(); + f64 interval = args[3].f64(); + u64 timeout = (u64)args[4].i64(); + bool repeat = args[5].i32() != 0; + auto run_callback = [self, callback_id]() { + String error = self->run_task_callback(callback_id); + if(error != "") + fprintf(stderr, "[wasm task] callback failed: %s\n", error.c_str()); + }; + pid_t pid = 0; + try + { + if(!repeat || (interval > 0 && std::isfinite(interval))) + pid = repeat + ? ::task_repeat(key, interval, run_callback, timeout) + : ::task(key, run_callback, timeout); + } + catch(const std::exception& e) + { + fprintf(stderr, "[wasm task] spawn failed for key '%s': %s\n", key.c_str(), e.what()); + } + catch(...) + { + fprintf(stderr, "[wasm task] spawn failed for key '%s'\n", key.c_str()); + } + results[0] = Val((int32_t)pid); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_task_pid") + return(add([self](Caller, Span args, Span results) -> Result { + String key; + self->hostcall_read(args[0].i32(), args[1].i32(), key); + results[0] = Val((int32_t)::task_pid(key)); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_task_kill") + return(add([](Caller, Span args, Span results) -> Result { + results[0] = Val((int32_t)::task_kill((pid_t)args[0].i32(), args[1].i32())); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_sleep_us") + return(add([self](Caller caller, Span args, Span results) -> Result { + u64 usec = (u64)args[0].i64(); + while(usec >= 1000000ull) + { + unsigned int remaining = ::sleep((unsigned int)(usec / 1000000ull)); + if(remaining != 0) + { + caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); + results[0] = Val((int32_t)remaining); + return(std::monostate()); + } + usec %= 1000000ull; + } + if(usec > 0) + ::usleep((useconds_t)usec); + caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); + results[0] = Val((int32_t)0); + return(std::monostate()); + })); if(mod == "env" && name == "uce_host_regex") return(add([self](Caller, Span args, Span results) -> Result { // {op,pattern,subject,flags,replacement} in (UCEB1) → result out. diff --git a/spikes/wasm-phase5/benchmark.py b/tests/wasm_benchmark.py similarity index 100% rename from spikes/wasm-phase5/benchmark.py rename to tests/wasm_benchmark.py diff --git a/spikes/wasm-phase5/audit_site_statics.py b/tests/wasm_site_audit.py similarity index 99% rename from spikes/wasm-phase5/audit_site_statics.py rename to tests/wasm_site_audit.py index e903501..9392a06 100755 --- a/spikes/wasm-phase5/audit_site_statics.py +++ b/tests/wasm_site_audit.py @@ -7,7 +7,7 @@ import json from dataclasses import asdict, dataclass from pathlib import Path -ROOT = Path(__file__).resolve().parents[2] +ROOT = Path(__file__).resolve().parents[1] SITE = ROOT / "site" PATTERNS = [