Compare commits

...
4 Commits
Author SHA1 Message Date
udo c84fc86e6c W6 2026-06-13 16:19:52 +00:00
udo 5a56d4f39e W5 2026-06-13 15:10:42 +00:00
udo afaa4dd7c0 feat: cut over to WASM backend 2026-06-13 08:42:31 +00:00
udo 577aae076e W3 2026-06-13 02:07:38 +00:00
73 changed files with 4205 additions and 3093 deletions
+335 -8
View File
@@ -1,10 +1,21 @@
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
- **Status:** design guide; Phase 04 spikes complete and gated (2026-06-12).
Next: production implementation, starting with the Phase 3 work plan
(item 1, real `uce_lib` core compile); the starter parity bar
(`tests/run_network_tests.py --match starter`, 14 cases) is already green
against the native backend.
- **Status:** building the production worker per §9.1 (W-phases). W1 (core
module), W2 (wasm unit compile target), W3 (workspace runtime + membrane,
`src/wasm/worker.cpp`), and W4 (config-selectable FastCGI backend,
`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.
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 ~67× 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,
@@ -424,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
@@ -517,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):
@@ -614,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
@@ -630,6 +641,322 @@ The native `.so` backend remains in-tree (as a reference) and selectable by conf
but we switch over to the wasm backend as soon as it's available and test
only on that; both backends share the Phase 1 C ABI.
**Cutover & cleanup checklist (gated, in order — no step before the gate
above it is green).**
1. **Build** — production Phase 3/4 work plan items 17: real `uce_lib`
`core.wasm`, WASI decision recorded, generator changes, production loader
in `src/`, starter-scoped hostcalls, `component_resolve` + lazy dispatch,
wasm worker backend selectable by config in `linux_fastcgi.cpp`.
2. **Prove** — the Phase 5 harness against the wasm worker URL: full network
suite green (≥ 80 cases), starter parity (≥ 10), benchmarks within the
≤2× paired-run budget; the Phase 4 kill-tests reproduced against the real
worker (OOB page, stack-exhaustion page, infinite loop, OOM) rendering
clean error pages from `wasm_trace.h` summaries.
3. **Switch** — config default flips to the wasm backend; the native backend
stays in-tree as reference per the paragraph above (archival is a separate,
later decision, not part of cutover).
4. **Clean up** — only after 3, and gates must be re-homed before their
spikes are deleted: the Phase 5 harness moves from `spikes/` into `tests/`,
the Phase 4 kill cases become worker integration tests, and the Phase 0
FINDINGS/erratum content folds into `docs/` — then `spikes/wasm-phase*`
can go. Native-only machinery that cutover obsoletes is retired in the
same pass: the SIGSEGV `sigsetjmp`/`siglongjmp` recovery, the tracking
`operator new` in `types.h`, the per-connector `cleanup_*_connections()`
pattern (§3.1/§3.2 supersede all three).
Status against this checklist (2026-06-12): W1 has landed enough `src/` /
`scripts/` integration to build and smoke-test `core.wasm`; the remaining gate
1 work is W2W4 plus the residual W1 wasi-libc import closure noted below. The
native backend is still the only server backend and serves the entire green
suite. The single cleanup item safe today is `src/lib/_scratchpad.cpp` (the
failed arena experiment, unreferenced by any build since 2022; §1 cites it as
history only).
### 9.1 Production build plan — the worker (W-phases)
The spike sequence above is closed; every architectural risk it could retire
is retired. The W-phases build the production worker. Ground rules: the
`.uce → C++` preprocessor does not change; every phase lands production code
in `src/`/`scripts/` gated by the existing network suite; no further
throwaway scaffolding. Dependency order is W1 → W3 → W4 → W5 → W6, with W2
parallelizable once W1's shim headers and ABI stamp exist.
**W1 — the core module, for real.**
Carve `uce_lib` so it compiles as `core.wasm` with the Phase 0 recipe:
gate the global allocator in `types.h` behind an `#ifdef` (core owns it,
units import it — replacing the spike's copied-header text patch); `#ifdef
__wasm__` carve-outs in `sys.h` (signals/fork/exec/sockets move behind
hostcalls); split the connectors — `mysql-connector`/`sqlite-connector`
keep their `.uce`-visible signatures but forward to membrane hostcalls in
the wasm build while native implementations stay host-side. Zero-WASI core
(record the decision in §11): time/random/env are `uce_host_*` hostcalls,
no `wasi_snapshot_preview1` imports. The core exports the full DValue C ABI
(§5.2), a name→funcptr symbol registry for `GOT.func` (replacing per-symbol
helpers), `uce_alloc`/`uce_free`, and output plumbing.
`scripts/build_core_wasm.sh` joins the normal build.
Exit: `core.wasm` builds reproducibly from the real `uce_lib`; a small
smoke driver instantiates it, runs `_initialize`, and exercises `uce_dv_*`;
the native build and suite remain untouched and green.
> **Status: DONE (2026-06-12).** `scripts/build_core_wasm.sh` builds
> `src/wasm/core.cpp` into `core.wasm` from the real `uce_lib` carve-out. The
> W1 carve-out keeps native builds unchanged while `__UCE_WASM_CORE__` removes
> native-only compiler/connector code from the core, provides WASM stubs for
> process/socket/task/file surfaces, gates generated-unit allocator definitions
> behind `__UCE_WASM_UNIT__`, and leaves the workspace-owned DValue C ABI in the
> core. `scripts/wasm/build_w1_smoke.sh` builds `src/wasm/w1_smoke.cpp`; the
> smoke driver instantiates `core.wasm`, runs `_initialize`, initializes the UCE
> request context, exercises `uce_dv_root/get/find/set_value/value/count/is_list`
> plus UCEB1 encode/decode, verifies output plumbing, and passes with
> `W1 EXIT CRITERION: PASS`. Native validation after the W1 carve-out: rebuilt
> and restarted `uce.service`; warm full network suite passed `83/83`. Remaining
> W1 follow-up before W3: close the residual wasi-libc/libc++
> `wasi_snapshot_preview1.*` imports in the produced binary; UCE's own time/env
> calls are routed through `uce_host_*`, but the smoke driver still supplies
> trap stubs for unused libc WASI imports.
**W2 — the compiler outputs wasm units.**
Preprocessor output unchanged. The unit compile path in `compiler.cpp`
gains the wasm target beside the `.so` target: `clang
--target=wasm32-wasip1 -fPIC` + `wasm-ld -shared` (Phase 0 unit recipe),
logical `uce_lib.h` include instead of the absolute path, a `uce.abi`
custom-section stamp (ABI version + toolchain id), and per-unit `.wasm`
artifacts in the same cache with the same invalidation as `.so`.
Exit: a batch compile of every unit the suite touches produces valid PIC
modules — `dylink.0` present, `uce.abi` stamped, no allocator definitions,
import shapes verified by a check tool — and compile-on-miss works for the
wasm target.
> **Status: DONE (2026-06-12).** Generated units now include the logical
> `#include "uce_lib.h"`; native `scripts/compile` supplies `-Isrc/lib`, and
> `scripts/compile_wasm_unit` builds PIC side modules with `__UCE_WASM_UNIT__`,
> `wasm-ld -shared --experimental-pic`, and an `llvm-objcopy`-inserted
> `uce.abi` custom section. `scripts/wasm/check_unit_wasm.py` validates wasm v1
> structure, `dylink.0` mem_info, `uce.abi` ABI/toolchain stamp, import policy,
> required PIC imports, and absence of allocator definitions. The compiler can
> optionally build the wasm artifact beside the native `.so` via
> `COMPILE_WASM_UNITS=1`; native remains default. Batch validation built/checked
> all 128 known/generated suite units, and a temporary `unit_compile()`
> compile-on-miss page produced and verified a fresh `.wasm`. Reused-artifact
> batch validation was tightened after the first slow full rebuild: unchanged
> units are rechecked instead of rebuilt, reducing a 128-unit no-op pass from
> several minutes to about 5 seconds. `scripts/compile_wasm_unit` now also uses
> a keyed Clang PCH for the stable `uce_lib.h` unit header by default
> (`UCE_WASM_UNIT_PCH=0` disables it); the key includes ABI version, clang
> version, common compile flags, and `src/lib/*.h` content. Spot checks: warm
> `hello.uce` wasm compile dropped from about 2.3s to 0.65s, `core.uce` from
> about 5.7s to 4.1s, and a full 128-unit rebuild with PCH took about 163s.
> Native validation stayed green (`83/83`).
**W3 — workspace runtime + membrane (the worker core).**
Productionize the loader into `src/wasm/` per §6, from the Phase 3 spike
plus everything it deferred: symbol registry, dispatch map, ABI stamp
verification, import discipline (reject units defining the allocator),
the `__memory_base`-relative data-export rule, multi-unit placement,
hardened binary parsing, an explicit export name-collision policy.
Workspace lifecycle: core snapshot born by memcpy (CoW is W5), dropped per
request; host handle table with closers. Starter-scoped hostcall set
(~12, the §5.1 subset): `ctx_read`, `respond`, `stream_write`, `log`,
`time`, `random`, `env`, `session_get`/`set`, `http_request`,
`component_resolve`, `last_error`.
Exit: a real request — UCEB1 context in, render, response out — served
end-to-end through a workspace running the real core and real generated
units, driven by a CLI test driver; epoch CPU limit and memory limiter
active; traps produce `wasm_trace.h` summaries.
> **Status: DONE (2026-06-13).** `src/wasm/worker.cpp` is the production
> workspace runtime (wasmtime.hh; per-request store, hardened dylink/uce.abi
> parsing, import discipline, GOT.func via host funcref placement, the
> `__memory_base` data-export rule, core-first/first-unit-wins symbol
> registry, per-worker compiled-module cache) and `src/wasm/w3_driver.cpp`
> is the CLI gate. Membrane so far: time/time_precise/env/random/log,
> `component_resolve` (lazy mid-request loading), and a policy-gated
> read-only file membrane (`file_exists`/`file_read`, current-unit-relative,
> site-tree-contained). The exit gate passed on k-uce: `/demo/hello.uce` and
> `/demo/components.uce` render **byte-identical to native** (incl. nested
> and named `COMPONENT:X` handlers through `ob_*` capture); starter
> dashboard/gauges/features/workspace/page1 all render `200` with ~12 units
> lazily loaded mid-request (≈700 ms first request — dominated by per-request
> Wasmtime module compilation, the W5 AOT/snapshot target — ≈2 ms warm);
> epoch kill mid-render traps as `interrupt` with a symbolicated
> `wasm_trace` summary and a clean workspace drop. Carve findings recorded
> on the way: header function templates must be `inline` (self-import rule,
> now commented in `types.h`/`functionlib.h`), the core needs vague-linkage
> and libc link anchors (`uce_wasm_link_anchors()` +
> `core_libc_exports.syms`), units build `-fno-rtti`/`-fno-exceptions` to
> match the core ABI, and connector classes have explicit fail-clean stubs
> until the W5 hostcall connectors. Deferred to W4 as planned: `respond`/
> `stream_write`/`session`/`http_request` hostcalls (response metadata
> currently returns as UCEB1 via `uce_wasm_response_meta`), kill-pages, and
> the FastCGI backend; `error-reporting.uce` and `tests/zip.uce` are
> native-only pending the trap error path and a zip hostcall.
**W4 — the FastCGI worker.**
Wire W3 into `linux_fastcgi.cpp` as a config-selectable backend (native
stays default until W5): the §7 lifecycle, lazy mid-request
`component_resolve` → load → `call_indirect`, path dispatch, trap →
configured UCE error pages with collapsed guest traces, handle-table
cleanup on workspace drop.
Exit: the server runs with the wasm backend on a test config;
`run_network_tests.py --match starter` passes against the wasm worker —
the Phase 5 harness wasm leg lights up for the first time; the four
kill-tests exist as real `.uce` pages and produce clean error pages from
an unharmed worker.
> **Status: DONE (2026-06-13).** `src/wasm/backend.cpp` wires the W3 runtime
> into `src/linux_fastcgi.cpp` as a config-selectable page-render backend
> (`WASM_BACKEND_ENABLED`, default off — native stays default until W5). The
> seam is one branch in `handle_complete`: per forked worker, a lazily-built
> `WasmWorker` + epoch ticker thread; per request the native `Request`
> params/get/post/cookies/session are encoded to the UCEB1 context, served
> through a fresh workspace, and the response (status/headers/cookies/session
> /body) is written back onto the native `Request` so the existing transport
> emits it unchanged. CLI/serve_http/websocket stay native; units with no
> wasm artifact fall through to native automatically. `ONCE()` is now honored
> in the workspace (host resolves `__uce_once`, core dedups on the resolved
> path via `once_units`; the entry renders through `uce_wasm_render_entry` so
> it shares the component dispatch + ONCE path).
>
> **Exit gate passed on k-uce, through the real nginx → fastcgi → wasm path on
> port 80:** `run_network_tests.py --match starter` is **14/14** against the
> wasm backend (incl. the two ONCE-asset-in-`<head>` cases); three real kill
> pages under `site/tests/wasm-kill/` (OOB write, runaway loop, unbounded
> recursion) each return a clean error page carrying a demangled
> `wasm_trace` summary (`out of bounds memory access` / epoch `interrupt`
> with `wasm_kill_recurse(unsigned long long)` framing), and the worker keeps
> serving `200`s through a barrage of kills — no native signal, the trap is a
> returned error at the membrane. CPU budget is enforced by epoch
> (`WASM_EPOCH_DEADLINE_TICKS` × `WASM_EPOCH_PERIOD_MS`); memory by the store
> limiter. Native default restored after the gate: full suite **83/83**.
> Robustness folded in along the way: the ctype libc family added to the core
> export anchors (`core_libc_exports.syms`), and a page with no `RENDER`
> renders empty-200 (native parity).
>
> **W5 surface, made concrete** (23 full-suite pages still 500 on the wasm
> backend, all by design): the regex/xml/yaml core stubs to un-stub or move
> behind hostcalls; markdown/zip/tasks/sqlite/file-write membrane hostcalls;
> the `unit_call` bridge; and compiler-introspection pages (`unit-info`,
> `unit-browser`, `sharedunit`) which likely stay native. These are the W5
> parity workload, not regressions.
**W5 — parity, performance, cutover.**
Full network suite green on the wasm backend; §3.2 statics-audit findings
(50 code candidates) fixed as differential native-vs-wasm runs surface
them; CoW snapshot birth and placement memoization as needed to meet the
budgets (≤2× page latency against the pinned baseline shape, workspace
birth ≤100µs, component-call overhead ≤10×) with worker-internal probes
for the latter two.
Exit: cutover checklist gates 23 — harness fully green against the wasm
worker URL, config default flips to wasm, native stays in-tree as
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 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.
>
> **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`/
> `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); **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
> `__cxa_demangle`, so a unit must not import it). Two backend robustness bugs
> fixed: a failed wasm side-compile (e.g. try/catch units) is now non-fatal —
> the native `.so` still serves and the stale `.wasm` is removed — and
> `wasm_artifact_exists` now requires the artifact be newer than its source, so
> a source edit can't be served from a stale `.wasm` (the wasm path bypasses the
> native JIT recompile).
>
> **Signal-trap conflict resolved (this was open in the prior W5 draft):** a
> guest `unreachable`/OOB used to surface as a host signal that the native
> SIGILL/SIGSEGV handler caught and `abort()`'d the worker (502). `make_engine`
> now sets `signals_based_traps(false)`, so guest traps are explicit and stay
> pure wasm traps returned as errors. The `__builtin_trap()` kill page now
> returns a clean 500 and the worker survives a kill barrage (0 PID churn); it
> is back in the kill-test gate alongside loop/recurse.
>
> `scripts/wasm/run_w5.sh` is the cutover gate: measure native, switch
> `/etc/uce/settings.cfg` to wasm, run the wasm full suite + kill tests +
> 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, 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`,
> `X-UCE-Wasm-Workspace-Birth-Us`, component resolve count/total/avg) are emitted
> for wasm responses when `WASM_BACKEND_VERBOSE=1`; warm workspace birth is
> ~300380µs on k-uce (above the aspirational 100µs CoW target — that and
> placement memoization are the remaining perf items — but page latency meets the
> cutover budget). Live `/etc/uce/settings.cfg` left with `WASM_BACKEND_ENABLED=1`;
> native remains in-tree and config-selectable.
>
> **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
(Phase 5 harness → `tests/`, kill cases → worker tests, FINDINGS/erratum →
`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-04 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
+13
View File
@@ -21,6 +21,19 @@ SITE_DIRECTORY=site
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
JIT_COMPILE_ON_REQUEST=1
# OPTIONAL W2 WEBASSEMBLY SIDE-MODULE COMPILATION BESIDE NATIVE .so UNITS
COMPILE_WASM_UNITS=0
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
# DEFAULT PAGE RENDER BACKEND. W5 defaults to the WASM backend with explicit
# native fallbacks for host-owned surfaces that are not membrane APIs yet.
WASM_BACKEND_ENABLED=1
WASM_BACKEND_VERBOSE=0
WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm
WASM_MEMORY_LIMIT_BYTES=536870912
WASM_EPOCH_DEADLINE_TICKS=200
WASM_EPOCH_PERIOD_MS=50
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
PROACTIVE_COMPILE_ENABLED=1
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# Build the production W1 UCE WASM core from the real runtime carve-out.
# Run on k-uce from any working directory.
set -euo pipefail
cd "$(dirname "$0")/.."
SDK=${WASI_SDK:-/opt/wasi-sdk}
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
mkdir -p "$OUT" bin/wasm
if [ ! -x "$SDK/bin/clang++" ]; then
echo "wasi-sdk clang++ not found; set WASI_SDK" >&2
exit 1
fi
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
-O1 -g -std=c++20 -fno-exceptions -fno-rtti \
-D__UCE_WASM_CORE__ \
-I. -Isrc/lib \
src/wasm/core.cpp -o "$OUT/core.wasm" \
-Wl,--export-all \
-Wl,--export=__heap_base \
-Wl,--export=__stack_pointer \
-Wl,--import-table \
-Wl,-z,stack-size=8388608 \
-Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \
-Wl,--no-entry \
$(sed "s/^/-Wl,--export-if-defined=/" src/wasm/core_libc_exports.syms | tr "\n" " ")
cp "$OUT/core.wasm" bin/wasm/core.wasm
ls -lh "$OUT/core.wasm" bin/wasm/core.wasm
+7 -2
View File
@@ -14,8 +14,13 @@ mkdir work > /dev/null 2>&1
COMPILER="clang++"
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`"
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
# Wasmtime C API for the W4 wasm backend (src/wasm/backend.cpp).
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
WASM_FLAGS="-I$WASMTIME_HOME/include"
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs` $WASM_LIBS"
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\" $WASM_FLAGS"
echo "Compiling SQLite..."
clang -g -O2 -fPIC \
+1 -1
View File
@@ -26,7 +26,7 @@ OPT_FLAG="O0"
COMPILER="clang++"
#COMPILER="g++"
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC"
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC -Isrc/lib"
LIBS="-ldl -lm -lpthread"
SRCFLAGS="-D PLATFORM_NAME=\"linux\""
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# Compile a preprocessed UCE unit into a PIC WebAssembly side module.
set -euo pipefail
cd "$(dirname "$0")"
cd ..
SRC_DIR="$1"
DEST_DIR="$2"
SRC_FN="$3"
PP_FN="$4"
WASM_FN="$5"
SDK=${WASI_SDK:-/opt/wasi-sdk}
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
ROOT=$(pwd)
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
COMMON_FLAGS=(
--target=wasm32-wasip1
-fPIC -fvisibility=default -fvisibility-inlines-hidden
-O1 -g -std=c++20
# -w as in scripts/compile: warnings are not failures. The server captures
# this script's output and treats any non-empty result as a compile failure
# (then drops the .wasm), so a successful build must be silent.
-w
# must match the core build ABI: units with RTTI/EH enabled import
# typeinfo/unwind symbols the -fno-rtti/-fno-exceptions core cannot provide
-fno-exceptions -fno-rtti
-D__UCE_WASM_UNIT__
-DPLATFORM_NAME=\"wasm32-wasip1\"
)
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ]; then
echo "wasi-sdk tools not found; set WASI_SDK" >&2
exit 1
fi
TOOLCHAIN_ID=$(${SDK}/bin/clang++ --version | head -n 1)
HEADER_HASH=$(find src/lib -maxdepth 1 -name '*.h' -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | cut -c1-16)
FLAGS_HASH=$(printf '%s\0' "${COMMON_FLAGS[@]}" -Isrc/lib | sha1sum | cut -c1-16)
PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH" "$FLAGS_HASH" | sha1sum | cut -c1-16)
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
mkdir -p "$DEST_DIR" >/dev/null 2>&1
build_pch_if_needed() {
if [ "$PCH_ENABLED" = "0" ]; then
return 0
fi
mkdir -p "$PCH_DIR"
if [ -s "$PCH_FN" ]; then
return 0
fi
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
-Isrc/lib \
-x c++-header src/lib/uce_lib.h -o "$PCH_FN.tmp"
mv "$PCH_FN.tmp" "$PCH_FN"
}
cat > "$ABI_TMP" <<EOF
format=uce-wasm-unit-abi-v1
unit_abi_version=$ABI_VERSION
toolchain=$TOOLCHAIN_ID
source=$SRC_FN
EOF
build_pch_if_needed
PCH_FLAGS=()
if [ "$PCH_ENABLED" != "0" ]; then
PCH_FLAGS=(-include-pch "$PCH_FN")
fi
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
-I"$SRC_DIR" -I"$ROOT/src/lib" \
"${PCH_FLAGS[@]}" \
-c "$DEST_DIR/$PP_FN" -o "$OBJ_FN"
"$SDK/bin/wasm-ld" -shared --experimental-pic \
--unresolved-symbols=import-dynamic \
--Bsymbolic \
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
--export-if-defined=__uce_set_current_request \
--export-if-defined=__uce_render \
--export-if-defined=__uce_component \
--export-if-defined=__uce_websocket \
--export-if-defined=__uce_cli \
--export-if-defined=__uce_serve_http \
--export-if-defined=__uce_once \
--export-if-defined=__uce_init
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
python3 scripts/wasm/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
rm -f "$OBJ_FN" "$ABI_TMP"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Build the W1 host smoke driver. Run on k-uce.
set -euo pipefail
cd "$(dirname "$0")/../.."
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
WASMTIME_INCLUDE=${WASMTIME_INCLUDE:-$WASMTIME_HOME/include}
WASMTIME_LIB=${WASMTIME_LIB:-$WASMTIME_HOME/lib}
mkdir -p "$OUT"
if [ ! -d "$WASMTIME_INCLUDE" ] || [ ! -d "$WASMTIME_LIB" ]; then
echo "Wasmtime C API not found; set WASMTIME_HOME or WASMTIME_INCLUDE/WASMTIME_LIB" >&2
exit 1
fi
g++ -std=c++17 -O2 -Wall -Wextra \
-I"$WASMTIME_INCLUDE" \
src/wasm/w1_smoke.cpp \
-L"$WASMTIME_LIB" \
-Wl,-rpath,"$WASMTIME_LIB" \
-lwasmtime \
-o "$OUT/w1_smoke"
ls -lh "$OUT/w1_smoke"
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# Batch-build W2 wasm side modules for already-known/generated UCE units.
set -euo pipefail
cd "$(dirname "$0")/../.."
BIN_DIR=${UCE_BIN_DIRECTORY:-/tmp/uce/work}
KNOWN_FILE=${UCE_KNOWN_UNITS_FILE:-$BIN_DIR/known-uce-files.txt}
MIN_UNITS=${UCE_W2_MIN_UNITS:-1}
if [ "$#" -gt 0 ]; then
UNITS=("$@")
else
if [ ! -f "$KNOWN_FILE" ]; then
echo "known unit registry not found: $KNOWN_FILE" >&2
exit 1
fi
mapfile -t UNITS < <(grep -v '^[[:space:]]*$' "$KNOWN_FILE")
fi
count=0
checked=0
skipped=0
# Native-only units that cannot be wasm side modules (yet).
# - error-reporting.uce deliberately throws to exercise the native exception
# path; the wasm backend replaces that machinery with traps (§11.1).
# - tests/zip.uce uses try/catch around the zip library, which is carved out
# of the wasm core until it moves behind a hostcall (W4+ membrane work).
SKIP_PATTERN=${UCE_W2_SKIP:-(error-reporting|tests/zip)\.uce$}
for unit in "${UNITS[@]}"; do
case "$unit" in
*.uce|*.ws.uce) ;;
*) continue ;;
esac
if [[ "$unit" =~ $SKIP_PATTERN ]]; then
continue
fi
src_dir=$(dirname "$unit")
base=$(basename "$unit")
dest_dir="$BIN_DIR$src_dir"
pp_fn="$base.cpp"
wasm_fn="$base.wasm"
if [ ! -f "$dest_dir/$pp_fn" ]; then
echo "missing preprocessed unit: $dest_dir/$pp_fn" >&2
exit 1
fi
if [ -s "$dest_dir/$wasm_fn" ] && [ "$dest_dir/$wasm_fn" -nt "$dest_dir/$pp_fn" ] && [ "$dest_dir/$wasm_fn" -nt "$unit" ]; then
scripts/wasm/check_unit_wasm.py "$dest_dir/$wasm_fn"
skipped=$((skipped + 1))
else
scripts/compile_wasm_unit "$src_dir" "$dest_dir" "$unit" "$pp_fn" "$wasm_fn"
count=$((count + 1))
fi
checked=$((checked + 1))
done
if [ "$checked" -lt "$MIN_UNITS" ]; then
echo "checked only $checked wasm units, expected at least $MIN_UNITS" >&2
exit 1
fi
echo "W2 batch wasm units: checked=$checked compiled=$count reused=$skipped"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Build the W3 workspace-runtime CLI driver. Run on k-uce.
set -euo pipefail
cd "$(dirname "$0")/../.."
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w3}
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
mkdir -p "$OUT"
g++ -std=c++20 -O1 -g -w \
-Isrc/lib -Isrc/wasm \
-I"$WASMTIME_HOME/include" \
src/wasm/w3_driver.cpp \
-L"$WASMTIME_HOME/lib" \
-Wl,-rpath,"$WASMTIME_HOME/lib" \
-lwasmtime -lpcre2-8 -lpthread -ldl \
-o "$OUT/w3_driver"
ls -lh "$OUT/w3_driver"
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""Validate a W2 UCE PIC unit wasm artifact.
Checks intentionally stay small and explicit: section walk for dylink.0,
uce.abi, imports/exports, plus llvm-nm for allocator definitions.
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
def read_u32leb(data: bytes, pos: int) -> tuple[int, int]:
result = 0
shift = 0
while True:
if pos >= len(data):
raise ValueError("truncated leb128")
b = data[pos]
pos += 1
result |= (b & 0x7F) << shift
if (b & 0x80) == 0:
return result, pos
shift += 7
if shift > 35:
raise ValueError("oversized leb128")
def read_name(data: bytes, pos: int) -> tuple[str, int]:
n, pos = read_u32leb(data, pos)
end = pos + n
if end > len(data):
raise ValueError("truncated name")
return data[pos:end].decode("utf-8", "replace"), end
def walk_sections(data: bytes):
if not data.startswith(b"\0asm\x01\0\0\0"):
raise ValueError("not a wasm v1 module")
pos = 8
while pos < len(data):
section_id = data[pos]
pos += 1
size, pos = read_u32leb(data, pos)
end = pos + size
if end > len(data):
raise ValueError("section extends past EOF")
payload = data[pos:end]
yield section_id, payload
pos = end
def parse_imports(payload: bytes):
pos = 0
count, pos = read_u32leb(payload, pos)
imports = []
for _ in range(count):
module, pos = read_name(payload, pos)
name, pos = read_name(payload, pos)
if pos >= len(payload):
raise ValueError("truncated import kind")
kind = payload[pos]
pos += 1
# Skip type descriptors. We only need module/name/kind for W2 policy.
if kind == 0: # func type index
_, pos = read_u32leb(payload, pos)
elif kind == 1: # table
if pos >= len(payload): raise ValueError("truncated table import")
pos += 1
flags, pos = read_u32leb(payload, pos)
_, pos = read_u32leb(payload, pos)
if flags & 1: _, pos = read_u32leb(payload, pos)
elif kind == 2: # memory
flags, pos = read_u32leb(payload, pos)
_, pos = read_u32leb(payload, pos)
if flags & 1: _, pos = read_u32leb(payload, pos)
elif kind == 3: # global
pos += 2
else:
raise ValueError(f"unknown import kind {kind}")
imports.append((module, name, kind))
return imports
def parse_exports(payload: bytes):
pos = 0
count, pos = read_u32leb(payload, pos)
exports = []
for _ in range(count):
name, pos = read_name(payload, pos)
if pos >= len(payload):
raise ValueError("truncated export kind")
kind = payload[pos]
pos += 1
_, pos = read_u32leb(payload, pos)
exports.append((name, kind))
return exports
def collect(path: Path):
data = path.read_bytes()
customs: dict[str, list[bytes]] = {}
imports = []
exports = []
for section_id, payload in walk_sections(data):
if section_id == 0:
name, pos = read_name(payload, 0)
customs.setdefault(name, []).append(payload[pos:])
elif section_id == 2:
imports = parse_imports(payload)
elif section_id == 7:
exports = parse_exports(payload)
return customs, imports, exports
def dylink_has_valid_mem_info(payload: bytes) -> bool:
pos = 0
while pos < len(payload):
subsection_id = payload[pos]
pos += 1
size, pos = read_u32leb(payload, pos)
end = pos + size
if end > len(payload):
raise ValueError("dylink.0 subsection extends past section")
if subsection_id == 1:
mem_size, p = read_u32leb(payload, pos)
mem_align, p = read_u32leb(payload, p)
table_size, p = read_u32leb(payload, p)
table_align, p = read_u32leb(payload, p)
if p > end:
raise ValueError("truncated dylink.0 mem_info")
return mem_align < 32 and table_align < 32 and mem_size < (1 << 31) and table_size < (1 << 31)
pos = end
return False
def defined_symbols(path: Path, llvm_nm: str) -> list[str]:
proc = subprocess.run([llvm_nm, "--defined-only", str(path)], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "llvm-nm failed")
symbols = []
for line in proc.stdout.splitlines():
parts = line.split()
if parts:
symbols.append(parts[-1])
return symbols
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("wasm", type=Path)
ap.add_argument("--abi-version", default="6")
ap.add_argument("--llvm-nm", default=None)
ap.add_argument("--verbose", action="store_true")
args = ap.parse_args()
try:
customs, imports, exports = collect(args.wasm)
errors = []
dylink_payloads = customs.get("dylink.0", [])
if not dylink_payloads:
errors.append("missing dylink.0 custom section")
elif not any(dylink_has_valid_mem_info(payload) for payload in dylink_payloads):
errors.append("dylink.0 missing valid mem_info subsection")
abi_payloads = customs.get("uce.abi", [])
if not abi_payloads:
errors.append("missing uce.abi custom section")
else:
abi_text = abi_payloads[-1].decode("utf-8", "replace")
required = ["format=uce-wasm-unit-abi-v1", f"unit_abi_version={args.abi_version}", "toolchain="]
for needle in required:
if needle not in abi_text:
errors.append(f"uce.abi missing {needle!r}")
export_names = {name for name, _ in exports}
forbidden_exports = {"uce_alloc", "uce_free"}
for name in sorted(export_names & forbidden_exports):
errors.append(f"forbidden allocator export {name}")
import_map = {(module, name): kind for module, name, kind in imports}
required_imports = {
("env", "memory"): 2,
("env", "__memory_base"): 3,
}
# units without indirect calls / stack spills / table needs
# legitimately omit these; if present, the kind must be right
optional_imports = {
("env", "__indirect_function_table"): 1,
("env", "__stack_pointer"): 3,
("env", "__table_base"): 3,
}
for key, kind in required_imports.items():
if import_map.get(key) != kind:
errors.append(f"missing required import {key[0]}.{key[1]}")
for key, kind in optional_imports.items():
if key in import_map and import_map[key] != kind:
errors.append(f"wrong kind for import {key[0]}.{key[1]}")
for module, name, kind in imports:
if module.startswith("wasi_") or module == "wasi_snapshot_preview1":
errors.append(f"forbidden WASI import {module}.{name}")
if module not in {"env", "GOT.mem", "GOT.func"} and not module.startswith("GOT."):
errors.append(f"unexpected import module {module}.{name}")
if module.startswith("GOT.") and kind != 3:
errors.append(f"GOT import is not a global: {module}.{name}")
llvm_nm = args.llvm_nm or shutil.which("llvm-nm") or "/opt/wasi-sdk/bin/llvm-nm"
if Path(llvm_nm).exists():
bad_prefixes = ("_Znwm", "_Znam", "_ZdlPv", "_ZdaPv", "_ZdlPvm", "_ZdaPvm")
for sym in defined_symbols(args.wasm, llvm_nm):
if sym in {"uce_alloc", "uce_free"} or sym.startswith(bad_prefixes):
errors.append(f"forbidden allocator definition {sym}")
else:
errors.append("llvm-nm not found; cannot verify allocator definitions")
if errors:
for e in errors:
print(f"ERROR: {e}", file=sys.stderr)
return 1
if args.verbose:
print(f"UCE W2 unit check PASS: {args.wasm}")
return 0
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
# W5 parity/performance gate for the config-selectable WASM backend.
# Runs on k-uce. Requires root because the live service reads /etc/uce/settings.cfg.
set -euo pipefail
cd "$(dirname "$0")/../.."
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
echo "run_w5.sh must run as root so it can switch /etc/uce/settings.cfg and restart uce.service" >&2
exit 1
fi
OUT=${UCE_W5_OUT:-/tmp/uce/wasm-w5}
CONFIG=${UCE_CONFIG:-/etc/uce/settings.cfg}
mkdir -p "$OUT"
BACKUP="$OUT/settings.cfg.before-w5"
cp "$CONFIG" "$BACKUP"
restore_on_error() {
if [ "${UCE_W5_KEEP_BACKEND:-0}" != "1" ]; then
cp "$BACKUP" "$CONFIG"
systemctl restart uce.service >/dev/null 2>&1 || true
fi
}
trap restore_on_error EXIT
set_backend() {
local enabled="$1"
python3 - "$CONFIG" "$enabled" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
enabled = sys.argv[2]
s = path.read_text()
lines = s.splitlines()
found = False
for i, line in enumerate(lines):
if line.startswith('WASM_BACKEND_ENABLED='):
lines[i] = f'WASM_BACKEND_ENABLED={enabled}'
found = True
if not found:
lines.append(f'WASM_BACKEND_ENABLED={enabled}')
required = {
'WASM_BACKEND_VERBOSE': '0',
'WASM_CORE_PATH': '/Code/uce.openfu.com/uce/bin/wasm/core.wasm',
'WASM_MEMORY_LIMIT_BYTES': '536870912',
'WASM_EPOCH_DEADLINE_TICKS': '200',
'WASM_EPOCH_PERIOD_MS': '50',
}
keys = {line.split('=', 1)[0] for line in lines if '=' in line}
for key, value in required.items():
if key not in keys:
lines.append(f'{key}={value}')
path.write_text('\n'.join(lines) + '\n')
PY
systemctl restart uce.service >/dev/null
sleep 1
}
summarize_json() {
python3 - "$1" <<'PY'
import json, sys
rows = json.load(open(sys.argv[1]))
print(f"{sys.argv[1]}: {sum(1 for r in rows if r.get('ok'))}/{len(rows)}")
PY
}
# Native reference baseline.
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 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 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 tests/wasm_site_audit.py --out-dir "$OUT" >/dev/null
python3 - "$OUT" <<'PY'
import json, sys
from pathlib import Path
out = Path(sys.argv[1])
network = json.loads((out / 'wasm-network.json').read_text())
starter = json.loads((out / 'wasm-starter.json').read_text())
bench = json.loads((out / 'benchmark' / 'benchmark.json').read_text())
failures = [r for r in network if not r.get('ok')] + [r for r in starter if not r.get('ok')] + [r for r in bench if not r.get('ok')]
structural = []
if len(network) < 80: structural.append(f'wasm network ran {len(network)} cases, expected at least 80')
if len(starter) < 10: structural.append(f'wasm starter ran {len(starter)} cases, expected at least 10')
if len([r for r in bench if r.get('backend') == 'wasm']) < 3: structural.append('wasm benchmark rows < 3')
if failures or structural:
print('W5 HARNESS: FAIL')
for item in structural: print(item)
for item in failures: print(item)
raise SystemExit(1)
print('W5 HARNESS: PASS')
print(f'network_cases={len(network)} starter_cases={len(starter)} benchmark_rows={len(bench)}')
print(f'reports={out}')
PY
if [ "${UCE_W5_KEEP_BACKEND:-0}" = "1" ]; then
trap - EXIT
printf 'WASM backend left enabled in %s\n' "$CONFIG"
else
cp "$BACKUP" "$CONFIG"
systemctl restart uce.service >/dev/null
trap - EXIT
fi
+1 -1
View File
@@ -32,7 +32,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
## Pipeline
- The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`.
- The generated file starts by including the logical runtime header `uce_lib.h`; native and WASM compile scripts provide the include path.
- It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`.
- It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file.
- Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content.
+4 -1
View File
@@ -244,7 +244,10 @@ RENDER(Request& context)
trace_msg += "\nCaused by:\n wasm trap: call stack exhausted\n";
WasmTraceSummary trace_summary = wasm_trace_summarize(trace_msg);
String trace_text = wasm_trace_format(trace_summary);
check("wasm_trace_summarize() collapse + demangle", trace_summary.parsed && trace_summary.total_frames == 41 && trace_summary.cause == "wasm trap: call stack exhausted" && trace_summary.frames.size() == 2 && contains(trace_text, "×40") && contains(trace_text, "recursion()"), trace_text);
// Demangling is a host-only capability (wasi-libc++ has no __cxa_demangle),
// so accept either the demangled name (native) or the raw symbol (wasm).
bool trace_symbol_ok = contains(trace_text, "recursion()") || contains(trace_text, "_Z9recursionv");
check("wasm_trace_summarize() collapse", trace_summary.parsed && trace_summary.total_frames == 41 && trace_summary.cause == "wasm trap: call stack exhausted" && trace_summary.frames.size() == 2 && contains(trace_text, "×40") && trace_symbol_ok, trace_text);
String fault_msg = "error while executing at wasm backtrace:\n 0: 0x2a - <unknown>!<wasm function 0>\n\nCaused by:\n 0: memory fault at wasm address 0x20000 in linear memory of size 0x10000\n 1: wasm trap: out of bounds memory access\n";
WasmTraceSummary fault_summary = wasm_trace_summarize(fault_msg);
+10
View File
@@ -0,0 +1,10 @@
// W4 kill-test: runaway CPU. Epoch interruption traps it at the configured
// deadline; the worker stays healthy.
RENDER(Request& context)
{
print("about to loop\n");
volatile u64 i = 0;
while(i >= 0)
i++;
print("unreachable\n");
}
+8
View File
@@ -0,0 +1,8 @@
// W4/W5 kill-test: explicit guest trap. Native must not run this page; the
// wasm backend converts it into a clean UCE error page and keeps serving.
RENDER(Request& context)
{
print("about to trap\n");
__builtin_trap();
print("unreachable\n");
}
+14
View File
@@ -0,0 +1,14 @@
// W4 kill-test: stack exhaustion via unbounded recursion. Traps as
// "call stack exhausted"; the workspace drops cleanly.
u64 wasm_kill_recurse(volatile u64 depth)
{
volatile u64 next = depth + 1;
return(next + wasm_kill_recurse(next));
}
RENDER(Request& context)
{
print("about to recurse\n");
volatile u64 sink = wasm_kill_recurse(0);
print("unreachable ", sink, "\n");
}
-30
View File
@@ -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)
-17
View File
@@ -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"
-51
View File
@@ -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"
-74
View File
@@ -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 <string>
#include <functional>
#include <cstdio>
#include <cstring>
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<int(int)>* 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<intptr_t>(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(); }
}
-446
View File
@@ -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 <wasm.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#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<uint8_t> 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<uint8_t> 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<uint8_t>& 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<std::string, wasm_extern_t*> 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<int32_t> 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<uint8_t> 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<wasm_extern_t*> 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<uint8_t> 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<wasm_extern_t*> 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<std::pair<std::string, wasm_global_t*>> 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;
}
-165
View File
@@ -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 `<signal.h>` 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 `<signal.h>` 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.
-73
View File
@@ -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 <string>
#include <functional>
#include <map>
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<int(int)>* 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<std::string, int> 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(int)>([](int v) { return(v * 3); });
core_invoke_function(fn, 14);
delete fn;
core_counter++;
}
-95
View File
@@ -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)
-51
View File
@@ -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.
-16
View File
@@ -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"
-21
View File
@@ -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"
-142
View File
@@ -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 <cstdlib>
#include <cstring>
#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());
}
}
-2
View File
@@ -1,2 +0,0 @@
uce_host_ctx_read
uce_host_log
-280
View File
@@ -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 <wasm.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <vector>
#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<uint8_t> 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<uint8_t> 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<std::pair<std::string, Node>> 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<wasm_valkind_t> params, std::vector<wasm_valkind_t> 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<std::string, wasm_extern_t*> 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<uint8_t> 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<wasm_extern_t*> externs(imports.size);
std::vector<wasm_func_t*> 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;
}
-12
View File
@@ -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");
}
-63
View File
@@ -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.
-16
View File
@@ -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"
-70
View File
@@ -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"
-188
View File
@@ -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 <cstdlib>
#include <cstring>
#include <functional>
#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<int(int)>* f, int value)
{
String out = "<p>fn=" + std::to_string((*f)(value)) + "</p>\n";
phase3_core_print(out.data(), out.size());
}
extern "C" intptr_t core_table_index_of_phase3_core_print()
{
return(reinterpret_cast<intptr_t>(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 += "&amp;"; break;
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '"': result += "&quot;"; break;
case '\'': result += "&#39;"; 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());
}
}
-6
View File
@@ -1,6 +0,0 @@
extern int phase3_unit_state;
extern "C" int phase3_helper_state_plus(int value)
{
return(phase3_unit_state + value);
}
-72
View File
@@ -1,72 +0,0 @@
#include "uce_lib.h"
#include <functional>
#include <map>
#include <tuple>
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<int(int)>* 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 = "<p>callback=" + std::to_string(value + phase3_unit_state) + "</p>\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"(<section class="phase3">
<h1>PHASE3 PAGE OK</h1>
<p>host=)");
print(html_escape(context.params["HTTP_HOST"]));
print(R"(</p>
<p>route=)");
print(html_escape(context.call["route"].to_string()));
print(R"(</p>
<p>answer=)");
print(html_escape(context.call["nested"]["answer"].to_string()));
print(R"(</p>
)");
context.call["nested"].each([&](const DValue& value, String key) {
print(R"( <p>each=)");
print(html_escape(key + ":" + value.to_string()));
print(R"(</p>
)");
});
print(R"( <p>self-got=)");
phase3_weak_data = 7;
print(std::to_string(phase3_helper_state_plus(2) + phase3_weak_data - 7));
print(R"(</p>
)");
std::map<String, String> phase3_map;
phase3_map.emplace(std::piecewise_construct, std::forward_as_tuple("piece"), std::forward_as_tuple("wise"));
print(R"( <p>map=)");
print(html_escape(phase3_map["piece"]));
print(R"(</p>
)");
phase3_core_invoke_callback(phase3_unit_callback, 2);
void (*volatile print_ptr)(const char*, size_t) = phase3_core_print;
print_ptr("<p>got-func-ok</p>\n", 19);
auto* fn = new std::function<int(int)>([](int value) { return(value * 3); });
phase3_core_invoke_function(fn, 14);
delete fn;
print(R"( </section>
)");
}
-2
View File
@@ -1,2 +0,0 @@
uce_host_ctx_read
uce_host_log
-554
View File
@@ -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 <wasm.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#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<uint8_t> 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<uint8_t> 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<uint8_t>& 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<std::pair<std::string, Node>> 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<std::string, wasm_extern_t*> 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<int32_t> 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<uint8_t> 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<wasm_extern_t*> 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<uint8_t> 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<wasm_extern_t*> 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<std::pair<std::string, wasm_global_t*>> 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;
}
-12
View File
@@ -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)
{
<><section class="phase3">
<h1>PHASE3 PAGE OK</h1>
<p>host=<?= context.params["HTTP_HOST"] ?></p>
<p>route=<?= context.call["route"].to_string() ?></p>
<p>answer=<?= context.call["nested"]["answer"].to_string() ?></p>
</section>
}
-77
View File
@@ -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 `<unknown>` — 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).
-16
View File
@@ -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"
-298
View File
@@ -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 <wasmtime.hh>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#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<Func>(&*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<Func>(&*run_export);
auto call = run.call(cx, std::vector<Val>{});
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;
}
-60
View File
@@ -1,60 +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` once a WASM worker endpoint exists.
When provided, it compares WASM medians against the Phase 5 budget of ≤2× native
page latency. Workspace birth and internal component call overhead budgets still
need worker-internal probes; this harness documents the gap rather than faking
those numbers.
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.
-62
View File
@@ -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
-90
View File
@@ -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
+1 -1
View File
@@ -494,7 +494,7 @@ String compiler_rewrite_named_render_syntax(String content)
String compiler_preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String content)
{
String parsed_content =
("#include \"")+context->server->config["COMPILER_SYS_PATH"] +"/src/lib/uce_lib.h\" \n"+
"#include \"uce_lib.h\" \n"+
file_get_contents(
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]
)+
+44 -1
View File
@@ -132,6 +132,23 @@ String compiler_unit_metadata_text(Request* context, SharedUnit* su)
);
}
bool compiler_wasm_unit_compile_enabled(Request* context)
{
if(!context || !context->server)
return(false);
return(config_bool("COMPILE_WASM_UNITS", false));
}
String compiler_wasm_compile_script(Request* context)
{
String script = "scripts/compile_wasm_unit";
if(context && context->server)
script = first(context->server->config["WASM_COMPILE_SCRIPT"], script);
if(script != "" && script[0] != '/' && context && context->server)
script = path_join(context->server->config["COMPILER_SYS_PATH"], script);
return(script);
}
SharedUnitCompileCheck shared_unit_compile_check(const SharedUnitFilesystemState& state)
{
SharedUnitCompileCheck result;
@@ -647,9 +664,12 @@ void setup_unit_paths(Request* context, SharedUnit* su, String file_name)
su->src_file_name = basename(file_name);
su->bin_file_name = su->src_file_name + ".so";
su->wasm_file_name = su->src_file_name + ".wasm";
su->pre_file_name = su->src_file_name + ".cpp";
su->so_name = su->bin_path + "/" + su->bin_file_name;
su->wasm_name = su->bin_path + "/" + su->wasm_file_name;
su->wasm_check_file_name = su->bin_path + "/" + su->src_file_name + ".wasm-check.txt";
su->api_file_name = su->bin_path + "/" + su->src_file_name + ".exports.txt";
su->meta_file_name = su->bin_path + "/" + su->src_file_name + ".meta.txt";
su->compile_output_file_name = su->bin_path + "/" + su->src_file_name + ".compile.txt";
@@ -740,7 +760,7 @@ void load_shared_unit(Request* context, SharedUnit* su)
String result =
String("#ifndef UCE_LIB_INCLUDED\n") +
"#define UCE_LIB_INCLUDED\n" +
("#include \"")+context->server->config["COMPILER_SYS_PATH"] +"/src/lib/uce_lib.h\" \n"+
"#include \"uce_lib.h\" \n"+
file_get_contents(
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]) +
"#endif \n";
@@ -839,6 +859,27 @@ void compile_shared_unit(Request* context, SharedUnit* su)
shell_escape(su->bin_file_name)
));
if(su->compiler_messages.length() == 0 && !su->opt_so_optional && compiler_wasm_unit_compile_enabled(context))
{
// The wasm side-module build is best-effort: a unit that cannot compile
// to wasm (e.g. try/catch) still works via the native .so and falls back
// to native at request time. Record the failure, never fail the unit on
// it. On failure, remove any stale .wasm so the backend won't serve it.
String wasm_messages = trim(shell_exec(shell_escape(compiler_wasm_compile_script(context))+" "+
shell_escape(su->src_path)+" "+
shell_escape(su->bin_path)+" "+
shell_escape(su->file_name)+" "+
shell_escape(su->pre_file_name)+" "+
shell_escape(su->wasm_file_name)
));
if(wasm_messages.length() > 0)
{
file_put_contents(su->wasm_check_file_name, wasm_messages + "\n");
file_unlink(su->wasm_name);
printf("(i) wasm side-module unavailable for %s (native .so serves it)\n", su->file_name.c_str());
}
}
if(su->compiler_messages.length() > 0)
{
String raw_messages = su->compiler_messages;
@@ -1171,6 +1212,8 @@ DValue unit_info(String path)
info["bin_file_name"] = su->bin_file_name;
info["pre_file_name"] = su->pre_file_name;
info["so_name"] = su->so_name;
info["wasm_name"] = su->wasm_name;
info["wasm_exists"].set_bool(file_exists(su->wasm_name));
info["api_file_name"] = su->api_file_name;
info["meta_file_name"] = su->meta_file_name;
info["compile_output_file_name"] = su->compile_output_file_name;
+82
View File
@@ -1,7 +1,9 @@
#include "functionlib.h"
#ifndef __UCE_WASM_CORE__
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
#endif
#include <cctype>
#include <stdexcept>
#include <algorithm>
@@ -296,6 +298,7 @@ String replace(String s, String search, String replace_with)
return(result);
}
#ifndef __UCE_WASM_CORE__
namespace {
String regex_flags_label(String flags)
@@ -637,6 +640,75 @@ StringList regex_split(String pattern, String subject, String flags)
return(result);
}
#else
// PCRE2 is not compiled into the wasm core; regex runs host-side (the host
// already links libpcre2). One UCEB1-marshalled hostcall carries the request
// {op,pattern,subject,flags,replacement} in and the result tree out — the host
// runs the real regex_* and packs the answer. See uce_host_regex in
// src/wasm/worker.cpp.
extern "C" size_t uce_host_regex(const char* in, size_t in_len, char* out, size_t cap);
static DValue wasm_regex_call(String op, String pattern, String subject, String flags, String replacement = "")
{
DValue request;
request["op"] = op;
request["pattern"] = pattern;
request["subject"] = subject;
request["flags"] = flags;
request["replacement"] = replacement;
String encoded = ucb_encode(request);
size_t need = uce_host_regex(encoded.data(), encoded.size(), 0, 0);
if(need == 0)
return(DValue());
String buffer(need, 0);
size_t got = uce_host_regex(encoded.data(), encoded.size(), &buffer[0], need);
if(got == 0 || got > need)
return(DValue());
DValue response;
String error;
ucb_decode(String(buffer.data(), got), response, &error);
return(response);
}
bool regex_match(String pattern, String subject, String flags)
{
return(wasm_regex_call("match", pattern, subject, flags)["bool"].to_bool());
}
DValue regex_search(String pattern, String subject, String flags)
{
DValue response = wasm_regex_call("search", pattern, subject, flags);
DValue* tree = response.key("tree");
return(tree ? *tree : DValue());
}
DValue regex_search_all(String pattern, String subject, String flags)
{
DValue response = wasm_regex_call("search_all", pattern, subject, flags);
DValue* tree = response.key("tree");
return(tree ? *tree : DValue());
}
String regex_replace(String pattern, String replacement, String subject, String flags)
{
return(wasm_regex_call("replace", pattern, subject, flags, replacement)["text"].to_string());
}
StringList regex_split(String pattern, String subject, String flags)
{
DValue response = wasm_regex_call("split", pattern, subject, flags);
StringList result;
DValue* list = response.key("list");
if(list)
list->each([&](const DValue& part, String) {
result.push_back(part.to_string());
});
return(result);
}
#endif
String trim(String raw)
{
s64 len = raw.length();
@@ -1323,7 +1395,12 @@ String xml_decode_text(String s)
void xml_throw(String message)
{
#ifdef __UCE_WASM_CORE__
(void)message;
__builtin_trap();
#else
throw std::runtime_error("xml_decode(): " + message);
#endif
}
struct XmlParser
@@ -1856,7 +1933,12 @@ String yaml_decode_quoted(String raw)
void yaml_throw(String message)
{
#ifdef __UCE_WASM_CORE__
(void)message;
__builtin_trap();
#else
throw std::runtime_error("yaml_decode(): " + message);
#endif
}
struct YamlParser
+7 -4
View File
@@ -55,8 +55,11 @@ String to_string(SharedUnit* u) {
return(result);
}
// NB: header-defined function templates must be inline — wasm side modules
// compile with -fvisibility-inlines-hidden so instantiations bind locally
// instead of becoming unresolvable self-imports (WASM-PROPOSAL §6)
template <typename ITYPE>
String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
inline String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
{
static const char* digits = "0123456789ABCDEF";
String rc(hex_len,'0');
@@ -66,7 +69,7 @@ String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
}
template<typename T, typename F>
std::vector<T> filter(std::vector<T> items, F f)
inline std::vector<T> filter(std::vector<T> items, F f)
{
std::vector<T> new_items;
for(auto item : items)
@@ -78,7 +81,7 @@ std::vector<T> filter(std::vector<T> items, F f)
}
template<typename T, typename F>
auto map(std::vector<T> items, F f)
inline auto map(std::vector<T> items, F f)
{
using ResultType = decltype(f(items[0]));
std::vector<ResultType> new_items;
@@ -101,7 +104,7 @@ DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f);
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f);
template <class ...Args>
String first(Args... args)
inline String first(Args... args)
{
std::vector<String> vec = {args...};
for(auto s : vec)
+5
View File
@@ -19,6 +19,11 @@ A million repetitions of "a"
/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
/* #define SHA1HANDSOFF * Copies data before messing with it. */
#ifdef __UCE_WASM_CORE__
#include <stdint.h>
typedef uint32_t u_int32_t;
#endif
typedef struct {
u_int32_t state[5];
u_int32_t count[2];
+217
View File
@@ -1,3 +1,211 @@
#ifdef __UCE_WASM_CORE__
#include <cmath>
#include "types.h"
#include "functionlib.h"
#include "sys.h"
extern "C" {
uint64_t uce_host_time(void);
double uce_host_time_precise(void);
size_t uce_host_env(const char* key, size_t key_len, char* buf, size_t cap);
size_t uce_host_random(char* buf, size_t len);
void uce_host_log(int level, const char* buf, size_t len);
// read-only file membrane, policy-gated host-side to the site tree;
// relative paths resolve against the current unit's directory (the native
// cwd convention); file_read uses the length-query convention
int uce_host_file_exists(const char* path, size_t path_len, const char* current, size_t current_len);
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()
{
return(context ? context->resources.current_unit_file : String(""));
}
String shell_exec(String cmd) { (void)cmd; return(""); }
String shell_escape(String raw) { return(raw); }
String basename(String fn) { while(fn.find("/") != String::npos) fn = fn.substr(fn.find("/") + 1); return(fn); }
String dirname(String fn) { auto pos = fn.find_last_of('/'); return(pos == String::npos ? "" : fn.substr(0, pos)); }
String path_join(String base, String child) { if(base == "") return(child); if(child == "") return(base); if(child[0] == '/') return(child); return(base + (base.back() == '/' ? "" : "/") + child); }
String path_real(String path) { return(path); }
bool path_is_within(String path, String root) { return(str_starts_with(path, root)); }
bool mkdir(String path) { (void)path; return(false); }
bool file_exists(String path)
{
String current = wasm_current_unit_file();
return(uce_host_file_exists(path.data(), path.size(), current.data(), current.size()) != 0);
}
int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode, f64 wait_timeout_seconds, String purpose) { (void)file_name; (void)open_flags; (void)lock_type; (void)create_mode; (void)wait_timeout_seconds; (void)purpose; return(-1); }
void file_close_locked(int fd) { (void)fd; }
void file_release_process_locks(String reason) { (void)reason; }
String file_get_contents_locked_fd(int fd) { (void)fd; return(""); }
bool file_put_contents_locked_fd(int fd, String content) { (void)fd; (void)content; return(false); }
String file_get_contents(String file_name)
{
String current = wasm_current_unit_file();
size_t required = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), 0, 0);
if(required == 0)
return("");
String content(required, 0);
size_t got = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), &content[0], required);
content.resize(got <= required ? got : 0);
return(content);
}
bool file_put_contents(String file_name, String content)
{
String current = wasm_current_unit_file();
return(uce_host_file_write(file_name.data(), file_name.size(), current.data(), current.size(), content.data(), content.size(), 0) != 0);
}
bool file_append_contents(String file_name, String content)
{
String current = wasm_current_unit_file();
return(uce_host_file_write(file_name.data(), file_name.size(), current.data(), current.size(), content.data(), content.size(), 1) != 0);
}
String cwd_get() { return("/"); }
void cwd_set(String path) { (void)path; }
String process_start_directory() { return("/"); }
time_t file_mtime(String file_name) { (void)file_name; return(0); }
void file_unlink(String file_name)
{
String current = wasm_current_unit_file();
uce_host_file_unlink(file_name.data(), file_name.size(), current.data(), current.size());
}
String expand_path(String path, String relative_to_path) { return(path_join(relative_to_path, path)); }
StringList ls(String dir) { (void)dir; return(StringList()); }
u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); }
f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); }
bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); }
bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); }
u64 config_u64(String key, u64 fallback) { return(context ? config_map_u64(context->server->config, key, fallback) : fallback); }
f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(context->server->config, key, fallback) : fallback); }
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()); }
// 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; }
bool socket_write(u64 sockfd, String data) { (void)sockfd; (void)data; return(false); }
String socket_read(u64 sockfd, u32 max_length, u32 timeout) { (void)sockfd; (void)max_length; (void)timeout; return(""); }
String ws_message() { return(context ? context->in : ""); }
String ws_connection_id() { return(context ? context->resources.websocket_connection_id : ""); }
String ws_scope() { return(context ? context->resources.websocket_scope : ""); }
u8 ws_opcode() { return(context ? context->resources.websocket_opcode : 0); }
bool ws_is_binary() { return(context && context->resources.websocket_is_binary); }
StringList ws_connections(String scope) { (void)scope; return(StringList()); }
u64 ws_connection_count(String scope) { (void)scope; return(0); }
bool ws_send(String message, bool binary, String scope) { (void)message; (void)binary; (void)scope; return(false); }
bool ws_send_to(String connection_id, String message, bool binary) { (void)connection_id; (void)message; (void)binary; return(false); }
bool ws_close(String connection_id) { (void)connection_id; return(false); }
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames) { (void)frames; (void)size; (void)skip_frames; return(""); }
String capture_backtrace_string(u32 max_frames, u32 skip_frames) { (void)max_frames; (void)skip_frames; return(""); }
String signal_name(int sig) { (void)sig; return(""); }
String memcache_escape_key(String key) { return(key); }
StringList memcache_escape_keys(StringList keys) { return(keys); }
u64 memcache_connect(String host, short port) { (void)host; (void)port; return(0); }
String memcache_command(u64 connection, String command) { (void)connection; (void)command; return(""); }
bool memcache_set(u64 connection, String key, String value, u64 expires_in) { (void)connection; (void)key; (void)value; (void)expires_in; return(false); }
bool memcache_delete(u64 connection, String key) { (void)connection; (void)key; return(false); }
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; }
static u64 wasm_next_task_callback_id = 1;
static std::map<u64, std::function<void()>> 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<void()> 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<void()> 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()
{
StringMap cfg;
cfg["SESSION_TIME"] = std::to_string(60*60*24*30);
cfg["MAX_MEMORY"] = std::to_string(1024*1024*16);
return(cfg);
}
#else
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
@@ -942,6 +1150,14 @@ StringMap make_server_settings()
cfg["BIN_DIRECTORY"] = "/tmp/uce/work";
cfg["COMPILE_SCRIPT"] = "scripts/compile";
cfg["WASM_COMPILE_SCRIPT"] = "scripts/compile_wasm_unit";
cfg["COMPILE_WASM_UNITS"] = "0";
cfg["WASM_BACKEND_ENABLED"] = "1";
cfg["WASM_BACKEND_VERBOSE"] = "0";
cfg["WASM_CORE_PATH"] = "";
cfg["WASM_MEMORY_LIMIT_BYTES"] = std::to_string(512ull * 1024 * 1024);
cfg["WASM_EPOCH_DEADLINE_TICKS"] = "200";
cfg["WASM_EPOCH_PERIOD_MS"] = "50";
cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template";
cfg["LIT_ESC"] = "3d5b5_1";
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
@@ -999,3 +1215,4 @@ StringMap make_server_settings()
return(cfg);
}
#endif
+22 -1
View File
@@ -1,7 +1,28 @@
#pragma once
#if defined(__UCE_WASM_CORE__) || defined(__UCE_WASM_UNIT__)
#ifndef LOCK_SH
#define LOCK_SH 1
#define LOCK_EX 2
#define LOCK_UN 8
#endif
#ifndef SIGABRT
#define SIGABRT 6
#endif
#ifndef SIGSEGV
#define SIGSEGV 11
#endif
typedef int pid_t;
extern "C" {
int raise(int);
unsigned int sleep(unsigned int seconds);
int usleep(unsigned int usec);
}
#else
#include <sys/file.h>
#include <signal.h>
#endif
#include <ctime>
#include <sstream>
String shell_exec(String cmd);
@@ -22,7 +43,7 @@ String file_get_contents(String file_name);
bool file_put_contents(String file_name, String content);
bool file_append_contents(String file_name, String content);
template <typename... Ts>
bool file_append(String file_name, Ts... args)
inline bool file_append(String file_name, Ts... args)
{
std::ostringstream out;
((out << args), ...);
+9 -4
View File
@@ -1,16 +1,17 @@
#ifndef __UCE_WASM_CORE__
#include <sys/file.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dlfcn.h>
#endif
#include <stdlib.h>
#include <iostream>
#include <filesystem>
#include <ctype.h>
#include <fstream>
#include <sys/stat.h>
#include <ctime>
#include <dlfcn.h>
#include <limits.h>
#include <algorithm>
#include <sys/stat.h>
#include <iostream>
#include "types.h"
@@ -52,10 +53,12 @@ String http_status_reason(s32 code)
SharedUnit::~SharedUnit()
{
#ifndef __UCE_WASM_CORE__
if(so_handle)
{
dlclose(so_handle);
}
#endif
}
String nibble(String div, String& haystack)
@@ -98,6 +101,8 @@ Request::~Request()
delete stream;
ob_stack.clear();
ob = 0;
#ifndef __UCE_WASM_CORE__
for(auto& sockfd : resources.sockets)
close(sockfd);
#endif
}
+10 -3
View File
@@ -65,6 +65,8 @@ struct SharedUnit {
String file_name;
String so_name;
String wasm_name;
String wasm_check_file_name;
String api_file_name;
String meta_file_name;
String compile_output_file_name;
@@ -77,6 +79,7 @@ struct SharedUnit {
String pre_path;
String src_file_name;
String bin_file_name;
String wasm_file_name;
String pre_file_name;
void* so_handle = 0;
@@ -243,26 +246,29 @@ Request* context;
#include <iostream>
// NB: header templates must be inline — wasm units rely on
// -fvisibility-inlines-hidden binding instantiations locally (§6)
template <typename... Ts>
void print(Ts... args)
inline void print(Ts... args)
{
((*context->ob << args), ...);
}
template <typename... Ts>
void out(Ts... args)
inline void out(Ts... args)
{
((*context->ob << args), ...);
}
template <typename... Ts>
String concat(Ts... args)
inline String concat(Ts... args)
{
ByteStream out;
((out << args), ...);
return(out.str());
}
#ifndef __UCE_WASM_UNIT__
void * operator new(decltype(sizeof(0)) n) noexcept(false)
{
void* ptr = malloc(n);
@@ -280,3 +286,4 @@ void operator delete(void * p) throw()
//TO DO: track deallocations
free(p);
}
#endif
+13
View File
@@ -11,9 +11,22 @@
#include "sys.cpp"
#include "uri.cpp"
#include "cli.cpp"
#ifdef __UCE_WASM_CORE__
// markdown is pure compute (no PCRE/syscalls/regex) — it belongs in the wasm
// core so markdown_to_html/markdown_to_ast render in-workspace. compiler.cpp
// (which declares component() ahead of markdown in the native build) is carved
// out here, and the wasm core defines component() later in src/wasm/core.cpp,
// so markdown just needs the forward declaration.
String component(String name, DValue props, Request& context);
#include "markdown.cpp"
#endif
#ifndef __UCE_WASM_CORE__
#include "compiler-parser.cpp"
#include "compiler.cpp"
#include "markdown.cpp"
#include "zip.cpp"
#include "mysql-connector.cpp"
#include "sqlite-connector.cpp"
#endif
+18
View File
@@ -4,6 +4,10 @@
#include <fcntl.h>
#include <unistd.h>
#ifdef __UCE_WASM_CORE__
extern "C" size_t uce_host_random(char* buf, size_t len);
#endif
String base64_encode(String raw)
{
static const char* chars =
@@ -714,6 +718,19 @@ StringMap parse_cookies(String cookie_String)
String session_id_create()
{
#ifdef __UCE_WASM_CORE__
unsigned char bytes[32];
if(uce_host_random((char*)bytes, sizeof(bytes)) != sizeof(bytes))
__builtin_trap();
String result;
static const char* hex = "0123456789abcdef";
for(unsigned char b : bytes)
{
result.push_back(hex[b >> 4]);
result.push_back(hex[b & 0x0f]);
}
return(result);
#else
unsigned char bytes[32];
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if(fd == -1)
@@ -738,6 +755,7 @@ String session_id_create()
result.push_back(hex[b & 0x0f]);
}
return(result);
#endif
}
bool is_valid_session_id(String session_id)
+6 -2
View File
@@ -15,7 +15,11 @@
#include <string>
#include <vector>
#if defined(__GNUG__)
// The demangler is a host-side nicety for trap traces. wasi-libc++ does not
// provide __cxa_demangle, and a wasm unit that merely includes uce_lib.h must
// not pull it in as an unresolvable import — so it is host-only.
#if defined(__GNUG__) && !defined(__wasm__)
#define UCE_WASM_TRACE_HAVE_DEMANGLE 1
#include <cxxabi.h>
#endif
@@ -30,7 +34,7 @@ struct WasmTraceSummary
inline std::string wasm_trace_demangle(const std::string& name)
{
#if defined(__GNUG__)
#if defined(UCE_WASM_TRACE_HAVE_DEMANGLE)
// frame names look like "module!symbol"; demangle the symbol part
auto bang = name.find('!');
std::string prefix = bang == std::string::npos ? "" : name.substr(0, bang + 1);
+19
View File
@@ -1,4 +1,5 @@
#include "lib/uce_lib.cpp"
#include "wasm/backend.cpp"
#include <csetjmp>
#include <deque>
#include <errno.h>
@@ -940,6 +941,23 @@ int handle_complete(FastCGIRequest& request) {
compiler_invoke_cli(&request, request.params["SCRIPT_FILENAME"]);
else if(request.params["UCE_SERVE_HTTP"] == "1")
compiler_invoke_serve_http(&request, request.params["SCRIPT_FILENAME"], request.params["UCE_SERVE_HTTP_FUNCTION"]);
else if(wasm_backend_should_handle(request, compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"])))
{
// W4/W5: Wasmtime uses host signals internally to implement guest
// traps. The native SIGSEGV/SIGILL request recovery handler must not
// intercept those, or clean guest traps become native fatal signals.
request_fault_active = 0;
restore_request_fault_handlers();
String wasm_error = wasm_backend_serve(request, compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"]));
install_request_fault_handlers();
request_fault_active = 1;
if(wasm_error != "")
{
failure_title = "wasm runtime error during request";
failure_details = "";
failure_trace = wasm_error;
}
}
else
compiler_invoke(&request, request.params["SCRIPT_FILENAME"]);
}
@@ -1023,6 +1041,7 @@ void on_terminate(int sig)
if(getpid() != parent_pid)
exit(1);
printf("Terminating... PID %i:%i\n", getpid(), parent_pid);
wasm_backend_shutdown();
server.shutdown();
exit(1);
}
+240
View File
@@ -0,0 +1,240 @@
// W4 — FastCGI backend glue for the W3 wasm workspace runtime.
//
// Included into the native server TU (src/linux_fastcgi.cpp) after uce_lib.cpp,
// so it shares String/DValue/config and the UCEB1 codec. Provides a
// config-selectable page-render backend: when WASM_BACKEND_ENABLED and a wasm
// artifact exists for the entry unit, the request is served through a
// per-request wasm workspace instead of the native dlopen path.
//
// The seam is narrow on purpose — only the page render branch in
// handle_complete() changes. CLI, serve_http, and websocket stay native.
#include "../lib/wasm_trace.h"
// The native server TU has the real connectors (sqlite/mysql) compiled in, so
// the worker's host-side connector hostcalls are available here. The W3 CLI
// driver does not define this and gets a named-trap stub for those imports.
#define UCE_WASM_HOST_CONNECTORS 1
#include "worker.cpp"
#include <atomic>
#include <sys/stat.h>
#include <thread>
// per forked worker process: one engine + compiled-core cache, one epoch ticker
static WasmWorker* g_wasm_worker = 0;
static std::thread g_wasm_epoch_ticker;
static std::atomic<bool> g_wasm_epoch_running(false);
static String g_wasm_init_error;
static bool g_wasm_init_attempted = false;
bool wasm_backend_configured(Request* context)
{
if(!context || !context->server)
return(false);
return(config_bool("WASM_BACKEND_ENABLED", false));
}
// Lazily bring up the per-process worker on first use inside a forked child
// (the engine must not be inherited across fork). Returns "" on success.
static String wasm_backend_ensure_started(Request* context)
{
if(g_wasm_init_attempted)
return(g_wasm_init_error);
g_wasm_init_attempted = true;
StringMap& cfg = context->server->config;
WasmWorkerConfig wc;
wc.core_wasm_path = first(cfg["WASM_CORE_PATH"],
path_join(cfg["COMPILER_SYS_PATH"], "bin/wasm/core.wasm"));
wc.site_root = path_join(cfg["COMPILER_SYS_PATH"], cfg["SITE_DIRECTORY"]);
wc.cache_root = cfg["BIN_DIRECTORY"];
// write membrane allowlist: the site tree plus the runtime scratch dirs
// pages legitimately write to (matches native reachable write targets).
wc.write_roots = { wc.site_root, "/tmp" };
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
if(cfg[key] != "")
wc.write_roots.push_back(cfg[key]);
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
g_wasm_worker = new WasmWorker(wc);
g_wasm_init_error = g_wasm_worker->init();
if(g_wasm_init_error != "")
{
delete g_wasm_worker;
g_wasm_worker = 0;
return(g_wasm_init_error);
}
g_wasm_epoch_running.store(true);
WasmWorker* worker = g_wasm_worker;
u64 period_ms = config_u64("WASM_EPOCH_PERIOD_MS", 50);
g_wasm_epoch_ticker = std::thread([worker, period_ms] {
while(g_wasm_epoch_running.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(period_ms));
worker->engine.increment_epoch();
}
});
return("");
}
static bool wasm_artifact_exists(Request* context, const String& entry_unit)
{
if(entry_unit == "")
return(false);
String wasm_path = context->server->config["BIN_DIRECTORY"] + entry_unit + ".wasm";
struct stat wasm_st;
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
return(false);
// The wasm path bypasses the native JIT recompile, so a source edit would
// otherwise be served from a stale .wasm. Require the artifact to be newer
// than the source; if it is stale, fall back to native — which JIT-rebuilds
// both .so and .wasm — so the next request gets a fresh artifact.
struct stat src_st;
if(stat(entry_unit.c_str(), &src_st) == 0 && wasm_st.st_mtime < src_st.st_mtime)
return(false);
return(true);
}
// Decide, by source inspection, whether a page must stay on the native
// backend for now (W5 surfaces not yet behind the membrane).
//
// Caveats this deliberately accepts:
// - ENTRY-UNIT ONLY: a clean entry page that component()s into a native-only
// unit is still served by wasm. With signals_based_traps off (see
// make_engine) the worst case is a clean wasm error or a stubbed-empty
// result, never a worker crash — so this is a best-effort routing hint,
// not a guarantee. Full coverage is a W5 concern (un-stub the APIs).
// - SUBSTRING match, intentionally conservative: a token like "xml_" also
// matches an identifier or doc string containing it. That only ever
// over-routes to native (always correct), never the reverse.
static bool wasm_backend_native_fallback_uncached(Request* context, const String& entry_unit)
{
String source = file_get_contents(entry_unit);
// Supported by the wasm core, intentionally NOT in this list:
// - 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, 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_", "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)
return(true);
return(false);
}
static bool wasm_backend_native_fallback_needed(Request* context, const String& entry_unit)
{
if(!context || !context->server || entry_unit == "")
return(true);
// Cache the verdict per process, keyed on path + mtime: the source scan is
// otherwise a file read + 20 substring searches on every page request.
struct CachedVerdict { time_t mtime; bool fallback; };
static std::map<String, CachedVerdict> cache;
struct stat st;
time_t mtime = (stat(entry_unit.c_str(), &st) == 0) ? st.st_mtime : 0;
auto it = cache.find(entry_unit);
if(it != cache.end() && it->second.mtime == mtime)
return(it->second.fallback);
bool fallback = wasm_backend_native_fallback_uncached(context, entry_unit);
cache[entry_unit] = { mtime, fallback };
return(fallback);
}
// True if this request should be served by the wasm backend. Falls through to
// native when disabled, when init failed, or when the unit has no wasm artifact
// (e.g. units skip-listed for try/catch — automatic, graceful fallback).
bool wasm_backend_should_handle(Request& request, const String& entry_unit)
{
if(!wasm_backend_configured(&request))
return(false);
if(request.resources.is_cli)
return(false);
if(wasm_backend_native_fallback_needed(&request, entry_unit))
return(false);
if(!wasm_artifact_exists(&request, entry_unit))
return(false);
if(wasm_backend_ensure_started(&request) != "")
return(false);
return(true);
}
// Serve the page render through a wasm workspace. Populates the native Request
// (status/headers/cookies/session/body) so the existing transport writes the
// response unchanged. Returns "" on success, or a collapsed error/trace string
// for the caller to route into the configured error page.
String wasm_backend_serve(Request& request, const String& entry_unit)
{
DValue ctx;
auto copy_map = [&](const StringMap& source, const char* key) {
for(auto& entry : source)
ctx[key][entry.first] = entry.second;
};
copy_map(request.params, "params");
copy_map(request.get, "get");
copy_map(request.post, "post");
copy_map(request.cookies, "cookies");
copy_map(request.session, "session");
ctx["entry_unit"] = entry_unit;
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit);
if(!response.ok)
return(response.error == "" ? String("wasm workspace failed") : response.error);
// Diagnostic timing headers are opt-in: they leak workspace internals and
// belong to the W5 benchmark harness, not public responses.
if(config_bool("WASM_BACKEND_VERBOSE", false))
{
request.header["X-UCE-Backend"] = "wasm";
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
}
// status line: keep the native default unless the unit set one
String status = response.meta["status"].to_string();
if(status != "")
request.response_code = status;
// merge headers over the native defaults (so Content-Type survives unless
// the unit overrode it); replace cookies/session with the unit's view
if(response.meta.key("headers"))
response.meta["headers"].each([&](const DValue& value, String name) {
request.header[name] = value.to_string();
});
if(response.meta.key("cookies"))
response.meta["cookies"].each([&](const DValue& value, String) {
request.set_cookies.push_back(value.to_string());
});
if(response.meta.key("session"))
{
request.session.clear();
response.meta["session"].each([&](const DValue& value, String name) {
request.session[name] = value.to_string();
});
}
// body into the request's primary output stream (ob_stack[0]); the
// transport's assemble_output_buffer concatenates the stack
if(request.ob)
request.ob->write(response.body.data(), response.body.size());
return("");
}
// Stop the ticker before the worker process exits (best-effort; forked workers
// are usually killed, but a clean ager-out path should join the thread).
void wasm_backend_shutdown()
{
if(g_wasm_epoch_running.exchange(false) && g_wasm_epoch_ticker.joinable())
g_wasm_epoch_ticker.join();
}
+520
View File
@@ -0,0 +1,520 @@
// Production WASM W1 core entrypoint.
//
// This file deliberately includes the real UCE runtime amalgamation with
// __UCE_WASM_CORE__ enabled. Native-only pieces are carved out in the runtime
// sources, while the workspace-owned DValue ABI and output plumbing are built
// into core.wasm.
#define __UCE_WASM_CORE__ 1
#include "../lib/uce_lib.cpp"
#include "../lib/mysql-connector.h"
#include "../lib/sqlite-connector.h"
// ---- W3 connector membrane stubs -------------------------------------------
// Generated units reference the connector class surface through uce_lib.h, so
// the core must define it. Until the real hostcall connectors land (W5 parity
// scope: the starter uses no database), every operation fails cleanly with an
// explanatory error instead of trapping.
static const char* WASM_DB_UNAVAILABLE =
"database connectors are not yet available in the wasm workspace";
bool MySQL::connect(String host, String username, String password)
{
(void)host; (void)username; (void)password;
connection = 0;
statement_info = WASM_DB_UNAVAILABLE;
return(false);
}
void MySQL::disconnect() { connection = 0; }
String MySQL::error() { return(WASM_DB_UNAVAILABLE); }
String MySQL::escape(String raw, char quote_char) { (void)quote_char; return(raw); }
String MySQL::parse_query_parameters(String query, StringMap m) { (void)m; return(query); }
DValue MySQL::query(String q) { (void)q; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
DValue MySQL::query(String q, StringMap params) { (void)q; (void)params; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
DValue MySQL::get_pending_result() { return(DValue()); }
String mysql_escape(String raw, char quote_char) { (void)quote_char; return(raw); }
// sqlite runs host-side (the host links libsqlite and owns the connections in
// a per-workspace handle table). One UCEB1-marshalled hostcall carries
// {op,handle,path,query,params} in and {handle,result,insert_id,affected,
// error_code,statement_info} out. `connection` holds the host handle (>0).
extern "C" size_t uce_host_sqlite(const char* in, size_t in_len, char* out, size_t cap);
static DValue wasm_sqlite_call(DValue request)
{
String encoded = ucb_encode(request);
size_t need = uce_host_sqlite(encoded.data(), encoded.size(), 0, 0);
if(need == 0)
return(DValue());
String buffer(need, 0);
size_t got = uce_host_sqlite(encoded.data(), encoded.size(), &buffer[0], need);
if(got == 0 || got > need)
return(DValue());
DValue response;
String error;
ucb_decode(String(buffer.data(), got), response, &error);
return(response);
}
void SQLite::set_error(s32 code, String info) { error_code = code; statement_info = info; }
bool SQLite::connect(String path)
{
this->path = path;
DValue request;
request["op"] = "connect";
request["path"] = path;
DValue response = wasm_sqlite_call(request);
u64 handle = response["handle"].to_u64();
connection = (void*)(uintptr_t)handle;
error_code = (s32)response["error_code"].to_s64();
statement_info = response["statement_info"].to_string();
return(handle != 0 && error_code == 0);
}
void SQLite::disconnect()
{
if(connection)
{
DValue request;
request["op"] = "disconnect";
request["handle"] = (f64)(uintptr_t)connection;
wasm_sqlite_call(request);
connection = 0;
}
}
String SQLite::error()
{
return(statement_info);
}
DValue SQLite::query(String q, const StringMap& params)
{
DValue request;
request["op"] = "query";
request["handle"] = (f64)(uintptr_t)connection;
request["query"] = q;
for(auto& entry : params)
request["params"][entry.first] = entry.second;
DValue response = wasm_sqlite_call(request);
insert_id = response["insert_id"].to_u64();
affected_rows = (u32)response["affected"].to_u64();
error_code = (s32)response["error_code"].to_s64();
statement_info = response["statement_info"].to_string();
DValue* result = response.key("result");
return(result ? *result : DValue());
}
DValue SQLite::query(String q) { return(query(q, StringMap())); }
bool SQLite::apply_default_pragmas() { return(true); }
bool SQLite::bind_params(void* statement, const StringMap& params) { (void)statement; (void)params; return(true); }
DValue SQLite::collect_rows(void* statement) { (void)statement; return(DValue()); }
SQLite* sqlite_connect(String path)
{
SQLite* db = new SQLite();
db->request_cleanup_delete = true;
db->connect(path);
return(db);
}
void sqlite_disconnect(SQLite* db) { if(db) { db->disconnect(); if(db->request_cleanup_delete) delete db; } }
String sqlite_error(SQLite* db) { return(db ? db->error() : String(WASM_DB_UNAVAILABLE)); }
DValue sqlite_query(SQLite* db, String q) { return(db ? db->query(q) : DValue()); }
DValue sqlite_query(SQLite* db, String q, const StringMap& params) { return(db ? db->query(q, params) : DValue()); }
u64 sqlite_insert_id(SQLite* db) { return(db ? db->insert_id : 0); }
u32 sqlite_affected_rows(SQLite* db) { return(db ? db->affected_rows : 0); }
void cleanup_sqlite_connections() { }
static ServerState wasm_server;
static Request wasm_request;
static String wasm_output;
static String wasm_response_meta;
// ---- vague-linkage link anchors --------------------------------------------
// Units import libc++ template instantiations they use; --export-all only
// exports what the core itself instantiated. Some libc++ internals lack the
// hide-from-ABI attribute (the Phase 0 lambda finding), so units emit them as
// imports rather than binding locally. This function exists purely to make
// the core instantiate — and therefore export — the ones the site tree needs.
// Extend it when the loader reports "unresolved import env.<libc++ symbol>".
extern "C" void uce_wasm_link_anchors()
{
StringMap string_map;
string_map["k"] = "v";
string_map.erase(String("k")); // __tree::__erase_unique<String>
std::map<String, DValue> dvalue_map;
dvalue_map["k"] = DValue();
dvalue_map.erase(String("k"));
std::vector<String> string_list = { "a", "b" };
string_list.erase(string_list.begin());
std::set<String> string_set;
string_set.insert("k");
string_set.erase(String("k"));
// libc functions units may call that the core itself never references —
// taking their address forces them into the link (and --export-all)
static void* volatile libc_anchors[] = {
(void*)&atof, (void*)&atoi, (void*)&atol, (void*)&atoll,
(void*)&strtol, (void*)&strtoul, (void*)&strtoll, (void*)&strtoull,
(void*)&strtod, (void*)&strtof,
(void*)&qsort, (void*)&bsearch,
(void*)&snprintf, (void*)&sscanf,
(void*)&memmove, (void*)&strncmp, (void*)&strncpy,
// memchr/strchr/strrchr/strstr are C++-overloaded; cast to the C shape
(void*)(const void* (*)(const void*, int, size_t))&memchr,
(void*)(const char* (*)(const char*, int))&strchr,
(void*)(const char* (*)(const char*, int))&strrchr,
(void*)(const char* (*)(const char*, const char*))&strstr,
// ctype family (int(int)); units use these directly
(void*)&isalnum, (void*)&isalpha, (void*)&isblank, (void*)&iscntrl,
(void*)&isdigit, (void*)&isgraph, (void*)&islower, (void*)&isprint,
(void*)&ispunct, (void*)&isspace, (void*)&isupper, (void*)&isxdigit,
(void*)&tolower, (void*)&toupper,
};
(void)libc_anchors;
}
// W3 membrane: the host resolves component/render targets to funcref-table
// slots (loading units lazily) and writes the resolved unit path back so
// nested relative component resolution keeps working.
extern "C" int32_t uce_host_component_resolve(
const char* target, size_t target_len, int32_t kind,
const char* current_unit, size_t current_unit_len,
char* resolved_buf, size_t resolved_cap);
// target → table slot, reset per request (workspaces die with the request,
// but a single workspace can render the same component many times)
static std::map<String, s32> wasm_component_slots;
// These mirror small page-runtime pieces of compiler.cpp, which is carved
// out of the wasm core wholesale (it is the native toolchain: parser, clang
// driver, dlopen). Kept byte-identical where possible.
String component_normalize_path(String name)
{
name = trim(name);
if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce")
return(name);
return(name + ".uce");
}
void component_parse_target(String target, String& file_name, String& render_name)
{
target = trim(target);
render_name = "";
auto render_split_pos = target.find(":");
if(render_split_pos != String::npos)
{
render_name = trim(target.substr(render_split_pos + 1));
target = trim(target.substr(0, render_split_pos));
}
file_name = target;
}
String component_error_banner(String message)
{
return("<div class=\"banner\">" + html_escape(message) + "</div>");
}
struct RequestPropsScope
{
Request* context = 0;
DValue previous_props;
RequestPropsScope(Request* context, const DValue& props)
{
this->context = context;
if(this->context)
{
previous_props = this->context->props;
this->context->props = props;
}
}
~RequestPropsScope()
{
if(context)
context->props = previous_props;
}
};
// kind values shared with the host loader (src/wasm/worker.cpp)
enum WasmResolveKind {
WASM_RESOLVE_COMPONENT = 0,
WASM_RESOLVE_RENDER = 1,
WASM_RESOLVE_EXISTS = 2,
WASM_RESOLVE_ONCE = 3,
};
static s32 wasm_resolve_target(String target, s32 kind, String* resolved_out = 0)
{
String cache_key = std::to_string(kind) + ":" + target;
auto cached = wasm_component_slots.find(cache_key);
if(cached != wasm_component_slots.end() && kind != WASM_RESOLVE_EXISTS)
return(cached->second);
char resolved[512];
String current = context ? context->resources.current_unit_file : "";
s32 slot = uce_host_component_resolve(
target.data(), target.size(), kind,
current.data(), current.size(),
resolved, sizeof(resolved));
if(resolved_out && slot)
*resolved_out = String(resolved, strnlen(resolved, sizeof(resolved)));
if(kind != WASM_RESOLVE_EXISTS)
wasm_component_slots[cache_key] = slot;
return(slot);
}
String component_resolve(String name)
{
String resolved;
if(wasm_resolve_target(trim(name), WASM_RESOLVE_EXISTS, &resolved))
return(resolved);
return("");
}
bool component_exists(String name)
{
return(component_resolve(name) != "");
}
// Run a unit's ONCE() handler at most once per request (native
// compiler_run_unit_once_if_needed semantics): dedup on the resolved unit
// path via request.once_units. The handler emits head assets, etc.
static void wasm_run_once(const String& resolved, Request& request)
{
if(resolved == "")
return;
if(request.once_units.find(resolved) != request.once_units.end())
return;
request.once_units.insert(resolved);
s32 once_slot = wasm_resolve_target(resolved, WASM_RESOLVE_ONCE);
if(once_slot == 0)
return;
String previous_unit = request.resources.current_unit_file;
request.resources.current_unit_file = resolved;
request_ref_handler once_handler = (request_ref_handler)(uintptr_t)once_slot;
once_handler(request);
request.resources.current_unit_file = previous_unit;
}
void component_render(String name, DValue props, Request& request)
{
String resolved;
s32 slot = wasm_resolve_target(trim(name), WASM_RESOLVE_COMPONENT, &resolved);
if(!slot)
{
print(component_error_banner("component not found: " + trim(name)));
return;
}
wasm_run_once(resolved, request);
RequestPropsScope props_scope(&request, props);
String previous_unit = request.resources.current_unit_file;
if(resolved != "")
request.resources.current_unit_file = resolved;
// a wasm function pointer is its index in the shared funcref table; the
// host returned the handler's slot, so this is a plain call_indirect
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
handler(request);
request.resources.current_unit_file = previous_unit;
}
void component_render(String name) { DValue props; component_render(name, props, *context); }
void component_render(String name, Request& request) { DValue props; component_render(name, props, request); }
void component_render(String name, DValue props) { component_render(name, props, *context); }
String component(String name, DValue props, Request& request)
{
ob_start();
component_render(name, props, request);
return(ob_get_close());
}
String component(String name) { DValue props; return(component(name, props, *context)); }
String component(String name, Request& request) { DValue props; return(component(name, props, request)); }
String component(String name, DValue props) { return(component(name, props, *context)); }
void unit_render(String file_name, Request& request)
{
String resolved;
s32 slot = wasm_resolve_target(trim(file_name), WASM_RESOLVE_RENDER, &resolved);
if(!slot)
{
print(component_error_banner("unit not found: " + trim(file_name)));
return;
}
wasm_run_once(resolved, request);
String previous_unit = request.resources.current_unit_file;
if(resolved != "")
request.resources.current_unit_file = resolved;
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
handler(request);
request.resources.current_unit_file = previous_unit;
}
void unit_render(String file_name) { unit_render(file_name, *context); }
extern "C" {
// Host calls this to render the request's entry unit. Routing through
// unit_render gives the entry the same ONCE() + dispatch semantics as
// components (the host pre-loaded it, so resolution is a cache hit).
void uce_wasm_render_entry(const char* path, size_t len)
{
// the host always runs uce_wasm_core_init + apply_context before this
unit_render(String(path, len), *context);
}
void* uce_alloc(size_t len)
{
return(malloc(len));
}
void uce_free(void* ptr)
{
free(ptr);
}
u32 uce_wasm_core_abi_version()
{
return(6);
}
int uce_wasm_core_init()
{
wasm_server.config = default_config();
wasm_request.server = &wasm_server;
// the primary output stream must live ON ob_stack (native semantics):
// ob_get_close()/ob_close() pop and rebalance against the stack, so a
// stream outside it would be orphaned by the first component() capture
if(wasm_request.ob_stack.empty())
wasm_request.ob_start();
context = &wasm_request;
return(0);
}
void uce_wasm_core_reset_request()
{
if(context == 0)
uce_wasm_core_init();
wasm_request.call = DValue();
wasm_request.props = DValue();
wasm_request.params.clear();
wasm_request.get.clear();
wasm_request.post.clear();
wasm_request.header.clear();
wasm_request.set_cookies.clear();
wasm_request.response_code = "HTTP/1.1 200 OK";
wasm_request.flags = Request::Flags();
wasm_request.stats = Request::Stats();
for(auto* stream : wasm_request.ob_stack)
delete stream;
wasm_request.ob_stack.clear();
wasm_request.ob_start();
wasm_output = "";
wasm_response_meta = "";
wasm_request.cookies.clear();
wasm_request.session.clear();
wasm_request.out = "";
wasm_request.resources.current_unit_file = "";
wasm_component_slots.clear();
}
// Host pushes the UCEB1-encoded request context into a guest buffer
// (uce_alloc) and applies it here; mirrors the native param population.
int uce_wasm_apply_context(const char* buf, size_t len)
{
if(context == 0)
uce_wasm_core_init();
DValue decoded;
String error;
if(!ucb_decode(String(buf, len), decoded, &error))
{
uce_host_log(3, error.data(), error.size());
return(1);
}
wasm_request.call = decoded;
auto apply_map = [](DValue* source, StringMap& dest) {
dest.clear();
if(source)
source->each([&](const DValue& item, String key) {
dest[key] = item.to_string();
});
};
apply_map(decoded.key("params"), wasm_request.params);
apply_map(decoded.key("get"), wasm_request.get);
apply_map(decoded.key("post"), wasm_request.post);
apply_map(decoded.key("cookies"), wasm_request.cookies);
apply_map(decoded.key("session"), wasm_request.session);
DValue* entry = decoded.key("entry_unit");
if(entry)
wasm_request.resources.current_unit_file = entry->to_string();
return(0);
}
Request* uce_wasm_request()
{
if(context == 0)
uce_wasm_core_init();
return(&wasm_request);
}
// After render: response metadata (status line, headers, cookies, session)
// goes back to the host as UCEB1.
void uce_wasm_finish_response_meta()
{
DValue meta;
meta["status"] = wasm_request.response_code;
for(auto& header : wasm_request.header)
meta["headers"][header.first] = header.second;
for(auto& cookie : wasm_request.set_cookies)
{
DValue cookie_value;
cookie_value = cookie;
meta["cookies"].push(cookie_value);
}
for(auto& entry : wasm_request.session)
meta["session"][entry.first] = entry.second;
wasm_response_meta = ucb_encode(meta);
}
const char* uce_wasm_response_meta_data()
{
return(wasm_response_meta.data());
}
size_t uce_wasm_response_meta_size()
{
return(wasm_response_meta.size());
}
void uce_print_bytes(const char* data, size_t len)
{
if(context == 0)
uce_wasm_core_init();
if(context->ob && data && len)
context->ob->write(data, len);
}
void uce_wasm_finish_output()
{
// ob_stack[0] is the request's primary stream; nested captures above it
// belong to unbalanced ob_start() calls and are intentionally ignored
wasm_output = wasm_request.ob_stack.empty() ? String("") : wasm_request.ob_stack[0]->str();
}
const char* uce_wasm_output_data()
{
return(wasm_output.data());
}
size_t uce_wasm_output_size()
{
return(wasm_output.size());
}
}
+16
View File
@@ -0,0 +1,16 @@
uce_host_time
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
uce_host_file_write
uce_host_file_unlink
uce_host_regex
uce_host_sqlite
+35
View File
@@ -0,0 +1,35 @@
atof
atoi
atol
atoll
strtol
strtoul
strtoll
strtoull
strtod
strtof
qsort
bsearch
snprintf
sscanf
memmove
memchr
strncmp
strncpy
strchr
strrchr
strstr
isalnum
isalpha
isblank
iscntrl
isdigit
isgraph
islower
isprint
ispunct
isspace
isupper
isxdigit
tolower
toupper
+308
View File
@@ -0,0 +1,308 @@
// W1 smoke driver for the production UCE core.wasm.
#include <wasm.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <vector>
#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<uint8_t> read_file(const char* path)
{
FILE* f = fopen(path, "rb");
CHECK(f, "cannot open %s", path);
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
std::vector<uint8_t> data((size_t)n);
CHECK(fread(data.data(), 1, data.size(), f) == data.size(), "short read on %s", path);
fclose(f);
return(data);
}
static std::string wasm_name(const wasm_name_t* name)
{
std::string s(name->data, name->size);
while(!s.empty() && s.back() == '\0') s.pop_back();
return(s);
}
static wasm_store_t* g_store_for_traps = nullptr;
static void set_i32(wasm_val_t* result, int32_t value)
{
result->kind = WASM_I32;
result->of.i32 = value;
}
static void set_i64(wasm_val_t* result, int64_t value)
{
result->kind = WASM_I64;
result->of.i64 = value;
}
static void set_f64(wasm_val_t* result, double value)
{
result->kind = WASM_F64;
result->of.f64 = value;
}
static wasm_trap_t* host_time(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
{
set_i64(&results->data[0], 1700000000);
return(nullptr);
}
static wasm_trap_t* host_time_precise(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
{
set_f64(&results->data[0], 1700000000.25);
return(nullptr);
}
static wasm_trap_t* host_env(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
{
set_i32(&results->data[0], 0);
return(nullptr);
}
static wasm_memory_t* g_memory = nullptr;
static wasm_trap_t* host_random(void*, const wasm_val_vec_t* args, wasm_val_vec_t* results)
{
uint32_t ptr = args->data[0].of.i32;
uint32_t len = args->data[1].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)
{
set_i32(&results->data[0], 0);
return(nullptr);
}
for(uint32_t i = 0; i < len; ++i)
mem[ptr + i] = (uint8_t)(0x5au ^ (i * 29u));
set_i32(&results->data[0], len);
return(nullptr);
}
static wasm_trap_t* host_log(void*, const wasm_val_vec_t*, wasm_val_vec_t*)
{
return(nullptr);
}
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t*, wasm_val_vec_t*)
{
std::string label = (const char*)env;
std::string msg = "unexpected import called: " + label;
wasm_byte_vec_t message;
wasm_byte_vec_new(&message, msg.size(), msg.data());
wasm_trap_t* trap = wasm_trap_new(g_store_for_traps, &message);
wasm_byte_vec_delete(&message);
return(trap);
}
struct Instance
{
wasm_module_t* module = nullptr;
wasm_instance_t* instance = nullptr;
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
std::map<std::string, wasm_extern_t*> 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 count mismatch");
for(size_t i = 0; i < types.size; ++i)
by_name[wasm_name(wasm_exporttype_name(types.data[i]))] = 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_memory_t* memory()
{
auto it = by_name.find("memory");
return(it == by_name.end() ? nullptr : wasm_extern_as_memory(it->second));
}
};
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<int32_t> argv = {})
{
wasm_func_t* f = inst.func(name);
CHECK(f, "missing function %s", name);
CHECK(argv.size() <= 4, "too many args for %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 result_buf[1] = { WASM_INIT_VAL };
wasm_val_vec_t args = { argv.size(), args_buf };
wasm_val_vec_t results = { 1, result_buf };
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
wasm_trap_t* trap = wasm_func_call(f, &args, wasm_func_result_arity(f) ? &results : &no_results);
report_trap(trap, name);
return(wasm_func_result_arity(f) ? result_buf[0].of.i32 : 0);
}
static void write_bytes(wasm_memory_t* memory, uint32_t ptr, const std::string& data)
{
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
size_t mem_size = wasm_memory_data_size(memory);
CHECK((size_t)ptr + data.size() <= mem_size, "write outside memory");
memcpy(mem + ptr, data.data(), data.size());
}
static std::string read_bytes(wasm_memory_t* memory, uint32_t ptr, uint32_t len)
{
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
size_t mem_size = wasm_memory_data_size(memory);
CHECK((size_t)ptr + len <= mem_size, "read outside memory");
return(std::string((const char*)mem + ptr, len));
}
static std::string read_cstr(wasm_memory_t* memory, uint32_t ptr, uint32_t cap = 4096)
{
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
size_t mem_size = wasm_memory_data_size(memory);
CHECK(ptr < mem_size, "cstr starts outside memory");
std::string out;
for(uint32_t i = 0; i < cap && (size_t)ptr + i < mem_size; ++i)
{
if(mem[ptr + i] == 0)
return(out);
out.push_back((char)mem[ptr + i]);
}
FAIL("unterminated cstr");
}
static uint32_t read_u32(wasm_memory_t* memory, uint32_t ptr)
{
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
size_t mem_size = wasm_memory_data_size(memory);
CHECK((size_t)ptr + 4 <= mem_size, "u32 read outside memory");
return((uint32_t)mem[ptr] | ((uint32_t)mem[ptr + 1] << 8) | ((uint32_t)mem[ptr + 2] << 16) | ((uint32_t)mem[ptr + 3] << 24));
}
int main(int argc, char** argv)
{
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-w1/core.wasm";
wasm_engine_t* engine = wasm_engine_new();
CHECK(engine, "engine");
wasm_store_t* store = wasm_store_new(engine);
CHECK(store, "store");
g_store_for_traps = store;
std::vector<uint8_t> 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(store, &bv);
wasm_byte_vec_delete(&bv);
CHECK(core.module, "module load");
wasm_importtype_vec_t imports = WASM_EMPTY_VEC;
wasm_module_imports(core.module, &imports);
std::vector<wasm_extern_t*> import_externs(imports.size);
for(size_t i = 0; i < imports.size; ++i)
{
std::string mod = wasm_name(wasm_importtype_module(imports.data[i]));
std::string name = wasm_name(wasm_importtype_name(imports.data[i]));
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());
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
wasm_func_t* fn = nullptr;
if(mod == "env" && name == "uce_host_time") fn = wasm_func_new_with_env(store, ft, host_time, nullptr, nullptr);
else if(mod == "env" && name == "uce_host_time_precise") fn = wasm_func_new_with_env(store, ft, host_time_precise, nullptr, nullptr);
else if(mod == "env" && name == "uce_host_env") fn = wasm_func_new_with_env(store, ft, host_env, nullptr, nullptr);
else if(mod == "env" && name == "uce_host_random") fn = wasm_func_new_with_env(store, ft, host_random, nullptr, nullptr);
else if(mod == "env" && name == "uce_host_log") fn = wasm_func_new_with_env(store, ft, host_log, nullptr, nullptr);
else if(mod == "wasi_snapshot_preview1")
{
char* label = strdup((mod + "." + name).c_str());
fn = wasm_func_new_with_env(store, ft, stub_callback, label, nullptr);
}
else
FAIL("unexpected core import %s.%s", mod.c_str(), name.c_str());
CHECK(fn, "import function %s.%s", mod.c_str(), name.c_str());
import_externs[i] = wasm_func_as_extern(fn);
}
wasm_extern_vec_t iv = { import_externs.size(), import_externs.data() };
wasm_trap_t* trap = nullptr;
core.instance = wasm_instance_new(store, core.module, &iv, &trap);
report_trap(trap, "core instantiation");
CHECK(core.instance, "core instantiate");
core.index_exports();
CHECK(core.memory(), "core exports memory");
g_memory = core.memory();
if(core.func("_initialize")) call_i32(core, "_initialize");
CHECK(call_i32(core, "uce_wasm_core_init") == 0, "core init failed");
call_i32(core, "uce_wasm_core_reset_request");
CHECK(call_i32(core, "uce_wasm_core_abi_version") == 6, "unexpected ABI version");
wasm_memory_t* memory = core.memory();
int32_t root = call_i32(core, "uce_dv_root");
CHECK(root != 0, "uce_dv_root returned null");
std::string key = "message";
std::string value = "hello from W1 core";
int32_t key_ptr = call_i32(core, "uce_alloc", { (int32_t)key.size() });
int32_t value_ptr = call_i32(core, "uce_alloc", { (int32_t)value.size() });
write_bytes(memory, key_ptr, key);
write_bytes(memory, value_ptr, value);
int32_t child = call_i32(core, "uce_dv_get", { root, key_ptr, (int32_t)key.size() });
CHECK(child != 0, "uce_dv_get returned null");
call_i32(core, "uce_dv_set_value", { child, value_ptr, (int32_t)value.size() });
CHECK(call_i32(core, "uce_dv_find", { root, key_ptr, (int32_t)key.size() }) == child, "uce_dv_find mismatch");
CHECK(call_i32(core, "uce_dv_count", { root }) == 1, "root count mismatch");
CHECK(call_i32(core, "uce_dv_is_list", { root }) == 0, "root unexpectedly list-shaped");
int32_t value_len_ptr = call_i32(core, "uce_alloc", { 4 });
int32_t value_result_ptr = call_i32(core, "uce_dv_value", { child, value_len_ptr });
uint32_t value_result_len = read_u32(memory, value_len_ptr);
CHECK(read_bytes(memory, value_result_ptr, value_result_len) == value, "uce_dv_value mismatch");
int32_t encoded_len = call_i32(core, "uce_dv_encode", { root, 0, 0 });
CHECK(encoded_len > 5, "encoded length too small");
int32_t encoded_ptr = call_i32(core, "uce_alloc", { encoded_len });
CHECK(call_i32(core, "uce_dv_encode", { root, encoded_ptr, encoded_len }) == encoded_len, "encode length mismatch");
std::string encoded = read_bytes(memory, encoded_ptr, encoded_len);
CHECK(encoded.rfind("UCEB\x01", 0) == 0, "UCEB1 header missing");
int32_t decoded = call_i32(core, "uce_dv_decode", { encoded_ptr, encoded_len });
CHECK(decoded != 0, "uce_dv_decode failed");
CHECK(call_i32(core, "uce_dv_count", { decoded }) == 1, "decoded root count mismatch");
int32_t last_error_ptr = call_i32(core, "uce_dv_last_error");
CHECK(last_error_ptr != 0, "uce_dv_last_error returned null");
CHECK(read_cstr(memory, last_error_ptr) == "", "last error not clear after successful decode");
std::string bad = "bad";
int32_t bad_ptr = call_i32(core, "uce_alloc", { (int32_t)bad.size() });
write_bytes(memory, bad_ptr, bad);
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB1 decode unexpectedly succeeded");
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB1 decode did not set error");
std::string out = "W1 output";
int32_t out_ptr = call_i32(core, "uce_alloc", { (int32_t)out.size() });
write_bytes(memory, out_ptr, out);
call_i32(core, "uce_print_bytes", { out_ptr, (int32_t)out.size() });
call_i32(core, "uce_wasm_finish_output");
int32_t output_len = call_i32(core, "uce_wasm_output_size");
int32_t output_ptr = call_i32(core, "uce_wasm_output_data");
CHECK(read_bytes(memory, output_ptr, output_len) == out, "output plumbing mismatch");
printf("W1 core.wasm smoke: abi=6 encoded=%d output=%d\n", encoded_len, output_len);
printf("W1 EXIT CRITERION: PASS\n");
return(0);
}
+184
View File
@@ -0,0 +1,184 @@
// W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3.
//
// Serves real requests through the production workspace runtime
// (src/wasm/worker.cpp): UCEB1 context in → core + lazily loaded real
// generated units → body/response-meta out. Each --repeat gets a fresh
// workspace, proving birth/drop. An epoch ticker thread enforces the CPU
// budget; the store limiter enforces memory; traps come back as collapsed
// wasm_trace summaries.
//
// Amalgamation TU (project style, like uce_lib.cpp): native types + DValue
// codec first, then the worker.
// same order as uce_lib.cpp, minus the native compiler/connector block
#include "../lib/types.cpp"
#include "../lib/dvalue.cpp"
#include "../lib/functionlib.cpp"
#include "../lib/hash.cpp"
#include "../lib/sys.cpp"
#include "../lib/uri.cpp"
#include "../lib/cli.cpp"
#include "../lib/wasm_trace.h"
#include "worker.cpp"
#include <atomic>
#include <thread>
static String arg_value(int argc, char** argv, int& i, const char* flag)
{
if(i + 1 >= argc)
{
fprintf(stderr, "missing value for %s\n", flag);
exit(2);
}
return(String(argv[++i]));
}
static String absolute_path(String path)
{
if(path.rfind("/", 0) == 0)
return(path);
char buf[4096];
if(!getcwd(buf, sizeof(buf)))
return(path);
return(String(buf) + "/" + path);
}
int main(int argc, char** argv)
{
WasmWorkerConfig cfg;
cfg.site_root = absolute_path("site");
String page;
String route;
StringMap params;
StringMap get_params;
std::vector<String> expects;
String expect_status;
int repeat = 1;
u64 epoch_period_ms = 50;
bool show_body = true;
for(int i = 1; i < argc; i++)
{
String arg = argv[i];
if(arg == "--core") cfg.core_wasm_path = arg_value(argc, argv, i, "--core");
else if(arg == "--site") cfg.site_root = absolute_path(arg_value(argc, argv, i, "--site"));
else if(arg == "--cache") cfg.cache_root = arg_value(argc, argv, i, "--cache");
else if(arg == "--page") page = arg_value(argc, argv, i, "--page");
else if(arg == "--route") route = arg_value(argc, argv, i, "--route");
else if(arg == "--repeat") repeat = atoi(arg_value(argc, argv, i, "--repeat").c_str());
else if(arg == "--expect") expects.push_back(arg_value(argc, argv, i, "--expect"));
else if(arg == "--expect-status") expect_status = arg_value(argc, argv, i, "--expect-status");
else if(arg == "--epoch-ticks") cfg.epoch_deadline_ticks = strtoull(arg_value(argc, argv, i, "--epoch-ticks").c_str(), 0, 10);
else if(arg == "--epoch-ms") epoch_period_ms = strtoull(arg_value(argc, argv, i, "--epoch-ms").c_str(), 0, 10);
else if(arg == "--mem-limit") cfg.memory_limit = strtoll(arg_value(argc, argv, i, "--mem-limit").c_str(), 0, 10);
else if(arg == "--table-headroom") cfg.table_headroom = (u32)atoi(arg_value(argc, argv, i, "--table-headroom").c_str());
else if(arg == "--quiet") show_body = false;
else if(arg == "--verbose") cfg.verbose = true;
else if(arg == "--param" || arg == "--get")
{
String pair = arg_value(argc, argv, i, arg.c_str());
auto eq = pair.find("=");
if(eq == String::npos)
{
fprintf(stderr, "%s expects K=V\n", arg.c_str());
return(2);
}
(arg == "--param" ? params : get_params)[pair.substr(0, eq)] = pair.substr(eq + 1);
}
else
{
fprintf(stderr, "unknown argument: %s\n", arg.c_str());
return(2);
}
}
if(page == "")
{
fprintf(stderr, "usage: w3_driver --page /demo/hello.uce [--site site] [--cache /tmp/uce/work]\n"
" [--core bin/wasm/core.wasm] [--param K=V ...] [--get K=V ...] [--route path]\n"
" [--repeat N] [--expect STR ...] [--expect-status STR] [--quiet] [--verbose]\n"
" [--epoch-ticks N] [--epoch-ms N] [--mem-limit BYTES]\n");
return(2);
}
String entry_unit = cfg.site_root + page;
WasmWorker worker(cfg);
String init_error = worker.init();
if(init_error != "")
{
fprintf(stderr, "FAIL: %s\n", init_error.c_str());
return(1);
}
// the epoch only advances through this ticker; period × deadline-ticks
// is the per-request CPU budget
std::atomic<bool> running(true);
std::thread ticker([&] {
while(running.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(epoch_period_ms));
worker.engine.increment_epoch();
}
});
DValue context_tree;
for(auto& entry : params)
context_tree["params"][entry.first] = entry.second;
if(context_tree.key("params") == 0 || !params.count("HTTP_HOST"))
context_tree["params"]["HTTP_HOST"] = "w3.test";
context_tree["params"]["SCRIPT_URL"] = page;
context_tree["params"]["REQUEST_METHOD"] = "GET";
for(auto& entry : get_params)
context_tree["get"][entry.first] = entry.second;
if(route != "")
{
context_tree["route"]["l_path"] = route;
context_tree["params"]["ROUTE_PATH"] = route;
}
context_tree["entry_unit"] = entry_unit;
bool all_ok = true;
WasmResponse last;
for(int request = 0; request < repeat && all_ok; request++)
{
f64 started = (f64)clock() / CLOCKS_PER_SEC;
last = wasm_worker_serve(worker, context_tree, entry_unit);
f64 elapsed_ms = ((f64)clock() / CLOCKS_PER_SEC - started) * 1000.0;
printf("==== request %d/%d (%.1f ms cpu) ====\n", request + 1, repeat, elapsed_ms);
if(!last.ok)
{
printf("ERROR:\n%s\n", last.error.c_str());
all_ok = false;
break;
}
printf("status: %s\n", last.meta["status"].to_string().c_str());
if(last.meta.key("headers"))
last.meta["headers"].each([](const DValue& value, String key) {
printf("header: %s: %s\n", key.c_str(), value.to_string().c_str());
});
if(show_body)
printf("---- body (%zu bytes) ----\n%.*s\n----\n",
last.body.size(), (int)last.body.size(), last.body.data());
else
printf("body: %zu bytes\n", last.body.size());
}
running.store(false);
ticker.join();
if(all_ok && expect_status != "" && last.meta["status"].to_string().find(expect_status) == String::npos)
{
printf("FAIL: status %s does not contain %s\n", last.meta["status"].to_string().c_str(), expect_status.c_str());
all_ok = false;
}
for(auto& expect : expects)
if(all_ok && last.body.find(expect) == String::npos)
{
printf("FAIL: body does not contain %s\n", expect.c_str());
all_ok = false;
}
printf(all_ok ? "W3 RESULT: PASS\n" : "W3 RESULT: FAIL\n");
return(all_ok ? 0 : 1);
}
+1441
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
import os
def register(registry):
# W4/W5 kill pages are only meaningful with the wasm backend enabled. Native
# requests may terminate the worker by design, so keep them out of the normal
# native suite unless the W5 harness opts in explicitly.
if os.environ.get("UCE_INCLUDE_WASM_KILL") != "1":
return
pages = [
# oob is __builtin_trap() → wasm `unreachable`, a signal-delivering trap.
# It crashed the worker until signals_based_traps(false) (see make_engine
# in src/wasm/worker.cpp); keeping it in the gate guards that fix.
("wasm kill trap", "/tests/wasm-kill/oob.uce", "unreachable"),
("wasm kill loop", "/tests/wasm-kill/loop.uce", "interrupt"),
("wasm kill recurse", "/tests/wasm-kill/recurse.uce", "wasm_kill_recurse"),
]
for name, path, marker in pages:
def make_case(page_path=path, expected_marker=marker):
def run(context):
response = context.expect_status(page_path, 500)
context.expect_body_contains(response, "wasm runtime error during request")
context.expect_body_contains(response, expected_marker)
# The worker should remain healthy after the trap.
health = context.expect_status("/demo/hello.uce", 200)
context.expect_body_contains(health, "hello world")
return "clean wasm trap page and post-trap health check for %s" % page_path
return run
registry.case(name, make_case(), tags=["http", "uce", "wasm", "kill", "internal"])
@@ -122,6 +122,8 @@ def main() -> int:
parser = argparse.ArgumentParser(description="Phase 5 native/wasm benchmark harness")
parser.add_argument("--native-base-url", default="http://localhost:80")
parser.add_argument("--wasm-base-url", default="", help="optional wasm worker URL; omitted until worker exists")
parser.add_argument("--backend-label", default="native", help="label for --native-base-url measurements when running one backend at a time")
parser.add_argument("--compare-native-json", default="", help="optional prior native benchmark.json for budget comparison")
parser.add_argument("--host-header", default="uce.openfu.com")
parser.add_argument("--warmups", type=int, default=2)
parser.add_argument("--samples", type=int, default=20)
@@ -130,7 +132,11 @@ def main() -> int:
args = parser.parse_args()
targets = build_targets()
results = measure_backend("native", args.native_base_url, args.host_header, targets, args.warmups, args.samples, args.timeout)
results: list[Measurement] = []
if args.compare_native_json:
for row in json.loads(Path(args.compare_native_json).read_text()):
results.append(Measurement(**row))
results.extend(measure_backend(args.backend_label, args.native_base_url, args.host_header, targets, args.warmups, args.samples, args.timeout))
if args.wasm_base_url:
results.extend(measure_backend("wasm", args.wasm_base_url, args.host_header, targets, args.warmups, args.samples, args.timeout))
@@ -138,7 +144,7 @@ def main() -> int:
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "benchmark.json").write_text(json.dumps([asdict(r) for r in results], indent=2) + "\n")
md_lines = ["# Phase 5 benchmark report", "", *compare(results), ""]
if not args.wasm_base_url:
if not args.wasm_base_url and args.backend_label == "native" and not args.compare_native_json:
md_lines.append("WASM worker URL was not provided; this report is the native baseline that future wasm runs compare against.")
(out_dir / "benchmark.md").write_text("\n".join(md_lines) + "\n")
print("\n".join(md_lines))
@@ -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 = [