diff --git a/WASM-PROPOSAL.md b/WASM-PROPOSAL.md index 7f7fd2d..ea5978f 100644 --- a/WASM-PROPOSAL.md +++ b/WASM-PROPOSAL.md @@ -1,10 +1,13 @@ # WASM-PROPOSAL: WebAssembly Unit Runtime for UCE -- **Status:** design guide; Phase 0–4 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), and W3 (workspace runtime + + membrane, `src/wasm/worker.cpp`) are **done**: real pages — including the + starter views — render through per-request wasm workspaces with lazy + mid-request component loading, epoch/memory limits, and trap summaries. + Current phase: **W4 — the FastCGI worker backend.** The cutover bar + (`--match starter` + full suite + kill-tests + budgets) is already + executable and green against the native backend. - **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, @@ -630,6 +633,186 @@ 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 1–7: 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 W2–W4 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. + +**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 2–3 — harness fully green against the wasm +worker URL, config default flips to wasm, native stays in-tree as +reference. + +**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()`. + --- ## 10. Risks & mitigations diff --git a/etc/uce/settings.cfg b/etc/uce/settings.cfg index 70123c6..6d04029 100644 --- a/etc/uce/settings.cfg +++ b/etc/uce/settings.cfg @@ -21,6 +21,10 @@ 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 + # ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP PROACTIVE_COMPILE_ENABLED=1 diff --git a/scripts/build_core_wasm.sh b/scripts/build_core_wasm.sh new file mode 100755 index 0000000..2ae8a0b --- /dev/null +++ b/scripts/build_core_wasm.sh @@ -0,0 +1,30 @@ +#!/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,--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 diff --git a/scripts/compile b/scripts/compile index 50d9272..4000ce6 100755 --- a/scripts/compile +++ b/scripts/compile @@ -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\"" diff --git a/scripts/compile_wasm_unit b/scripts/compile_wasm_unit new file mode 100755 index 0000000..aaef0ef --- /dev/null +++ b/scripts/compile_wasm_unit @@ -0,0 +1,93 @@ +#!/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 + # 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" <&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" diff --git a/scripts/wasm/build_w2_units.sh b/scripts/wasm/build_w2_units.sh new file mode 100755 index 0000000..20a3955 --- /dev/null +++ b/scripts/wasm/build_w2_units.sh @@ -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" diff --git a/scripts/wasm/build_w3_driver.sh b/scripts/wasm/build_w3_driver.sh new file mode 100644 index 0000000..db27714 --- /dev/null +++ b/scripts/wasm/build_w3_driver.sh @@ -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" diff --git a/scripts/wasm/check_unit_wasm.py b/scripts/wasm/check_unit_wasm.py new file mode 100755 index 0000000..81496ab --- /dev/null +++ b/scripts/wasm/check_unit_wasm.py @@ -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()) diff --git a/site/doc/pages/3_C++ Preprocessor.txt b/site/doc/pages/3_C++ Preprocessor.txt index ce54879..bb0d8e9 100644 --- a/site/doc/pages/3_C++ Preprocessor.txt +++ b/site/doc/pages/3_C++ Preprocessor.txt @@ -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. diff --git a/src/lib/compiler-parser.cpp b/src/lib/compiler-parser.cpp index cde4159..4460c14 100644 --- a/src/lib/compiler-parser.cpp +++ b/src/lib/compiler-parser.cpp @@ -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"] )+ diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index 7ef652a..e5469e2 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -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,15 @@ 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)) + su->compiler_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(su->compiler_messages.length() > 0) { String raw_messages = su->compiler_messages; @@ -1171,6 +1200,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; diff --git a/src/lib/functionlib.cpp b/src/lib/functionlib.cpp index 0fbbeed..c1b1daa 100644 --- a/src/lib/functionlib.cpp +++ b/src/lib/functionlib.cpp @@ -1,7 +1,9 @@ #include "functionlib.h" +#ifndef __UCE_WASM_CORE__ #define PCRE2_CODE_UNIT_WIDTH 8 #include +#endif #include #include #include @@ -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,47 @@ StringList regex_split(String pattern, String subject, String flags) return(result); } +#else + +bool regex_match(String pattern, String subject, String flags) +{ + (void)pattern; (void)subject; (void)flags; + return(false); +} + +DValue regex_search(String pattern, String subject, String flags) +{ + DValue result; + result["matched"].set_bool(false); + result["pattern"] = pattern; + result["flags"] = flags == "" ? "default" : flags; + return(result); +} + +DValue regex_search_all(String pattern, String subject, String flags) +{ + DValue result; + result["matched"].set_bool(false); + result["pattern"] = pattern; + result["flags"] = flags == "" ? "default" : flags; + result["count"] = (f64)0; + return(result); +} + +String regex_replace(String pattern, String replacement, String subject, String flags) +{ + (void)pattern; (void)replacement; (void)flags; + return(subject); +} + +StringList regex_split(String pattern, String subject, String flags) +{ + (void)pattern; (void)flags; + return(StringList{ subject }); +} + +#endif + String trim(String raw) { s64 len = raw.length(); @@ -1323,7 +1367,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 +1905,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 diff --git a/src/lib/functionlib.h b/src/lib/functionlib.h index 120e47e..d9c682f 100644 --- a/src/lib/functionlib.h +++ b/src/lib/functionlib.h @@ -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 -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 -std::vector filter(std::vector items, F f) +inline std::vector filter(std::vector items, F f) { std::vector new_items; for(auto item : items) @@ -78,7 +81,7 @@ std::vector filter(std::vector items, F f) } template -auto map(std::vector items, F f) +inline auto map(std::vector items, F f) { using ResultType = decltype(f(items[0])); std::vector new_items; @@ -101,7 +104,7 @@ DValue dv_filter(DValue tree, std::function f); DValue dv_group_by(DValue tree, std::function f); template -String first(Args... args) +inline String first(Args... args) { std::vector vec = {args...}; for(auto s : vec) diff --git a/src/lib/hash.cpp b/src/lib/hash.cpp index 85351df..71c8505 100644 --- a/src/lib/hash.cpp +++ b/src/lib/hash.cpp @@ -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 +typedef uint32_t u_int32_t; +#endif + typedef struct { u_int32_t state[5]; u_int32_t count[2]; diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp index 4702489..df3fe89 100644 --- a/src/lib/sys.cpp +++ b/src/lib/sys.cpp @@ -1,3 +1,119 @@ +#ifdef __UCE_WASM_CORE__ +#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); +} + +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) { (void)file_name; (void)content; return(false); } +bool file_append_contents(String file_name, String content) { (void)file_name; (void)content; return(false); } +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) { (void)file_name; } +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()); } +String time_format_local(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); } +String time_format_utc(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); } +String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) { (void)format_very_recent; (void)medium_recency_seconds; (void)format_medium_recent; (void)not_recent_seconds; (void)format_not_recent; return(std::to_string(timestamp)); } +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; } +int task_kill(pid_t pid, int sig) { (void)pid; (void)sig; return(-1); } +String runtime_safe_key(String key, String label) { (void)label; return(key); } +pid_t task(String key, std::function exec_after_spawn, u64 timeout) { (void)key; (void)exec_after_spawn; (void)timeout; return(0); } +pid_t task_repeat(String key, f64 interval, std::function exec_after_spawn, u64 timeout) { (void)key; (void)interval; (void)exec_after_spawn; (void)timeout; return(0); } +pid_t task_pid(String key) { (void)key; return(0); } +pid_t 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 #include #include @@ -942,6 +1058,8 @@ 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["SETUP_TEMPLATE"] = "scripts/setup.h.template"; cfg["LIT_ESC"] = "3d5b5_1"; cfg["CONTENT_TYPE"] = "text/html; charset=utf-8"; @@ -999,3 +1117,4 @@ StringMap make_server_settings() return(cfg); } +#endif diff --git a/src/lib/sys.h b/src/lib/sys.h index a24732f..2ea0a2f 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -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 #include +#endif +#include #include 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 -bool file_append(String file_name, Ts... args) +inline bool file_append(String file_name, Ts... args) { std::ostringstream out; ((out << args), ...); diff --git a/src/lib/types.cpp b/src/lib/types.cpp index 33fb047..71f190d 100644 --- a/src/lib/types.cpp +++ b/src/lib/types.cpp @@ -1,16 +1,17 @@ +#ifndef __UCE_WASM_CORE__ #include -#include #include +#include +#include +#endif +#include #include #include #include #include -#include #include -#include #include #include -#include #include #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 } diff --git a/src/lib/types.h b/src/lib/types.h index e0cfa57..18df2bd 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -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 +// NB: header templates must be inline — wasm units rely on +// -fvisibility-inlines-hidden binding instantiations locally (§6) template -void print(Ts... args) +inline void print(Ts... args) { ((*context->ob << args), ...); } template -void out(Ts... args) +inline void out(Ts... args) { ((*context->ob << args), ...); } template -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 diff --git a/src/lib/uce_lib.cpp b/src/lib/uce_lib.cpp index 0624a9e..dea421e 100644 --- a/src/lib/uce_lib.cpp +++ b/src/lib/uce_lib.cpp @@ -11,9 +11,12 @@ #include "sys.cpp" #include "uri.cpp" #include "cli.cpp" + +#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 diff --git a/src/lib/uri.cpp b/src/lib/uri.cpp index 85882d1..8e7d4e5 100644 --- a/src/lib/uri.cpp +++ b/src/lib/uri.cpp @@ -4,6 +4,10 @@ #include #include +#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) diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp new file mode 100644 index 0000000..d8a2ef8 --- /dev/null +++ b/src/wasm/core.cpp @@ -0,0 +1,412 @@ +// 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); } + +bool SQLite::connect(String path) { this->path = path; connection = 0; set_error(1, WASM_DB_UNAVAILABLE); return(false); } +void SQLite::disconnect() { connection = 0; } +String SQLite::error() { return(error_code ? String(WASM_DB_UNAVAILABLE) : String("")); } +DValue SQLite::query(String q) { (void)q; set_error(1, WASM_DB_UNAVAILABLE); return(DValue()); } +DValue SQLite::query(String q, const StringMap& params) { (void)q; (void)params; set_error(1, WASM_DB_UNAVAILABLE); return(DValue()); } +void SQLite::set_error(s32 code, String info) { error_code = code; statement_info = info; } +bool SQLite::apply_default_pragmas() { return(false); } +bool SQLite::bind_params(void* statement, const StringMap& params) { (void)statement; (void)params; return(false); } +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.". +extern "C" void uce_wasm_link_anchors() +{ + StringMap string_map; + string_map["k"] = "v"; + string_map.erase(String("k")); // __tree::__erase_unique + std::map dvalue_map; + dvalue_map["k"] = DValue(); + dvalue_map.erase(String("k")); + std::vector string_list = { "a", "b" }; + string_list.erase(string_list.begin()); + std::set 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, + }; + (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 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("
" + html_escape(message) + "
"); +} + +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 (see src/wasm/worker.h) +enum WasmResolveKind { WASM_RESOLVE_COMPONENT = 0, WASM_RESOLVE_RENDER = 1, WASM_RESOLVE_EXISTS = 2 }; + +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) != ""); +} + +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; + } + 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; + } + 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" { + +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()); +} + +} diff --git a/src/wasm/core_hostcalls.syms b/src/wasm/core_hostcalls.syms new file mode 100644 index 0000000..805c28a --- /dev/null +++ b/src/wasm/core_hostcalls.syms @@ -0,0 +1,8 @@ +uce_host_time +uce_host_time_precise +uce_host_env +uce_host_log +uce_host_random +uce_host_component_resolve +uce_host_file_exists +uce_host_file_read diff --git a/src/wasm/core_libc_exports.syms b/src/wasm/core_libc_exports.syms new file mode 100644 index 0000000..573457b --- /dev/null +++ b/src/wasm/core_libc_exports.syms @@ -0,0 +1,21 @@ +atof +atoi +atol +atoll +strtol +strtoul +strtoll +strtoull +strtod +strtof +qsort +bsearch +snprintf +sscanf +memmove +memchr +strncmp +strncpy +strchr +strrchr +strstr diff --git a/src/wasm/w1_smoke.cpp b/src/wasm/w1_smoke.cpp new file mode 100644 index 0000000..8b6ebc8 --- /dev/null +++ b/src/wasm/w1_smoke.cpp @@ -0,0 +1,308 @@ +// W1 smoke driver for the production UCE core.wasm. + +#include + +#include +#include +#include +#include +#include +#include + +#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0) +#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0) + +static std::vector read_file(const char* 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 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 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 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 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 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); +} diff --git a/src/wasm/w3_driver.cpp b/src/wasm/w3_driver.cpp new file mode 100644 index 0000000..467d152 --- /dev/null +++ b/src/wasm/w3_driver.cpp @@ -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 +#include + +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 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 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); +} diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp new file mode 100644 index 0000000..a4a270a --- /dev/null +++ b/src/wasm/worker.cpp @@ -0,0 +1,1074 @@ +// W3 — production wasm workspace runtime + membrane (WASM-PROPOSAL §6, §7). +// +// One workspace per request: a fresh Wasmtime store holding the core module +// instance (real uce_lib carve-out, owns memory/allocator/DValue) plus unit +// PIC side modules loaded lazily — including mid-request via the +// uce_host_component_resolve hostcall, which is how component()/unit_render() +// inside the guest trigger dynamic loading. +// +// Designed for amalgamation-include (like uce_lib.cpp): the including TU must +// provide String/DValue/ucb_encode/ucb_decode (types.cpp + dvalue.cpp) and +// wasm_trace.h. W4 includes this from the FastCGI worker build; the W3 CLI +// driver (w3_driver.cpp) is the first consumer. +// +// Loader rules carried from the spike phases, now enforced as policy: +// - import discipline: units may import only env / GOT.mem / GOT.func +// (no WASI — the core is zero-WASI for UCE's own calls, and units get +// everything from the core); units defining allocator symbols are rejected +// - GOT.mem of a PIC module's data exports are __memory_base-relative +// offsets; the loader adds the owning module's base (Phase 0 erratum) +// - GOT.func resolves host-side: the target function's funcref is placed +// into the shared table and the slot index becomes the GOT value +// - export name collisions: core wins, then first-loading unit wins +// (mirrors native dlopen global-symbol interposition for identical +// vague-linkage template instantiations) +// - uce.abi stamp must match the core ABI version; fail loudly, never guess + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct WasmDylinkInfo +{ + u32 mem_size = 0; + u32 mem_align = 0; + u32 table_size = 0; + u32 table_align = 0; + bool found = false; +}; + +struct WasmAbiInfo +{ + u32 version = 0; + String toolchain; + bool found = false; +}; + +struct WasmUnitModule +{ + String source_path; // absolute .uce path + String wasm_path; // artifact in the unit cache + time_t mtime = 0; + WasmDylinkInfo dylink; + WasmAbiInfo abi; + std::optional module; +}; + +struct WasmWorkerConfig +{ + String core_wasm_path = "bin/wasm/core.wasm"; + String site_root; // absolute + String cache_root = "/tmp/uce/work"; + int64_t memory_limit = 512ll * 1024 * 1024; + u32 table_headroom = 4096; + u64 epoch_deadline_ticks = 200; // ticker period × ticks = CPU budget + bool verbose = false; +}; + +struct WasmResponse +{ + bool ok = false; + String body; + DValue meta; // status / headers / cookies / session + String error; // collapsed trace or loader error when !ok +}; + +// ---- module byte parsing (hardened; carried from the phase 3 spike) ------- + +static bool wasm_read_uleb(const std::vector& buf, size_t& pos, size_t end, u64& out) +{ + out = 0; + u32 shift = 0; + while(true) + { + if(pos >= end || shift >= 64) + return(false); + u8 byte = buf[pos++]; + out |= ((u64)(byte & 0x7f)) << shift; + if((byte & 0x80) == 0) + return(true); + shift += 7; + } +} + +static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dylink, WasmAbiInfo& abi, String& error) +{ + if(bytes.size() < 8 || memcmp(bytes.data(), "\0asm", 4) != 0) + { + error = "not a wasm module"; + return(false); + } + if(!(bytes[4] == 1 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0)) + { + error = "unsupported wasm binary version"; + return(false); + } + size_t pos = 8; + while(pos < bytes.size()) + { + u8 section_id = bytes[pos++]; + u64 size = 0; + if(!wasm_read_uleb(bytes, pos, bytes.size(), size) || size > bytes.size() - pos) + { + error = "malformed wasm section header"; + return(false); + } + size_t end = pos + (size_t)size; + if(section_id == 0) + { + u64 name_len = 0; + size_t cursor = pos; + if(!wasm_read_uleb(bytes, cursor, end, name_len) || name_len > end - cursor) + { + error = "malformed custom section name"; + return(false); + } + String name((const char*)bytes.data() + cursor, (size_t)name_len); + cursor += (size_t)name_len; + if(name == "dylink.0") + { + while(cursor < end) + { + u8 sub = bytes[cursor++]; + u64 sub_len = 0; + if(!wasm_read_uleb(bytes, cursor, end, sub_len) || sub_len > end - cursor) + { + error = "malformed dylink.0 subsection"; + return(false); + } + size_t sub_end = cursor + (size_t)sub_len; + if(sub == 1) // WASM_DYLINK_MEM_INFO + { + u64 v[4]; + for(int i = 0; i < 4; i++) + if(!wasm_read_uleb(bytes, cursor, sub_end, v[i])) + { + error = "malformed dylink.0 mem_info"; + return(false); + } + if(dylink.found) + { + error = "duplicate dylink.0 mem_info"; + return(false); + } + if(v[1] >= 31 || v[3] >= 31) + { + error = "unsupported dylink alignment"; + return(false); + } + dylink.mem_size = (u32)v[0]; + dylink.mem_align = (u32)v[1]; + dylink.table_size = (u32)v[2]; + dylink.table_align = (u32)v[3]; + dylink.found = true; + } + cursor = sub_end; + } + } + else if(name == "uce.abi") + { + String text((const char*)bytes.data() + cursor, end - cursor); + abi.found = true; + size_t line_start = 0; + while(line_start < text.size()) + { + size_t line_end = text.find('\n', line_start); + if(line_end == String::npos) + line_end = text.size(); + String line = text.substr(line_start, line_end - line_start); + if(line.rfind("unit_abi_version=", 0) == 0) + abi.version = (u32)strtoul(line.c_str() + 17, 0, 10); + if(line.rfind("toolchain=", 0) == 0) + abi.toolchain = line.substr(10); + line_start = line_end + 1; + } + } + } + pos = end; + } + return(true); +} + +static bool wasm_read_file(const String& path, std::vector& out) +{ + std::ifstream in(path, std::ios::binary); + if(!in) + return(false); + in.seekg(0, std::ios::end); + std::streamoff n = in.tellg(); + in.seekg(0, std::ios::beg); + out.resize((size_t)n); + in.read((char*)out.data(), n); + return((bool)in); +} + +// ---- worker (per process): engine + compiled module caches ---------------- + +class WasmWorker +{ +public: + WasmWorkerConfig cfg; + + explicit WasmWorker(WasmWorkerConfig config) : cfg(std::move(config)), engine(make_engine()) + { + } + + String init() + { + std::vector bytes; + if(!wasm_read_file(cfg.core_wasm_path, bytes)) + return("cannot read core module: " + cfg.core_wasm_path); + WasmAbiInfo abi_ignored; + String parse_error; + if(!wasm_parse_sections(bytes, core_dylink, abi_ignored, parse_error)) + return("core module: " + parse_error); + auto compiled = wasmtime::Module::compile(engine, bytes); + if(!compiled) + return("core module compile failed: " + String(compiled.err().message())); + core_module.emplace(compiled.ok()); + return(""); + } + + wasmtime::Engine engine; + std::optional core_module; + WasmDylinkInfo core_dylink; + + // unit artifact path in the W2 cache: cache_root + + /.wasm + String unit_wasm_path(const String& source_path) const + { + return(cfg.cache_root + source_path + ".wasm"); + } + + std::shared_ptr unit_module(const String& source_path, String& error) + { + String wasm_path = unit_wasm_path(source_path); + struct stat st; + if(stat(wasm_path.c_str(), &st) != 0) + { + error = "no wasm artifact for " + source_path + " (expected " + wasm_path + ")"; + return(nullptr); + } + auto cached = module_cache.find(wasm_path); + if(cached != module_cache.end() && cached->second->mtime == st.st_mtime) + return(cached->second); + + auto unit = std::make_shared(); + unit->source_path = source_path; + unit->wasm_path = wasm_path; + unit->mtime = st.st_mtime; + std::vector bytes; + if(!wasm_read_file(wasm_path, bytes)) + { + error = "cannot read " + wasm_path; + return(nullptr); + } + if(!wasm_parse_sections(bytes, unit->dylink, unit->abi, error)) + { + error = wasm_path + ": " + error; + return(nullptr); + } + if(!unit->dylink.found) + { + error = wasm_path + ": missing dylink.0 mem_info (not a PIC side module)"; + return(nullptr); + } + if(!unit->abi.found) + { + error = wasm_path + ": missing uce.abi stamp"; + return(nullptr); + } + auto compiled = wasmtime::Module::compile(engine, bytes); + if(!compiled) + { + error = wasm_path + ": compile failed: " + String(compiled.err().message()); + return(nullptr); + } + unit->module.emplace(compiled.ok()); + module_cache[wasm_path] = unit; + return(unit); + } + +private: + static wasmtime::Engine make_engine() + { + wasmtime::Config config; + config.epoch_interruption(true); + return(wasmtime::Engine(std::move(config))); + } + + std::map> module_cache; +}; + +// ---- workspace (per request) ---------------------------------------------- + +class WasmWorkspace +{ +public: + WasmWorker& worker; + wasmtime::Store store; + + explicit WasmWorkspace(WasmWorker& w) : worker(w), store(w.engine) + { + } + + // resolve-kind values shared with the guest core (src/wasm/core.cpp) + enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2 }; + + String birth() + { + auto cx = ctx(); + store.limiter(worker.cfg.memory_limit, -1, -1, -1, -1); + cx.set_epoch_deadline(worker.cfg.epoch_deadline_ticks); + + auto& module = *worker.core_module; + std::vector imports; + for(auto import_type : module.imports()) + { + String mod(import_type.module()); + String name(import_type.name()); + auto extern_type = wasmtime::ExternType::from_import(import_type); + if(auto* table_ref = std::get_if(&extern_type)) + { + if(name.rfind("__indirect_function_table", 0) != 0) + return("core imports unexpected table " + mod + "." + name); + u32 core_min = table_ref->min(); + u32 total = core_min + worker.cfg.table_headroom; + wasmtime::TableType table_type(wasmtime::ValType::funcref(), total, total); + auto created = wasmtime::Table::create(cx, table_type, wasmtime::Val(std::optional())); + if(!created) + return("table create failed: " + String(created.err().message())); + table.emplace(created.ok()); + table_next_free = core_min; + imports.push_back(*table); + continue; + } + auto* func_ref = std::get_if(&extern_type); + if(!func_ref) + return("core has unexpected non-func import " + mod + "." + name); + wasmtime::FuncType func_type(*func_ref); + imports.push_back(make_host_import(cx, mod, name, func_type)); + } + if(!table) + return("core does not import __indirect_function_table — rebuild core with --import-table"); + + auto created = wasmtime::Instance::create(cx, module, imports); + if(!created) + return("core instantiation failed: " + trap_text(created.err())); + core.emplace(created.ok()); + + auto exported_memory = core->get(cx, "memory"); + if(!exported_memory || !std::get_if(&*exported_memory)) + return("core does not export memory"); + memory.emplace(std::get(*exported_memory)); + + String error = call_core("_initialize", {}, 0); + if(error != "") + return(error); + int32_t rc = 0; + error = call_core("uce_wasm_core_init", {}, &rc); + if(error != "") + return(error); + if(rc != 0) + return("uce_wasm_core_init returned " + std::to_string(rc)); + int32_t core_abi = 0; + error = call_core("uce_wasm_core_abi_version", {}, &core_abi); + if(error != "") + return(error); + abi_version = (u32)core_abi; + error = call_core("uce_wasm_request", {}, &request_ptr); + if(error != "") + return(error); + if(request_ptr == 0) + return("core returned null Request*"); + return(""); + } + + String apply_context(const DValue& context_tree) + { + String encoded = ucb_encode(context_tree); + int32_t guest_ptr = 0; + String error = call_core("uce_alloc", { (int32_t)encoded.size() }, &guest_ptr); + if(error != "") + return(error); + if(guest_ptr == 0) + return("guest uce_alloc failed for context buffer"); + error = guest_write((u32)guest_ptr, encoded); + if(error != "") + return(error); + int32_t rc = 0; + error = call_core("uce_wasm_apply_context", { guest_ptr, (int32_t)encoded.size() }, &rc); + if(error != "") + return(error); + call_core("uce_free", { guest_ptr }, 0); + if(rc != 0) + return("uce_wasm_apply_context returned " + std::to_string(rc)); + return(""); + } + + String render_entry(const String& entry_source_path) + { + entry_dir = dir_of(entry_source_path); + size_t unit_index = 0; + String error = load_unit(entry_source_path, unit_index); + if(error != "") + return(error); + auto handler = unit_func(unit_index, "__uce_render"); + if(!handler) + return(entry_source_path + " does not export __uce_render"); + auto result = handler->call(ctx(), { wasmtime::Val(request_ptr) }); + if(!result) + return(trap_text(result.err())); + return(""); + } + + String collect(WasmResponse& response) + { + String error = call_core("uce_wasm_finish_output", {}, 0); + if(error != "") + return(error); + error = call_core("uce_wasm_finish_response_meta", {}, 0); + if(error != "") + return(error); + int32_t body_ptr = 0, body_len = 0, meta_ptr = 0, meta_len = 0; + if((error = call_core("uce_wasm_output_data", {}, &body_ptr)) != "") return(error); + if((error = call_core("uce_wasm_output_size", {}, &body_len)) != "") return(error); + if((error = call_core("uce_wasm_response_meta_data", {}, &meta_ptr)) != "") return(error); + if((error = call_core("uce_wasm_response_meta_size", {}, &meta_len)) != "") return(error); + error = guest_read((u32)body_ptr, (u32)body_len, response.body); + if(error != "") + return(error); + String meta_encoded; + error = guest_read((u32)meta_ptr, (u32)meta_len, meta_encoded); + if(error != "") + return(error); + String decode_error; + if(!ucb_decode(meta_encoded, response.meta, &decode_error)) + return("response meta decode failed: " + decode_error); + return(""); + } + +private: + std::optional core; + std::optional memory; + std::optional table; + u32 table_next_free = 0; + u32 abi_version = 0; + int32_t request_ptr = 0; + String entry_dir; + + struct LoadedUnit + { + std::shared_ptr mod; + std::optional instance; + u32 memory_base = 0; + }; + std::vector units; + std::map units_by_source; + std::map handler_slots; // source + ":" + symbol → table slot + std::vector host_funcs; // keep host imports alive + + wasmtime::Store::Context ctx() + { + return(wasmtime::Store::Context(store)); + } + + static String dir_of(const String& path) + { + auto pos = path.find_last_of('/'); + return(pos == String::npos ? String("") : path.substr(0, pos)); + } + + static String trap_text(const wasmtime::TrapError& error) + { + return(wasm_trace_collapse(String(error.message()))); + } + + // ---- guest memory access (pointer re-derived per call: it moves) ------ + + String guest_write(u32 ptr, const String& data) + { + auto span = memory->data(ctx()); + if((size_t)ptr + data.size() > span.size()) + return("guest write out of bounds"); + memcpy(span.data() + ptr, data.data(), data.size()); + return(""); + } + + String guest_read(u32 ptr, u32 len, String& out) + { + auto span = memory->data(ctx()); + if((size_t)ptr + len > span.size()) + return("guest read out of bounds"); + out.assign((const char*)span.data() + ptr, len); + return(""); + } + + // ---- calls ------------------------------------------------------------- + + std::optional core_func(const String& name) + { + auto exported = core->get(ctx(), std::string_view(name)); + if(!exported) + return(std::nullopt); + if(auto* func = std::get_if(&*exported)) + return(*func); + return(std::nullopt); + } + + std::optional unit_func(size_t unit_index, const String& name) + { + auto exported = units[unit_index].instance->get(ctx(), std::string_view(name)); + if(!exported) + return(std::nullopt); + if(auto* func = std::get_if(&*exported)) + return(*func); + return(std::nullopt); + } + + String call_core(const String& name, std::vector argv, int32_t* result_out) + { + auto func = core_func(name); + if(!func) + return("core does not export " + name); + std::vector args; + for(auto value : argv) + args.push_back(wasmtime::Val(value)); + auto result = func->call(ctx(), args); + if(!result) + return(trap_text(result.err())); + auto values = result.ok(); + if(result_out) + *result_out = values.empty() ? 0 : values[0].i32(); + return(""); + } + + // ---- symbol registry (core first, then units in load order) ----------- + + std::optional resolve_func(const String& name) + { + if(auto func = core_func(name)) + return(func); + for(size_t i = 0; i < units.size(); i++) + if(auto func = unit_func(i, name)) + return(func); + return(std::nullopt); + } + + // data symbol: value of the exported i32 global + owning module's base + // (core is non-PIC → base 0; PIC units export __memory_base-relative) + bool resolve_data(const String& name, u32& address_out) + { + auto from_core = core->get(ctx(), std::string_view(name)); + if(from_core) + if(auto* global = std::get_if(&*from_core)) + { + address_out = (u32)global->get(ctx()).i32(); + return(true); + } + for(size_t i = 0; i < units.size(); i++) + { + auto exported = units[i].instance->get(ctx(), std::string_view(name)); + if(exported) + if(auto* global = std::get_if(&*exported)) + { + address_out = units[i].memory_base + (u32)global->get(ctx()).i32(); + return(true); + } + } + return(false); + } + + String place_funcref(const wasmtime::Func& func, u32& slot_out) + { + auto cx = ctx(); + if(table_next_free >= table->size(cx)) + return("funcref table headroom exhausted"); + auto set = table->set(cx, table_next_free, wasmtime::Val(std::optional(func))); + if(!set) + return("table set failed: " + String(set.err().message())); + slot_out = table_next_free++; + return(""); + } + + // ---- unit loading (the §6 sequence) ------------------------------------ + + String load_unit(const String& source_path, size_t& unit_index_out) + { + auto known = units_by_source.find(source_path); + if(known != units_by_source.end()) + { + unit_index_out = known->second; + return(""); + } + + String error; + auto mod = worker.unit_module(source_path, error); + if(!mod) + return(error); + if(mod->abi.version != abi_version) + return(mod->wasm_path + ": uce.abi version " + std::to_string(mod->abi.version) + + " does not match core ABI " + std::to_string(abi_version)); + + auto cx = ctx(); + auto& module = *mod->module; + + // base allocation + u32 align = 1u << mod->dylink.mem_align; + int32_t raw_base = 0; + error = call_core("malloc", { (int32_t)(mod->dylink.mem_size + align) }, &raw_base); + if(error != "") + return(error); + if(raw_base == 0) + return("core malloc failed for unit data segment"); + u32 memory_base = ((u32)raw_base + (align - 1)) & ~(align - 1); + u32 table_base = table_next_free; + if(mod->dylink.table_size > table->size(cx) - table_next_free) + return("funcref table headroom exhausted by " + source_path); + table_next_free += mod->dylink.table_size; + + // import resolution + std::vector imports; + std::vector> deferred_got; + for(auto import_type : module.imports()) + { + String mod_name(import_type.module()); + String name(import_type.name()); + auto extern_type = wasmtime::ExternType::from_import(import_type); + bool is_func_import = std::get_if(&extern_type) != 0; + + if(mod_name == "env" && name == "memory") + { + imports.push_back(*memory); + continue; + } + if(mod_name == "env" && name == "__indirect_function_table") + { + imports.push_back(*table); + continue; + } + if(mod_name == "env" && name == "__stack_pointer") + { + auto sp = core->get(cx, "__stack_pointer"); + if(!sp || !std::get_if(&*sp)) + return("core does not export __stack_pointer"); + imports.push_back(std::get(*sp)); + continue; + } + if(mod_name == "env" && (name == "__memory_base" || name == "__table_base")) + { + int32_t value = name == "__memory_base" ? (int32_t)memory_base : (int32_t)table_base; + wasmtime::GlobalType global_type(wasmtime::ValType::i32(), false); + auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val(value)); + if(!global) + return("global create failed: " + String(global.err().message())); + imports.push_back(global.ok()); + continue; + } + if(mod_name == "env" && is_func_import) + { + auto func = resolve_func(name); + if(!func) + return(source_path + ": unresolved import env." + wasm_trace_demangle(name)); + imports.push_back(*func); + continue; + } + if(mod_name == "GOT.mem") + { + wasmtime::GlobalType global_type(wasmtime::ValType::i32(), true); + u32 address = 0; + if(resolve_data(name, address)) + { + auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)address)); + if(!global) + return("global create failed: " + String(global.err().message())); + imports.push_back(global.ok()); + } + else + { + // provisional; self-resolved from the unit's own export + // (plus its memory base) after instantiation + auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)0)); + if(!global) + return("global create failed: " + String(global.err().message())); + deferred_got.push_back({ name, global.ok() }); + imports.push_back(deferred_got.back().second); + } + continue; + } + if(mod_name == "GOT.func") + { + auto func = resolve_func(name); + if(!func) + return(source_path + ": unresolved GOT.func." + wasm_trace_demangle(name)); + u32 slot = 0; + error = place_funcref(*func, slot); + if(error != "") + return(error); + wasmtime::GlobalType global_type(wasmtime::ValType::i32(), true); + auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)slot)); + if(!global) + return("global create failed: " + String(global.err().message())); + imports.push_back(global.ok()); + continue; + } + return(source_path + ": import policy violation: " + mod_name + "." + name); + } + + auto created = wasmtime::Instance::create(cx, module, imports); + if(!created) + return(source_path + ": instantiation failed: " + trap_text(created.err())); + + LoadedUnit unit; + unit.mod = mod; + unit.instance.emplace(created.ok()); + unit.memory_base = memory_base; + units.push_back(std::move(unit)); + size_t unit_index = units.size() - 1; + units_by_source[source_path] = unit_index; + unit_index_out = unit_index; + + // deferred GOT: the unit's own data exports are module-relative — + // add this unit's memory base (Phase 0 FINDINGS erratum) + for(auto& [name, got] : deferred_got) + { + auto own = units[unit_index].instance->get(cx, std::string_view(name)); + if(!own || !std::get_if(&*own)) + return(source_path + ": GOT.mem." + name + " defined neither by core nor by any unit"); + u32 offset = (u32)std::get(*own).get(cx).i32(); + auto set = got.set(cx, wasmtime::Val((int32_t)(memory_base + offset))); + if(!set) + return("GOT patch failed: " + String(set.err().message())); + } + + // init sequence, then bind this unit's context to the request + if(auto relocs = unit_func(unit_index, "__wasm_apply_data_relocs")) + { + auto result = relocs->call(ctx(), {}); + if(!result) + return(trap_text(result.err())); + } + if(auto ctors = unit_func(unit_index, "__wasm_call_ctors")) + { + auto result = ctors->call(ctx(), {}); + if(!result) + return(trap_text(result.err())); + } + if(auto set_request = unit_func(unit_index, "__uce_set_current_request")) + { + auto result = set_request->call(ctx(), { wasmtime::Val(request_ptr) }); + if(!result) + return(trap_text(result.err())); + } + if(worker.cfg.verbose) + fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n", + source_path.c_str(), memory_base, table_base); + return(""); + } + + // ---- component target resolution (host side of the membrane) ---------- + + static String normalize_component_path(String name) + { + // mirrors component_normalize_path in compiler.cpp / core.cpp + if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce") + return(name); + return(name + ".uce"); + } + + static bool file_exists_host(const String& path) + { + struct stat st; + return(stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)); + } + + String resolve_source_path(const String& file_name, const String& current_unit) + { + std::vector bases; + if(file_name.rfind("/", 0) == 0) + bases.push_back(""); // absolute target + if(entry_dir != "") + bases.push_back(entry_dir + "/"); + if(current_unit != "") + { + String current_dir = dir_of(current_unit); + if(current_dir != "" && current_dir != entry_dir) + bases.push_back(current_dir + "/"); + } + bases.push_back(worker.cfg.site_root + "/"); + + for(auto& base : bases) + { + std::vector candidates; + candidates.push_back(base + file_name); + candidates.push_back(base + normalize_component_path(file_name)); + if(file_name.rfind("components/", 0) != 0 && base != "") + { + candidates.push_back(base + "components/" + file_name); + candidates.push_back(base + "components/" + normalize_component_path(file_name)); + } + for(auto& candidate : candidates) + if(file_exists_host(candidate)) + return(candidate); + } + return(""); + } + + // guest file access policy: only inside the site tree, resolved against + // the entry unit's directory first (the native cwd convention), then the + // site root; containment checked on the canonicalized path + String resolve_guest_file(const String& raw, const String& current_unit = "") + { + if(raw == "" || raw.find('\0') != String::npos) + return(""); + std::vector candidates; + if(raw.rfind("/", 0) == 0) + candidates.push_back(raw); + else + { + String current_dir = current_unit != "" ? dir_of(current_unit) : String(""); + if(current_dir != "") + candidates.push_back(current_dir + "/" + raw); + if(entry_dir != "" && entry_dir != current_dir) + candidates.push_back(entry_dir + "/" + raw); + candidates.push_back(worker.cfg.site_root + "/" + raw); + } + char site_real[4096]; + if(!realpath(worker.cfg.site_root.c_str(), site_real)) + return(""); + String site_prefix = String(site_real) + "/"; + for(auto& candidate : candidates) + { + char resolved[4096]; + if(!realpath(candidate.c_str(), resolved)) + continue; + String path(resolved); + if(path.rfind(site_prefix, 0) != 0) + continue; + if(file_exists_host(path)) + return(path); + } + return(""); + } + + static String sanitize_symbol_suffix(const String& raw) + { + // mirrors ascii_safe_name in functionlib.cpp + String result; + for(auto c : raw) + if(isalnum((unsigned char)c) || c == '_') + result.push_back(c); + return(result); + } + + // hostcall body: uce_host_component_resolve(target, kind, current) → slot + int32_t component_resolve(const String& target, int32_t kind, const String& current_unit, String& resolved_out) + { + String file_name = target; + String render_name; + auto split = target.find(":"); + if(split != String::npos) + { + render_name = target.substr(split + 1); + file_name = target.substr(0, split); + } + if(file_name == "" && current_unit != "") + file_name = current_unit; + if(file_name == "") + return(0); + + String resolved = resolve_source_path(file_name, current_unit); + if(resolved == "") + return(0); + resolved_out = resolved; + if(kind == RESOLVE_EXISTS) + return(1); + + size_t unit_index = 0; + String error = load_unit(resolved, unit_index); + if(error != "") + { + fprintf(stderr, "[wasm] component load failed: %s\n", error.c_str()); + return(0); + } + String symbol = kind == RESOLVE_RENDER ? "__uce_render" : "__uce_component"; + if(render_name != "") + symbol += "_" + sanitize_symbol_suffix(render_name); + String slot_key = resolved + ":" + symbol; + auto cached = handler_slots.find(slot_key); + if(cached != handler_slots.end()) + return((int32_t)cached->second); + auto handler = unit_func(unit_index, symbol); + if(!handler) + { + fprintf(stderr, "[wasm] %s does not export %s\n", resolved.c_str(), symbol.c_str()); + return(0); + } + u32 slot = 0; + error = place_funcref(*handler, slot); + if(error != "") + { + fprintf(stderr, "[wasm] %s\n", error.c_str()); + return(0); + } + handler_slots[slot_key] = slot; + return((int32_t)slot); + } + + // ---- host imports for the core ----------------------------------------- + + wasmtime::Extern make_host_import(wasmtime::Store::Context cx, const String& mod, const String& name, const wasmtime::FuncType& func_type) + { + using namespace wasmtime; + WasmWorkspace* self = this; + + auto add = [&](auto&& callback) -> Extern { + Func func(cx, func_type, callback); + host_funcs.push_back(func); + return(host_funcs.back()); + }; + + if(mod == "env" && name == "uce_host_time") + return(add([](Caller, Span, Span results) -> Result { + results[0] = Val((int64_t)::time(0)); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_time_precise") + return(add([](Caller, Span, Span results) -> Result { + struct timeval tv; + gettimeofday(&tv, 0); + results[0] = Val((double)tv.tv_sec + (double)tv.tv_usec / 1e6); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_env") + return(add([self](Caller caller, Span args, Span results) -> Result { + String key, value; + if(self->hostcall_read(args[0].i32(), args[1].i32(), key) == "") + if(const char* raw = getenv(key.c_str())) + value = raw; + u32 cap = (u32)args[3].i32(); + if(value.size() && cap >= value.size()) + self->hostcall_write(args[2].i32(), value); + results[0] = Val((int32_t)value.size()); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_random") + return(add([self](Caller, Span args, Span results) -> Result { + u32 len = (u32)args[1].i32(); + String bytes(len, 0); + FILE* urandom = fopen("/dev/urandom", "rb"); + if(urandom) + { + size_t got = fread(&bytes[0], 1, len, urandom); + fclose(urandom); + bytes.resize(got); + } + self->hostcall_write(args[0].i32(), bytes); + results[0] = Val((int32_t)bytes.size()); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_log") + return(add([self](Caller, Span args, Span) -> Result { + String text; + self->hostcall_read(args[1].i32(), args[2].i32(), text); + fprintf(stderr, "[guest log %d] %.*s\n", args[0].i32(), (int)text.size(), text.data()); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_exists") + return(add([self](Caller, Span args, Span results) -> Result { + String path, current; + self->hostcall_read(args[0].i32(), args[1].i32(), path); + self->hostcall_read(args[2].i32(), args[3].i32(), current); + String resolved = self->resolve_guest_file(path, current); + if(self->worker.cfg.verbose) + fprintf(stderr, "[wasm] file_exists(%s, current=%s) -> %s\n", path.c_str(), current.c_str(), resolved.c_str()); + results[0] = Val(resolved != "" ? (int32_t)1 : (int32_t)0); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_read") + return(add([self](Caller, Span args, Span results) -> Result { + String path, current; + self->hostcall_read(args[0].i32(), args[1].i32(), path); + self->hostcall_read(args[2].i32(), args[3].i32(), current); + String resolved = self->resolve_guest_file(path, current); + std::vector bytes; + if(resolved == "" || !wasm_read_file(resolved, bytes)) + { + results[0] = Val((int32_t)0); + return(std::monostate()); + } + u32 cap = (u32)args[5].i32(); + int32_t buf = args[4].i32(); + // length-query convention: no copy unless the buffer fits + if(buf != 0 && cap >= bytes.size()) + self->hostcall_write(buf, String((const char*)bytes.data(), bytes.size())); + results[0] = Val((int32_t)bytes.size()); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_component_resolve") + return(add([self](Caller, Span args, Span results) -> Result { + String target, current, resolved; + self->hostcall_read(args[0].i32(), args[1].i32(), target); + self->hostcall_read(args[3].i32(), args[4].i32(), current); + int32_t slot = self->component_resolve(target, args[2].i32(), current, resolved); + u32 cap = (u32)args[6].i32(); + if(cap > 0) + { + if(resolved.size() >= cap) + resolved = resolved.substr(0, cap - 1); + resolved.push_back('\0'); + self->hostcall_write(args[5].i32(), resolved); + } + results[0] = Val(slot); + return(std::monostate()); + })); + + // anything else (wasi-libc residue): a named trap — tolerated as long + // as it is never called + String label = mod + "." + name; + return(add([label](Caller, Span, Span) -> Result { + return(wasmtime::Trap("unimplemented host import called: " + std::string(label))); + })); + } + + String hostcall_read(int32_t ptr, int32_t len, String& out) + { + return(guest_read((u32)ptr, (u32)len, out)); + } + + void hostcall_write(int32_t ptr, const String& data) + { + guest_write((u32)ptr, data); + } +}; + +// ---- public entry: one request through one workspace ----------------------- + +inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_tree, const String& entry_source_path) +{ + WasmResponse response; + WasmWorkspace workspace(worker); + String error = workspace.birth(); + if(error == "") + error = workspace.apply_context(context_tree); + if(error == "") + error = workspace.render_entry(entry_source_path); + if(error == "") + error = workspace.collect(response); + if(error != "") + { + response.ok = false; + response.error = error; + return(response); + } + response.ok = true; + return(response); +}