Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c84fc86e6c | ||
|
|
5a56d4f39e | ||
|
|
afaa4dd7c0 | ||
|
|
577aae076e |
+335
-8
@@ -1,10 +1,21 @@
|
|||||||
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
|
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
|
||||||
|
|
||||||
- **Status:** design guide; Phase 0–4 spikes complete and gated (2026-06-12).
|
- **Status:** building the production worker per §9.1 (W-phases). W1 (core
|
||||||
Next: production implementation, starting with the Phase 3 work plan
|
module), W2 (wasm unit compile target), W3 (workspace runtime + membrane,
|
||||||
(item 1, real `uce_lib` core compile); the starter parity bar
|
`src/wasm/worker.cpp`), and W4 (config-selectable FastCGI backend,
|
||||||
(`tests/run_network_tests.py --match starter`, 14 cases) is already green
|
`src/wasm/backend.cpp`) are **done**: with `WASM_BACKEND_ENABLED=1` the
|
||||||
against the native backend.
|
real nginx → fastcgi → wasm path serves the starter app (parity 14/14) and
|
||||||
|
produces clean error pages from an unharmed worker on real kill pages.
|
||||||
|
W5 cutover is done: WASM is the default page-render backend with native kept
|
||||||
|
as the config-selectable reference/fallback; the membrane now covers regex,
|
||||||
|
xml/yaml, markdown, filesystem read/write, sqlite, background tasks, sleep,
|
||||||
|
and time formatting. W6 spike cleanup is done (prototypes deleted, gates
|
||||||
|
re-homed to `tests/`/`docs/`); the native-machinery retirements stay gated
|
||||||
|
while native still serves the remaining fallback surfaces (zip, sockets,
|
||||||
|
memcache, mysql, `unit_call`, compiler-introspection). Open items: the
|
||||||
|
sqlite membrane is ~6–7× native (per-query double hostcall + UCEB1
|
||||||
|
marshalling — §11.4 cursor/bulk decision), and CoW snapshot birth for the
|
||||||
|
workspace-birth budget.
|
||||||
- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` →
|
- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` →
|
||||||
`dlopen`) with per-unit WebAssembly modules executed in a per-request,
|
`dlopen`) with per-unit WebAssembly modules executed in a per-request,
|
||||||
runtime-linked workspace, exposing the same API surface to page code,
|
runtime-linked workspace, exposing the same API surface to page code,
|
||||||
@@ -424,7 +435,7 @@ stub) hello-world linked at runtime by a minimal loader, in the chosen
|
|||||||
vendored runtime.
|
vendored runtime.
|
||||||
|
|
||||||
> **Status: DONE (2026-06-12).** Exit criterion passed on k-uce; see
|
> **Status: DONE (2026-06-12).** Exit criterion passed on k-uce; see
|
||||||
> `spikes/wasm-phase0/FINDINGS.md`. Runtime selected: **Wasmtime v45.0.1**
|
> `docs/wasm-toolchain-findings.md`. Runtime selected: **Wasmtime v45.0.1**
|
||||||
> — WAMR is blocked on the load-bearing requirement (its wasm-c-api ignores
|
> — WAMR is blocked on the load-bearing requirement (its wasm-c-api ignores
|
||||||
> imported memories/tables; host-side table growth unsupported). wasi-sdk-33
|
> imported memories/tables; host-side table growth unsupported). wasi-sdk-33
|
||||||
> PIC validated on stubs **and** on real generated units
|
> PIC validated on stubs **and** on real generated units
|
||||||
@@ -517,7 +528,7 @@ Exit: the uce-starter renders end-to-end with components loading lazily;
|
|||||||
> wrong values and wrote into core memory at low addresses; both spike loaders
|
> wrong values and wrote into core memory at low addresses; both spike loaders
|
||||||
> are fixed and the Phase 3 exit gate now asserts every GOT-derived output
|
> are fixed and the Phase 3 exit gate now asserts every GOT-derived output
|
||||||
> marker (`self-got`/`callback`/`each`/`map`) so a regression cannot pass.
|
> marker (`self-got`/`callback`/`each`/`map`) so a regression cannot pass.
|
||||||
> Details in the `spikes/wasm-phase0/FINDINGS.md` erratum.
|
> Details in the `docs/wasm-toolchain-findings.md` erratum.
|
||||||
>
|
>
|
||||||
> **Production Phase 3 work plan** (ordered by dependency/risk; spike-proven
|
> **Production Phase 3 work plan** (ordered by dependency/risk; spike-proven
|
||||||
> mechanics not repeated here):
|
> mechanics not repeated here):
|
||||||
@@ -614,7 +625,7 @@ call cost. Exit: numbers published in this document, all tests and reviews pass.
|
|||||||
> measured gate to absorb cold unit-cache timeouts. Informational native medians
|
> measured gate to absorb cold unit-cache timeouts. Informational native medians
|
||||||
> from the 2026-06-12 baseline are: template-heavy doc `313.8 ms`, sqlite page
|
> from the 2026-06-12 baseline are: template-heavy doc `313.8 ms`, sqlite page
|
||||||
> `3.4 ms`, starter dashboard `41.1 ms`. A durable snapshot lives in
|
> `3.4 ms`, starter dashboard `41.1 ms`. A durable snapshot lives in
|
||||||
> `spikes/wasm-phase5/reports/native-baseline-2026-06-12.md`; paired wasm/native
|
> `docs/wasm-baselines/native-baseline-2026-06-12.md`; paired wasm/native
|
||||||
> gate runs still recompute the native medians for the actual ≤2× comparison.
|
> gate runs still recompute the native medians for the actual ≤2× comparison.
|
||||||
> True Phase 5 completion still requires passing the same harness against a real
|
> True Phase 5 completion still requires passing the same harness against a real
|
||||||
> wasm worker URL and adding worker-internal probes for workspace birth and
|
> wasm worker URL and adding worker-internal probes for workspace birth and
|
||||||
@@ -630,6 +641,322 @@ The native `.so` backend remains in-tree (as a reference) and selectable by conf
|
|||||||
but we switch over to the wasm backend as soon as it's available and test
|
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.
|
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.
|
||||||
|
|
||||||
|
> **Status: DONE (2026-06-13).** `src/wasm/backend.cpp` wires the W3 runtime
|
||||||
|
> into `src/linux_fastcgi.cpp` as a config-selectable page-render backend
|
||||||
|
> (`WASM_BACKEND_ENABLED`, default off — native stays default until W5). The
|
||||||
|
> seam is one branch in `handle_complete`: per forked worker, a lazily-built
|
||||||
|
> `WasmWorker` + epoch ticker thread; per request the native `Request`
|
||||||
|
> params/get/post/cookies/session are encoded to the UCEB1 context, served
|
||||||
|
> through a fresh workspace, and the response (status/headers/cookies/session
|
||||||
|
> /body) is written back onto the native `Request` so the existing transport
|
||||||
|
> emits it unchanged. CLI/serve_http/websocket stay native; units with no
|
||||||
|
> wasm artifact fall through to native automatically. `ONCE()` is now honored
|
||||||
|
> in the workspace (host resolves `__uce_once`, core dedups on the resolved
|
||||||
|
> path via `once_units`; the entry renders through `uce_wasm_render_entry` so
|
||||||
|
> it shares the component dispatch + ONCE path).
|
||||||
|
>
|
||||||
|
> **Exit gate passed on k-uce, through the real nginx → fastcgi → wasm path on
|
||||||
|
> port 80:** `run_network_tests.py --match starter` is **14/14** against the
|
||||||
|
> wasm backend (incl. the two ONCE-asset-in-`<head>` cases); three real kill
|
||||||
|
> pages under `site/tests/wasm-kill/` (OOB write, runaway loop, unbounded
|
||||||
|
> recursion) each return a clean error page carrying a demangled
|
||||||
|
> `wasm_trace` summary (`out of bounds memory access` / epoch `interrupt`
|
||||||
|
> with `wasm_kill_recurse(unsigned long long)` framing), and the worker keeps
|
||||||
|
> serving `200`s through a barrage of kills — no native signal, the trap is a
|
||||||
|
> returned error at the membrane. CPU budget is enforced by epoch
|
||||||
|
> (`WASM_EPOCH_DEADLINE_TICKS` × `WASM_EPOCH_PERIOD_MS`); memory by the store
|
||||||
|
> limiter. Native default restored after the gate: full suite **83/83**.
|
||||||
|
> Robustness folded in along the way: the ctype libc family added to the core
|
||||||
|
> export anchors (`core_libc_exports.syms`), and a page with no `RENDER`
|
||||||
|
> renders empty-200 (native parity).
|
||||||
|
>
|
||||||
|
> **W5 surface, made concrete** (23 full-suite pages still 500 on the wasm
|
||||||
|
> backend, all by design): the regex/xml/yaml core stubs to un-stub or move
|
||||||
|
> behind hostcalls; markdown/zip/tasks/sqlite/file-write membrane hostcalls;
|
||||||
|
> the `unit_call` bridge; and compiler-introspection pages (`unit-info`,
|
||||||
|
> `unit-browser`, `sharedunit`) which likely stay native. These are the W5
|
||||||
|
> parity workload, not regressions.
|
||||||
|
|
||||||
|
**W5 — parity, performance, cutover.**
|
||||||
|
Full network suite green on the wasm backend; §3.2 statics-audit findings
|
||||||
|
(50 code candidates) fixed as differential native-vs-wasm runs surface
|
||||||
|
them; CoW snapshot birth and placement memoization as needed to meet the
|
||||||
|
budgets (≤2× page latency against the pinned baseline shape, workspace
|
||||||
|
birth ≤100µs, component-call overhead ≤10×) with worker-internal probes
|
||||||
|
for the latter two.
|
||||||
|
Exit: cutover checklist gates 2–3 — harness fully green against the wasm
|
||||||
|
worker URL, config default flips to wasm, native stays in-tree as
|
||||||
|
reference.
|
||||||
|
|
||||||
|
> **Status: DONE (2026-06-13).** W5 flips the page-render default to the WASM
|
||||||
|
> backend while retaining explicit native fallback for the surfaces still
|
||||||
|
> genuinely host-owned or not yet membraned: zip, sockets/custom servers,
|
||||||
|
> memcache/mysql, and `unit_call`/compiler-introspection (the last need the
|
||||||
|
> native toolchain). The fallback is selected before workspace creation by
|
||||||
|
> scanning the entry source for a small native-only token set, so unsupported
|
||||||
|
> pages neither fail through wasm nor hide as trap regressions.
|
||||||
|
>
|
||||||
|
> **Promoted onto wasm during W5** (no longer fallback): **regex** runs through
|
||||||
|
> a single UCEB1-marshalled `uce_host_regex` hostcall against the host's PCRE2;
|
||||||
|
> **markdown** is compiled into the core (pure compute); **xml/yaml** were
|
||||||
|
> already in-core; **filesystem writes** (`file_put_contents`/`file_append`/
|
||||||
|
> `file_unlink`) go through policy-gated hostcalls, and the read membrane was
|
||||||
|
> widened to the same allowlist (site tree + scratch roots: `/tmp`,
|
||||||
|
> `BIN_DIRECTORY`, `SESSION_PATH`, `TMP_UPLOAD_PATH`) so a page can read back
|
||||||
|
> what it writes (e.g. the `io.uce` /tmp round-trip); **sqlite** runs through a
|
||||||
|
> `uce_host_sqlite` hostcall against the host's real connector, with
|
||||||
|
> connections held in a per-workspace handle table that is closed on workspace
|
||||||
|
> drop — the first concrete realization of the §3.1 "handle-table drop =
|
||||||
|
> resource cleanup" model; **background tasks** now use a host-managed process
|
||||||
|
> membrane (`uce_host_task_spawn`/`pid`/`kill`) with a guest callback trampoline
|
||||||
|
> (`uce_wasm_task_run`) in the forked child, so `task()`, `task_repeat()`,
|
||||||
|
> `task_pid()`, and `task_kill()` run under the wasm backend; **sleep/usleep**
|
||||||
|
> use a host sleep call that renews the epoch deadline after blocking. The
|
||||||
|
> `/doc/*` blanket fallback is gone — doc pages render on wasm.
|
||||||
|
>
|
||||||
|
> One subtle bug fixed here: the sized-hostcall convention has the guest call
|
||||||
|
> twice (buf=0 to learn the length, then to fetch). That re-executed the op —
|
||||||
|
> harmless for idempotent regex/file-read, but for sqlite it ran every
|
||||||
|
> `INSERT`/`CREATE` twice and the second pass failed on its own side effects.
|
||||||
|
> The host now stages the encoded result on the first call (keyed on the exact
|
||||||
|
> input) and replays it on the fetch, so a side-effecting op runs once. Two wasm-stack/ABI issues surfaced and were fixed: the core stack was
|
||||||
|
> raised to 8 MB so recursive `ucb_decode_node` reaches the 1024 depth limit
|
||||||
|
> like native (the core.uce depth-bomb negative test overflowed the small
|
||||||
|
> default stack), and `wasm_trace`'s demangler is host-only (wasi-libc++ has no
|
||||||
|
> `__cxa_demangle`, so a unit must not import it). Two backend robustness bugs
|
||||||
|
> fixed: a failed wasm side-compile (e.g. try/catch units) is now non-fatal —
|
||||||
|
> the native `.so` still serves and the stale `.wasm` is removed — and
|
||||||
|
> `wasm_artifact_exists` now requires the artifact be newer than its source, so
|
||||||
|
> a source edit can't be served from a stale `.wasm` (the wasm path bypasses the
|
||||||
|
> native JIT recompile).
|
||||||
|
>
|
||||||
|
> **Signal-trap conflict resolved (this was open in the prior W5 draft):** a
|
||||||
|
> guest `unreachable`/OOB used to surface as a host signal that the native
|
||||||
|
> SIGILL/SIGSEGV handler caught and `abort()`'d the worker (502). `make_engine`
|
||||||
|
> now sets `signals_based_traps(false)`, so guest traps are explicit and stay
|
||||||
|
> pure wasm traps returned as errors. The `__builtin_trap()` kill page now
|
||||||
|
> returns a clean 500 and the worker survives a kill barrage (0 PID churn); it
|
||||||
|
> is back in the kill-test gate alongside loop/recurse.
|
||||||
|
>
|
||||||
|
> `scripts/wasm/run_w5.sh` is the cutover gate: measure native, switch
|
||||||
|
> `/etc/uce/settings.cfg` to wasm, run the wasm full suite + kill tests +
|
||||||
|
> starter subset + the native-vs-wasm benchmark, optionally leave the backend
|
||||||
|
> enabled (`UCE_W5_KEEP_BACKEND=1`). Final k-uce gate: native reference suite
|
||||||
|
> **83/83**; wasm/default full suite **83/83** warm (starter, docs, core tests,
|
||||||
|
> markdown, regex, sqlite, and tasks on wasm; zip and native-only service/
|
||||||
|
> compiler surfaces routed to native fallback); starter subset **14/14**; kill pages (trap/loop/recurse) clean with the worker
|
||||||
|
> unharmed. Benchmark medians passed the ≤2× budget: doc singlepage native
|
||||||
|
> ~324ms vs wasm ~318ms, sqlite 3.7ms vs 3.8ms, starter dashboard 44ms vs wasm
|
||||||
|
> ~6ms. Worker-internal probe headers (`X-UCE-Backend`,
|
||||||
|
> `X-UCE-Wasm-Workspace-Birth-Us`, component resolve count/total/avg) are emitted
|
||||||
|
> for wasm responses when `WASM_BACKEND_VERBOSE=1`; warm workspace birth is
|
||||||
|
> ~300–380µs on k-uce (above the aspirational 100µs CoW target — that and
|
||||||
|
> placement memoization are the remaining perf items — but page latency meets the
|
||||||
|
> cutover budget). Live `/etc/uce/settings.cfg` left with `WASM_BACKEND_ENABLED=1`;
|
||||||
|
> native remains in-tree and config-selectable.
|
||||||
|
>
|
||||||
|
> **Remaining native fallback** (the only pages still routed to native): **zip**
|
||||||
|
> (its `.uce` source uses try/catch, so it can't be a `-fno-exceptions` wasm
|
||||||
|
> side module regardless of a hostcall), socket/custom-server/memcache/mysql
|
||||||
|
> service surfaces, and `unit_call` + compiler/unit-introspection
|
||||||
|
> (`unit-info`/`unit-browser`/`sharedunit` — need the native toolchain;
|
||||||
|
> candidates for host-serviced hostcalls if native is to be fully retired).
|
||||||
|
> **Still deferred, not blocking:** CoW snapshot birth + placement memoization
|
||||||
|
> for the workspace-birth budget, and the §3.2 statics-audit follow-through.
|
||||||
|
|
||||||
|
**W6 — cleanup.**
|
||||||
|
Cutover checklist gate 4, verbatim: re-home the spike-hosted gates
|
||||||
|
(Phase 5 harness → `tests/`, kill cases → worker tests, FINDINGS/erratum →
|
||||||
|
`docs/`), delete `spikes/wasm-phase*`, retire the SIGSEGV recovery, the
|
||||||
|
tracking `operator new`, and `cleanup_*_connections()`.
|
||||||
|
|
||||||
|
> **Status: SPIKE CLEANUP DONE; native-machinery retirement GATED
|
||||||
|
> (2026-06-13).** Re-homing complete: the Phase 5 benchmark + site-audit are
|
||||||
|
> now `tests/wasm_benchmark.py` / `tests/wasm_site_audit.py` (with
|
||||||
|
> `scripts/wasm/run_w5.sh` repointed), the kill-test gate already lives in
|
||||||
|
> `tests/plugins/uce_wasm_kill.py`, the Phase 0 toolchain findings + GOT
|
||||||
|
> erratum are `docs/wasm-toolchain-findings.md`, and the pinned native
|
||||||
|
> baselines are `docs/wasm-baselines/`. **All `spikes/wasm-phase*` deleted**
|
||||||
|
> (prototypes superseded by `src/wasm/`), and the dead 2022
|
||||||
|
> `src/lib/_scratchpad.cpp` arena experiment is removed. Build + full suite
|
||||||
|
> stay green (83/83). The phase-0–4 status blocks above keep their narrative;
|
||||||
|
> their `spikes/...` paths are now historical — the artifacts that mattered
|
||||||
|
> were re-homed, the rest was throwaway scaffolding by design.
|
||||||
|
>
|
||||||
|
> **The three native-machinery retirements remain BLOCKED, deliberately:**
|
||||||
|
> native is still the live backend for pages touching zip, sockets / custom
|
||||||
|
> HTTP servers, memcache, mysql, `unit_call`, and compiler/unit-introspection
|
||||||
|
> — everything still in `native_only_tokens`. While *any* page renders on
|
||||||
|
> native, retiring its SIGSEGV/`sigsetjmp` recovery, the tracking
|
||||||
|
> `operator new`, or `cleanup_*_connections()` would strip crash protection
|
||||||
|
> and resource cleanup from live traffic. They retire only once those surfaces
|
||||||
|
> move behind membrane hostcalls (so nothing falls back) or the native backend
|
||||||
|
> is formally decommissioned. This is the honest end-state of W6 given the
|
||||||
|
> current fallback set, not an oversight.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Risks & mitigations
|
## 10. Risks & mitigations
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ SITE_DIRECTORY=site
|
|||||||
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
|
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
|
||||||
JIT_COMPILE_ON_REQUEST=1
|
JIT_COMPILE_ON_REQUEST=1
|
||||||
|
|
||||||
|
# OPTIONAL W2 WEBASSEMBLY SIDE-MODULE COMPILATION BESIDE NATIVE .so UNITS
|
||||||
|
COMPILE_WASM_UNITS=0
|
||||||
|
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
||||||
|
|
||||||
|
# DEFAULT PAGE RENDER BACKEND. W5 defaults to the WASM backend with explicit
|
||||||
|
# native fallbacks for host-owned surfaces that are not membrane APIs yet.
|
||||||
|
WASM_BACKEND_ENABLED=1
|
||||||
|
WASM_BACKEND_VERBOSE=0
|
||||||
|
WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm
|
||||||
|
WASM_MEMORY_LIMIT_BYTES=536870912
|
||||||
|
WASM_EPOCH_DEADLINE_TICKS=200
|
||||||
|
WASM_EPOCH_PERIOD_MS=50
|
||||||
|
|
||||||
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
||||||
PROACTIVE_COMPILE_ENABLED=1
|
PROACTIVE_COMPILE_ENABLED=1
|
||||||
|
|
||||||
|
|||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the production W1 UCE WASM core from the real runtime carve-out.
|
||||||
|
# Run on k-uce from any working directory.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
||||||
|
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
|
||||||
|
mkdir -p "$OUT" bin/wasm
|
||||||
|
|
||||||
|
if [ ! -x "$SDK/bin/clang++" ]; then
|
||||||
|
echo "wasi-sdk clang++ not found; set WASI_SDK" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
|
||||||
|
-O1 -g -std=c++20 -fno-exceptions -fno-rtti \
|
||||||
|
-D__UCE_WASM_CORE__ \
|
||||||
|
-I. -Isrc/lib \
|
||||||
|
src/wasm/core.cpp -o "$OUT/core.wasm" \
|
||||||
|
-Wl,--export-all \
|
||||||
|
-Wl,--export=__heap_base \
|
||||||
|
-Wl,--export=__stack_pointer \
|
||||||
|
-Wl,--import-table \
|
||||||
|
-Wl,-z,stack-size=8388608 \
|
||||||
|
-Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \
|
||||||
|
-Wl,--no-entry \
|
||||||
|
$(sed "s/^/-Wl,--export-if-defined=/" src/wasm/core_libc_exports.syms | tr "\n" " ")
|
||||||
|
|
||||||
|
cp "$OUT/core.wasm" bin/wasm/core.wasm
|
||||||
|
ls -lh "$OUT/core.wasm" bin/wasm/core.wasm
|
||||||
@@ -14,8 +14,13 @@ mkdir work > /dev/null 2>&1
|
|||||||
COMPILER="clang++"
|
COMPILER="clang++"
|
||||||
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
|
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
|
||||||
|
|
||||||
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`"
|
# Wasmtime C API for the W4 wasm backend (src/wasm/backend.cpp).
|
||||||
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
|
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
|
||||||
|
WASM_FLAGS="-I$WASMTIME_HOME/include"
|
||||||
|
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
|
||||||
|
|
||||||
|
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs` $WASM_LIBS"
|
||||||
|
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\" $WASM_FLAGS"
|
||||||
|
|
||||||
echo "Compiling SQLite..."
|
echo "Compiling SQLite..."
|
||||||
clang -g -O2 -fPIC \
|
clang -g -O2 -fPIC \
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ OPT_FLAG="O0"
|
|||||||
|
|
||||||
COMPILER="clang++"
|
COMPILER="clang++"
|
||||||
#COMPILER="g++"
|
#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"
|
LIBS="-ldl -lm -lpthread"
|
||||||
SRCFLAGS="-D PLATFORM_NAME=\"linux\""
|
SRCFLAGS="-D PLATFORM_NAME=\"linux\""
|
||||||
|
|||||||
Executable
+97
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Compile a preprocessed UCE unit into a PIC WebAssembly side module.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
SRC_DIR="$1"
|
||||||
|
DEST_DIR="$2"
|
||||||
|
SRC_FN="$3"
|
||||||
|
PP_FN="$4"
|
||||||
|
WASM_FN="$5"
|
||||||
|
|
||||||
|
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
||||||
|
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
|
||||||
|
ROOT=$(pwd)
|
||||||
|
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
|
||||||
|
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
|
||||||
|
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
|
||||||
|
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
|
||||||
|
COMMON_FLAGS=(
|
||||||
|
--target=wasm32-wasip1
|
||||||
|
-fPIC -fvisibility=default -fvisibility-inlines-hidden
|
||||||
|
-O1 -g -std=c++20
|
||||||
|
# -w as in scripts/compile: warnings are not failures. The server captures
|
||||||
|
# this script's output and treats any non-empty result as a compile failure
|
||||||
|
# (then drops the .wasm), so a successful build must be silent.
|
||||||
|
-w
|
||||||
|
# must match the core build ABI: units with RTTI/EH enabled import
|
||||||
|
# typeinfo/unwind symbols the -fno-rtti/-fno-exceptions core cannot provide
|
||||||
|
-fno-exceptions -fno-rtti
|
||||||
|
-D__UCE_WASM_UNIT__
|
||||||
|
-DPLATFORM_NAME=\"wasm32-wasip1\"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ]; then
|
||||||
|
echo "wasi-sdk tools not found; set WASI_SDK" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOOLCHAIN_ID=$(${SDK}/bin/clang++ --version | head -n 1)
|
||||||
|
HEADER_HASH=$(find src/lib -maxdepth 1 -name '*.h' -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | cut -c1-16)
|
||||||
|
FLAGS_HASH=$(printf '%s\0' "${COMMON_FLAGS[@]}" -Isrc/lib | sha1sum | cut -c1-16)
|
||||||
|
PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH" "$FLAGS_HASH" | sha1sum | cut -c1-16)
|
||||||
|
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
|
||||||
|
|
||||||
|
mkdir -p "$DEST_DIR" >/dev/null 2>&1
|
||||||
|
|
||||||
|
build_pch_if_needed() {
|
||||||
|
if [ "$PCH_ENABLED" = "0" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$PCH_DIR"
|
||||||
|
if [ -s "$PCH_FN" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
||||||
|
-Isrc/lib \
|
||||||
|
-x c++-header src/lib/uce_lib.h -o "$PCH_FN.tmp"
|
||||||
|
mv "$PCH_FN.tmp" "$PCH_FN"
|
||||||
|
}
|
||||||
|
|
||||||
|
cat > "$ABI_TMP" <<EOF
|
||||||
|
format=uce-wasm-unit-abi-v1
|
||||||
|
unit_abi_version=$ABI_VERSION
|
||||||
|
toolchain=$TOOLCHAIN_ID
|
||||||
|
source=$SRC_FN
|
||||||
|
EOF
|
||||||
|
|
||||||
|
build_pch_if_needed
|
||||||
|
PCH_FLAGS=()
|
||||||
|
if [ "$PCH_ENABLED" != "0" ]; then
|
||||||
|
PCH_FLAGS=(-include-pch "$PCH_FN")
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
||||||
|
-I"$SRC_DIR" -I"$ROOT/src/lib" \
|
||||||
|
"${PCH_FLAGS[@]}" \
|
||||||
|
-c "$DEST_DIR/$PP_FN" -o "$OBJ_FN"
|
||||||
|
|
||||||
|
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
||||||
|
--unresolved-symbols=import-dynamic \
|
||||||
|
--Bsymbolic \
|
||||||
|
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
|
||||||
|
--export-if-defined=__uce_set_current_request \
|
||||||
|
--export-if-defined=__uce_render \
|
||||||
|
--export-if-defined=__uce_component \
|
||||||
|
--export-if-defined=__uce_websocket \
|
||||||
|
--export-if-defined=__uce_cli \
|
||||||
|
--export-if-defined=__uce_serve_http \
|
||||||
|
--export-if-defined=__uce_once \
|
||||||
|
--export-if-defined=__uce_init
|
||||||
|
|
||||||
|
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
|
||||||
|
|
||||||
|
python3 scripts/wasm/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
|
||||||
|
|
||||||
|
rm -f "$OBJ_FN" "$ABI_TMP"
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the W1 host smoke driver. Run on k-uce.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
|
||||||
|
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
|
||||||
|
WASMTIME_INCLUDE=${WASMTIME_INCLUDE:-$WASMTIME_HOME/include}
|
||||||
|
WASMTIME_LIB=${WASMTIME_LIB:-$WASMTIME_HOME/lib}
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
if [ ! -d "$WASMTIME_INCLUDE" ] || [ ! -d "$WASMTIME_LIB" ]; then
|
||||||
|
echo "Wasmtime C API not found; set WASMTIME_HOME or WASMTIME_INCLUDE/WASMTIME_LIB" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
g++ -std=c++17 -O2 -Wall -Wextra \
|
||||||
|
-I"$WASMTIME_INCLUDE" \
|
||||||
|
src/wasm/w1_smoke.cpp \
|
||||||
|
-L"$WASMTIME_LIB" \
|
||||||
|
-Wl,-rpath,"$WASMTIME_LIB" \
|
||||||
|
-lwasmtime \
|
||||||
|
-o "$OUT/w1_smoke"
|
||||||
|
|
||||||
|
ls -lh "$OUT/w1_smoke"
|
||||||
Executable
+62
@@ -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"
|
||||||
@@ -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"
|
||||||
Executable
+227
@@ -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())
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# W5 parity/performance gate for the config-selectable WASM backend.
|
||||||
|
# Runs on k-uce. Requires root because the live service reads /etc/uce/settings.cfg.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
|
||||||
|
echo "run_w5.sh must run as root so it can switch /etc/uce/settings.cfg and restart uce.service" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUT=${UCE_W5_OUT:-/tmp/uce/wasm-w5}
|
||||||
|
CONFIG=${UCE_CONFIG:-/etc/uce/settings.cfg}
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
BACKUP="$OUT/settings.cfg.before-w5"
|
||||||
|
cp "$CONFIG" "$BACKUP"
|
||||||
|
|
||||||
|
restore_on_error() {
|
||||||
|
if [ "${UCE_W5_KEEP_BACKEND:-0}" != "1" ]; then
|
||||||
|
cp "$BACKUP" "$CONFIG"
|
||||||
|
systemctl restart uce.service >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap restore_on_error EXIT
|
||||||
|
|
||||||
|
set_backend() {
|
||||||
|
local enabled="$1"
|
||||||
|
python3 - "$CONFIG" "$enabled" <<'PY'
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
enabled = sys.argv[2]
|
||||||
|
s = path.read_text()
|
||||||
|
lines = s.splitlines()
|
||||||
|
found = False
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith('WASM_BACKEND_ENABLED='):
|
||||||
|
lines[i] = f'WASM_BACKEND_ENABLED={enabled}'
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
lines.append(f'WASM_BACKEND_ENABLED={enabled}')
|
||||||
|
required = {
|
||||||
|
'WASM_BACKEND_VERBOSE': '0',
|
||||||
|
'WASM_CORE_PATH': '/Code/uce.openfu.com/uce/bin/wasm/core.wasm',
|
||||||
|
'WASM_MEMORY_LIMIT_BYTES': '536870912',
|
||||||
|
'WASM_EPOCH_DEADLINE_TICKS': '200',
|
||||||
|
'WASM_EPOCH_PERIOD_MS': '50',
|
||||||
|
}
|
||||||
|
keys = {line.split('=', 1)[0] for line in lines if '=' in line}
|
||||||
|
for key, value in required.items():
|
||||||
|
if key not in keys:
|
||||||
|
lines.append(f'{key}={value}')
|
||||||
|
path.write_text('\n'.join(lines) + '\n')
|
||||||
|
PY
|
||||||
|
systemctl restart uce.service >/dev/null
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
|
summarize_json() {
|
||||||
|
python3 - "$1" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
rows = json.load(open(sys.argv[1]))
|
||||||
|
print(f"{sys.argv[1]}: {sum(1 for r in rows if r.get('ok'))}/{len(rows)}")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
# Native reference baseline.
|
||||||
|
set_backend 0
|
||||||
|
python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/native-warmup.json" >/dev/null || true
|
||||||
|
python3 tests/run_network_tests.py --include-internal --json-report "$OUT/native-network.json"
|
||||||
|
python3 tests/wasm_benchmark.py --out-dir "$OUT/native-benchmark" --samples "${UCE_W5_BENCH_SAMPLES:-20}" --timeout 30 >/dev/null
|
||||||
|
|
||||||
|
# WASM default backend with W5 native fallbacks for host-owned surfaces.
|
||||||
|
set_backend 1
|
||||||
|
UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/wasm-warmup.json" >/dev/null || true
|
||||||
|
UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --json-report "$OUT/wasm-network.json"
|
||||||
|
python3 tests/run_network_tests.py --include-internal --match starter --json-report "$OUT/wasm-starter.json"
|
||||||
|
python3 tests/wasm_benchmark.py \
|
||||||
|
--out-dir "$OUT/benchmark" \
|
||||||
|
--backend-label wasm \
|
||||||
|
--compare-native-json "$OUT/native-benchmark/benchmark.json" \
|
||||||
|
--samples "${UCE_W5_BENCH_SAMPLES:-20}" \
|
||||||
|
--timeout 30
|
||||||
|
python3 tests/wasm_site_audit.py --out-dir "$OUT" >/dev/null
|
||||||
|
|
||||||
|
python3 - "$OUT" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
from pathlib import Path
|
||||||
|
out = Path(sys.argv[1])
|
||||||
|
network = json.loads((out / 'wasm-network.json').read_text())
|
||||||
|
starter = json.loads((out / 'wasm-starter.json').read_text())
|
||||||
|
bench = json.loads((out / 'benchmark' / 'benchmark.json').read_text())
|
||||||
|
failures = [r for r in network if not r.get('ok')] + [r for r in starter if not r.get('ok')] + [r for r in bench if not r.get('ok')]
|
||||||
|
structural = []
|
||||||
|
if len(network) < 80: structural.append(f'wasm network ran {len(network)} cases, expected at least 80')
|
||||||
|
if len(starter) < 10: structural.append(f'wasm starter ran {len(starter)} cases, expected at least 10')
|
||||||
|
if len([r for r in bench if r.get('backend') == 'wasm']) < 3: structural.append('wasm benchmark rows < 3')
|
||||||
|
if failures or structural:
|
||||||
|
print('W5 HARNESS: FAIL')
|
||||||
|
for item in structural: print(item)
|
||||||
|
for item in failures: print(item)
|
||||||
|
raise SystemExit(1)
|
||||||
|
print('W5 HARNESS: PASS')
|
||||||
|
print(f'network_cases={len(network)} starter_cases={len(starter)} benchmark_rows={len(bench)}')
|
||||||
|
print(f'reports={out}')
|
||||||
|
PY
|
||||||
|
|
||||||
|
if [ "${UCE_W5_KEEP_BACKEND:-0}" = "1" ]; then
|
||||||
|
trap - EXIT
|
||||||
|
printf 'WASM backend left enabled in %s\n' "$CONFIG"
|
||||||
|
else
|
||||||
|
cp "$BACKUP" "$CONFIG"
|
||||||
|
systemctl restart uce.service >/dev/null
|
||||||
|
trap - EXIT
|
||||||
|
fi
|
||||||
@@ -32,7 +32,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
|
|||||||
|
|
||||||
## Pipeline
|
## 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 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.
|
- 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.
|
- Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content.
|
||||||
|
|||||||
+4
-1
@@ -244,7 +244,10 @@ RENDER(Request& context)
|
|||||||
trace_msg += "\nCaused by:\n wasm trap: call stack exhausted\n";
|
trace_msg += "\nCaused by:\n wasm trap: call stack exhausted\n";
|
||||||
WasmTraceSummary trace_summary = wasm_trace_summarize(trace_msg);
|
WasmTraceSummary trace_summary = wasm_trace_summarize(trace_msg);
|
||||||
String trace_text = wasm_trace_format(trace_summary);
|
String trace_text = wasm_trace_format(trace_summary);
|
||||||
check("wasm_trace_summarize() collapse + demangle", trace_summary.parsed && trace_summary.total_frames == 41 && trace_summary.cause == "wasm trap: call stack exhausted" && trace_summary.frames.size() == 2 && contains(trace_text, "×40") && contains(trace_text, "recursion()"), trace_text);
|
// Demangling is a host-only capability (wasi-libc++ has no __cxa_demangle),
|
||||||
|
// so accept either the demangled name (native) or the raw symbol (wasm).
|
||||||
|
bool trace_symbol_ok = contains(trace_text, "recursion()") || contains(trace_text, "_Z9recursionv");
|
||||||
|
check("wasm_trace_summarize() collapse", trace_summary.parsed && trace_summary.total_frames == 41 && trace_summary.cause == "wasm trap: call stack exhausted" && trace_summary.frames.size() == 2 && contains(trace_text, "×40") && trace_symbol_ok, trace_text);
|
||||||
|
|
||||||
String fault_msg = "error while executing at wasm backtrace:\n 0: 0x2a - <unknown>!<wasm function 0>\n\nCaused by:\n 0: memory fault at wasm address 0x20000 in linear memory of size 0x10000\n 1: wasm trap: out of bounds memory access\n";
|
String fault_msg = "error while executing at wasm backtrace:\n 0: 0x2a - <unknown>!<wasm function 0>\n\nCaused by:\n 0: memory fault at wasm address 0x20000 in linear memory of size 0x10000\n 1: wasm trap: out of bounds memory access\n";
|
||||||
WasmTraceSummary fault_summary = wasm_trace_summarize(fault_msg);
|
WasmTraceSummary fault_summary = wasm_trace_summarize(fault_msg);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// W4 kill-test: runaway CPU. Epoch interruption traps it at the configured
|
||||||
|
// deadline; the worker stays healthy.
|
||||||
|
RENDER(Request& context)
|
||||||
|
{
|
||||||
|
print("about to loop\n");
|
||||||
|
volatile u64 i = 0;
|
||||||
|
while(i >= 0)
|
||||||
|
i++;
|
||||||
|
print("unreachable\n");
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// W4/W5 kill-test: explicit guest trap. Native must not run this page; the
|
||||||
|
// wasm backend converts it into a clean UCE error page and keeps serving.
|
||||||
|
RENDER(Request& context)
|
||||||
|
{
|
||||||
|
print("about to trap\n");
|
||||||
|
__builtin_trap();
|
||||||
|
print("unreachable\n");
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// W4 kill-test: stack exhaustion via unbounded recursion. Traps as
|
||||||
|
// "call stack exhausted"; the workspace drops cleanly.
|
||||||
|
u64 wasm_kill_recurse(volatile u64 depth)
|
||||||
|
{
|
||||||
|
volatile u64 next = depth + 1;
|
||||||
|
return(next + wasm_kill_recurse(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
RENDER(Request& context)
|
||||||
|
{
|
||||||
|
print("about to recurse\n");
|
||||||
|
volatile u64 sink = wasm_kill_recurse(0);
|
||||||
|
print("unreachable ", sink, "\n");
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# spikes/wasm-phase0 — WASM-PROPOSAL Phase 0 (toolchain & runtime spike)
|
|
||||||
|
|
||||||
Validates the dynamic-linking foundation for the WASM unit runtime:
|
|
||||||
wasi-sdk PIC side modules + a host loader linking a core module and a unit
|
|
||||||
module at runtime with shared memory/table. **Exit criterion passed; runtime
|
|
||||||
selected: Wasmtime.** See `FINDINGS.md` for results and `realunit-report.md`
|
|
||||||
for the real-generated-unit compile log.
|
|
||||||
|
|
||||||
Everything builds and runs **on k-uce** (toolchains under `/opt`, artifacts
|
|
||||||
under `/tmp/uce/wasm-phase0`):
|
|
||||||
|
|
||||||
```
|
|
||||||
bash spikes/wasm-phase0/build_modules.sh # core.wasm + unit.wasm (wasi-sdk)
|
|
||||||
bash spikes/wasm-phase0/build_loader.sh # loader (Wasmtime C API)
|
|
||||||
/tmp/uce/wasm-phase0/loader # expect: PHASE0 EXIT CRITERION: PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
Files:
|
|
||||||
|
|
||||||
- `core.cpp` — core-module stand-in: owns memory/allocator/libc++, exports
|
|
||||||
everything; includes the guest-side GOT.func resolver pattern
|
|
||||||
- `unit.cpp` — unit stand-in: PIC, imports allocator/runtime, exercises GOT,
|
|
||||||
cross-module C++ objects, function pointers, lambdas
|
|
||||||
- `loader.cpp` — minimal runtime linker (standard wasm-c-api): dylink.0
|
|
||||||
parsing, base allocation, GOT resolution, init sequencing
|
|
||||||
- `wasm_inspect.py` — dumps imports/exports/dylink.0 of a module
|
|
||||||
- `FINDINGS.md` — toolchain pins, WAMR rejection evidence, flag recipe,
|
|
||||||
Phase 2/3 implications
|
|
||||||
- `realunit-report.md` — `collections.uce.cpp`/`hello.uce.cpp` compiled as
|
|
||||||
side modules (worker-agent log)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 0: build the minimal loader against the WAMR build at /opt/wamr.
|
|
||||||
# Runs on k-uce.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# Runtime: Wasmtime C API. (WAMR was evaluated first per WASM-PROPOSAL §9 but
|
|
||||||
# its wasm-c-api ignores imported memories/tables — see FINDINGS.md.)
|
|
||||||
WASMTIME=/opt/wasmtime
|
|
||||||
OUT=/tmp/uce/wasm-phase0
|
|
||||||
|
|
||||||
clang++ -std=c++17 -O1 -g loader.cpp -o "$OUT/loader" \
|
|
||||||
-I"$WASMTIME/include" \
|
|
||||||
-L"$WASMTIME/lib" -lwasmtime -Wl,-rpath,"$WASMTIME/lib" \
|
|
||||||
-lpthread -lm -ldl
|
|
||||||
|
|
||||||
echo built: "$OUT/loader"
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 0: build core (non-PIC reactor, libc statically linked, all-exported)
|
|
||||||
# and unit (-fPIC, wasm-ld -shared, dylink.0) with wasi-sdk.
|
|
||||||
# Runs on k-uce. Artifacts go to /tmp/uce/wasm-phase0.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
SDK=/opt/wasi-sdk
|
|
||||||
OUT=/tmp/uce/wasm-phase0
|
|
||||||
mkdir -p "$OUT"
|
|
||||||
|
|
||||||
# Core module: reactor (no main, exports _initialize), exceptions off per
|
|
||||||
# WASM-PROPOSAL §11.1 (error codes at unit boundaries).
|
|
||||||
# --export-all so unit modules can import libc/libc++/core symbols from it;
|
|
||||||
# --import-table so the host creates the shared funcref table (sized with
|
|
||||||
# headroom up front — WAMR cannot grow or write tables from the host side);
|
|
||||||
# __stack_pointer exported because PIC side modules import it (dylink ABI).
|
|
||||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
|
|
||||||
-O1 -fno-exceptions \
|
|
||||||
core.cpp -o "$OUT/core.wasm" \
|
|
||||||
-Wl,--export-all \
|
|
||||||
-Wl,--import-table \
|
|
||||||
-Wl,--export=__stack_pointer \
|
|
||||||
-Wl,--export=__heap_base \
|
|
||||||
-Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE
|
|
||||||
|
|
||||||
# Unit module: PIC object, linked -shared with no libc/libc++ of its own —
|
|
||||||
# every undefined symbol must become an import (resolved from core by the
|
|
||||||
# loader). Inline/template code (std::string etc.) instantiates locally,
|
|
||||||
# which is fine; the allocator must NOT be defined here (§3.2).
|
|
||||||
# -fvisibility-inlines-hidden: vague-linkage code (templates, lambdas)
|
|
||||||
# binds locally instead of being interposable — without it, one libc++
|
|
||||||
# tree-emplace lambda became a self-import the loader cannot satisfy.
|
|
||||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \
|
|
||||||
-fvisibility-inlines-hidden \
|
|
||||||
-O1 -fno-exceptions \
|
|
||||||
-c unit.cpp -o "$OUT/unit.o"
|
|
||||||
|
|
||||||
# --Bsymbolic: bind weak/vague-linkage symbols the unit defines itself
|
|
||||||
# (template instantiations, lambdas) locally instead of emitting
|
|
||||||
# self-imports that the loader would have to lazy-bind.
|
|
||||||
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
|
||||||
--unresolved-symbols=import-dynamic \
|
|
||||||
--Bsymbolic \
|
|
||||||
"$OUT/unit.o" -o "$OUT/unit.wasm" \
|
|
||||||
--export=uce_unit_render
|
|
||||||
|
|
||||||
echo "--- core.wasm ---"
|
|
||||||
ls -la "$OUT/core.wasm"
|
|
||||||
echo "--- unit.wasm ---"
|
|
||||||
ls -la "$OUT/unit.wasm"
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 0 — core module stub.
|
|
||||||
//
|
|
||||||
// Stands in for the future "core module" (uce_lib + libc compiled to wasm):
|
|
||||||
// owns linear memory, the allocator, and libc/libc++ (statically linked,
|
|
||||||
// per the §10 mitigation: avoid shared wasi-libc entirely). Built as a
|
|
||||||
// non-PIC wasm32-wasi reactor with everything exported so unit modules can
|
|
||||||
// import from it.
|
|
||||||
//
|
|
||||||
// Deliberately avoids WASI I/O (no printf-to-fd): output accumulates in a
|
|
||||||
// buffer the host reads back, so the module can be instantiated through the
|
|
||||||
// plain wasm-c-api without a WASI context if need be.
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <functional>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
static std::string g_output;
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
// data symbol referenced from the unit module → must arrive there as a
|
|
||||||
// GOT.mem import
|
|
||||||
int core_counter = 7;
|
|
||||||
|
|
||||||
void uce_print(const char* s, size_t len)
|
|
||||||
{
|
|
||||||
g_output.append(s, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
// heap C++ object created in core, handed to the unit by pointer —
|
|
||||||
// validates one-heap/one-allocator pointer semantics (§3.4)
|
|
||||||
std::string* core_make_string(const char* s)
|
|
||||||
{
|
|
||||||
return(new std::string(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
void core_append_string(std::string* str, const char* s)
|
|
||||||
{
|
|
||||||
str->append(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
// plain function pointer crossing: unit passes its own function, core calls
|
|
||||||
// it back — validates the shared funcref table
|
|
||||||
void core_invoke_callback(void (*cb)(int), int arg)
|
|
||||||
{
|
|
||||||
cb(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// std::function allocated by unit code, executed here — validates fat
|
|
||||||
// callable objects (lambdas) across module boundaries
|
|
||||||
void core_invoke_function(std::function<int(int)>* f, int arg)
|
|
||||||
{
|
|
||||||
char buf[64];
|
|
||||||
snprintf(buf, sizeof(buf), "[fn:%d]", (*f)(arg));
|
|
||||||
g_output.append(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GOT.func resolution helper: taking the address here forces uce_print into
|
|
||||||
// core's elem segment at link time, and on wasm a function pointer IS its
|
|
||||||
// table index — so the loader can resolve GOT.func.uce_print with a plain
|
|
||||||
// call instead of host-side funcref injection (which WAMR does not allow).
|
|
||||||
// The production core will generalize this into a name → funcptr registry.
|
|
||||||
intptr_t core_table_index_of_uce_print()
|
|
||||||
{
|
|
||||||
return(reinterpret_cast<intptr_t>(uce_print));
|
|
||||||
}
|
|
||||||
|
|
||||||
// host reads the result out of linear memory via these
|
|
||||||
const char* core_output_data() { return(g_output.data()); }
|
|
||||||
size_t core_output_size() { return(g_output.size()); }
|
|
||||||
void core_output_clear() { g_output.clear(); }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 0 — minimal runtime loader (exit criterion).
|
|
||||||
//
|
|
||||||
// Embeds a wasm runtime through the standard wasm-c-api and links two
|
|
||||||
// modules at runtime, the way the production loader (§6) will:
|
|
||||||
//
|
|
||||||
// 1. instantiate core.wasm (owns memory/table/allocator/libc), stubbing
|
|
||||||
// its WASI imports with named trap functions (the core stub does no
|
|
||||||
// real I/O; any stub that actually gets called names itself)
|
|
||||||
// 2. parse unit.wasm's dylink.0 → data size/align, table slots needed
|
|
||||||
// 3. allocate __memory_base by calling core's exported malloc, and
|
|
||||||
// __table_base by growing core's exported funcref table
|
|
||||||
// 4. build the unit's import vector: env.memory / env.__indirect_function_table /
|
|
||||||
// env.__stack_pointer straight from core's exports; env.* functions from
|
|
||||||
// core's exports; GOT.mem.* as mutable i32 globals holding resolved
|
|
||||||
// addresses (self-resolved from the unit's own exports after
|
|
||||||
// instantiation when core lacks the symbol — weak data case);
|
|
||||||
// GOT.func.* as mutable i32 globals holding freshly grown table slots
|
|
||||||
// pointing at core export funcrefs
|
|
||||||
// 5. instantiate unit, patch deferred GOT entries, run
|
|
||||||
// __wasm_apply_data_relocs then __wasm_call_ctors
|
|
||||||
// 6. call uce_unit_render and read the output back out of linear memory
|
|
||||||
//
|
|
||||||
// Everything here is deliberately validation-grade: fail loudly, never guess.
|
|
||||||
|
|
||||||
#include <wasm.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
|
|
||||||
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
|
|
||||||
|
|
||||||
static std::vector<uint8_t> read_file(const char* fn)
|
|
||||||
{
|
|
||||||
FILE* f = fopen(fn, "rb");
|
|
||||||
CHECK(f, "cannot open %s", fn);
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
long n = ftell(f);
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
std::vector<uint8_t> buf(n);
|
|
||||||
CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn);
|
|
||||||
fclose(f);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- minimal dylink.0 parser -------------------------------------------
|
|
||||||
|
|
||||||
struct DylinkInfo
|
|
||||||
{
|
|
||||||
uint32_t mem_size = 0;
|
|
||||||
uint32_t mem_align = 0; // power of 2
|
|
||||||
uint32_t table_size = 0;
|
|
||||||
uint32_t table_align = 0;
|
|
||||||
bool found = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
static uint64_t read_uleb(const uint8_t* buf, size_t& pos)
|
|
||||||
{
|
|
||||||
uint64_t result = 0;
|
|
||||||
int shift = 0;
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
uint8_t b = buf[pos++];
|
|
||||||
result |= (uint64_t)(b & 0x7f) << shift;
|
|
||||||
if(!(b & 0x80))
|
|
||||||
return result;
|
|
||||||
shift += 7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static DylinkInfo parse_dylink(const std::vector<uint8_t>& wasm)
|
|
||||||
{
|
|
||||||
DylinkInfo info;
|
|
||||||
CHECK(wasm.size() > 8 && !memcmp(wasm.data(), "\0asm", 4), "not a wasm module");
|
|
||||||
size_t pos = 8;
|
|
||||||
while(pos < wasm.size())
|
|
||||||
{
|
|
||||||
uint8_t sec_id = wasm[pos++];
|
|
||||||
uint64_t size = read_uleb(wasm.data(), pos);
|
|
||||||
size_t end = pos + size;
|
|
||||||
if(sec_id == 0)
|
|
||||||
{
|
|
||||||
uint64_t name_len = read_uleb(wasm.data(), pos);
|
|
||||||
std::string name((const char*)wasm.data() + pos, name_len);
|
|
||||||
pos += name_len;
|
|
||||||
if(name == "dylink.0")
|
|
||||||
{
|
|
||||||
while(pos < end)
|
|
||||||
{
|
|
||||||
uint8_t sub = wasm[pos++];
|
|
||||||
uint64_t sub_len = read_uleb(wasm.data(), pos);
|
|
||||||
size_t sub_end = pos + sub_len;
|
|
||||||
if(sub == 1) // WASM_DYLINK_MEM_INFO
|
|
||||||
{
|
|
||||||
info.mem_size = read_uleb(wasm.data(), pos);
|
|
||||||
info.mem_align = read_uleb(wasm.data(), pos);
|
|
||||||
info.table_size = read_uleb(wasm.data(), pos);
|
|
||||||
info.table_align = read_uleb(wasm.data(), pos);
|
|
||||||
info.found = true;
|
|
||||||
}
|
|
||||||
pos = sub_end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pos = end;
|
|
||||||
}
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- named trap stubs for unsatisfied (WASI) imports --------------------
|
|
||||||
|
|
||||||
static wasm_store_t* g_store_for_stubs = nullptr;
|
|
||||||
|
|
||||||
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
|
||||||
{
|
|
||||||
(void)args; (void)results;
|
|
||||||
fprintf(stderr, "[stub called: %s]\n", (const char*)env);
|
|
||||||
char msg[256];
|
|
||||||
snprintf(msg, sizeof(msg), "unimplemented host import called: %s", (const char*)env);
|
|
||||||
wasm_message_t message;
|
|
||||||
wasm_byte_vec_new(&message, strlen(msg) + 1, msg);
|
|
||||||
wasm_trap_t* trap = wasm_trap_new(g_store_for_stubs, &message);
|
|
||||||
wasm_byte_vec_delete(&message);
|
|
||||||
return trap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- instance wrapper: name → extern map --------------------------------
|
|
||||||
|
|
||||||
struct Instance
|
|
||||||
{
|
|
||||||
wasm_module_t* module = nullptr;
|
|
||||||
wasm_instance_t* instance = nullptr;
|
|
||||||
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
|
|
||||||
std::map<std::string, wasm_extern_t*> by_name;
|
|
||||||
|
|
||||||
void index_exports()
|
|
||||||
{
|
|
||||||
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_exports(module, &types);
|
|
||||||
wasm_instance_exports(instance, &exports);
|
|
||||||
CHECK(types.size == exports.size, "export type/extern count mismatch");
|
|
||||||
for(size_t i = 0; i < types.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* nm = wasm_exporttype_name(types.data[i]);
|
|
||||||
std::string key(nm->data, nm->size);
|
|
||||||
// some wasm-c-api impls include the trailing NUL in name size
|
|
||||||
while(!key.empty() && key.back() == '\0')
|
|
||||||
key.pop_back();
|
|
||||||
by_name[key] = exports.data[i];
|
|
||||||
}
|
|
||||||
wasm_exporttype_vec_delete(&types);
|
|
||||||
}
|
|
||||||
|
|
||||||
wasm_func_t* func(const char* name)
|
|
||||||
{
|
|
||||||
auto it = by_name.find(name);
|
|
||||||
return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second);
|
|
||||||
}
|
|
||||||
wasm_global_t* global(const char* name)
|
|
||||||
{
|
|
||||||
auto it = by_name.find(name);
|
|
||||||
return it == by_name.end() ? nullptr : wasm_extern_as_global(it->second);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static wasm_store_t* g_store = nullptr;
|
|
||||||
|
|
||||||
static void report_trap(wasm_trap_t* trap, const char* what)
|
|
||||||
{
|
|
||||||
if(!trap)
|
|
||||||
return;
|
|
||||||
wasm_message_t msg;
|
|
||||||
wasm_trap_message(trap, &msg);
|
|
||||||
FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t call_i32(Instance& inst, const char* name, std::vector<int32_t> argv = {})
|
|
||||||
{
|
|
||||||
wasm_func_t* f = inst.func(name);
|
|
||||||
CHECK(f, "missing export func %s", name);
|
|
||||||
wasm_val_t args_buf[4];
|
|
||||||
for(size_t i = 0; i < argv.size(); i++)
|
|
||||||
args_buf[i] = WASM_I32_VAL(argv[i]);
|
|
||||||
wasm_val_t results_buf[1] = { WASM_INIT_VAL };
|
|
||||||
wasm_val_vec_t args = { argv.size(), args_buf };
|
|
||||||
wasm_val_vec_t results = { 1, results_buf };
|
|
||||||
size_t result_arity = wasm_func_result_arity(f);
|
|
||||||
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
|
|
||||||
wasm_trap_t* trap = wasm_func_call(f, &args, result_arity ? &results : &no_results);
|
|
||||||
report_trap(trap, name);
|
|
||||||
return result_arity ? results_buf[0].of.i32 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_global_t* make_i32_global(int32_t value, wasm_mutability_t mut)
|
|
||||||
{
|
|
||||||
wasm_globaltype_t* gt = wasm_globaltype_new(wasm_valtype_new(WASM_I32), mut);
|
|
||||||
wasm_val_t val = WASM_I32_VAL(value);
|
|
||||||
wasm_global_t* g = wasm_global_new(g_store, gt, &val);
|
|
||||||
CHECK(g, "wasm_global_new failed (host-created globals unsupported?)");
|
|
||||||
wasm_globaltype_delete(gt);
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase0/core.wasm";
|
|
||||||
const char* unit_path = argc > 2 ? argv[2] : "/tmp/uce/wasm-phase0/unit.wasm";
|
|
||||||
|
|
||||||
wasm_engine_t* engine = wasm_engine_new();
|
|
||||||
CHECK(engine, "engine");
|
|
||||||
g_store = wasm_store_new(engine);
|
|
||||||
CHECK(g_store, "store");
|
|
||||||
g_store_for_stubs = g_store;
|
|
||||||
|
|
||||||
// ---- 1. core module ---------------------------------------------------
|
|
||||||
std::vector<uint8_t> core_bytes = read_file(core_path);
|
|
||||||
wasm_byte_vec_t core_bv;
|
|
||||||
wasm_byte_vec_new(&core_bv, core_bytes.size(), (const char*)core_bytes.data());
|
|
||||||
Instance core;
|
|
||||||
core.module = wasm_module_new(g_store, &core_bv);
|
|
||||||
wasm_byte_vec_delete(&core_bv);
|
|
||||||
CHECK(core.module, "core module load failed");
|
|
||||||
|
|
||||||
// the shared funcref table is host-created (core links with --import-table)
|
|
||||||
// because WAMR cannot grow a table from the host: size it up front as
|
|
||||||
// core's declared minimum (= its own elem needs) plus headroom for unit
|
|
||||||
// module table regions. __table_base allocation bumps from the minimum.
|
|
||||||
wasm_table_t* table = nullptr;
|
|
||||||
uint32_t table_next_free = 0;
|
|
||||||
const uint32_t TABLE_HEADROOM = 2048;
|
|
||||||
|
|
||||||
wasm_importtype_vec_t core_imports = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_imports(core.module, &core_imports);
|
|
||||||
std::vector<wasm_extern_t*> core_import_externs(core_imports.size);
|
|
||||||
for(size_t i = 0; i < core_imports.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* mod = wasm_importtype_module(core_imports.data[i]);
|
|
||||||
const wasm_name_t* nm = wasm_importtype_name(core_imports.data[i]);
|
|
||||||
const wasm_externtype_t* et = wasm_importtype_type(core_imports.data[i]);
|
|
||||||
std::string name(nm->data, nm->size);
|
|
||||||
if(wasm_externtype_kind(et) == WASM_EXTERN_TABLE)
|
|
||||||
{
|
|
||||||
CHECK(name.rfind("__indirect_function_table", 0) == 0,
|
|
||||||
"unexpected core table import %s", name.c_str());
|
|
||||||
const wasm_tabletype_t* tt = wasm_externtype_as_tabletype_const(et);
|
|
||||||
uint32_t core_min = wasm_tabletype_limits(tt)->min;
|
|
||||||
wasm_limits_t limits = { core_min + TABLE_HEADROOM, core_min + TABLE_HEADROOM };
|
|
||||||
wasm_tabletype_t* host_tt = wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &limits);
|
|
||||||
table = wasm_table_new(g_store, host_tt, nullptr);
|
|
||||||
CHECK(table, "wasm_table_new failed (host-created tables unsupported?)");
|
|
||||||
wasm_tabletype_delete(host_tt);
|
|
||||||
table_next_free = core_min;
|
|
||||||
printf("host table created: size=%u, core region=[0,%u)\n", core_min + TABLE_HEADROOM, core_min);
|
|
||||||
core_import_externs[i] = wasm_table_as_extern(table);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC,
|
|
||||||
"core has unexpected non-func import %.*s.%s",
|
|
||||||
(int)mod->size, mod->data, name.c_str());
|
|
||||||
// named trap stub; leaks the name string, fine for a spike
|
|
||||||
char* label = strdup((std::string(mod->data, mod->size) + "." + name).c_str());
|
|
||||||
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
|
|
||||||
wasm_func_t* stub = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr);
|
|
||||||
CHECK(stub, "stub func creation failed for %s", label);
|
|
||||||
core_import_externs[i] = wasm_func_as_extern(stub);
|
|
||||||
}
|
|
||||||
CHECK(table, "core does not import __indirect_function_table — rebuild with --import-table");
|
|
||||||
wasm_extern_vec_t core_iv = { core_import_externs.size(), core_import_externs.data() };
|
|
||||||
wasm_trap_t* trap = nullptr;
|
|
||||||
core.instance = wasm_instance_new(g_store, core.module, &core_iv, &trap);
|
|
||||||
report_trap(trap, "core instantiation");
|
|
||||||
CHECK(core.instance, "core instantiation failed");
|
|
||||||
core.index_exports();
|
|
||||||
printf("core instantiated: %zu exports\n", core.by_name.size());
|
|
||||||
|
|
||||||
// reactor init (runs ctors). Wasmtime does not auto-run _initialize;
|
|
||||||
// WAMR does — and calling it twice trips wasi-libc's double-init guard
|
|
||||||
// (__builtin_trap → "unreachable"), which cost us a debugging round.
|
|
||||||
if(core.func("_initialize"))
|
|
||||||
call_i32(core, "_initialize");
|
|
||||||
|
|
||||||
wasm_memory_t* memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr);
|
|
||||||
CHECK(memory, "core does not export memory");
|
|
||||||
|
|
||||||
// ---- 2./3. dylink + base allocation ------------------------------------
|
|
||||||
std::vector<uint8_t> unit_bytes = read_file(unit_path);
|
|
||||||
DylinkInfo dl = parse_dylink(unit_bytes);
|
|
||||||
CHECK(dl.found, "unit has no dylink.0 mem_info");
|
|
||||||
printf("dylink.0: memsize=%u memalign=2^%u tablesize=%u\n", dl.mem_size, dl.mem_align, dl.table_size);
|
|
||||||
|
|
||||||
uint32_t align = 1u << dl.mem_align;
|
|
||||||
int32_t raw = call_i32(core, "malloc", { (int32_t)(dl.mem_size + align) });
|
|
||||||
CHECK(raw, "core malloc returned 0");
|
|
||||||
int32_t memory_base = (raw + (align - 1)) & ~(int32_t)(align - 1);
|
|
||||||
|
|
||||||
uint32_t table_base = table_next_free;
|
|
||||||
table_next_free += dl.table_size;
|
|
||||||
CHECK(table_next_free <= wasm_table_size(table), "table headroom exhausted");
|
|
||||||
printf("bases: __memory_base=%d __table_base=%u (table size %u)\n",
|
|
||||||
memory_base, table_base, wasm_table_size(table));
|
|
||||||
|
|
||||||
// ---- 4. unit import resolution -----------------------------------------
|
|
||||||
wasm_byte_vec_t unit_bv;
|
|
||||||
wasm_byte_vec_new(&unit_bv, unit_bytes.size(), (const char*)unit_bytes.data());
|
|
||||||
Instance unit;
|
|
||||||
unit.module = wasm_module_new(g_store, &unit_bv);
|
|
||||||
wasm_byte_vec_delete(&unit_bv);
|
|
||||||
CHECK(unit.module, "unit module load failed");
|
|
||||||
|
|
||||||
wasm_importtype_vec_t unit_imports = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_imports(unit.module, &unit_imports);
|
|
||||||
std::vector<wasm_extern_t*> unit_import_externs(unit_imports.size);
|
|
||||||
// GOT.mem entries that must be self-resolved from the unit's own exports
|
|
||||||
// after instantiation (weak data the core does not define)
|
|
||||||
std::vector<std::pair<std::string, wasm_global_t*>> deferred_got;
|
|
||||||
|
|
||||||
for(size_t i = 0; i < unit_imports.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* mod_n = wasm_importtype_module(unit_imports.data[i]);
|
|
||||||
const wasm_name_t* nm_n = wasm_importtype_name(unit_imports.data[i]);
|
|
||||||
std::string mod(mod_n->data, mod_n->size);
|
|
||||||
std::string nm(nm_n->data, nm_n->size);
|
|
||||||
const wasm_externtype_t* et = wasm_importtype_type(unit_imports.data[i]);
|
|
||||||
wasm_externkind_t kind = wasm_externtype_kind(et);
|
|
||||||
wasm_extern_t* resolved = nullptr;
|
|
||||||
|
|
||||||
if(mod == "env" && nm == "memory")
|
|
||||||
resolved = wasm_memory_as_extern(memory);
|
|
||||||
else if(mod == "env" && nm == "__indirect_function_table")
|
|
||||||
resolved = wasm_table_as_extern(table);
|
|
||||||
else if(mod == "env" && nm == "__stack_pointer")
|
|
||||||
{
|
|
||||||
CHECK(core.global("__stack_pointer"), "core does not export __stack_pointer");
|
|
||||||
resolved = core.by_name["__stack_pointer"];
|
|
||||||
}
|
|
||||||
else if(mod == "env" && nm == "__memory_base")
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global(memory_base, WASM_CONST));
|
|
||||||
else if(mod == "env" && nm == "__table_base")
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global((int32_t)table_base, WASM_CONST));
|
|
||||||
else if(mod == "env" && kind == WASM_EXTERN_FUNC)
|
|
||||||
{
|
|
||||||
auto it = core.by_name.find(nm);
|
|
||||||
CHECK(it != core.by_name.end(), "unresolved unit func import env.%s", nm.c_str());
|
|
||||||
resolved = it->second;
|
|
||||||
}
|
|
||||||
else if(mod == "GOT.mem")
|
|
||||||
{
|
|
||||||
wasm_global_t* src = core.global(nm.c_str());
|
|
||||||
if(src)
|
|
||||||
{
|
|
||||||
wasm_val_t v;
|
|
||||||
wasm_global_get(src, &v);
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global(v.of.i32, WASM_VAR));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// provisional 0; patched from the unit's own export post-instantiation
|
|
||||||
wasm_global_t* g = make_i32_global(0, WASM_VAR);
|
|
||||||
deferred_got.push_back({ nm, g });
|
|
||||||
resolved = wasm_global_as_extern(g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(mod == "GOT.func")
|
|
||||||
{
|
|
||||||
// resolved guest-side: the core exports a helper returning
|
|
||||||
// (intptr_t)&func, which on wasm is the function's table index —
|
|
||||||
// no host-side funcref injection needed (WAMR forbids it anyway)
|
|
||||||
std::string helper = "core_table_index_of_" + nm;
|
|
||||||
CHECK(core.func(helper.c_str()), "no GOT.func resolver %s in core", helper.c_str());
|
|
||||||
int32_t slot = call_i32(core, helper.c_str());
|
|
||||||
CHECK(slot > 0, "GOT.func resolver %s returned %d", helper.c_str(), slot);
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global(slot, WASM_VAR));
|
|
||||||
}
|
|
||||||
CHECK(resolved, "unhandled unit import %s.%s (kind %d)", mod.c_str(), nm.c_str(), (int)kind);
|
|
||||||
unit_import_externs[i] = resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 5. instantiate unit, patch GOT, run init ---------------------------
|
|
||||||
wasm_extern_vec_t unit_iv = { unit_import_externs.size(), unit_import_externs.data() };
|
|
||||||
trap = nullptr;
|
|
||||||
unit.instance = wasm_instance_new(g_store, unit.module, &unit_iv, &trap);
|
|
||||||
report_trap(trap, "unit instantiation");
|
|
||||||
CHECK(unit.instance, "unit instantiation failed");
|
|
||||||
unit.index_exports();
|
|
||||||
printf("unit instantiated: %zu exports\n", unit.by_name.size());
|
|
||||||
|
|
||||||
for(auto& [nm, got] : deferred_got)
|
|
||||||
{
|
|
||||||
wasm_global_t* own = unit.global(nm.c_str());
|
|
||||||
CHECK(own, "GOT.mem.%s defined neither by core nor by unit", nm.c_str());
|
|
||||||
wasm_val_t v;
|
|
||||||
wasm_global_get(own, &v);
|
|
||||||
// dylink ABI: a PIC module's exported data symbols are offsets relative
|
|
||||||
// to its __memory_base; the linker must add the base when resolving
|
|
||||||
wasm_val_t nv = WASM_I32_VAL(memory_base + v.of.i32);
|
|
||||||
wasm_global_set(got, &nv);
|
|
||||||
printf("self-resolved GOT.mem.%s = %d (offset %d)\n", nm.c_str(), memory_base + v.of.i32, v.of.i32);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(unit.func("__wasm_apply_data_relocs"))
|
|
||||||
call_i32(unit, "__wasm_apply_data_relocs");
|
|
||||||
if(unit.func("__wasm_call_ctors"))
|
|
||||||
call_i32(unit, "__wasm_call_ctors");
|
|
||||||
|
|
||||||
// ---- 6. render and read back --------------------------------------------
|
|
||||||
int32_t counter_addr = 0;
|
|
||||||
{
|
|
||||||
wasm_global_t* cc = core.global("core_counter");
|
|
||||||
CHECK(cc, "core_counter not exported");
|
|
||||||
wasm_val_t v;
|
|
||||||
wasm_global_get(cc, &v);
|
|
||||||
counter_addr = v.of.i32;
|
|
||||||
}
|
|
||||||
byte_t* mem = wasm_memory_data(memory);
|
|
||||||
int32_t counter_before;
|
|
||||||
memcpy(&counter_before, mem + counter_addr, 4);
|
|
||||||
|
|
||||||
call_i32(unit, "uce_unit_render");
|
|
||||||
|
|
||||||
int32_t out_ptr = call_i32(core, "core_output_data");
|
|
||||||
int32_t out_len = call_i32(core, "core_output_size");
|
|
||||||
mem = wasm_memory_data(memory); // may have moved if memory grew
|
|
||||||
int32_t counter_after;
|
|
||||||
memcpy(&counter_after, mem + counter_addr, 4);
|
|
||||||
|
|
||||||
printf("---- unit output (%d bytes) ----\n%.*s\n--------------------------------\n",
|
|
||||||
out_len, out_len, mem + out_ptr);
|
|
||||||
printf("core_counter (in linear memory): before=%d after=%d\n", counter_before, counter_after);
|
|
||||||
|
|
||||||
bool ok = out_len > 0 &&
|
|
||||||
memmem(mem + out_ptr, out_len, "unit-data-segment-ok", 20) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "counter=7", 9) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "mapsum=3", 8) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "core-string+unit", 16) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "[cb:42]", 7) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "[got-func-ok]", 13) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "[fn:42]", 7) &&
|
|
||||||
counter_before == 7 && counter_after == 8;
|
|
||||||
|
|
||||||
printf(ok ? "PHASE0 EXIT CRITERION: PASS\n" : "PHASE0 EXIT CRITERION: FAIL (see output above)\n");
|
|
||||||
return ok ? 0 : 1;
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
# UCE real generated unit -> WASM PIC side module report
|
|
||||||
|
|
||||||
Remote host: `k-uce`
|
|
||||||
Working directory/artifacts left in place: `/tmp/uce/wasm-phase0/realunit/`
|
|
||||||
Repository `/Code/uce.openfu.com/uce` was treated read-only; patched copies/shims only under `/tmp/uce/wasm-phase0/realunit/`.
|
|
||||||
|
|
||||||
## Result
|
|
||||||
|
|
||||||
Both real generated units compiled and linked as WASM PIC side modules with `dylink.0`:
|
|
||||||
|
|
||||||
- `collections.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/collections.wasm`
|
|
||||||
- `dylink.0`: `memsize=1208 memalign=2^2 tablesize=3 tablealign=2^0`
|
|
||||||
- imports: 52
|
|
||||||
- exports: 23
|
|
||||||
- allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`).
|
|
||||||
- `hello.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/hello.wasm`
|
|
||||||
- `dylink.0`: `memsize=5544 memalign=2^2 tablesize=0 tablealign=2^0`
|
|
||||||
- imports: 38
|
|
||||||
- exports: 24
|
|
||||||
- allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`).
|
|
||||||
|
|
||||||
Inspector outputs are saved remotely as:
|
|
||||||
|
|
||||||
- `/tmp/uce/wasm-phase0/realunit/collections.inspect.txt`
|
|
||||||
- `/tmp/uce/wasm-phase0/realunit/hello.inspect.txt`
|
|
||||||
|
|
||||||
## Final command lines
|
|
||||||
|
|
||||||
Run on `k-uce` from `/tmp/uce/wasm-phase0/realunit`.
|
|
||||||
|
|
||||||
`collections`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \
|
|
||||||
-I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \
|
|
||||||
-c collections.uce.cpp -o collections.o
|
|
||||||
|
|
||||||
/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \
|
|
||||||
collections.o -o collections.wasm --export=__uce_render
|
|
||||||
|
|
||||||
python3 /Code/uce.openfu.com/uce/spikes/wasm-phase0/wasm_inspect.py collections.wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
`hello`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \
|
|
||||||
-I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \
|
|
||||||
-c hello.uce.cpp -o hello.o
|
|
||||||
|
|
||||||
/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \
|
|
||||||
hello.o -o hello.wasm --export=__uce_render
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: the stub-spike command exported `uce_unit_render`, but these generated units define/export `__uce_render` from the repo `RENDER` macro, so the real-unit link exports `__uce_render`.
|
|
||||||
|
|
||||||
## Ordered friction points and fixes
|
|
||||||
|
|
||||||
1. **WASI sysroot rejects `<signal.h>` by default.**
|
|
||||||
|
|
||||||
First compile failed via `/Code/uce.openfu.com/uce/src/lib/sys.h`:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
/share/wasi-sysroot/include/wasm32-wasip1/signal.h:2:2: error:
|
|
||||||
"wasm lacks signal support; to enable minimal signal emulation, compile with -D_WASI_EMULATED_SIGNAL ..."
|
|
||||||
```
|
|
||||||
|
|
||||||
Fix: added `/tmp/uce/wasm-phase0/realunit/shim/signal.h` before repo/system includes. Key lines:
|
|
||||||
|
|
||||||
```c
|
|
||||||
typedef int sig_atomic_t;
|
|
||||||
typedef void (*sighandler_t)(int);
|
|
||||||
#define SIGTERM 15
|
|
||||||
int raise(int);
|
|
||||||
sighandler_t signal(int, sighandler_t);
|
|
||||||
```
|
|
||||||
|
|
||||||
This is only enough for declarations in `sys.h`; real signal behavior has no WASI equivalent.
|
|
||||||
|
|
||||||
2. **Generated `collections.uce.cpp` includes missing local `demo_guard.h`.**
|
|
||||||
|
|
||||||
First compile also failed:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
fatal error: 'demo_guard.h' file not found
|
|
||||||
```
|
|
||||||
|
|
||||||
Fix: added empty include guard shim `/tmp/uce/wasm-phase0/realunit/shim/demo_guard.h`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#ifndef UCE_WASM_SHIM_DEMO_GUARD_H
|
|
||||||
#define UCE_WASM_SHIM_DEMO_GUARD_H
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Generated source uses an absolute repo include, preventing header interposition.**
|
|
||||||
|
|
||||||
Original generated line:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "/Code/uce.openfu.com/uce/src/lib/uce_lib.h"
|
|
||||||
```
|
|
||||||
|
|
||||||
To use patched shim headers without touching `/Code`, copied the generated units into the workdir and changed only that line to:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
#include "uce_lib.h"
|
|
||||||
```
|
|
||||||
|
|
||||||
Copies are `/tmp/uce/wasm-phase0/realunit/collections.uce.cpp` and `hello.uce.cpp`.
|
|
||||||
|
|
||||||
4. **`types.h` defines global `operator new/delete` in every unit.**
|
|
||||||
|
|
||||||
A straight build linked, but exported allocator definitions (`_Znwm`, `_ZdlPv`), violating the side-module allocator rule. The repo code in `types.h` contains:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
void * operator new(decltype(sizeof(0)) n) noexcept(false) { ... malloc(n) ... }
|
|
||||||
void operator delete(void * p) throw() { free(p); }
|
|
||||||
```
|
|
||||||
|
|
||||||
Fix: copied `uce_lib.h` and `types.h` to the shim tree, then patched only the copied `shim/types.h` allocator block to declarations:
|
|
||||||
|
|
||||||
```c++
|
|
||||||
// WASM side-module shim: allocator must be provided by the host/core module,
|
|
||||||
// not defined in each generated unit. Keep declarations only.
|
|
||||||
void * operator new(decltype(sizeof(0)) n) noexcept(false);
|
|
||||||
void operator delete(void * p) throw();
|
|
||||||
```
|
|
||||||
|
|
||||||
Verification from `llvm-nm -C collections.o`:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
U operator delete(void*, unsigned long)
|
|
||||||
U operator new(unsigned long)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notable imports / GOT entries
|
|
||||||
|
|
||||||
`collections.wasm` notable imports:
|
|
||||||
|
|
||||||
- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.__stack_pointer`, `env.memory`, `env.__indirect_function_table`
|
|
||||||
- UCE/runtime functions: `DValue::set`, `DValue::operator[]`, `DValue::push`, `dv_filter`, `dv_map`, `dv_group_by`, `list_unique`, `list_sort`, `replace`, `to_upper`, `html_escape`, `json_encode`, `var_dump`, `time`
|
|
||||||
- MySQL wrappers are imported even for this unit because included header inline functions reference `MySQL` methods.
|
|
||||||
- GOT.mem globals:
|
|
||||||
- `GOT.mem.context`
|
|
||||||
- `GOT.mem._ZNSt3__25ctypeIcE2idE`
|
|
||||||
- `GOT.mem._ZTVN10__cxxabiv117__class_type_infoE`
|
|
||||||
|
|
||||||
`hello.wasm` notable imports:
|
|
||||||
|
|
||||||
- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.memcmp`
|
|
||||||
- UCE/runtime functions: `time`, `html_escape`, `html_escapey`, `var_dump`
|
|
||||||
- GOT.mem globals:
|
|
||||||
- `GOT.mem.context`
|
|
||||||
- `GOT.mem._ZNSt3__219piecewise_constructE`
|
|
||||||
- `GOT.mem._ZNSt3__25ctypeIcE2idE`
|
|
||||||
|
|
||||||
## Phase 2 land mines
|
|
||||||
|
|
||||||
- `src/lib/sys.h` includes `<signal.h>` and declares signals/tasks/socket/process/file-lock APIs. WASI has no normal POSIX signals, fork/exec, traditional sockets, or process model. These need real repo-level `#ifdef __wasm__` / runtime boundary design, not local shims.
|
|
||||||
- `types.h` defines global allocator operators in a header. For side modules this must move to the core/runtime or be gated out under WASM side-module builds; otherwise each unit defines its own allocator entry points.
|
|
||||||
- Generated units include `/Code/.../uce_lib.h` by absolute path. That makes cross-target header selection brittle. The generator should emit a logical include (`"uce_lib.h"`) and the build should supply target-specific include order.
|
|
||||||
- Header-defined helper functions cause many exports from each unit (`to_string`, MySQL wrapper functions, vector slow-path/lambda helpers, globals `parent_pid`, `my_pid`, `context`). For a clean side-module ABI, these should probably be hidden, moved out of headers, marked inline/static where appropriate, or provided by the core module.
|
|
||||||
- MySQL inline wrappers are pulled into every unit by `uce_lib.h` even when the unit does not use MySQL directly. This is why `MySQL::connect/disconnect/query/error` imports appear in both outputs.
|
|
||||||
- Kept `-fno-exceptions`; no try/catch blocker was hit for these two generated units.
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 0 — unit module stub.
|
|
||||||
//
|
|
||||||
// Stands in for a generated UCE unit: compiled with -fPIC and linked with
|
|
||||||
// wasm-ld -shared into a PIC module carrying a dylink.0 section. Defines no
|
|
||||||
// allocator and links no libc — operator new/delete, memcpy, and the core
|
|
||||||
// API all arrive as imports resolved by the loader against the core module
|
|
||||||
// (§5.4 unit module contract).
|
|
||||||
//
|
|
||||||
// Exercises, in one render call:
|
|
||||||
// - unit-local data segment with relocation (__memory_base placement)
|
|
||||||
// - GOT.mem read+write of a core-defined global (core_counter)
|
|
||||||
// - C++ containers (std::string/std::map) on the core's heap
|
|
||||||
// - heap object created in core, mutated and read from the unit
|
|
||||||
// - function pointer from unit through core and back (shared table)
|
|
||||||
// - std::function/lambda handed across the module boundary
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <functional>
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
void uce_print(const char* s, size_t len);
|
|
||||||
extern int core_counter;
|
|
||||||
std::string* core_make_string(const char* s);
|
|
||||||
void core_append_string(std::string* str, const char* s);
|
|
||||||
void core_invoke_callback(void (*cb)(int), int arg);
|
|
||||||
void core_invoke_function(std::function<int(int)>* f, int arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char* unit_static_message = "unit-data-segment-ok";
|
|
||||||
static int unit_state = 41;
|
|
||||||
|
|
||||||
static void my_callback(int x)
|
|
||||||
{
|
|
||||||
std::string s = "[cb:" + std::to_string(x + unit_state) + "]";
|
|
||||||
uce_print(s.data(), s.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void uce_unit_render()
|
|
||||||
{
|
|
||||||
std::string out = "hello from unit; ";
|
|
||||||
out += unit_static_message;
|
|
||||||
out += "; counter=" + std::to_string(core_counter);
|
|
||||||
uce_print(out.data(), out.size());
|
|
||||||
|
|
||||||
std::map<std::string, int> m;
|
|
||||||
m["a"] = 1;
|
|
||||||
m["b"] = 2;
|
|
||||||
int sum = 0;
|
|
||||||
for(auto& kv : m)
|
|
||||||
sum += kv.second;
|
|
||||||
std::string s2 = "; mapsum=" + std::to_string(sum);
|
|
||||||
uce_print(s2.data(), s2.size());
|
|
||||||
|
|
||||||
std::string* cs = core_make_string("; core-string");
|
|
||||||
core_append_string(cs, "+unit");
|
|
||||||
uce_print(cs->data(), cs->size());
|
|
||||||
delete cs;
|
|
||||||
|
|
||||||
core_invoke_callback(my_callback, 1);
|
|
||||||
|
|
||||||
// address of a core-defined function taken in the unit → GOT.func import;
|
|
||||||
// the loader must ensure the core function has a funcref table entry.
|
|
||||||
// volatile so the optimizer cannot fold it back into a direct call.
|
|
||||||
void (*volatile print_ptr)(const char*, size_t) = uce_print;
|
|
||||||
print_ptr("[got-func-ok]", 13);
|
|
||||||
|
|
||||||
auto* fn = new std::function<int(int)>([](int v) { return(v * 3); });
|
|
||||||
core_invoke_function(fn, 14);
|
|
||||||
delete fn;
|
|
||||||
|
|
||||||
core_counter++;
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Phase 0 helper: dump a wasm module's imports, exports, and dylink.0
|
|
||||||
custom section (memory/table size requirements). Doubles as the reference
|
|
||||||
for the loader's dylink.0 parsing (§6 step 4)."""
|
|
||||||
import sys, struct
|
|
||||||
|
|
||||||
def uleb(buf, pos):
|
|
||||||
result = 0
|
|
||||||
shift = 0
|
|
||||||
while True:
|
|
||||||
b = buf[pos]
|
|
||||||
pos += 1
|
|
||||||
result |= (b & 0x7f) << shift
|
|
||||||
if not (b & 0x80):
|
|
||||||
return result, pos
|
|
||||||
shift += 7
|
|
||||||
|
|
||||||
def name(buf, pos):
|
|
||||||
n, pos = uleb(buf, pos)
|
|
||||||
return buf[pos:pos+n].decode("utf-8", "replace"), pos + n
|
|
||||||
|
|
||||||
def limits(buf, pos):
|
|
||||||
flags = buf[pos]; pos += 1
|
|
||||||
mn, pos = uleb(buf, pos)
|
|
||||||
mx = None
|
|
||||||
if flags & 1:
|
|
||||||
mx, pos = uleb(buf, pos)
|
|
||||||
return (mn, mx), pos
|
|
||||||
|
|
||||||
KIND = {0: "func", 1: "table", 2: "memory", 3: "global"}
|
|
||||||
VALTYPE = {0x7f: "i32", 0x7e: "i64", 0x7d: "f32", 0x7c: "f64", 0x70: "funcref", 0x6f: "externref"}
|
|
||||||
|
|
||||||
def main(fn):
|
|
||||||
buf = open(fn, "rb").read()
|
|
||||||
assert buf[:8] == b"\0asm\x01\0\0\0", "not a wasm module"
|
|
||||||
pos = 8
|
|
||||||
while pos < len(buf):
|
|
||||||
sec_id = buf[pos]; pos += 1
|
|
||||||
size, pos = uleb(buf, pos)
|
|
||||||
end = pos + size
|
|
||||||
if sec_id == 0:
|
|
||||||
sname, p = name(buf, pos)
|
|
||||||
if sname == "dylink.0":
|
|
||||||
print("== dylink.0 ==")
|
|
||||||
while p < end:
|
|
||||||
sub = buf[p]; p += 1
|
|
||||||
sublen, p = uleb(buf, p)
|
|
||||||
subend = p + sublen
|
|
||||||
if sub == 1: # WASM_DYLINK_MEM_INFO
|
|
||||||
memsize, p2 = uleb(buf, p)
|
|
||||||
memalign, p2 = uleb(buf, p2)
|
|
||||||
tabsize, p2 = uleb(buf, p2)
|
|
||||||
tabalign, p2 = uleb(buf, p2)
|
|
||||||
print(f" mem_info: memsize={memsize} memalign=2^{memalign} tablesize={tabsize} tablealign=2^{tabalign}")
|
|
||||||
else:
|
|
||||||
print(f" subsection type={sub} len={sublen}")
|
|
||||||
p = subend
|
|
||||||
elif sname in ("uce.abi",):
|
|
||||||
print(f"== custom section {sname} ({end - p} bytes) ==")
|
|
||||||
elif sec_id == 2:
|
|
||||||
count, p = uleb(buf, pos)
|
|
||||||
print(f"== imports ({count}) ==")
|
|
||||||
for _ in range(count):
|
|
||||||
mod, p = name(buf, p)
|
|
||||||
nm, p = name(buf, p)
|
|
||||||
kind = buf[p]; p += 1
|
|
||||||
detail = ""
|
|
||||||
if kind == 0:
|
|
||||||
_, p = uleb(buf, p)
|
|
||||||
elif kind == 1:
|
|
||||||
et = buf[p]; p += 1
|
|
||||||
lim, p = limits(buf, p)
|
|
||||||
detail = f" {VALTYPE.get(et, hex(et))} {lim}"
|
|
||||||
elif kind == 2:
|
|
||||||
lim, p = limits(buf, p)
|
|
||||||
detail = f" {lim}"
|
|
||||||
elif kind == 3:
|
|
||||||
vt = buf[p]; p += 1
|
|
||||||
mut = buf[p]; p += 1
|
|
||||||
detail = f" {VALTYPE.get(vt, hex(vt))}{' mut' if mut else ''}"
|
|
||||||
print(f" {KIND.get(kind, kind):6} {mod}.{nm}{detail}")
|
|
||||||
elif sec_id == 7:
|
|
||||||
count, p = uleb(buf, pos)
|
|
||||||
print(f"== exports ({count}) ==")
|
|
||||||
for _ in range(count):
|
|
||||||
nm, p = name(buf, p)
|
|
||||||
kind = buf[p]; p += 1
|
|
||||||
_, p = uleb(buf, p)
|
|
||||||
print(f" {KIND.get(kind, kind):6} {nm}")
|
|
||||||
pos = end
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
for fn in sys.argv[1:]:
|
|
||||||
print(f"### {fn}")
|
|
||||||
main(fn)
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# spikes/wasm-phase2 — core module + membrane scaffold
|
|
||||||
|
|
||||||
Phase 2 validates the next WASM step without the Phase 3 dynamic loader. The
|
|
||||||
scaffold compiles a native UCE core subset (`Request`, `DValue`, UCEB1, print
|
|
||||||
buffering) plus one statically linked real `.uce` page into a WASM reactor.
|
|
||||||
A small Wasmtime C-API host provides the first membrane hostcalls, sends a UCEB1
|
|
||||||
request context into the guest, invokes render, and reads the response from
|
|
||||||
linear memory. `uce_host_ctx_read(ptr, cap)` follows a length-query contract:
|
|
||||||
`ptr == 0` or `cap == 0` returns the required length; a short non-zero buffer
|
|
||||||
also returns the required length without a partial copy; an adequately sized
|
|
||||||
buffer receives the full context and returns the copied length.
|
|
||||||
|
|
||||||
This is intentionally not the final worker. It proves the Phase 2 membrane path:
|
|
||||||
core-owned DValue/UCEB1 code runs in WASM and a `.uce` render entry sees a
|
|
||||||
host-provided request context through the membrane. The checked-in page uses the
|
|
||||||
same `RENDER(Request&)` entry shape as generated units, but it is included
|
|
||||||
directly by the scaffold rather than emitted by the UCE preprocessor.
|
|
||||||
|
|
||||||
Run on `k-uce`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash spikes/wasm-phase2/build_modules.sh
|
|
||||||
bash spikes/wasm-phase2/build_loader.sh
|
|
||||||
/tmp/uce/wasm-phase2/loader
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected final line:
|
|
||||||
|
|
||||||
```text
|
|
||||||
PHASE2 EXIT CRITERION: PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
Files:
|
|
||||||
|
|
||||||
- `core.cpp` — WASM reactor core subset and membrane decode/render entry.
|
|
||||||
- `page.uce` — real UCE page statically linked for Phase 2 only.
|
|
||||||
- `loader.cpp` — host runner with `uce_host_ctx_read` and `uce_host_log`.
|
|
||||||
Its small host-side UCEB1 encoder is spike-only; the production UCE server
|
|
||||||
host should use the native `ucb_encode()` implementation to avoid format drift.
|
|
||||||
- `build_modules.sh` — wasi-sdk build for `/tmp/uce/wasm-phase2/core.wasm`.
|
|
||||||
- `build_loader.sh` — Wasmtime C-API build for `/tmp/uce/wasm-phase2/loader`.
|
|
||||||
|
|
||||||
Deferred to Phase 3:
|
|
||||||
|
|
||||||
- PIC side modules and dylink loader integration.
|
|
||||||
- Lazy unit/component loading.
|
|
||||||
- Full `uce_lib` hostcall surface beyond the minimal context/log membrane.
|
|
||||||
- Generator-emitted units (`.uce` preprocessor output with literal markup,
|
|
||||||
`__uce_set_current_request`, and generated includes) rendering through this
|
|
||||||
membrane; Phase 0 only proved those generated units can compile as side
|
|
||||||
modules.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 2: build the minimal host/membrane runner against Wasmtime's wasm-c-api.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
OUT=/tmp/uce/wasm-phase2
|
|
||||||
mkdir -p "$OUT"
|
|
||||||
|
|
||||||
c++ -std=c++17 -O2 loader.cpp \
|
|
||||||
-I/opt/wasmtime/include \
|
|
||||||
-L/opt/wasmtime/lib \
|
|
||||||
-Wl,-rpath,/opt/wasmtime/lib \
|
|
||||||
-lwasmtime \
|
|
||||||
-o "$OUT/loader"
|
|
||||||
|
|
||||||
echo "built: $OUT/loader"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 2: build the native DValue/UCEB1 core subset plus one statically
|
|
||||||
# linked real .uce page as a WASM reactor. Runs on k-uce; artifacts go under
|
|
||||||
# /tmp/uce/wasm-phase2.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
SDK=/opt/wasi-sdk
|
|
||||||
OUT=/tmp/uce/wasm-phase2
|
|
||||||
mkdir -p "$OUT"
|
|
||||||
|
|
||||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
|
|
||||||
-O1 -fno-exceptions \
|
|
||||||
-I../.. -I../../src/lib \
|
|
||||||
core.cpp -o "$OUT/core.wasm" \
|
|
||||||
-Wl,--export-all \
|
|
||||||
-Wl,--export=__heap_base \
|
|
||||||
-Wl,--allow-undefined-file=hostcalls.syms
|
|
||||||
|
|
||||||
echo "--- phase2 core.wasm ---"
|
|
||||||
ls -la "$OUT/core.wasm"
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 2 — native UCE core subset compiled to WASM.
|
|
||||||
//
|
|
||||||
// This scaffold validates the Phase 2 membrane without the Phase 3 dynamic
|
|
||||||
// loader: the core owns memory/libc++/DValue/UCEB1, imports a tiny hostcall
|
|
||||||
// surface, decodes a host-provided UCEB1 request context, and invokes one
|
|
||||||
// statically linked real .uce page.
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include "../../src/lib/types.h"
|
|
||||||
#include "../../src/lib/dvalue.cpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
size_t uce_host_ctx_read(char* buf, size_t cap);
|
|
||||||
void uce_host_log(int level, const char* buf, size_t len);
|
|
||||||
}
|
|
||||||
|
|
||||||
#define RENDER(X) extern "C" void __uce_render(X)
|
|
||||||
#include "page.uce"
|
|
||||||
|
|
||||||
static Request g_request;
|
|
||||||
static ByteStream g_ob;
|
|
||||||
static String g_output;
|
|
||||||
|
|
||||||
SharedUnit::~SharedUnit() {}
|
|
||||||
|
|
||||||
String nibble(String div, String& haystack)
|
|
||||||
{
|
|
||||||
auto pos = haystack.find(div);
|
|
||||||
if(pos == String::npos)
|
|
||||||
{
|
|
||||||
auto result = haystack;
|
|
||||||
haystack.clear();
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
auto result = haystack.substr(0, pos);
|
|
||||||
haystack.erase(0, pos + div.length());
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Request::ob_start()
|
|
||||||
{
|
|
||||||
ob_stack.push_back(new ByteStream());
|
|
||||||
ob = ob_stack.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Request::set_status(s32 code, String reason)
|
|
||||||
{
|
|
||||||
if(reason == "")
|
|
||||||
reason = code == 200 ? "OK" : "Status";
|
|
||||||
response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
Request::~Request()
|
|
||||||
{
|
|
||||||
for(auto* stream : ob_stack)
|
|
||||||
delete stream;
|
|
||||||
ob_stack.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void phase2_clear_ob_stack()
|
|
||||||
{
|
|
||||||
for(auto* stream : g_request.ob_stack)
|
|
||||||
delete stream;
|
|
||||||
g_request.ob_stack.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void phase2_apply_context(DValue& root)
|
|
||||||
{
|
|
||||||
g_request.call = root;
|
|
||||||
g_request.params.clear();
|
|
||||||
DValue* params = root.key("params");
|
|
||||||
if(params)
|
|
||||||
{
|
|
||||||
params->each([&](const DValue& item, String key) {
|
|
||||||
g_request.params[key] = item.to_string();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
void* uce_alloc(size_t len)
|
|
||||||
{
|
|
||||||
return(malloc(len));
|
|
||||||
}
|
|
||||||
|
|
||||||
void uce_free(void* ptr)
|
|
||||||
{
|
|
||||||
free(ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
int uce_phase2_render()
|
|
||||||
{
|
|
||||||
context = &g_request;
|
|
||||||
phase2_clear_ob_stack();
|
|
||||||
g_ob.str("");
|
|
||||||
g_ob.clear();
|
|
||||||
g_request.ob = &g_ob;
|
|
||||||
g_request.out = "";
|
|
||||||
g_output = "";
|
|
||||||
|
|
||||||
size_t ctx_required = uce_host_ctx_read(0, 0);
|
|
||||||
if(ctx_required == 0)
|
|
||||||
return(10);
|
|
||||||
char* ctx_buf = (char*)malloc(ctx_required);
|
|
||||||
if(ctx_buf == 0)
|
|
||||||
return(11);
|
|
||||||
size_t ctx_len = uce_host_ctx_read(ctx_buf, ctx_required);
|
|
||||||
if(ctx_len != ctx_required)
|
|
||||||
{
|
|
||||||
free(ctx_buf);
|
|
||||||
return(12);
|
|
||||||
}
|
|
||||||
DValue decoded;
|
|
||||||
String error;
|
|
||||||
bool ok = ucb_decode(String(ctx_buf, ctx_len), decoded, &error);
|
|
||||||
free(ctx_buf);
|
|
||||||
if(!ok)
|
|
||||||
{
|
|
||||||
uce_host_log(3, error.data(), error.size());
|
|
||||||
return(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
phase2_apply_context(decoded);
|
|
||||||
__uce_render(g_request);
|
|
||||||
g_output = g_ob.str();
|
|
||||||
return(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* uce_phase2_output_data()
|
|
||||||
{
|
|
||||||
return(g_output.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t uce_phase2_output_size()
|
|
||||||
{
|
|
||||||
return(g_output.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
uce_host_ctx_read
|
|
||||||
uce_host_log
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 2 — minimal membrane host.
|
|
||||||
// Instantiates the statically linked core/page module, serves a UCEB1 request
|
|
||||||
// context via hostcall, invokes render, and reads the response from guest
|
|
||||||
// memory. Dynamic linking is intentionally Phase 3 work.
|
|
||||||
|
|
||||||
#include <wasm.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <map>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
|
|
||||||
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
|
|
||||||
|
|
||||||
static wasm_store_t* g_store = nullptr;
|
|
||||||
static wasm_memory_t* g_memory = nullptr;
|
|
||||||
static std::string g_context;
|
|
||||||
|
|
||||||
static std::vector<uint8_t> read_file(const char* fn)
|
|
||||||
{
|
|
||||||
FILE* f = fopen(fn, "rb");
|
|
||||||
CHECK(f, "cannot open %s", fn);
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
long n = ftell(f);
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
std::vector<uint8_t> buf(n);
|
|
||||||
CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn);
|
|
||||||
fclose(f);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void append_varuint(std::string& out, uint64_t value)
|
|
||||||
{
|
|
||||||
while(value >= 0x80)
|
|
||||||
{
|
|
||||||
out.push_back((char)((value & 0x7f) | 0x80));
|
|
||||||
value >>= 7;
|
|
||||||
}
|
|
||||||
out.push_back((char)value);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Node
|
|
||||||
{
|
|
||||||
std::string scalar;
|
|
||||||
bool is_list = false;
|
|
||||||
std::vector<std::pair<std::string, Node>> children;
|
|
||||||
};
|
|
||||||
|
|
||||||
static Node scalar(const char* value)
|
|
||||||
{
|
|
||||||
Node n;
|
|
||||||
n.scalar = value;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void encode_node(std::string& out, const Node& node)
|
|
||||||
{
|
|
||||||
out.push_back(node.is_list ? 1 : 0);
|
|
||||||
append_varuint(out, node.scalar.size());
|
|
||||||
out.append(node.scalar);
|
|
||||||
append_varuint(out, node.children.size());
|
|
||||||
for(const auto& child : node.children)
|
|
||||||
{
|
|
||||||
append_varuint(out, child.first.size());
|
|
||||||
out.append(child.first);
|
|
||||||
encode_node(out, child.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string make_context()
|
|
||||||
{
|
|
||||||
Node params;
|
|
||||||
params.children.push_back({"HTTP_HOST", scalar("phase2.example.test")});
|
|
||||||
params.children.push_back({"SCRIPT_URL", scalar("/spikes/wasm-phase2/page.uce")});
|
|
||||||
|
|
||||||
Node nested;
|
|
||||||
nested.children.push_back({"answer", scalar("42")});
|
|
||||||
|
|
||||||
Node root;
|
|
||||||
root.children.push_back({"params", params});
|
|
||||||
root.children.push_back({"route", scalar("/spikes/wasm-phase2/page.uce")});
|
|
||||||
root.children.push_back({"nested", nested});
|
|
||||||
|
|
||||||
std::string out = "UCEB";
|
|
||||||
out.push_back((char)1);
|
|
||||||
encode_node(out, root);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_trap_t* host_ctx_read(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
|
||||||
{
|
|
||||||
(void)env;
|
|
||||||
uint32_t ptr = args->data[0].of.i32;
|
|
||||||
uint32_t cap = args->data[1].of.i32;
|
|
||||||
if(ptr == 0 || cap == 0 || cap < g_context.size())
|
|
||||||
{
|
|
||||||
results->data[0] = WASM_I32_VAL((int32_t)g_context.size());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
CHECK(g_memory, "host ctx_read called before memory export was captured");
|
|
||||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
|
||||||
size_t mem_size = wasm_memory_data_size(g_memory);
|
|
||||||
CHECK((size_t)ptr <= mem_size, "ctx_read pointer outside memory");
|
|
||||||
CHECK((size_t)ptr + g_context.size() <= mem_size, "ctx_read buffer outside memory");
|
|
||||||
memcpy(mem + ptr, g_context.data(), g_context.size());
|
|
||||||
results->data[0] = WASM_I32_VAL((int32_t)g_context.size());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_trap_t* host_log(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
|
||||||
{
|
|
||||||
(void)env; (void)results;
|
|
||||||
int level = args->data[0].of.i32;
|
|
||||||
uint32_t ptr = args->data[1].of.i32;
|
|
||||||
uint32_t len = args->data[2].of.i32;
|
|
||||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
|
||||||
size_t mem_size = wasm_memory_data_size(g_memory);
|
|
||||||
if((size_t)ptr + len <= mem_size)
|
|
||||||
fprintf(stderr, "[guest log %d] %.*s\n", level, (int)len, (const char*)mem + ptr);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
|
||||||
{
|
|
||||||
(void)args; (void)results;
|
|
||||||
char msg[256];
|
|
||||||
snprintf(msg, sizeof(msg), "unimplemented import called: %s", (const char*)env);
|
|
||||||
wasm_message_t message;
|
|
||||||
wasm_byte_vec_new(&message, strlen(msg) + 1, msg);
|
|
||||||
wasm_trap_t* trap = wasm_trap_new(g_store, &message);
|
|
||||||
wasm_byte_vec_delete(&message);
|
|
||||||
return trap;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_func_t* make_func(wasm_store_t* store, std::vector<wasm_valkind_t> params, std::vector<wasm_valkind_t> results, wasm_func_callback_with_env_t cb, void* env = nullptr)
|
|
||||||
{
|
|
||||||
wasm_valtype_vec_t ps;
|
|
||||||
wasm_valtype_vec_new_uninitialized(&ps, params.size());
|
|
||||||
for(size_t i = 0; i < params.size(); i++)
|
|
||||||
ps.data[i] = wasm_valtype_new(params[i]);
|
|
||||||
wasm_valtype_vec_t rs;
|
|
||||||
wasm_valtype_vec_new_uninitialized(&rs, results.size());
|
|
||||||
for(size_t i = 0; i < results.size(); i++)
|
|
||||||
rs.data[i] = wasm_valtype_new(results[i]);
|
|
||||||
wasm_functype_t* ft = wasm_functype_new(&ps, &rs);
|
|
||||||
wasm_func_t* fn = wasm_func_new_with_env(store, ft, cb, env, nullptr);
|
|
||||||
wasm_functype_delete(ft);
|
|
||||||
return fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Instance
|
|
||||||
{
|
|
||||||
wasm_module_t* module = nullptr;
|
|
||||||
wasm_instance_t* instance = nullptr;
|
|
||||||
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
|
|
||||||
std::map<std::string, wasm_extern_t*> by_name;
|
|
||||||
|
|
||||||
void index_exports()
|
|
||||||
{
|
|
||||||
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_exports(module, &types);
|
|
||||||
wasm_instance_exports(instance, &exports);
|
|
||||||
for(size_t i = 0; i < types.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* nm = wasm_exporttype_name(types.data[i]);
|
|
||||||
std::string key(nm->data, nm->size);
|
|
||||||
while(!key.empty() && key.back() == '\0') key.pop_back();
|
|
||||||
by_name[key] = exports.data[i];
|
|
||||||
}
|
|
||||||
wasm_exporttype_vec_delete(&types);
|
|
||||||
}
|
|
||||||
|
|
||||||
wasm_func_t* func(const char* name)
|
|
||||||
{
|
|
||||||
auto it = by_name.find(name);
|
|
||||||
return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static int32_t call_i32(Instance& inst, const char* name)
|
|
||||||
{
|
|
||||||
wasm_func_t* f = inst.func(name);
|
|
||||||
CHECK(f, "missing export func %s", name);
|
|
||||||
wasm_val_t result_buf[1] = { WASM_INIT_VAL };
|
|
||||||
wasm_val_vec_t args = WASM_EMPTY_VEC;
|
|
||||||
wasm_val_vec_t results = { wasm_func_result_arity(f), result_buf };
|
|
||||||
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
|
|
||||||
wasm_trap_t* trap = wasm_func_call(f, &args, results.size ? &results : &no_results);
|
|
||||||
if(trap)
|
|
||||||
{
|
|
||||||
wasm_message_t msg;
|
|
||||||
wasm_trap_message(trap, &msg);
|
|
||||||
FAIL("trap during %s: %.*s", name, (int)msg.size, msg.data);
|
|
||||||
}
|
|
||||||
return results.size ? result_buf[0].of.i32 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase2/core.wasm";
|
|
||||||
g_context = make_context();
|
|
||||||
|
|
||||||
wasm_engine_t* engine = wasm_engine_new();
|
|
||||||
CHECK(engine, "engine");
|
|
||||||
g_store = wasm_store_new(engine);
|
|
||||||
CHECK(g_store, "store");
|
|
||||||
|
|
||||||
std::vector<uint8_t> bytes = read_file(core_path);
|
|
||||||
wasm_byte_vec_t bv;
|
|
||||||
wasm_byte_vec_new(&bv, bytes.size(), (const char*)bytes.data());
|
|
||||||
Instance core;
|
|
||||||
core.module = wasm_module_new(g_store, &bv);
|
|
||||||
wasm_byte_vec_delete(&bv);
|
|
||||||
CHECK(core.module, "core module load failed");
|
|
||||||
|
|
||||||
wasm_importtype_vec_t imports = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_imports(core.module, &imports);
|
|
||||||
std::vector<wasm_extern_t*> externs(imports.size);
|
|
||||||
std::vector<wasm_func_t*> owned_funcs;
|
|
||||||
for(size_t i = 0; i < imports.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* mod_n = wasm_importtype_module(imports.data[i]);
|
|
||||||
const wasm_name_t* name_n = wasm_importtype_name(imports.data[i]);
|
|
||||||
std::string mod(mod_n->data, mod_n->size);
|
|
||||||
std::string name(name_n->data, name_n->size);
|
|
||||||
while(!mod.empty() && mod.back() == '\0') mod.pop_back();
|
|
||||||
while(!name.empty() && name.back() == '\0') name.pop_back();
|
|
||||||
const wasm_externtype_t* et = wasm_importtype_type(imports.data[i]);
|
|
||||||
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, "unexpected non-func import %s.%s", mod.c_str(), name.c_str());
|
|
||||||
|
|
||||||
wasm_func_t* fn = nullptr;
|
|
||||||
if(mod == "env" && name == "uce_host_ctx_read")
|
|
||||||
fn = make_func(g_store, {WASM_I32, WASM_I32}, {WASM_I32}, host_ctx_read);
|
|
||||||
else if(mod == "env" && name == "uce_host_log")
|
|
||||||
fn = make_func(g_store, {WASM_I32, WASM_I32, WASM_I32}, {}, host_log);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
char* label = strdup((mod + "." + name).c_str());
|
|
||||||
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
|
|
||||||
fn = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr);
|
|
||||||
}
|
|
||||||
CHECK(fn, "failed to create import %s.%s", mod.c_str(), name.c_str());
|
|
||||||
owned_funcs.push_back(fn);
|
|
||||||
externs[i] = wasm_func_as_extern(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
wasm_extern_vec_t iv = { externs.size(), externs.data() };
|
|
||||||
wasm_trap_t* trap = nullptr;
|
|
||||||
core.instance = wasm_instance_new(g_store, core.module, &iv, &trap);
|
|
||||||
if(trap)
|
|
||||||
{
|
|
||||||
wasm_message_t msg;
|
|
||||||
wasm_trap_message(trap, &msg);
|
|
||||||
FAIL("trap during instantiation: %.*s", (int)msg.size, msg.data);
|
|
||||||
}
|
|
||||||
CHECK(core.instance, "instantiation failed");
|
|
||||||
core.index_exports();
|
|
||||||
g_memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr);
|
|
||||||
CHECK(g_memory, "core does not export memory");
|
|
||||||
if(core.func("_initialize"))
|
|
||||||
call_i32(core, "_initialize");
|
|
||||||
|
|
||||||
int rc = call_i32(core, "uce_phase2_render");
|
|
||||||
CHECK(rc == 0, "render returned %d", rc);
|
|
||||||
int32_t data = call_i32(core, "uce_phase2_output_data");
|
|
||||||
int32_t size = call_i32(core, "uce_phase2_output_size");
|
|
||||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
|
||||||
CHECK((size_t)data + (size_t)size <= wasm_memory_data_size(g_memory), "output outside memory");
|
|
||||||
std::string output((const char*)mem + data, size);
|
|
||||||
printf("---- phase2 output (%d bytes) ----\n%s", size, output.c_str());
|
|
||||||
printf("----------------------------------\n");
|
|
||||||
CHECK(output.find("PHASE2 PAGE OK") != std::string::npos, "missing page marker");
|
|
||||||
CHECK(output.find("host=phase2.example.test") != std::string::npos, "missing context param");
|
|
||||||
CHECK(output.find("answer=42") != std::string::npos, "missing nested context value");
|
|
||||||
printf("PHASE2 EXIT CRITERION: PASS\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 2 scaffold page.
|
|
||||||
// This is a real .uce source file with a normal RENDER entry point. The
|
|
||||||
// Phase 2 core statically links it to validate the host-context membrane
|
|
||||||
// before the dynamic loader is introduced in Phase 3.
|
|
||||||
|
|
||||||
RENDER(Request& context)
|
|
||||||
{
|
|
||||||
print("PHASE2 PAGE OK\n");
|
|
||||||
print("host=", context.params["HTTP_HOST"], "\n");
|
|
||||||
print("route=", context.call["route"].to_string(), "\n");
|
|
||||||
print("answer=", context.call["nested"]["answer"].to_string(), "\n");
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# spikes/wasm-phase3 — dynamic loader + workspace scaffold
|
|
||||||
|
|
||||||
Phase 3 combines the Phase 0 dynamic PIC-module loader with the Phase 2 UCEB1
|
|
||||||
request-context membrane. It is still a spike, not the production wasm worker,
|
|
||||||
but it validates the load-bearing pieces together:
|
|
||||||
|
|
||||||
1. build a core WASM reactor that owns memory, allocator, `Request`, `DValue`,
|
|
||||||
UCEB1, and output buffering;
|
|
||||||
2. build a separate PIC side module with `dylink.0` from a generated-shape UCE
|
|
||||||
page C++ file;
|
|
||||||
3. parse `dylink.0`, allocate `__memory_base`/`__table_base`, resolve
|
|
||||||
`env.*`, `GOT.mem.*`, and `GOT.func.*` imports against the core/workspace;
|
|
||||||
4. instantiate the unit, run relocations/constructors, set its Request pointer,
|
|
||||||
call `__uce_render`, and read rendered output from core-owned memory.
|
|
||||||
|
|
||||||
The fixture intentionally exercises the loader branches that are easiest to
|
|
||||||
accidentally leave dark: non-zero `table_size` / `__table_base`, `GOT.func`
|
|
||||||
resolution (`phase3_core_print`), and deferred self-resolution of unit-owned
|
|
||||||
`GOT.mem` entries. The loader output prints those resolutions before the final
|
|
||||||
pass marker. Its generated-shape expressions call `html_escape(...)` so the
|
|
||||||
side-module import surface includes the first ordinary UCE helper dependency
|
|
||||||
rather than only `types.h`/`DValue`.
|
|
||||||
|
|
||||||
Run on `k-uce`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash spikes/wasm-phase3/build_modules.sh
|
|
||||||
bash spikes/wasm-phase3/build_loader.sh
|
|
||||||
/tmp/uce/wasm-phase3/loader
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected final line:
|
|
||||||
|
|
||||||
```text
|
|
||||||
PHASE3 EXIT CRITERION: PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
Files:
|
|
||||||
|
|
||||||
- `core.cpp` — core/workspace scaffold and UCEB1 membrane decode.
|
|
||||||
- `page.uce` — source page for the generated-shape fixture.
|
|
||||||
- `generated/page.uce.cpp` — checked-in UCE-preprocessor-shaped side-module
|
|
||||||
fixture: `uce_lib.h` include, `__uce_set_current_request`, `__uce_render`,
|
|
||||||
literal markup lowered to `print(R"(...)")`, expression prints wrapped in
|
|
||||||
`html_escape(...)`, callback/lambda/table exercises, and self-GOT data.
|
|
||||||
- `loader.cpp` — dynamic linker/runner adapted from Phase 0.
|
|
||||||
- `hostcalls.syms` — explicit core hostcall allowlist.
|
|
||||||
|
|
||||||
Still deferred to production Phase 3/4 work:
|
|
||||||
|
|
||||||
- running the actual UCE preprocessor inside this build script rather than using
|
|
||||||
a checked-in generated-shape fixture;
|
|
||||||
- lazy component/path dispatch for a full site tree;
|
|
||||||
- uce-starter end-to-end under a wasm FastCGI worker;
|
|
||||||
- productionizing the side-module allocator gate as a real `#ifdef` in
|
|
||||||
`types.h` instead of this spike's copied-header text patch;
|
|
||||||
- parser hardening beyond the spike's basic magic/version/bounds/alignment
|
|
||||||
checks, including fuzzing malformed wasm/dylink sections before accepting
|
|
||||||
cache or third-party units;
|
|
||||||
- import-policy hardening beyond this spike's fail-fast resolver;
|
|
||||||
- workspace birth/drop integration with the real server lifecycle, including
|
|
||||||
retaining unaligned allocation pointers needed to unload units or drop a
|
|
||||||
workspace cleanly.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 3: build the dynamic-loader/membrane runner against Wasmtime's wasm-c-api.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
OUT=/tmp/uce/wasm-phase3
|
|
||||||
mkdir -p "$OUT"
|
|
||||||
|
|
||||||
c++ -std=c++17 -O2 loader.cpp \
|
|
||||||
-I/opt/wasmtime/include \
|
|
||||||
-L/opt/wasmtime/lib \
|
|
||||||
-Wl,-rpath,/opt/wasmtime/lib \
|
|
||||||
-lwasmtime \
|
|
||||||
-o "$OUT/loader"
|
|
||||||
|
|
||||||
echo "built: $OUT/loader"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 3: build a core reactor plus one generated-shape PIC side module.
|
|
||||||
# Runs on k-uce; artifacts go under /tmp/uce/wasm-phase3.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
SDK=/opt/wasi-sdk
|
|
||||||
ROOT=$(cd ../.. && pwd)
|
|
||||||
OUT=/tmp/uce/wasm-phase3
|
|
||||||
SHIM="$OUT/shim"
|
|
||||||
mkdir -p "$OUT" "$SHIM"
|
|
||||||
|
|
||||||
cat > "$SHIM/uce_lib.h" <<'EOF'
|
|
||||||
#pragma once
|
|
||||||
#include "types.h"
|
|
||||||
String html_escape(String s);
|
|
||||||
EOF
|
|
||||||
cp "$ROOT/src/lib/types.h" "$SHIM/types.h"
|
|
||||||
python3 - <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
p = Path('/tmp/uce/wasm-phase3/shim/types.h')
|
|
||||||
s = p.read_text()
|
|
||||||
start = s.index('void * operator new(decltype(sizeof(0)) n) noexcept(false)')
|
|
||||||
end = s.index('void operator delete(void * p) throw()')
|
|
||||||
end = s.index('\n}', end) + 3
|
|
||||||
replacement = '''// WASM side-module shim: allocator is owned by the core module.\nvoid * operator new(decltype(sizeof(0)) n) noexcept(false);\nvoid operator delete(void * p) throw();\nvoid operator delete(void * p, decltype(sizeof(0)) n) noexcept;\n'''
|
|
||||||
p.write_text(s[:start] + replacement + s[end:])
|
|
||||||
PY
|
|
||||||
cat > "$SHIM/signal.h" <<'EOF'
|
|
||||||
#pragma once
|
|
||||||
typedef int sig_atomic_t;
|
|
||||||
typedef void (*sighandler_t)(int);
|
|
||||||
#define SIGTERM 15
|
|
||||||
int raise(int);
|
|
||||||
sighandler_t signal(int, sighandler_t);
|
|
||||||
EOF
|
|
||||||
|
|
||||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
|
|
||||||
-O1 -fno-exceptions \
|
|
||||||
-I../.. -I../../src/lib \
|
|
||||||
core.cpp -o "$OUT/core.wasm" \
|
|
||||||
-Wl,--export-all \
|
|
||||||
-Wl,--import-table \
|
|
||||||
-Wl,--export=__stack_pointer \
|
|
||||||
-Wl,--export=__heap_base \
|
|
||||||
-Wl,--allow-undefined-file=hostcalls.syms \
|
|
||||||
-Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE
|
|
||||||
|
|
||||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \
|
|
||||||
-fvisibility-inlines-hidden \
|
|
||||||
-O1 -fno-exceptions \
|
|
||||||
-I"$SHIM" -I"$ROOT/src/lib" \
|
|
||||||
-c generated/page.uce.cpp -o "$OUT/page.o"
|
|
||||||
|
|
||||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \
|
|
||||||
-fvisibility-inlines-hidden \
|
|
||||||
-O1 -fno-exceptions \
|
|
||||||
-I"$SHIM" -I"$ROOT/src/lib" \
|
|
||||||
-c generated/helper.cpp -o "$OUT/helper.o"
|
|
||||||
|
|
||||||
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
|
||||||
--unresolved-symbols=import-dynamic \
|
|
||||||
"$OUT/page.o" "$OUT/helper.o" -o "$OUT/page.wasm" \
|
|
||||||
--export=__uce_set_current_request \
|
|
||||||
--export=__uce_render
|
|
||||||
|
|
||||||
echo "--- phase3 core.wasm ---"
|
|
||||||
ls -la "$OUT/core.wasm"
|
|
||||||
echo "--- phase3 page.wasm ---"
|
|
||||||
ls -la "$OUT/page.wasm"
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 3 — dynamic-loader core scaffold.
|
|
||||||
//
|
|
||||||
// This core owns the UCE heap, Request, DValue/UCEB1, and output buffer. The
|
|
||||||
// Phase 3 loader dynamically links a PIC side module generated in the same
|
|
||||||
// shape as UCE preprocessor output, sets its Request* context, and invokes its
|
|
||||||
// __uce_render entry.
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "../../src/lib/types.h"
|
|
||||||
#include "../../src/lib/dvalue.cpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
size_t uce_host_ctx_read(char* buf, size_t cap);
|
|
||||||
void uce_host_log(int level, const char* buf, size_t len);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void phase3_core_print(const char* data, size_t len)
|
|
||||||
{
|
|
||||||
if(context && context->ob)
|
|
||||||
context->ob->write(data, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void phase3_core_invoke_callback(void (*cb)(int), int value)
|
|
||||||
{
|
|
||||||
cb(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void phase3_core_invoke_function(std::function<int(int)>* f, int value)
|
|
||||||
{
|
|
||||||
String out = "<p>fn=" + std::to_string((*f)(value)) + "</p>\n";
|
|
||||||
phase3_core_print(out.data(), out.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" intptr_t core_table_index_of_phase3_core_print()
|
|
||||||
{
|
|
||||||
return(reinterpret_cast<intptr_t>(phase3_core_print));
|
|
||||||
}
|
|
||||||
|
|
||||||
static Request g_request;
|
|
||||||
static ByteStream g_ob;
|
|
||||||
static String g_output;
|
|
||||||
|
|
||||||
SharedUnit::~SharedUnit() {}
|
|
||||||
|
|
||||||
String html_escape(String s)
|
|
||||||
{
|
|
||||||
String result;
|
|
||||||
for(char c : s)
|
|
||||||
{
|
|
||||||
switch(c)
|
|
||||||
{
|
|
||||||
case '&': result += "&"; break;
|
|
||||||
case '<': result += "<"; break;
|
|
||||||
case '>': result += ">"; break;
|
|
||||||
case '"': result += """; break;
|
|
||||||
case '\'': result += "'"; break;
|
|
||||||
default: result.push_back(c); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
String nibble(String div, String& haystack)
|
|
||||||
{
|
|
||||||
auto pos = haystack.find(div);
|
|
||||||
if(pos == String::npos)
|
|
||||||
{
|
|
||||||
auto result = haystack;
|
|
||||||
haystack.clear();
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
auto result = haystack.substr(0, pos);
|
|
||||||
haystack.erase(0, pos + div.length());
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Request::ob_start()
|
|
||||||
{
|
|
||||||
ob_stack.push_back(new ByteStream());
|
|
||||||
ob = ob_stack.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Request::set_status(s32 code, String reason)
|
|
||||||
{
|
|
||||||
if(reason == "")
|
|
||||||
reason = code == 200 ? "OK" : "Status";
|
|
||||||
response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
Request::~Request()
|
|
||||||
{
|
|
||||||
for(auto* stream : ob_stack)
|
|
||||||
delete stream;
|
|
||||||
ob_stack.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void phase3_clear_ob_stack()
|
|
||||||
{
|
|
||||||
for(auto* stream : g_request.ob_stack)
|
|
||||||
delete stream;
|
|
||||||
g_request.ob_stack.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void phase3_apply_context(DValue& root)
|
|
||||||
{
|
|
||||||
g_request.call = root;
|
|
||||||
g_request.params.clear();
|
|
||||||
DValue* params = root.key("params");
|
|
||||||
if(params)
|
|
||||||
{
|
|
||||||
params->each([&](const DValue& item, String key) {
|
|
||||||
g_request.params[key] = item.to_string();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
void* uce_alloc(size_t len)
|
|
||||||
{
|
|
||||||
return(malloc(len));
|
|
||||||
}
|
|
||||||
|
|
||||||
void uce_free(void* ptr)
|
|
||||||
{
|
|
||||||
free(ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
int uce_phase3_prepare()
|
|
||||||
{
|
|
||||||
context = &g_request;
|
|
||||||
phase3_clear_ob_stack();
|
|
||||||
g_ob.str("");
|
|
||||||
g_ob.clear();
|
|
||||||
g_request.ob = &g_ob;
|
|
||||||
g_request.out = "";
|
|
||||||
g_output = "";
|
|
||||||
|
|
||||||
size_t ctx_required = uce_host_ctx_read(0, 0);
|
|
||||||
if(ctx_required == 0)
|
|
||||||
return(10);
|
|
||||||
char* ctx_buf = (char*)malloc(ctx_required);
|
|
||||||
if(ctx_buf == 0)
|
|
||||||
return(11);
|
|
||||||
size_t ctx_len = uce_host_ctx_read(ctx_buf, ctx_required);
|
|
||||||
if(ctx_len != ctx_required)
|
|
||||||
{
|
|
||||||
free(ctx_buf);
|
|
||||||
return(12);
|
|
||||||
}
|
|
||||||
|
|
||||||
DValue decoded;
|
|
||||||
String error;
|
|
||||||
bool ok = ucb_decode(String(ctx_buf, ctx_len), decoded, &error);
|
|
||||||
free(ctx_buf);
|
|
||||||
if(!ok)
|
|
||||||
{
|
|
||||||
uce_host_log(3, error.data(), error.size());
|
|
||||||
return(20);
|
|
||||||
}
|
|
||||||
phase3_apply_context(decoded);
|
|
||||||
return(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Request* uce_phase3_request()
|
|
||||||
{
|
|
||||||
return(&g_request);
|
|
||||||
}
|
|
||||||
|
|
||||||
void uce_phase3_finish()
|
|
||||||
{
|
|
||||||
g_output = g_ob.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* uce_phase3_output_data()
|
|
||||||
{
|
|
||||||
return(g_output.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t uce_phase3_output_size()
|
|
||||||
{
|
|
||||||
return(g_output.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
extern int phase3_unit_state;
|
|
||||||
|
|
||||||
extern "C" int phase3_helper_state_plus(int value)
|
|
||||||
{
|
|
||||||
return(phase3_unit_state + value);
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#include "uce_lib.h"
|
|
||||||
#include <functional>
|
|
||||||
#include <map>
|
|
||||||
#include <tuple>
|
|
||||||
|
|
||||||
extern "C" void phase3_core_print(const char* data, size_t len);
|
|
||||||
extern "C" void phase3_core_invoke_callback(void (*cb)(int), int value);
|
|
||||||
extern "C" void phase3_core_invoke_function(std::function<int(int)>* f, int value);
|
|
||||||
extern "C" int phase3_helper_state_plus(int value);
|
|
||||||
|
|
||||||
int phase3_unit_state = 40;
|
|
||||||
int phase3_weak_data __attribute__((weak)) = 0;
|
|
||||||
|
|
||||||
static void phase3_unit_callback(int value)
|
|
||||||
{
|
|
||||||
String out = "<p>callback=" + std::to_string(value + phase3_unit_state) + "</p>\n";
|
|
||||||
print(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef UCE_SET_CURRENT_REQUEST_DEFINED
|
|
||||||
#define UCE_SET_CURRENT_REQUEST_DEFINED
|
|
||||||
|
|
||||||
/*load_declarations*/
|
|
||||||
|
|
||||||
extern "C" void __uce_set_current_request(Request* _request)
|
|
||||||
{
|
|
||||||
context = _request;
|
|
||||||
/*load_units*/
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
#line 4 "spikes/wasm-phase3/page.uce"
|
|
||||||
extern "C" void __uce_render(Request& context)
|
|
||||||
{
|
|
||||||
print(R"(<section class="phase3">
|
|
||||||
<h1>PHASE3 PAGE OK</h1>
|
|
||||||
<p>host=)");
|
|
||||||
print(html_escape(context.params["HTTP_HOST"]));
|
|
||||||
print(R"(</p>
|
|
||||||
<p>route=)");
|
|
||||||
print(html_escape(context.call["route"].to_string()));
|
|
||||||
print(R"(</p>
|
|
||||||
<p>answer=)");
|
|
||||||
print(html_escape(context.call["nested"]["answer"].to_string()));
|
|
||||||
print(R"(</p>
|
|
||||||
)");
|
|
||||||
context.call["nested"].each([&](const DValue& value, String key) {
|
|
||||||
print(R"( <p>each=)");
|
|
||||||
print(html_escape(key + ":" + value.to_string()));
|
|
||||||
print(R"(</p>
|
|
||||||
)");
|
|
||||||
});
|
|
||||||
print(R"( <p>self-got=)");
|
|
||||||
phase3_weak_data = 7;
|
|
||||||
print(std::to_string(phase3_helper_state_plus(2) + phase3_weak_data - 7));
|
|
||||||
print(R"(</p>
|
|
||||||
)");
|
|
||||||
std::map<String, String> phase3_map;
|
|
||||||
phase3_map.emplace(std::piecewise_construct, std::forward_as_tuple("piece"), std::forward_as_tuple("wise"));
|
|
||||||
print(R"( <p>map=)");
|
|
||||||
print(html_escape(phase3_map["piece"]));
|
|
||||||
print(R"(</p>
|
|
||||||
)");
|
|
||||||
phase3_core_invoke_callback(phase3_unit_callback, 2);
|
|
||||||
void (*volatile print_ptr)(const char*, size_t) = phase3_core_print;
|
|
||||||
print_ptr("<p>got-func-ok</p>\n", 19);
|
|
||||||
auto* fn = new std::function<int(int)>([](int value) { return(value * 3); });
|
|
||||||
phase3_core_invoke_function(fn, 14);
|
|
||||||
delete fn;
|
|
||||||
print(R"( </section>
|
|
||||||
)");
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
uce_host_ctx_read
|
|
||||||
uce_host_log
|
|
||||||
@@ -1,554 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 3 — dynamic loader + membrane scaffold.
|
|
||||||
//
|
|
||||||
// Embeds a wasm runtime through the standard wasm-c-api and links a core
|
|
||||||
// workspace module with a generated-shape PIC UCE side module:
|
|
||||||
//
|
|
||||||
// 1. instantiate core.wasm (owns memory/table/allocator/Request/DValue)
|
|
||||||
// 2. parse page.wasm's dylink.0 → data size/align, table slots needed
|
|
||||||
// 3. allocate __memory_base by calling core malloc, and __table_base by
|
|
||||||
// appending to the shared funcref table
|
|
||||||
// 4. build the unit's import vector: env.memory / env.__indirect_function_table /
|
|
||||||
// env.__stack_pointer from core; env.* functions from core exports;
|
|
||||||
// GOT.mem.* and GOT.func.* through the same rules as Phase 0
|
|
||||||
// 5. instantiate unit, patch deferred GOT entries, run relocations/ctors
|
|
||||||
// 6. prepare a UCEB1 request context in core, set the unit's Request*, call
|
|
||||||
// __uce_render, then read output back out of core-owned linear memory
|
|
||||||
//
|
|
||||||
// Everything here is deliberately validation-grade: fail loudly, never guess.
|
|
||||||
|
|
||||||
#include <wasm.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
|
|
||||||
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
|
|
||||||
|
|
||||||
static std::vector<uint8_t> read_file(const char* fn)
|
|
||||||
{
|
|
||||||
FILE* f = fopen(fn, "rb");
|
|
||||||
CHECK(f, "cannot open %s", fn);
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
long n = ftell(f);
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
std::vector<uint8_t> buf(n);
|
|
||||||
CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn);
|
|
||||||
fclose(f);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- minimal dylink.0 parser -------------------------------------------
|
|
||||||
|
|
||||||
struct DylinkInfo
|
|
||||||
{
|
|
||||||
uint32_t mem_size = 0;
|
|
||||||
uint32_t mem_align = 0; // power of 2
|
|
||||||
uint32_t table_size = 0;
|
|
||||||
uint32_t table_align = 0;
|
|
||||||
bool found = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
static uint64_t read_uleb(const uint8_t* buf, size_t& pos, size_t end)
|
|
||||||
{
|
|
||||||
uint64_t result = 0;
|
|
||||||
int shift = 0;
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
CHECK(pos < end, "truncated uleb in wasm custom section");
|
|
||||||
CHECK(shift < 64, "oversized uleb in wasm custom section");
|
|
||||||
uint8_t b = buf[pos++];
|
|
||||||
result |= (uint64_t)(b & 0x7f) << shift;
|
|
||||||
if(!(b & 0x80))
|
|
||||||
return result;
|
|
||||||
shift += 7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static DylinkInfo parse_dylink(const std::vector<uint8_t>& wasm)
|
|
||||||
{
|
|
||||||
DylinkInfo info;
|
|
||||||
CHECK(wasm.size() >= 8 && !memcmp(wasm.data(), "\0asm", 4), "not a wasm module");
|
|
||||||
CHECK(wasm[4] == 1 && wasm[5] == 0 && wasm[6] == 0 && wasm[7] == 0, "unsupported wasm binary version");
|
|
||||||
size_t pos = 8;
|
|
||||||
while(pos < wasm.size())
|
|
||||||
{
|
|
||||||
uint8_t sec_id = wasm[pos++];
|
|
||||||
uint64_t size = read_uleb(wasm.data(), pos, wasm.size());
|
|
||||||
CHECK(size <= wasm.size() - pos, "wasm section exceeds file size");
|
|
||||||
size_t end = pos + size;
|
|
||||||
if(sec_id == 0)
|
|
||||||
{
|
|
||||||
uint64_t name_len = read_uleb(wasm.data(), pos, end);
|
|
||||||
CHECK(name_len <= end - pos, "custom section name exceeds section size");
|
|
||||||
std::string name((const char*)wasm.data() + pos, name_len);
|
|
||||||
pos += name_len;
|
|
||||||
if(name == "dylink.0")
|
|
||||||
{
|
|
||||||
while(pos < end)
|
|
||||||
{
|
|
||||||
uint8_t sub = wasm[pos++];
|
|
||||||
uint64_t sub_len = read_uleb(wasm.data(), pos, end);
|
|
||||||
CHECK(sub_len <= end - pos, "dylink subsection exceeds section size");
|
|
||||||
size_t sub_end = pos + sub_len;
|
|
||||||
if(sub == 1) // WASM_DYLINK_MEM_INFO
|
|
||||||
{
|
|
||||||
info.mem_size = read_uleb(wasm.data(), pos, sub_end);
|
|
||||||
info.mem_align = read_uleb(wasm.data(), pos, sub_end);
|
|
||||||
info.table_size = read_uleb(wasm.data(), pos, sub_end);
|
|
||||||
info.table_align = read_uleb(wasm.data(), pos, sub_end);
|
|
||||||
CHECK(!info.found, "duplicate dylink.0 mem_info subsection");
|
|
||||||
CHECK(info.mem_align < 31 && info.table_align < 31, "unsupported dylink alignment");
|
|
||||||
info.found = true;
|
|
||||||
}
|
|
||||||
pos = sub_end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pos = end;
|
|
||||||
}
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- tiny UCEB1 request-context encoder ---------------------------------
|
|
||||||
|
|
||||||
static void append_varuint(std::string& out, uint64_t value)
|
|
||||||
{
|
|
||||||
while(value >= 0x80)
|
|
||||||
{
|
|
||||||
out.push_back((char)((value & 0x7f) | 0x80));
|
|
||||||
value >>= 7;
|
|
||||||
}
|
|
||||||
out.push_back((char)value);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Node
|
|
||||||
{
|
|
||||||
std::string scalar;
|
|
||||||
std::vector<std::pair<std::string, Node>> children;
|
|
||||||
};
|
|
||||||
|
|
||||||
static Node scalar(const char* value)
|
|
||||||
{
|
|
||||||
Node n;
|
|
||||||
n.scalar = value;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void encode_node(std::string& out, const Node& node)
|
|
||||||
{
|
|
||||||
out.push_back(0);
|
|
||||||
append_varuint(out, node.scalar.size());
|
|
||||||
out.append(node.scalar);
|
|
||||||
append_varuint(out, node.children.size());
|
|
||||||
for(const auto& child : node.children)
|
|
||||||
{
|
|
||||||
append_varuint(out, child.first.size());
|
|
||||||
out.append(child.first);
|
|
||||||
encode_node(out, child.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string make_context()
|
|
||||||
{
|
|
||||||
Node params;
|
|
||||||
params.children.push_back({"HTTP_HOST", scalar("phase3.example.test")});
|
|
||||||
params.children.push_back({"SCRIPT_URL", scalar("/spikes/wasm-phase3/page.uce")});
|
|
||||||
Node nested;
|
|
||||||
nested.children.push_back({"answer", scalar("42")});
|
|
||||||
Node root;
|
|
||||||
root.children.push_back({"params", params});
|
|
||||||
root.children.push_back({"route", scalar("/spikes/wasm-phase3/page.uce")});
|
|
||||||
root.children.push_back({"nested", nested});
|
|
||||||
std::string out = "UCEB";
|
|
||||||
out.push_back((char)1);
|
|
||||||
encode_node(out, root);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- host imports --------------------------------------------------------
|
|
||||||
|
|
||||||
static wasm_memory_t* g_memory = nullptr;
|
|
||||||
static std::string g_context;
|
|
||||||
|
|
||||||
static void set_i32_result(wasm_val_t* result, int32_t value)
|
|
||||||
{
|
|
||||||
result->kind = WASM_I32;
|
|
||||||
result->of.i32 = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_trap_t* host_ctx_read(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
|
||||||
{
|
|
||||||
(void)env;
|
|
||||||
uint32_t ptr = args->data[0].of.i32;
|
|
||||||
uint32_t cap = args->data[1].of.i32;
|
|
||||||
if(ptr == 0 || cap == 0 || cap < g_context.size())
|
|
||||||
{
|
|
||||||
set_i32_result(&results->data[0], (int32_t)g_context.size());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
CHECK(g_memory, "host ctx_read called before memory export was captured");
|
|
||||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
|
||||||
size_t mem_size = wasm_memory_data_size(g_memory);
|
|
||||||
CHECK((size_t)ptr + g_context.size() <= mem_size, "ctx_read buffer outside memory");
|
|
||||||
memcpy(mem + ptr, g_context.data(), g_context.size());
|
|
||||||
set_i32_result(&results->data[0], (int32_t)g_context.size());
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_trap_t* host_log(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
|
||||||
{
|
|
||||||
(void)env; (void)results;
|
|
||||||
int level = args->data[0].of.i32;
|
|
||||||
uint32_t ptr = args->data[1].of.i32;
|
|
||||||
uint32_t len = args->data[2].of.i32;
|
|
||||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
|
||||||
size_t mem_size = wasm_memory_data_size(g_memory);
|
|
||||||
if((size_t)ptr + len <= mem_size)
|
|
||||||
fprintf(stderr, "[guest log %d] %.*s\n", level, (int)len, (const char*)mem + ptr);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- named trap stubs for unsatisfied (WASI) imports --------------------
|
|
||||||
|
|
||||||
static wasm_store_t* g_store_for_stubs = nullptr;
|
|
||||||
|
|
||||||
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
|
||||||
{
|
|
||||||
(void)args; (void)results;
|
|
||||||
fprintf(stderr, "[stub called: %s]\n", (const char*)env);
|
|
||||||
char msg[256];
|
|
||||||
snprintf(msg, sizeof(msg), "unimplemented host import called: %s", (const char*)env);
|
|
||||||
wasm_message_t message;
|
|
||||||
wasm_byte_vec_new(&message, strlen(msg) + 1, msg);
|
|
||||||
wasm_trap_t* trap = wasm_trap_new(g_store_for_stubs, &message);
|
|
||||||
wasm_byte_vec_delete(&message);
|
|
||||||
return trap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- instance wrapper: name → extern map --------------------------------
|
|
||||||
|
|
||||||
struct Instance
|
|
||||||
{
|
|
||||||
wasm_module_t* module = nullptr;
|
|
||||||
wasm_instance_t* instance = nullptr;
|
|
||||||
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
|
|
||||||
std::map<std::string, wasm_extern_t*> by_name;
|
|
||||||
|
|
||||||
void index_exports()
|
|
||||||
{
|
|
||||||
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_exports(module, &types);
|
|
||||||
wasm_instance_exports(instance, &exports);
|
|
||||||
CHECK(types.size == exports.size, "export type/extern count mismatch");
|
|
||||||
for(size_t i = 0; i < types.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* nm = wasm_exporttype_name(types.data[i]);
|
|
||||||
std::string key(nm->data, nm->size);
|
|
||||||
// some wasm-c-api impls include the trailing NUL in name size
|
|
||||||
while(!key.empty() && key.back() == '\0')
|
|
||||||
key.pop_back();
|
|
||||||
by_name[key] = exports.data[i];
|
|
||||||
}
|
|
||||||
wasm_exporttype_vec_delete(&types);
|
|
||||||
}
|
|
||||||
|
|
||||||
wasm_func_t* func(const char* name)
|
|
||||||
{
|
|
||||||
auto it = by_name.find(name);
|
|
||||||
return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second);
|
|
||||||
}
|
|
||||||
wasm_global_t* global(const char* name)
|
|
||||||
{
|
|
||||||
auto it = by_name.find(name);
|
|
||||||
return it == by_name.end() ? nullptr : wasm_extern_as_global(it->second);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static wasm_store_t* g_store = nullptr;
|
|
||||||
|
|
||||||
static void report_trap(wasm_trap_t* trap, const char* what)
|
|
||||||
{
|
|
||||||
if(!trap)
|
|
||||||
return;
|
|
||||||
wasm_message_t msg;
|
|
||||||
wasm_trap_message(trap, &msg);
|
|
||||||
FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int32_t call_i32(Instance& inst, const char* name, std::vector<int32_t> argv = {})
|
|
||||||
{
|
|
||||||
wasm_func_t* f = inst.func(name);
|
|
||||||
CHECK(f, "missing export func %s", name);
|
|
||||||
wasm_val_t args_buf[4];
|
|
||||||
CHECK(argv.size() <= 4, "call_i32 argv overflow for %s", name);
|
|
||||||
for(size_t i = 0; i < argv.size(); i++)
|
|
||||||
args_buf[i] = WASM_I32_VAL(argv[i]);
|
|
||||||
wasm_val_t results_buf[1] = { WASM_INIT_VAL };
|
|
||||||
wasm_val_vec_t args = { argv.size(), args_buf };
|
|
||||||
wasm_val_vec_t results = { 1, results_buf };
|
|
||||||
size_t result_arity = wasm_func_result_arity(f);
|
|
||||||
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
|
|
||||||
wasm_trap_t* trap = wasm_func_call(f, &args, result_arity ? &results : &no_results);
|
|
||||||
report_trap(trap, name);
|
|
||||||
return result_arity ? results_buf[0].of.i32 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static wasm_global_t* make_i32_global(int32_t value, wasm_mutability_t mut)
|
|
||||||
{
|
|
||||||
wasm_globaltype_t* gt = wasm_globaltype_new(wasm_valtype_new(WASM_I32), mut);
|
|
||||||
wasm_val_t val = WASM_I32_VAL(value);
|
|
||||||
wasm_global_t* g = wasm_global_new(g_store, gt, &val);
|
|
||||||
CHECK(g, "wasm_global_new failed (host-created globals unsupported?)");
|
|
||||||
wasm_globaltype_delete(gt);
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase3/core.wasm";
|
|
||||||
const char* unit_path = argc > 2 ? argv[2] : "/tmp/uce/wasm-phase3/page.wasm";
|
|
||||||
g_context = make_context();
|
|
||||||
|
|
||||||
wasm_engine_t* engine = wasm_engine_new();
|
|
||||||
CHECK(engine, "engine");
|
|
||||||
g_store = wasm_store_new(engine);
|
|
||||||
CHECK(g_store, "store");
|
|
||||||
g_store_for_stubs = g_store;
|
|
||||||
|
|
||||||
// ---- 1. core module ---------------------------------------------------
|
|
||||||
std::vector<uint8_t> core_bytes = read_file(core_path);
|
|
||||||
wasm_byte_vec_t core_bv;
|
|
||||||
wasm_byte_vec_new(&core_bv, core_bytes.size(), (const char*)core_bytes.data());
|
|
||||||
Instance core;
|
|
||||||
core.module = wasm_module_new(g_store, &core_bv);
|
|
||||||
wasm_byte_vec_delete(&core_bv);
|
|
||||||
CHECK(core.module, "core module load failed");
|
|
||||||
|
|
||||||
// the shared funcref table is host-created (core links with --import-table)
|
|
||||||
// because WAMR cannot grow a table from the host: size it up front as
|
|
||||||
// core's declared minimum (= its own elem needs) plus headroom for unit
|
|
||||||
// module table regions. __table_base allocation bumps from the minimum.
|
|
||||||
wasm_table_t* table = nullptr;
|
|
||||||
uint32_t table_next_free = 0;
|
|
||||||
const uint32_t TABLE_HEADROOM = 2048;
|
|
||||||
|
|
||||||
wasm_importtype_vec_t core_imports = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_imports(core.module, &core_imports);
|
|
||||||
std::vector<wasm_extern_t*> core_import_externs(core_imports.size);
|
|
||||||
for(size_t i = 0; i < core_imports.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* mod = wasm_importtype_module(core_imports.data[i]);
|
|
||||||
const wasm_name_t* nm = wasm_importtype_name(core_imports.data[i]);
|
|
||||||
const wasm_externtype_t* et = wasm_importtype_type(core_imports.data[i]);
|
|
||||||
std::string name(nm->data, nm->size);
|
|
||||||
if(wasm_externtype_kind(et) == WASM_EXTERN_TABLE)
|
|
||||||
{
|
|
||||||
CHECK(name.rfind("__indirect_function_table", 0) == 0,
|
|
||||||
"unexpected core table import %s", name.c_str());
|
|
||||||
const wasm_tabletype_t* tt = wasm_externtype_as_tabletype_const(et);
|
|
||||||
uint32_t core_min = wasm_tabletype_limits(tt)->min;
|
|
||||||
wasm_limits_t limits = { core_min + TABLE_HEADROOM, core_min + TABLE_HEADROOM };
|
|
||||||
wasm_tabletype_t* host_tt = wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &limits);
|
|
||||||
table = wasm_table_new(g_store, host_tt, nullptr);
|
|
||||||
CHECK(table, "wasm_table_new failed (host-created tables unsupported?)");
|
|
||||||
wasm_tabletype_delete(host_tt);
|
|
||||||
table_next_free = core_min;
|
|
||||||
printf("host table created: size=%u, core region=[0,%u)\n", core_min + TABLE_HEADROOM, core_min);
|
|
||||||
core_import_externs[i] = wasm_table_as_extern(table);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC,
|
|
||||||
"core has unexpected non-func import %.*s.%s",
|
|
||||||
(int)mod->size, mod->data, name.c_str());
|
|
||||||
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
|
|
||||||
wasm_func_t* import_func = nullptr;
|
|
||||||
std::string mod_name(mod->data, mod->size);
|
|
||||||
while(!mod_name.empty() && mod_name.back() == '\0') mod_name.pop_back();
|
|
||||||
while(!name.empty() && name.back() == '\0') name.pop_back();
|
|
||||||
if(mod_name == "env" && name == "uce_host_ctx_read")
|
|
||||||
import_func = wasm_func_new_with_env(g_store, ft, host_ctx_read, nullptr, nullptr);
|
|
||||||
else if(mod_name == "env" && name == "uce_host_log")
|
|
||||||
import_func = wasm_func_new_with_env(g_store, ft, host_log, nullptr, nullptr);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// named trap stub; leaks the name string, fine for a spike
|
|
||||||
char* label = strdup((mod_name + "." + name).c_str());
|
|
||||||
import_func = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr);
|
|
||||||
}
|
|
||||||
CHECK(import_func, "stub func creation failed for %s", name.c_str());
|
|
||||||
core_import_externs[i] = wasm_func_as_extern(import_func);
|
|
||||||
}
|
|
||||||
CHECK(table, "core does not import __indirect_function_table — rebuild with --import-table");
|
|
||||||
wasm_extern_vec_t core_iv = { core_import_externs.size(), core_import_externs.data() };
|
|
||||||
wasm_trap_t* trap = nullptr;
|
|
||||||
core.instance = wasm_instance_new(g_store, core.module, &core_iv, &trap);
|
|
||||||
report_trap(trap, "core instantiation");
|
|
||||||
CHECK(core.instance, "core instantiation failed");
|
|
||||||
core.index_exports();
|
|
||||||
printf("core instantiated: %zu exports\n", core.by_name.size());
|
|
||||||
|
|
||||||
// reactor init (runs ctors). Wasmtime does not auto-run _initialize;
|
|
||||||
// WAMR does — and calling it twice trips wasi-libc's double-init guard
|
|
||||||
// (__builtin_trap → "unreachable"), which cost us a debugging round.
|
|
||||||
if(core.func("_initialize"))
|
|
||||||
call_i32(core, "_initialize");
|
|
||||||
|
|
||||||
wasm_memory_t* memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr);
|
|
||||||
CHECK(memory, "core does not export memory");
|
|
||||||
g_memory = memory;
|
|
||||||
|
|
||||||
// ---- 2./3. dylink + base allocation ------------------------------------
|
|
||||||
std::vector<uint8_t> unit_bytes = read_file(unit_path);
|
|
||||||
DylinkInfo dl = parse_dylink(unit_bytes);
|
|
||||||
CHECK(dl.found, "unit has no dylink.0 mem_info");
|
|
||||||
printf("dylink.0: memsize=%u memalign=2^%u tablesize=%u\n", dl.mem_size, dl.mem_align, dl.table_size);
|
|
||||||
|
|
||||||
uint32_t align = 1u << dl.mem_align;
|
|
||||||
uint32_t raw = (uint32_t)call_i32(core, "malloc", { (int32_t)(dl.mem_size + align) });
|
|
||||||
CHECK(raw, "core malloc returned 0");
|
|
||||||
uint32_t memory_base = (raw + (align - 1)) & ~(align - 1);
|
|
||||||
|
|
||||||
uint32_t table_base = table_next_free;
|
|
||||||
table_next_free += dl.table_size;
|
|
||||||
CHECK(table_next_free <= wasm_table_size(table), "table headroom exhausted");
|
|
||||||
printf("bases: __memory_base=%u __table_base=%u (table size %u)\n",
|
|
||||||
memory_base, table_base, wasm_table_size(table));
|
|
||||||
|
|
||||||
// ---- 4. unit import resolution -----------------------------------------
|
|
||||||
wasm_byte_vec_t unit_bv;
|
|
||||||
wasm_byte_vec_new(&unit_bv, unit_bytes.size(), (const char*)unit_bytes.data());
|
|
||||||
Instance unit;
|
|
||||||
unit.module = wasm_module_new(g_store, &unit_bv);
|
|
||||||
wasm_byte_vec_delete(&unit_bv);
|
|
||||||
CHECK(unit.module, "unit module load failed");
|
|
||||||
|
|
||||||
wasm_importtype_vec_t unit_imports = WASM_EMPTY_VEC;
|
|
||||||
wasm_module_imports(unit.module, &unit_imports);
|
|
||||||
std::vector<wasm_extern_t*> unit_import_externs(unit_imports.size);
|
|
||||||
// GOT.mem entries that must be self-resolved from the unit's own exports
|
|
||||||
// after instantiation (weak data the core does not define)
|
|
||||||
std::vector<std::pair<std::string, wasm_global_t*>> deferred_got;
|
|
||||||
|
|
||||||
for(size_t i = 0; i < unit_imports.size; i++)
|
|
||||||
{
|
|
||||||
const wasm_name_t* mod_n = wasm_importtype_module(unit_imports.data[i]);
|
|
||||||
const wasm_name_t* nm_n = wasm_importtype_name(unit_imports.data[i]);
|
|
||||||
std::string mod(mod_n->data, mod_n->size);
|
|
||||||
std::string nm(nm_n->data, nm_n->size);
|
|
||||||
const wasm_externtype_t* et = wasm_importtype_type(unit_imports.data[i]);
|
|
||||||
wasm_externkind_t kind = wasm_externtype_kind(et);
|
|
||||||
wasm_extern_t* resolved = nullptr;
|
|
||||||
|
|
||||||
if(mod == "env" && nm == "memory")
|
|
||||||
resolved = wasm_memory_as_extern(memory);
|
|
||||||
else if(mod == "env" && nm == "__indirect_function_table")
|
|
||||||
resolved = wasm_table_as_extern(table);
|
|
||||||
else if(mod == "env" && nm == "__stack_pointer")
|
|
||||||
{
|
|
||||||
CHECK(core.global("__stack_pointer"), "core does not export __stack_pointer");
|
|
||||||
resolved = core.by_name["__stack_pointer"];
|
|
||||||
}
|
|
||||||
else if(mod == "env" && nm == "__memory_base")
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global((int32_t)memory_base, WASM_CONST));
|
|
||||||
else if(mod == "env" && nm == "__table_base")
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global((int32_t)table_base, WASM_CONST));
|
|
||||||
else if(mod == "env" && kind == WASM_EXTERN_FUNC)
|
|
||||||
{
|
|
||||||
auto it = core.by_name.find(nm);
|
|
||||||
CHECK(it != core.by_name.end(), "unresolved unit func import env.%s", nm.c_str());
|
|
||||||
resolved = it->second;
|
|
||||||
}
|
|
||||||
else if(mod == "GOT.mem")
|
|
||||||
{
|
|
||||||
wasm_global_t* src = core.global(nm.c_str());
|
|
||||||
if(src)
|
|
||||||
{
|
|
||||||
wasm_val_t v;
|
|
||||||
wasm_global_get(src, &v);
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global(v.of.i32, WASM_VAR));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// provisional 0; patched from the unit's own export post-instantiation
|
|
||||||
wasm_global_t* g = make_i32_global(0, WASM_VAR);
|
|
||||||
deferred_got.push_back({ nm, g });
|
|
||||||
resolved = wasm_global_as_extern(g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(mod == "GOT.func")
|
|
||||||
{
|
|
||||||
// resolved guest-side: the core exports a helper returning
|
|
||||||
// (intptr_t)&func, which on wasm is the function's table index —
|
|
||||||
// no host-side funcref injection needed (WAMR forbids it anyway)
|
|
||||||
std::string helper = "core_table_index_of_" + nm;
|
|
||||||
CHECK(core.func(helper.c_str()), "no GOT.func resolver %s in core", helper.c_str());
|
|
||||||
int32_t slot = call_i32(core, helper.c_str());
|
|
||||||
CHECK(slot > 0, "GOT.func resolver %s returned %d", helper.c_str(), slot);
|
|
||||||
printf("resolved GOT.func.%s = table[%d]\n", nm.c_str(), slot);
|
|
||||||
resolved = wasm_global_as_extern(make_i32_global(slot, WASM_VAR));
|
|
||||||
}
|
|
||||||
CHECK(resolved, "unhandled unit import %s.%s (kind %d)", mod.c_str(), nm.c_str(), (int)kind);
|
|
||||||
unit_import_externs[i] = resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 5. instantiate unit, patch GOT, run init ---------------------------
|
|
||||||
wasm_extern_vec_t unit_iv = { unit_import_externs.size(), unit_import_externs.data() };
|
|
||||||
trap = nullptr;
|
|
||||||
unit.instance = wasm_instance_new(g_store, unit.module, &unit_iv, &trap);
|
|
||||||
report_trap(trap, "unit instantiation");
|
|
||||||
CHECK(unit.instance, "unit instantiation failed");
|
|
||||||
unit.index_exports();
|
|
||||||
printf("unit instantiated: %zu exports\n", unit.by_name.size());
|
|
||||||
|
|
||||||
for(auto& [nm, got] : deferred_got)
|
|
||||||
{
|
|
||||||
wasm_global_t* own = unit.global(nm.c_str());
|
|
||||||
CHECK(own, "GOT.mem.%s defined neither by core nor by unit", nm.c_str());
|
|
||||||
wasm_val_t v;
|
|
||||||
wasm_global_get(own, &v);
|
|
||||||
// dylink ABI: a PIC module's exported data symbols are offsets relative
|
|
||||||
// to its __memory_base; the linker must add the base when resolving
|
|
||||||
wasm_val_t nv = WASM_I32_VAL((int32_t)(memory_base + (uint32_t)v.of.i32));
|
|
||||||
wasm_global_set(got, &nv);
|
|
||||||
printf("self-resolved GOT.mem.%s = %u (offset %d)\n", nm.c_str(), memory_base + (uint32_t)v.of.i32, v.of.i32);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(unit.func("__wasm_apply_data_relocs"))
|
|
||||||
call_i32(unit, "__wasm_apply_data_relocs");
|
|
||||||
if(unit.func("__wasm_call_ctors"))
|
|
||||||
call_i32(unit, "__wasm_call_ctors");
|
|
||||||
|
|
||||||
// ---- 6. membrane prepare, render and read back ---------------------------
|
|
||||||
int32_t rc = call_i32(core, "uce_phase3_prepare");
|
|
||||||
CHECK(rc == 0, "uce_phase3_prepare returned %d", rc);
|
|
||||||
int32_t request_ptr = call_i32(core, "uce_phase3_request");
|
|
||||||
CHECK(request_ptr != 0, "core returned null Request*");
|
|
||||||
call_i32(unit, "__uce_set_current_request", { request_ptr });
|
|
||||||
call_i32(unit, "__uce_render", { request_ptr });
|
|
||||||
call_i32(core, "uce_phase3_finish");
|
|
||||||
|
|
||||||
int32_t out_ptr = call_i32(core, "uce_phase3_output_data");
|
|
||||||
int32_t out_len = call_i32(core, "uce_phase3_output_size");
|
|
||||||
byte_t* mem = wasm_memory_data(memory); // may have moved if memory grew
|
|
||||||
|
|
||||||
printf("---- phase3 unit output (%d bytes) ----\n%.*s\n--------------------------------------\n",
|
|
||||||
out_len, out_len, mem + out_ptr);
|
|
||||||
|
|
||||||
bool ok = out_len > 0 &&
|
|
||||||
memmem(mem + out_ptr, out_len, "PHASE3 PAGE OK", 14) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "host=phase3.example.test", 24) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "answer=42", 9) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "each=answer:42", 14) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "self-got=42", 11) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "map=wise", 8) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "callback=42", 11) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "got-func-ok", 11) &&
|
|
||||||
memmem(mem + out_ptr, out_len, "fn=42", 5);
|
|
||||||
|
|
||||||
printf(ok ? "PHASE3 EXIT CRITERION: PASS\n" : "PHASE3 EXIT CRITERION: FAIL (see output above)\n");
|
|
||||||
return ok ? 0 : 1;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
// Phase 3 generated-unit fixture source. The checked-in generated/page.uce.cpp
|
|
||||||
// is the UCE-preprocessor-shaped output used by the wasm side-module build.
|
|
||||||
|
|
||||||
RENDER(Request& context)
|
|
||||||
{
|
|
||||||
<><section class="phase3">
|
|
||||||
<h1>PHASE3 PAGE OK</h1>
|
|
||||||
<p>host=<?= context.params["HTTP_HOST"] ?></p>
|
|
||||||
<p>route=<?= context.call["route"].to_string() ?></p>
|
|
||||||
<p>answer=<?= context.call["nested"]["answer"].to_string() ?></p>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# spikes/wasm-phase4 — production mechanics kill-test spike
|
|
||||||
|
|
||||||
Phase 4 validates the operational mechanics needed before a WASM worker can be
|
|
||||||
trusted in production. This spike is intentionally smaller than the future UCE
|
|
||||||
worker integration: it uses tiny WAT modules instead of generated UCE units, but
|
|
||||||
it exercises the Wasmtime controls and workspace cleanup paths the worker will
|
|
||||||
use. It is also deliberately the first spike on the Wasmtime-specific C++ API
|
|
||||||
(`wasmtime.hh`) — fuel, epochs, store limiters, and snapshots do not exist in
|
|
||||||
the portable `wasm.h` surface, so runtime-agnosticism ends here by design.
|
|
||||||
|
|
||||||
Run on `k-uce`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash spikes/wasm-phase4/build_runner.sh
|
|
||||||
/tmp/uce/wasm-phase4/runner
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected final line:
|
|
||||||
|
|
||||||
```text
|
|
||||||
PHASE4 EXIT CRITERION: PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
What this proves:
|
|
||||||
|
|
||||||
- **Reusable compiled artifact / snapshot proxy:** modules are compiled once and
|
|
||||||
then instantiated in fresh stores. Not OS CoW yet, but it validates the shape
|
|
||||||
of "shared core artifact + per-request workspace birth".
|
|
||||||
- **Unharmed worker:** after all six kill cases, the same engine serves a
|
|
||||||
healthy request that completes with the expected value. This — not merely
|
|
||||||
"didn't crash" — is the worker half of the Phase 4 exit criterion.
|
|
||||||
- **CPU limits, both mechanisms:** fuel (deterministic, per-instruction cost)
|
|
||||||
and epoch interruption (near-zero overhead, ticker thread, traps as
|
|
||||||
`interrupt`). Production default is **epoch** per the Phase 0 findings; fuel
|
|
||||||
is validated as the deterministic fallback. Note: the engine epoch only
|
|
||||||
advances, so every store must set its own deadline.
|
|
||||||
- **Memory limit, actually load-bearing:** the OOM fixture grows by 10 pages —
|
|
||||||
within its declared max (100) — so only the store limiter (2 pages) can deny
|
|
||||||
the growth. The denial is observed by the guest (`memory.grow` → -1) and
|
|
||||||
converted to a trap.
|
|
||||||
- **Trap-to-error-page data path:** every kill case's gate asserts the captured
|
|
||||||
message contains a `wasm backtrace` and the expected cause. Fixture frames
|
|
||||||
show `<unknown>` — readable production traces require units to keep their
|
|
||||||
name section (or a symbolication side-file in the artifact cache).
|
|
||||||
- **Trace summarizer (`src/lib/wasm_trace.h`):** trap messages are rendered
|
|
||||||
through the production collapse facility (repeated frames → `×N` lines,
|
|
||||||
mangled symbols demangled, cause/detail split out). The stack-exhaustion
|
|
||||||
case gates it on live trap output here; `site/tests/core.uce` gates the
|
|
||||||
parsing on canned messages in the native suite.
|
|
||||||
- **Handle cleanup with falsifiable checks:** closers must run exactly once per
|
|
||||||
handle and *while the store is still alive* (production closers may flush
|
|
||||||
guest-resident state); destructor-only cleanup fails the ordering check.
|
|
||||||
|
|
||||||
Kill fixtures (all genuinely trap):
|
|
||||||
|
|
||||||
- `unreachable`: `__builtin_trap` analog.
|
|
||||||
- `oob-access`: wild pointer; load at 128 KiB from a 64 KiB memory.
|
|
||||||
- `stack-exhaustion`: runaway recursion → `call stack exhausted`.
|
|
||||||
- `infinite-loop-fuel` / `infinite-loop-epoch`: same loop, both CPU limits.
|
|
||||||
- `oom-limiter`: limiter-denied `memory.grow` → guest converts -1 to a trap.
|
|
||||||
|
|
||||||
**A literal C++ null-pointer dereference is deliberately absent:** address 0 is
|
|
||||||
valid wasm linear memory, so `*(int*)nullptr` does not trap — it silently
|
|
||||||
writes inside the workspace, which is then dropped at request end (still
|
|
||||||
strictly better than a native SIGSEGV). See WASM-PROPOSAL §10 for the recorded
|
|
||||||
risk/policy.
|
|
||||||
|
|
||||||
Still deferred to production Phase 4:
|
|
||||||
|
|
||||||
- real core snapshot + OS-level CoW birth;
|
|
||||||
- wiring `wasm_trace.h` summaries into the UCE error-page UI (the summarizer
|
|
||||||
itself is done and gated); name-section policy for unit artifacts;
|
|
||||||
- wiring these traps into `linux_fastcgi.cpp` / the WASM worker backend;
|
|
||||||
- real runtime handle-table cleanup for sqlite/mysql/sockets/tasks;
|
|
||||||
- artifact ABI versioning end-to-end;
|
|
||||||
- memory-limit policy for guest allocators that return failure instead of
|
|
||||||
trapping (bump allocator vs dlmalloc behavior under the limiter).
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 4: build the Wasmtime kill-test runner.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
OUT=/tmp/uce/wasm-phase4
|
|
||||||
mkdir -p "$OUT"
|
|
||||||
|
|
||||||
c++ -std=c++17 -O2 -pthread runner.cpp \
|
|
||||||
-I/opt/wasmtime/include \
|
|
||||||
-L/opt/wasmtime/lib \
|
|
||||||
-Wl,-rpath,/opt/wasmtime/lib \
|
|
||||||
-lwasmtime \
|
|
||||||
-o "$OUT/runner"
|
|
||||||
|
|
||||||
echo "built: $OUT/runner"
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
// WASM-PROPOSAL Phase 4 — production-mechanics kill-test spike.
|
|
||||||
//
|
|
||||||
// Uses Wasmtime's C++ API directly (this spike is deliberately where the
|
|
||||||
// portable wasm.h surface ends: fuel, epochs, store limiters, and snapshots
|
|
||||||
// are Wasmtime-specific by nature; the runtime was selected in Phase 0).
|
|
||||||
//
|
|
||||||
// Validates, per kill case, that: the guest trap is captured with a wasm
|
|
||||||
// backtrace, the workspace handle closers run exactly once and while the
|
|
||||||
// store is still alive, and — after all kills — the same engine still serves
|
|
||||||
// a healthy request (the actual meaning of "unharmed worker").
|
|
||||||
//
|
|
||||||
// Note: a literal C++ null-pointer dereference does NOT trap in wasm —
|
|
||||||
// address 0 is valid linear memory; the damage stays inside the dropped
|
|
||||||
// workspace (see WASM-PROPOSAL §10). The kill fixtures below therefore use
|
|
||||||
// faults that genuinely trap: unreachable (__builtin_trap analog), an
|
|
||||||
// out-of-bounds access (wild pointer), stack exhaustion (runaway recursion),
|
|
||||||
// fuel/epoch exhaustion (infinite loop), and a limiter-denied memory.grow.
|
|
||||||
|
|
||||||
#include <wasmtime.hh>
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../../src/lib/wasm_trace.h"
|
|
||||||
|
|
||||||
using namespace wasmtime;
|
|
||||||
|
|
||||||
// Handle-table proxy. The checks here are deliberately falsifiable: closers
|
|
||||||
// must run exactly once per handle, and they must run while the store (and
|
|
||||||
// thus guest memory) is still alive — production closers may need to flush
|
|
||||||
// guest-resident state. A destructor-only cleanup fails the ordering check.
|
|
||||||
struct Workspace
|
|
||||||
{
|
|
||||||
int handles_open = 0;
|
|
||||||
int handles_closed = 0;
|
|
||||||
bool cleanup_ran = false;
|
|
||||||
bool closed_while_store_alive = false;
|
|
||||||
|
|
||||||
void open_handles(int count)
|
|
||||||
{
|
|
||||||
handles_open = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
void cleanup(bool store_alive)
|
|
||||||
{
|
|
||||||
if(cleanup_ran)
|
|
||||||
return;
|
|
||||||
cleanup_ran = true;
|
|
||||||
closed_while_store_alive = store_alive;
|
|
||||||
for(int i = 0; i < handles_open; i++)
|
|
||||||
handles_closed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
~Workspace()
|
|
||||||
{
|
|
||||||
cleanup(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok() const
|
|
||||||
{
|
|
||||||
return(cleanup_ran && handles_closed == handles_open && closed_while_store_alive);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static Module compile_wat(Engine& engine, const std::string& wat)
|
|
||||||
{
|
|
||||||
auto module = Module::compile(engine, wat);
|
|
||||||
if(!module)
|
|
||||||
{
|
|
||||||
std::cerr << "compile failed: " << module.err_ref().message() << "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
return module.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RunResult
|
|
||||||
{
|
|
||||||
bool trapped = false;
|
|
||||||
std::string message;
|
|
||||||
bool cleanup_ok = false;
|
|
||||||
bool has_value = false;
|
|
||||||
int32_t value = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
static RunResult run_case(Engine& engine, const Module& module, uint64_t fuel, int64_t memory_limit, bool epoch_ticker = false)
|
|
||||||
{
|
|
||||||
RunResult result;
|
|
||||||
Workspace workspace;
|
|
||||||
workspace.open_handles(2);
|
|
||||||
{
|
|
||||||
Store store(engine);
|
|
||||||
store.limiter(memory_limit, -1, -1, -1, -1);
|
|
||||||
Store::Context cx(store);
|
|
||||||
// epoch_interruption is enabled engine-wide and the engine epoch only
|
|
||||||
// advances; every store needs a deadline beyond the current epoch or
|
|
||||||
// it traps immediately. The epoch kill case gets a deadline of 1 tick
|
|
||||||
// and a ticker thread; everything else gets effectively-unbounded.
|
|
||||||
cx.set_epoch_deadline(epoch_ticker ? 1 : 1'000'000'000);
|
|
||||||
auto fuel_result = cx.set_fuel(fuel);
|
|
||||||
if(!fuel_result)
|
|
||||||
{
|
|
||||||
result.trapped = true;
|
|
||||||
result.message = fuel_result.err_ref().message();
|
|
||||||
workspace.cleanup(true);
|
|
||||||
result.cleanup_ok = workspace.ok();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto instance = Instance::create(cx, module, {});
|
|
||||||
if(!instance)
|
|
||||||
{
|
|
||||||
result.trapped = true;
|
|
||||||
result.message = instance.err_ref().message();
|
|
||||||
workspace.cleanup(true);
|
|
||||||
result.cleanup_ok = workspace.ok();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto run_export = instance.ok_ref().get(cx, "run");
|
|
||||||
if(!run_export || !std::get_if<Func>(&*run_export))
|
|
||||||
{
|
|
||||||
result.trapped = true;
|
|
||||||
result.message = "missing run export";
|
|
||||||
workspace.cleanup(true);
|
|
||||||
result.cleanup_ok = workspace.ok();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::thread ticker;
|
|
||||||
if(epoch_ticker)
|
|
||||||
ticker = std::thread([&engine] {
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
||||||
engine.increment_epoch();
|
|
||||||
});
|
|
||||||
|
|
||||||
Func run = *std::get_if<Func>(&*run_export);
|
|
||||||
auto call = run.call(cx, std::vector<Val>{});
|
|
||||||
if(ticker.joinable())
|
|
||||||
ticker.join();
|
|
||||||
if(!call)
|
|
||||||
{
|
|
||||||
result.trapped = true;
|
|
||||||
result.message = call.err_ref().message();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.message = "completed without trap";
|
|
||||||
auto values = call.ok();
|
|
||||||
if(!values.empty())
|
|
||||||
{
|
|
||||||
result.has_value = true;
|
|
||||||
result.value = values[0].i32();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workspace.cleanup(true);
|
|
||||||
result.cleanup_ok = workspace.ok();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void print_case(const char* label, const RunResult& result)
|
|
||||||
{
|
|
||||||
std::cout << "--- " << label << " ---\n";
|
|
||||||
std::cout << "trapped=" << (result.trapped ? "yes" : "no") << "\n";
|
|
||||||
if(result.trapped)
|
|
||||||
std::cout << "summary:\n" << wasm_trace_collapse(result.message) << "\n";
|
|
||||||
else
|
|
||||||
std::cout << "message=" << result.message << "\n";
|
|
||||||
std::cout << "cleanup=" << (result.cleanup_ok ? "ok" : "failed") << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
static void check_kill(const char* label, const RunResult& result, const char* expect_in_message)
|
|
||||||
{
|
|
||||||
print_case(label, result);
|
|
||||||
bool has_trace = result.message.find("wasm backtrace") != std::string::npos;
|
|
||||||
bool has_cause = result.message.find(expect_in_message) != std::string::npos;
|
|
||||||
if(!result.trapped || !result.cleanup_ok || !has_trace || !has_cause)
|
|
||||||
{
|
|
||||||
std::cerr << "PHASE4 EXIT CRITERION: FAIL at " << label
|
|
||||||
<< (result.trapped ? "" : " (no trap)")
|
|
||||||
<< (result.cleanup_ok ? "" : " (cleanup)")
|
|
||||||
<< (has_trace ? "" : " (no backtrace)")
|
|
||||||
<< (has_cause ? "" : " (wrong cause)") << "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void check_healthy(const char* label, const RunResult& result)
|
|
||||||
{
|
|
||||||
print_case(label, result);
|
|
||||||
if(result.trapped || !result.cleanup_ok || !result.has_value || result.value != 42)
|
|
||||||
{
|
|
||||||
std::cerr << "PHASE4 EXIT CRITERION: FAIL at " << label << " (worker harmed)\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
Config config;
|
|
||||||
config.consume_fuel(true);
|
|
||||||
config.epoch_interruption(true);
|
|
||||||
Engine engine(std::move(config));
|
|
||||||
|
|
||||||
// Compiled modules are reused across stores: a deliberately small proxy
|
|
||||||
// for the future core snapshot — per-request workspaces (stores) are
|
|
||||||
// born fresh while compilation work is shared.
|
|
||||||
|
|
||||||
// __builtin_trap analog; a real C++ *nullptr does not trap (see header)
|
|
||||||
Module unreachable_module = compile_wat(engine, R"wat(
|
|
||||||
(module
|
|
||||||
(func (export "run")
|
|
||||||
unreachable))
|
|
||||||
)wat");
|
|
||||||
|
|
||||||
// wild/OOB pointer: load at 128 KiB from a 64 KiB memory
|
|
||||||
Module oob_module = compile_wat(engine, R"wat(
|
|
||||||
(module
|
|
||||||
(memory 1 1)
|
|
||||||
(func (export "run")
|
|
||||||
(drop (i32.load (i32.const 131072)))))
|
|
||||||
)wat");
|
|
||||||
|
|
||||||
// runaway recursion → call stack exhausted
|
|
||||||
Module stack_module = compile_wat(engine, R"wat(
|
|
||||||
(module
|
|
||||||
(func $f (export "run")
|
|
||||||
(call $f)))
|
|
||||||
)wat");
|
|
||||||
|
|
||||||
Module loop_module = compile_wat(engine, R"wat(
|
|
||||||
(module
|
|
||||||
(func (export "run")
|
|
||||||
(loop br 0)))
|
|
||||||
)wat");
|
|
||||||
|
|
||||||
// grows by 10 pages: within the module's own declared max (100), so only
|
|
||||||
// the store limiter (2 pages) can deny it — this proves the limiter is
|
|
||||||
// load-bearing, not the declared max
|
|
||||||
Module oom_module = compile_wat(engine, R"wat(
|
|
||||||
(module
|
|
||||||
(memory 1 100)
|
|
||||||
(func (export "run")
|
|
||||||
i32.const 10
|
|
||||||
memory.grow
|
|
||||||
i32.const -1
|
|
||||||
i32.eq
|
|
||||||
(if (then unreachable))))
|
|
||||||
)wat");
|
|
||||||
|
|
||||||
Module healthy_module = compile_wat(engine, R"wat(
|
|
||||||
(module
|
|
||||||
(func (export "run") (result i32)
|
|
||||||
i32.const 42))
|
|
||||||
)wat");
|
|
||||||
|
|
||||||
const int64_t MEM_LIMIT = 2 * 65536;
|
|
||||||
const uint64_t FUEL = 10'000;
|
|
||||||
const uint64_t FUEL_PLENTY = 1'000'000'000'000;
|
|
||||||
|
|
||||||
check_kill("unreachable", run_case(engine, unreachable_module, FUEL, MEM_LIMIT), "unreachable");
|
|
||||||
check_kill("oob-access", run_case(engine, oob_module, FUEL, MEM_LIMIT), "out of bounds");
|
|
||||||
|
|
||||||
RunResult stack_result = run_case(engine, stack_module, FUEL_PLENTY, MEM_LIMIT);
|
|
||||||
check_kill("stack-exhaustion", stack_result, "call stack exhausted");
|
|
||||||
|
|
||||||
// the trace formatter (src/lib/wasm_trace.h) is part of the gate: the
|
|
||||||
// recursion frames (Wasmtime caps the displayed backtrace at 20) must
|
|
||||||
// collapse to a bounded summary
|
|
||||||
WasmTraceSummary stack_summary = wasm_trace_summarize(stack_result.message);
|
|
||||||
if(!stack_summary.parsed || stack_summary.total_frames < 10
|
|
||||||
|| stack_summary.frames.size() > 12
|
|
||||||
|| stack_summary.cause.find("call stack exhausted") == std::string::npos
|
|
||||||
|| wasm_trace_format(stack_summary).find("×") == std::string::npos)
|
|
||||||
{
|
|
||||||
std::cerr << "PHASE4 EXIT CRITERION: FAIL at trace-collapse (parsed="
|
|
||||||
<< stack_summary.parsed << " total=" << stack_summary.total_frames
|
|
||||||
<< " lines=" << stack_summary.frames.size()
|
|
||||||
<< " cause=" << stack_summary.cause << ")\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
std::cout << "--- trace-collapse ---\n" << stack_summary.total_frames
|
|
||||||
<< " raw frames -> " << stack_summary.frames.size() << " summary lines\n";
|
|
||||||
check_kill("infinite-loop-fuel", run_case(engine, loop_module, FUEL, MEM_LIMIT), "fuel");
|
|
||||||
// Wasmtime reports epoch-deadline traps as "interrupt"
|
|
||||||
check_kill("infinite-loop-epoch", run_case(engine, loop_module, FUEL_PLENTY, MEM_LIMIT, true), "interrupt");
|
|
||||||
check_kill("oom-limiter", run_case(engine, oom_module, FUEL, MEM_LIMIT), "unreachable");
|
|
||||||
|
|
||||||
// after every kill above, the same engine must still serve a request —
|
|
||||||
// this is the "unharmed worker" half of the Phase 4 exit criterion
|
|
||||||
check_healthy("healthy-after-kills", run_case(engine, healthy_module, FUEL, MEM_LIMIT));
|
|
||||||
|
|
||||||
std::cout << "PHASE4 EXIT CRITERION: PASS\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# spikes/wasm-phase5 — parity, audit, and performance harness
|
|
||||||
|
|
||||||
Phase 5's production exit requires the full network suite to pass on the WASM
|
|
||||||
worker and performance numbers to be published. The production worker is not in
|
|
||||||
this branch yet, so this spike builds the Phase 5 harness and records the native
|
|
||||||
baseline that the WASM worker must match.
|
|
||||||
|
|
||||||
Run on `k-uce`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash spikes/wasm-phase5/run_phase5.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected final line:
|
|
||||||
|
|
||||||
```text
|
|
||||||
PHASE5 HARNESS: PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
The harness first runs a throwaway warmup pass to populate the unit cache after
|
|
||||||
binary rebuilds. The warmup excludes the stateful `site tests tasks` case so it
|
|
||||||
cannot perturb the measured task-lifecycle check. The measured full-suite and
|
|
||||||
starter-subset passes then become the gate.
|
|
||||||
|
|
||||||
Artifacts are written under `/tmp/uce/wasm-phase5/`:
|
|
||||||
|
|
||||||
- `native-network-warmup.json` — throwaway warmup result; allowed to fail on cold
|
|
||||||
compile timeouts.
|
|
||||||
- `native-network.json` — measured full native network suite result. The harness
|
|
||||||
fails if fewer than 80 cases run, so an empty or broken suite cannot pass.
|
|
||||||
- `native-starter.json` — measured starter-focused parity subset. The harness
|
|
||||||
fails if fewer than 10 cases run, which protects against a vacuous
|
|
||||||
`--match starter` exit gate.
|
|
||||||
- `site-static-audit.{json,md}` — candidate cross-request/static-state risks in
|
|
||||||
`site/` for the §3.2 semantic change audit. By default it scans code-like
|
|
||||||
`.uce` and `.h` files only; pass `--include-doc-text` to include `.txt` docs
|
|
||||||
as `documentation` severity findings.
|
|
||||||
- `benchmark.{json,md}` — warmed native baseline for the three Phase 5 budget
|
|
||||||
pages. The default is 20 samples per target; use a higher `--samples` value for
|
|
||||||
noisy shared-host gate runs if needed:
|
|
||||||
- `template-heavy-doc`: `/doc/singlepage.uce`
|
|
||||||
- `sqlite-page`: `/demo/sqlite.uce`
|
|
||||||
- `component-heavy-starter`: `/examples/uce-starter/?dashboard`
|
|
||||||
|
|
||||||
`benchmark.py` also accepts `--wasm-base-url` once a WASM worker endpoint exists.
|
|
||||||
When provided, it compares WASM medians against the Phase 5 budget of ≤2× native
|
|
||||||
page latency. Workspace birth and internal component call overhead budgets still
|
|
||||||
need worker-internal probes; this harness documents the gap rather than faking
|
|
||||||
those numbers.
|
|
||||||
|
|
||||||
A durable informational baseline snapshot is kept in `reports/`. Paired
|
|
||||||
native/WASM runs still recompute native medians for the actual budget decision.
|
|
||||||
|
|
||||||
Current scope:
|
|
||||||
|
|
||||||
- Native parity and baseline collection are automated.
|
|
||||||
- WASM parity/performance comparison is ready but blocked on the production WASM
|
|
||||||
worker endpoint.
|
|
||||||
- The static-state audit is heuristic and intentionally conservative; findings
|
|
||||||
must be reviewed by a human before migration work is scheduled.
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Phase 5 harness: native parity, site static audit, and native perf baseline.
|
|
||||||
# Run on k-uce from repo root.
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")/../.."
|
|
||||||
|
|
||||||
OUT=/tmp/uce/wasm-phase5
|
|
||||||
mkdir -p "$OUT"
|
|
||||||
|
|
||||||
# The network suite can show transient cold-compile timeouts immediately after a
|
|
||||||
# binary rebuild. Run a throwaway pass first so the measured gate is a warmed
|
|
||||||
# parity signal rather than a compiler-cache signal. The warmup is allowed to
|
|
||||||
# fail; the measured pass below is not.
|
|
||||||
python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/native-network-warmup.json" || true
|
|
||||||
|
|
||||||
network_status=0
|
|
||||||
starter_status=0
|
|
||||||
benchmark_status=0
|
|
||||||
python3 tests/run_network_tests.py --include-internal --json-report "$OUT/native-network.json" || network_status=$?
|
|
||||||
python3 tests/run_network_tests.py --include-internal --match starter --json-report "$OUT/native-starter.json" || starter_status=$?
|
|
||||||
python3 spikes/wasm-phase5/audit_site_statics.py --out-dir "$OUT"
|
|
||||||
python3 spikes/wasm-phase5/benchmark.py --out-dir "$OUT" || benchmark_status=$?
|
|
||||||
|
|
||||||
NETWORK_STATUS=$network_status STARTER_STATUS=$starter_status BENCHMARK_STATUS=$benchmark_status python3 - <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
MIN_NETWORK_CASES = 80
|
|
||||||
MIN_STARTER_CASES = 10
|
|
||||||
MIN_BENCHMARKS = 3
|
|
||||||
out = Path('/tmp/uce/wasm-phase5')
|
|
||||||
network = json.loads((out / 'native-network.json').read_text())
|
|
||||||
starter = json.loads((out / 'native-starter.json').read_text())
|
|
||||||
bench = json.loads((out / 'benchmark.json').read_text())
|
|
||||||
failures = [r for r in network if not r['ok']] + [r for r in starter if not r['ok']] + [r for r in bench if not r['ok']]
|
|
||||||
structural_failures = []
|
|
||||||
if len(network) < MIN_NETWORK_CASES:
|
|
||||||
structural_failures.append(f"network suite ran {len(network)} cases, expected at least {MIN_NETWORK_CASES}")
|
|
||||||
if len(starter) < MIN_STARTER_CASES:
|
|
||||||
structural_failures.append(f"starter subset ran {len(starter)} cases, expected at least {MIN_STARTER_CASES}")
|
|
||||||
if len(bench) < MIN_BENCHMARKS:
|
|
||||||
structural_failures.append(f"benchmark ran {len(bench)} rows, expected at least {MIN_BENCHMARKS}")
|
|
||||||
nonzero = {
|
|
||||||
'network_status': int(os.environ['NETWORK_STATUS']),
|
|
||||||
'starter_status': int(os.environ['STARTER_STATUS']),
|
|
||||||
'benchmark_status': int(os.environ['BENCHMARK_STATUS']),
|
|
||||||
}
|
|
||||||
for name, status in nonzero.items():
|
|
||||||
if status != 0:
|
|
||||||
structural_failures.append(f"{name} exited {status}")
|
|
||||||
if failures or structural_failures:
|
|
||||||
print('PHASE5 HARNESS: FAIL')
|
|
||||||
for failure in structural_failures:
|
|
||||||
print(failure)
|
|
||||||
for failure in failures:
|
|
||||||
print(failure)
|
|
||||||
raise SystemExit(1)
|
|
||||||
print('PHASE5 HARNESS: PASS')
|
|
||||||
print(f"network_cases={len(network)} starter_cases={len(starter)} benchmarks={len(bench)}")
|
|
||||||
print(f"reports={out}")
|
|
||||||
PY
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#define NO_GLOBAL_ARENA_ALLOCATOR
|
|
||||||
|
|
||||||
struct MemoryArena {
|
|
||||||
|
|
||||||
u8* data;
|
|
||||||
u64 size = 0;
|
|
||||||
u64 capacity = 0;
|
|
||||||
String name = "unnamed";
|
|
||||||
|
|
||||||
MemoryArena(u64 cap, String _name = "unnamed")
|
|
||||||
{
|
|
||||||
name = _name;
|
|
||||||
capacity = cap;
|
|
||||||
printf("(i) memory arena '%s' created with capacity of %llu bytes\n", name.c_str(), capacity);
|
|
||||||
data = (u8*)malloc(cap);
|
|
||||||
}
|
|
||||||
|
|
||||||
~MemoryArena()
|
|
||||||
{
|
|
||||||
free(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear()
|
|
||||||
{
|
|
||||||
#ifdef DEBUG_MEMORY
|
|
||||||
printf("(i) memory arena '%s' cleared after high mark of %llu bytes\n", name.c_str(), size);
|
|
||||||
#endif
|
|
||||||
size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void* get(u64 size_needed)
|
|
||||||
{
|
|
||||||
u64 size_aligned = 8 + (8 * ((size_needed) / 8));
|
|
||||||
u8* result = data + size;
|
|
||||||
if(size_aligned + size >= capacity)
|
|
||||||
{
|
|
||||||
printf("(!) memory arena '%s' capacity (%llu) exceeded %llu/%llu + %llu >= %llu\n",
|
|
||||||
name.c_str(), capacity, size_needed, size_aligned, size, capacity);
|
|
||||||
return(0);
|
|
||||||
}
|
|
||||||
size += size_aligned;
|
|
||||||
#ifdef DEBUG_MEMORY_DETAILED
|
|
||||||
printf("(i) memory arena '%s' [+%llu]:%p alloc %llu/%llu bytes\n", name.c_str(), size, result, size_needed, size_aligned);
|
|
||||||
#endif
|
|
||||||
return(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
MemoryArena* current_memory_arena = 0;
|
|
||||||
|
|
||||||
void switch_to_system_alloc()
|
|
||||||
{
|
|
||||||
#ifdef GLOBAL_ARENA_ALLOCATOR
|
|
||||||
current_memory_arena = 0;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void switch_to_arena(MemoryArena* a)
|
|
||||||
{
|
|
||||||
#ifdef GLOBAL_ARENA_ALLOCATOR
|
|
||||||
current_memory_arena = a;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef GLOBAL_ARENA_ALLOCATOR
|
|
||||||
void * operator new(decltype(sizeof(0)) n) noexcept(false)
|
|
||||||
{
|
|
||||||
if(current_memory_arena)
|
|
||||||
{
|
|
||||||
return(current_memory_arena->get(n));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return(malloc(n));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void operator delete(void * p) throw()
|
|
||||||
{
|
|
||||||
if(current_memory_arena)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
free(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -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 compiler_preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String content)
|
||||||
{
|
{
|
||||||
String parsed_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(
|
file_get_contents(
|
||||||
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]
|
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]
|
||||||
)+
|
)+
|
||||||
|
|||||||
+44
-1
@@ -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 shared_unit_compile_check(const SharedUnitFilesystemState& state)
|
||||||
{
|
{
|
||||||
SharedUnitCompileCheck result;
|
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->src_file_name = basename(file_name);
|
||||||
su->bin_file_name = su->src_file_name + ".so";
|
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->pre_file_name = su->src_file_name + ".cpp";
|
||||||
|
|
||||||
su->so_name = su->bin_path + "/" + su->bin_file_name;
|
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->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->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";
|
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 result =
|
||||||
String("#ifndef UCE_LIB_INCLUDED\n") +
|
String("#ifndef UCE_LIB_INCLUDED\n") +
|
||||||
"#define 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(
|
file_get_contents(
|
||||||
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]) +
|
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]) +
|
||||||
"#endif \n";
|
"#endif \n";
|
||||||
@@ -839,6 +859,27 @@ void compile_shared_unit(Request* context, SharedUnit* su)
|
|||||||
shell_escape(su->bin_file_name)
|
shell_escape(su->bin_file_name)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if(su->compiler_messages.length() == 0 && !su->opt_so_optional && compiler_wasm_unit_compile_enabled(context))
|
||||||
|
{
|
||||||
|
// The wasm side-module build is best-effort: a unit that cannot compile
|
||||||
|
// to wasm (e.g. try/catch) still works via the native .so and falls back
|
||||||
|
// to native at request time. Record the failure, never fail the unit on
|
||||||
|
// it. On failure, remove any stale .wasm so the backend won't serve it.
|
||||||
|
String wasm_messages = trim(shell_exec(shell_escape(compiler_wasm_compile_script(context))+" "+
|
||||||
|
shell_escape(su->src_path)+" "+
|
||||||
|
shell_escape(su->bin_path)+" "+
|
||||||
|
shell_escape(su->file_name)+" "+
|
||||||
|
shell_escape(su->pre_file_name)+" "+
|
||||||
|
shell_escape(su->wasm_file_name)
|
||||||
|
));
|
||||||
|
if(wasm_messages.length() > 0)
|
||||||
|
{
|
||||||
|
file_put_contents(su->wasm_check_file_name, wasm_messages + "\n");
|
||||||
|
file_unlink(su->wasm_name);
|
||||||
|
printf("(i) wasm side-module unavailable for %s (native .so serves it)\n", su->file_name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(su->compiler_messages.length() > 0)
|
if(su->compiler_messages.length() > 0)
|
||||||
{
|
{
|
||||||
String raw_messages = su->compiler_messages;
|
String raw_messages = su->compiler_messages;
|
||||||
@@ -1171,6 +1212,8 @@ DValue unit_info(String path)
|
|||||||
info["bin_file_name"] = su->bin_file_name;
|
info["bin_file_name"] = su->bin_file_name;
|
||||||
info["pre_file_name"] = su->pre_file_name;
|
info["pre_file_name"] = su->pre_file_name;
|
||||||
info["so_name"] = su->so_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["api_file_name"] = su->api_file_name;
|
||||||
info["meta_file_name"] = su->meta_file_name;
|
info["meta_file_name"] = su->meta_file_name;
|
||||||
info["compile_output_file_name"] = su->compile_output_file_name;
|
info["compile_output_file_name"] = su->compile_output_file_name;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "functionlib.h"
|
#include "functionlib.h"
|
||||||
|
|
||||||
|
#ifndef __UCE_WASM_CORE__
|
||||||
#define PCRE2_CODE_UNIT_WIDTH 8
|
#define PCRE2_CODE_UNIT_WIDTH 8
|
||||||
#include <pcre2.h>
|
#include <pcre2.h>
|
||||||
|
#endif
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -296,6 +298,7 @@ String replace(String s, String search, String replace_with)
|
|||||||
return(result);
|
return(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef __UCE_WASM_CORE__
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
String regex_flags_label(String flags)
|
String regex_flags_label(String flags)
|
||||||
@@ -637,6 +640,75 @@ StringList regex_split(String pattern, String subject, String flags)
|
|||||||
return(result);
|
return(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
// PCRE2 is not compiled into the wasm core; regex runs host-side (the host
|
||||||
|
// already links libpcre2). One UCEB1-marshalled hostcall carries the request
|
||||||
|
// {op,pattern,subject,flags,replacement} in and the result tree out — the host
|
||||||
|
// runs the real regex_* and packs the answer. See uce_host_regex in
|
||||||
|
// src/wasm/worker.cpp.
|
||||||
|
extern "C" size_t uce_host_regex(const char* in, size_t in_len, char* out, size_t cap);
|
||||||
|
|
||||||
|
static DValue wasm_regex_call(String op, String pattern, String subject, String flags, String replacement = "")
|
||||||
|
{
|
||||||
|
DValue request;
|
||||||
|
request["op"] = op;
|
||||||
|
request["pattern"] = pattern;
|
||||||
|
request["subject"] = subject;
|
||||||
|
request["flags"] = flags;
|
||||||
|
request["replacement"] = replacement;
|
||||||
|
String encoded = ucb_encode(request);
|
||||||
|
size_t need = uce_host_regex(encoded.data(), encoded.size(), 0, 0);
|
||||||
|
if(need == 0)
|
||||||
|
return(DValue());
|
||||||
|
String buffer(need, 0);
|
||||||
|
size_t got = uce_host_regex(encoded.data(), encoded.size(), &buffer[0], need);
|
||||||
|
if(got == 0 || got > need)
|
||||||
|
return(DValue());
|
||||||
|
DValue response;
|
||||||
|
String error;
|
||||||
|
ucb_decode(String(buffer.data(), got), response, &error);
|
||||||
|
return(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool regex_match(String pattern, String subject, String flags)
|
||||||
|
{
|
||||||
|
return(wasm_regex_call("match", pattern, subject, flags)["bool"].to_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
DValue regex_search(String pattern, String subject, String flags)
|
||||||
|
{
|
||||||
|
DValue response = wasm_regex_call("search", pattern, subject, flags);
|
||||||
|
DValue* tree = response.key("tree");
|
||||||
|
return(tree ? *tree : DValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
DValue regex_search_all(String pattern, String subject, String flags)
|
||||||
|
{
|
||||||
|
DValue response = wasm_regex_call("search_all", pattern, subject, flags);
|
||||||
|
DValue* tree = response.key("tree");
|
||||||
|
return(tree ? *tree : DValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
String regex_replace(String pattern, String replacement, String subject, String flags)
|
||||||
|
{
|
||||||
|
return(wasm_regex_call("replace", pattern, subject, flags, replacement)["text"].to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
StringList regex_split(String pattern, String subject, String flags)
|
||||||
|
{
|
||||||
|
DValue response = wasm_regex_call("split", pattern, subject, flags);
|
||||||
|
StringList result;
|
||||||
|
DValue* list = response.key("list");
|
||||||
|
if(list)
|
||||||
|
list->each([&](const DValue& part, String) {
|
||||||
|
result.push_back(part.to_string());
|
||||||
|
});
|
||||||
|
return(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
String trim(String raw)
|
String trim(String raw)
|
||||||
{
|
{
|
||||||
s64 len = raw.length();
|
s64 len = raw.length();
|
||||||
@@ -1323,7 +1395,12 @@ String xml_decode_text(String s)
|
|||||||
|
|
||||||
void xml_throw(String message)
|
void xml_throw(String message)
|
||||||
{
|
{
|
||||||
|
#ifdef __UCE_WASM_CORE__
|
||||||
|
(void)message;
|
||||||
|
__builtin_trap();
|
||||||
|
#else
|
||||||
throw std::runtime_error("xml_decode(): " + message);
|
throw std::runtime_error("xml_decode(): " + message);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct XmlParser
|
struct XmlParser
|
||||||
@@ -1856,7 +1933,12 @@ String yaml_decode_quoted(String raw)
|
|||||||
|
|
||||||
void yaml_throw(String message)
|
void yaml_throw(String message)
|
||||||
{
|
{
|
||||||
|
#ifdef __UCE_WASM_CORE__
|
||||||
|
(void)message;
|
||||||
|
__builtin_trap();
|
||||||
|
#else
|
||||||
throw std::runtime_error("yaml_decode(): " + message);
|
throw std::runtime_error("yaml_decode(): " + message);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct YamlParser
|
struct YamlParser
|
||||||
|
|||||||
@@ -55,8 +55,11 @@ String to_string(SharedUnit* u) {
|
|||||||
return(result);
|
return(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NB: header-defined function templates must be inline — wasm side modules
|
||||||
|
// compile with -fvisibility-inlines-hidden so instantiations bind locally
|
||||||
|
// instead of becoming unresolvable self-imports (WASM-PROPOSAL §6)
|
||||||
template <typename ITYPE>
|
template <typename ITYPE>
|
||||||
String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
|
inline String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
|
||||||
{
|
{
|
||||||
static const char* digits = "0123456789ABCDEF";
|
static const char* digits = "0123456789ABCDEF";
|
||||||
String rc(hex_len,'0');
|
String rc(hex_len,'0');
|
||||||
@@ -66,7 +69,7 @@ String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<typename T, typename F>
|
template<typename T, typename F>
|
||||||
std::vector<T> filter(std::vector<T> items, F f)
|
inline std::vector<T> filter(std::vector<T> items, F f)
|
||||||
{
|
{
|
||||||
std::vector<T> new_items;
|
std::vector<T> new_items;
|
||||||
for(auto item : items)
|
for(auto item : items)
|
||||||
@@ -78,7 +81,7 @@ std::vector<T> filter(std::vector<T> items, F f)
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<typename T, typename F>
|
template<typename T, typename F>
|
||||||
auto map(std::vector<T> items, F f)
|
inline auto map(std::vector<T> items, F f)
|
||||||
{
|
{
|
||||||
using ResultType = decltype(f(items[0]));
|
using ResultType = decltype(f(items[0]));
|
||||||
std::vector<ResultType> new_items;
|
std::vector<ResultType> new_items;
|
||||||
@@ -101,7 +104,7 @@ DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f);
|
|||||||
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f);
|
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f);
|
||||||
|
|
||||||
template <class ...Args>
|
template <class ...Args>
|
||||||
String first(Args... args)
|
inline String first(Args... args)
|
||||||
{
|
{
|
||||||
std::vector<String> vec = {args...};
|
std::vector<String> vec = {args...};
|
||||||
for(auto s : vec)
|
for(auto s : vec)
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ A million repetitions of "a"
|
|||||||
/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
|
/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
|
||||||
/* #define SHA1HANDSOFF * Copies data before messing with it. */
|
/* #define SHA1HANDSOFF * Copies data before messing with it. */
|
||||||
|
|
||||||
|
#ifdef __UCE_WASM_CORE__
|
||||||
|
#include <stdint.h>
|
||||||
|
typedef uint32_t u_int32_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
u_int32_t state[5];
|
u_int32_t state[5];
|
||||||
u_int32_t count[2];
|
u_int32_t count[2];
|
||||||
|
|||||||
+217
@@ -1,3 +1,211 @@
|
|||||||
|
#ifdef __UCE_WASM_CORE__
|
||||||
|
#include <cmath>
|
||||||
|
#include "types.h"
|
||||||
|
#include "functionlib.h"
|
||||||
|
#include "sys.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
uint64_t uce_host_time(void);
|
||||||
|
double uce_host_time_precise(void);
|
||||||
|
size_t uce_host_env(const char* key, size_t key_len, char* buf, size_t cap);
|
||||||
|
size_t uce_host_random(char* buf, size_t len);
|
||||||
|
void uce_host_log(int level, const char* buf, size_t len);
|
||||||
|
// read-only file membrane, policy-gated host-side to the site tree;
|
||||||
|
// relative paths resolve against the current unit's directory (the native
|
||||||
|
// cwd convention); file_read uses the length-query convention
|
||||||
|
int uce_host_file_exists(const char* path, size_t path_len, const char* current, size_t current_len);
|
||||||
|
size_t uce_host_file_read(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap);
|
||||||
|
int uce_host_file_write(const char* path, size_t path_len, const char* current, size_t current_len, const char* content, size_t content_len, int append);
|
||||||
|
void uce_host_file_unlink(const char* path, size_t path_len, const char* current, size_t current_len);
|
||||||
|
int uce_host_task_spawn(const char* key, size_t key_len, uint64_t callback_id, double interval, uint64_t timeout, int repeat);
|
||||||
|
int uce_host_task_pid(const char* key, size_t key_len);
|
||||||
|
int uce_host_task_kill(int pid, int sig);
|
||||||
|
unsigned int uce_host_sleep_us(uint64_t usec);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String wasm_current_unit_file()
|
||||||
|
{
|
||||||
|
return(context ? context->resources.current_unit_file : String(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
String shell_exec(String cmd) { (void)cmd; return(""); }
|
||||||
|
String shell_escape(String raw) { return(raw); }
|
||||||
|
String basename(String fn) { while(fn.find("/") != String::npos) fn = fn.substr(fn.find("/") + 1); return(fn); }
|
||||||
|
String dirname(String fn) { auto pos = fn.find_last_of('/'); return(pos == String::npos ? "" : fn.substr(0, pos)); }
|
||||||
|
String path_join(String base, String child) { if(base == "") return(child); if(child == "") return(base); if(child[0] == '/') return(child); return(base + (base.back() == '/' ? "" : "/") + child); }
|
||||||
|
String path_real(String path) { return(path); }
|
||||||
|
bool path_is_within(String path, String root) { return(str_starts_with(path, root)); }
|
||||||
|
bool mkdir(String path) { (void)path; return(false); }
|
||||||
|
bool file_exists(String path)
|
||||||
|
{
|
||||||
|
String current = wasm_current_unit_file();
|
||||||
|
return(uce_host_file_exists(path.data(), path.size(), current.data(), current.size()) != 0);
|
||||||
|
}
|
||||||
|
int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode, f64 wait_timeout_seconds, String purpose) { (void)file_name; (void)open_flags; (void)lock_type; (void)create_mode; (void)wait_timeout_seconds; (void)purpose; return(-1); }
|
||||||
|
void file_close_locked(int fd) { (void)fd; }
|
||||||
|
void file_release_process_locks(String reason) { (void)reason; }
|
||||||
|
String file_get_contents_locked_fd(int fd) { (void)fd; return(""); }
|
||||||
|
bool file_put_contents_locked_fd(int fd, String content) { (void)fd; (void)content; return(false); }
|
||||||
|
String file_get_contents(String file_name)
|
||||||
|
{
|
||||||
|
String current = wasm_current_unit_file();
|
||||||
|
size_t required = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), 0, 0);
|
||||||
|
if(required == 0)
|
||||||
|
return("");
|
||||||
|
String content(required, 0);
|
||||||
|
size_t got = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), &content[0], required);
|
||||||
|
content.resize(got <= required ? got : 0);
|
||||||
|
return(content);
|
||||||
|
}
|
||||||
|
bool file_put_contents(String file_name, String content)
|
||||||
|
{
|
||||||
|
String current = wasm_current_unit_file();
|
||||||
|
return(uce_host_file_write(file_name.data(), file_name.size(), current.data(), current.size(), content.data(), content.size(), 0) != 0);
|
||||||
|
}
|
||||||
|
bool file_append_contents(String file_name, String content)
|
||||||
|
{
|
||||||
|
String current = wasm_current_unit_file();
|
||||||
|
return(uce_host_file_write(file_name.data(), file_name.size(), current.data(), current.size(), content.data(), content.size(), 1) != 0);
|
||||||
|
}
|
||||||
|
String cwd_get() { return("/"); }
|
||||||
|
void cwd_set(String path) { (void)path; }
|
||||||
|
String process_start_directory() { return("/"); }
|
||||||
|
time_t file_mtime(String file_name) { (void)file_name; return(0); }
|
||||||
|
void file_unlink(String file_name)
|
||||||
|
{
|
||||||
|
String current = wasm_current_unit_file();
|
||||||
|
uce_host_file_unlink(file_name.data(), file_name.size(), current.data(), current.size());
|
||||||
|
}
|
||||||
|
String expand_path(String path, String relative_to_path) { return(path_join(relative_to_path, path)); }
|
||||||
|
StringList ls(String dir) { (void)dir; return(StringList()); }
|
||||||
|
u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); }
|
||||||
|
f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); }
|
||||||
|
bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); }
|
||||||
|
bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); }
|
||||||
|
u64 config_u64(String key, u64 fallback) { return(context ? config_map_u64(context->server->config, key, fallback) : fallback); }
|
||||||
|
f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(context->server->config, key, fallback) : fallback); }
|
||||||
|
bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); }
|
||||||
|
f64 time_precise() { return(uce_host_time_precise()); }
|
||||||
|
u64 time() { return(uce_host_time()); }
|
||||||
|
// The native build shells out to `date`; the wasm core has no shell, so it
|
||||||
|
// formats with wasi-libc strftime (no TZ data → local == UTC, acceptable).
|
||||||
|
static String wasm_time_strftime(String format, u64 timestamp, bool utc)
|
||||||
|
{
|
||||||
|
if(timestamp == 0)
|
||||||
|
timestamp = time();
|
||||||
|
time_t t = (time_t)timestamp;
|
||||||
|
struct tm tmv;
|
||||||
|
if(utc)
|
||||||
|
gmtime_r(&t, &tmv);
|
||||||
|
else
|
||||||
|
localtime_r(&t, &tmv);
|
||||||
|
char buffer[512];
|
||||||
|
size_t n = strftime(buffer, sizeof(buffer), format.c_str(), &tmv);
|
||||||
|
return(String(buffer, n));
|
||||||
|
}
|
||||||
|
String time_format_local(String format, u64 timestamp) { return(wasm_time_strftime(format, timestamp, false)); }
|
||||||
|
String time_format_utc(String format, u64 timestamp)
|
||||||
|
{
|
||||||
|
if(format == "RFC1123")
|
||||||
|
format = "%a, %d %b %Y %T GMT";
|
||||||
|
return(wasm_time_strftime(format, timestamp, true));
|
||||||
|
}
|
||||||
|
static String wasm_time_expand_delta(String format, u64 timestamp, u64 now_timestamp)
|
||||||
|
{
|
||||||
|
u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0;
|
||||||
|
format = replace(format, "%deltaY", std::to_string(delta_seconds / (60 * 60 * 24 * 365)));
|
||||||
|
format = replace(format, "%deltam", std::to_string(delta_seconds / (60 * 60 * 24 * 30)));
|
||||||
|
format = replace(format, "%deltad", std::to_string(delta_seconds / (60 * 60 * 24)));
|
||||||
|
format = replace(format, "%deltaH", std::to_string(delta_seconds / (60 * 60)));
|
||||||
|
format = replace(format, "%deltaM", std::to_string(delta_seconds / 60));
|
||||||
|
format = replace(format, "%deltaS", std::to_string(delta_seconds));
|
||||||
|
return(format);
|
||||||
|
}
|
||||||
|
String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent)
|
||||||
|
{
|
||||||
|
u64 now_timestamp = time();
|
||||||
|
u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0;
|
||||||
|
format_very_recent = first(format_very_recent, "just now");
|
||||||
|
medium_recency_seconds = medium_recency_seconds > 0 ? medium_recency_seconds : 90;
|
||||||
|
format_medium_recent = first(format_medium_recent, "%deltaM minutes ago");
|
||||||
|
not_recent_seconds = not_recent_seconds > 0 ? not_recent_seconds : 90 * 60;
|
||||||
|
format_not_recent = first(format_not_recent, "%deltaH hours ago");
|
||||||
|
if(delta_seconds < medium_recency_seconds)
|
||||||
|
return(wasm_time_expand_delta(format_very_recent, timestamp, now_timestamp));
|
||||||
|
if(delta_seconds < not_recent_seconds)
|
||||||
|
return(wasm_time_expand_delta(format_medium_recent, timestamp, now_timestamp));
|
||||||
|
return(wasm_time_expand_delta(format_not_recent, timestamp, now_timestamp));
|
||||||
|
}
|
||||||
|
u64 time_parse(String time_String) { char* end = 0; unsigned long long v = strtoull(time_String.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : 0); }
|
||||||
|
u64 socket_connect(String host, short port) { (void)host; (void)port; return(0); }
|
||||||
|
void socket_close(u64 sockfd) { (void)sockfd; }
|
||||||
|
bool socket_write(u64 sockfd, String data) { (void)sockfd; (void)data; return(false); }
|
||||||
|
String socket_read(u64 sockfd, u32 max_length, u32 timeout) { (void)sockfd; (void)max_length; (void)timeout; return(""); }
|
||||||
|
String ws_message() { return(context ? context->in : ""); }
|
||||||
|
String ws_connection_id() { return(context ? context->resources.websocket_connection_id : ""); }
|
||||||
|
String ws_scope() { return(context ? context->resources.websocket_scope : ""); }
|
||||||
|
u8 ws_opcode() { return(context ? context->resources.websocket_opcode : 0); }
|
||||||
|
bool ws_is_binary() { return(context && context->resources.websocket_is_binary); }
|
||||||
|
StringList ws_connections(String scope) { (void)scope; return(StringList()); }
|
||||||
|
u64 ws_connection_count(String scope) { (void)scope; return(0); }
|
||||||
|
bool ws_send(String message, bool binary, String scope) { (void)message; (void)binary; (void)scope; return(false); }
|
||||||
|
bool ws_send_to(String connection_id, String message, bool binary) { (void)connection_id; (void)message; (void)binary; return(false); }
|
||||||
|
bool ws_close(String connection_id) { (void)connection_id; return(false); }
|
||||||
|
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames) { (void)frames; (void)size; (void)skip_frames; return(""); }
|
||||||
|
String capture_backtrace_string(u32 max_frames, u32 skip_frames) { (void)max_frames; (void)skip_frames; return(""); }
|
||||||
|
String signal_name(int sig) { (void)sig; return(""); }
|
||||||
|
String memcache_escape_key(String key) { return(key); }
|
||||||
|
StringList memcache_escape_keys(StringList keys) { return(keys); }
|
||||||
|
u64 memcache_connect(String host, short port) { (void)host; (void)port; return(0); }
|
||||||
|
String memcache_command(u64 connection, String command) { (void)connection; (void)command; return(""); }
|
||||||
|
bool memcache_set(u64 connection, String key, String value, u64 expires_in) { (void)connection; (void)key; (void)value; (void)expires_in; return(false); }
|
||||||
|
bool memcache_delete(u64 connection, String key) { (void)connection; (void)key; return(false); }
|
||||||
|
String memcache_get(u64 connection, String key, String default_value) { (void)connection; (void)key; return(default_value); }
|
||||||
|
StringMap memcache_get_multiple(u64 connection, StringList keys) { (void)connection; (void)keys; return(StringMap()); }
|
||||||
|
void on_segfault(int sig) { (void)sig; }
|
||||||
|
|
||||||
|
static u64 wasm_next_task_callback_id = 1;
|
||||||
|
static std::map<u64, std::function<void()>> wasm_task_callbacks;
|
||||||
|
|
||||||
|
extern "C" int uce_wasm_task_run(uint64_t callback_id)
|
||||||
|
{
|
||||||
|
auto it = wasm_task_callbacks.find(callback_id);
|
||||||
|
if(it == wasm_task_callbacks.end())
|
||||||
|
return(1);
|
||||||
|
it->second();
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int task_kill(pid_t pid, int sig) { return(uce_host_task_kill(pid, sig)); }
|
||||||
|
String runtime_safe_key(String key, String label) { (void)label; return(key); }
|
||||||
|
pid_t task(String key, std::function<void()> exec_after_spawn, u64 timeout)
|
||||||
|
{
|
||||||
|
u64 id = wasm_next_task_callback_id++;
|
||||||
|
wasm_task_callbacks[id] = exec_after_spawn;
|
||||||
|
return((pid_t)uce_host_task_spawn(key.data(), key.size(), id, 0.0, timeout, 0));
|
||||||
|
}
|
||||||
|
pid_t task_repeat(String key, f64 interval, std::function<void()> exec_after_spawn, u64 timeout)
|
||||||
|
{
|
||||||
|
if(!(interval > 0) || !std::isfinite(interval))
|
||||||
|
return(0);
|
||||||
|
u64 id = wasm_next_task_callback_id++;
|
||||||
|
wasm_task_callbacks[id] = exec_after_spawn;
|
||||||
|
return((pid_t)uce_host_task_spawn(key.data(), key.size(), id, interval, timeout, 1));
|
||||||
|
}
|
||||||
|
pid_t task_pid(String key) { return((pid_t)uce_host_task_pid(key.data(), key.size())); }
|
||||||
|
extern "C" unsigned int sleep(unsigned int seconds) { return(uce_host_sleep_us((uint64_t)seconds * 1000000ull)); }
|
||||||
|
extern "C" int usleep(unsigned int usec) { uce_host_sleep_us(usec); return(0); }
|
||||||
|
pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_filename, String call_function) { (void)key; (void)socket_fn_or_port; (void)call_uce_filename; (void)call_function; return(0); }
|
||||||
|
bool server_stop(String key) { (void)key; return(false); }
|
||||||
|
StringMap default_config()
|
||||||
|
{
|
||||||
|
StringMap cfg;
|
||||||
|
cfg["SESSION_TIME"] = std::to_string(60*60*24*30);
|
||||||
|
cfg["MAX_MEMORY"] = std::to_string(1024*1024*16);
|
||||||
|
return(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
@@ -942,6 +1150,14 @@ StringMap make_server_settings()
|
|||||||
|
|
||||||
cfg["BIN_DIRECTORY"] = "/tmp/uce/work";
|
cfg["BIN_DIRECTORY"] = "/tmp/uce/work";
|
||||||
cfg["COMPILE_SCRIPT"] = "scripts/compile";
|
cfg["COMPILE_SCRIPT"] = "scripts/compile";
|
||||||
|
cfg["WASM_COMPILE_SCRIPT"] = "scripts/compile_wasm_unit";
|
||||||
|
cfg["COMPILE_WASM_UNITS"] = "0";
|
||||||
|
cfg["WASM_BACKEND_ENABLED"] = "1";
|
||||||
|
cfg["WASM_BACKEND_VERBOSE"] = "0";
|
||||||
|
cfg["WASM_CORE_PATH"] = "";
|
||||||
|
cfg["WASM_MEMORY_LIMIT_BYTES"] = std::to_string(512ull * 1024 * 1024);
|
||||||
|
cfg["WASM_EPOCH_DEADLINE_TICKS"] = "200";
|
||||||
|
cfg["WASM_EPOCH_PERIOD_MS"] = "50";
|
||||||
cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template";
|
cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template";
|
||||||
cfg["LIT_ESC"] = "3d5b5_1";
|
cfg["LIT_ESC"] = "3d5b5_1";
|
||||||
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
|
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
|
||||||
@@ -999,3 +1215,4 @@ StringMap make_server_settings()
|
|||||||
|
|
||||||
return(cfg);
|
return(cfg);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
+22
-1
@@ -1,7 +1,28 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(__UCE_WASM_CORE__) || defined(__UCE_WASM_UNIT__)
|
||||||
|
#ifndef LOCK_SH
|
||||||
|
#define LOCK_SH 1
|
||||||
|
#define LOCK_EX 2
|
||||||
|
#define LOCK_UN 8
|
||||||
|
#endif
|
||||||
|
#ifndef SIGABRT
|
||||||
|
#define SIGABRT 6
|
||||||
|
#endif
|
||||||
|
#ifndef SIGSEGV
|
||||||
|
#define SIGSEGV 11
|
||||||
|
#endif
|
||||||
|
typedef int pid_t;
|
||||||
|
extern "C" {
|
||||||
|
int raise(int);
|
||||||
|
unsigned int sleep(unsigned int seconds);
|
||||||
|
int usleep(unsigned int usec);
|
||||||
|
}
|
||||||
|
#else
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
#endif
|
||||||
|
#include <ctime>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
String shell_exec(String cmd);
|
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_put_contents(String file_name, String content);
|
||||||
bool file_append_contents(String file_name, String content);
|
bool file_append_contents(String file_name, String content);
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
bool file_append(String file_name, Ts... args)
|
inline bool file_append(String file_name, Ts... args)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
((out << args), ...);
|
((out << args), ...);
|
||||||
|
|||||||
+9
-4
@@ -1,16 +1,17 @@
|
|||||||
|
#ifndef __UCE_WASM_CORE__
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
|
#include <stdlib.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
@@ -52,10 +53,12 @@ String http_status_reason(s32 code)
|
|||||||
|
|
||||||
SharedUnit::~SharedUnit()
|
SharedUnit::~SharedUnit()
|
||||||
{
|
{
|
||||||
|
#ifndef __UCE_WASM_CORE__
|
||||||
if(so_handle)
|
if(so_handle)
|
||||||
{
|
{
|
||||||
dlclose(so_handle);
|
dlclose(so_handle);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
String nibble(String div, String& haystack)
|
String nibble(String div, String& haystack)
|
||||||
@@ -98,6 +101,8 @@ Request::~Request()
|
|||||||
delete stream;
|
delete stream;
|
||||||
ob_stack.clear();
|
ob_stack.clear();
|
||||||
ob = 0;
|
ob = 0;
|
||||||
|
#ifndef __UCE_WASM_CORE__
|
||||||
for(auto& sockfd : resources.sockets)
|
for(auto& sockfd : resources.sockets)
|
||||||
close(sockfd);
|
close(sockfd);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -65,6 +65,8 @@ struct SharedUnit {
|
|||||||
|
|
||||||
String file_name;
|
String file_name;
|
||||||
String so_name;
|
String so_name;
|
||||||
|
String wasm_name;
|
||||||
|
String wasm_check_file_name;
|
||||||
String api_file_name;
|
String api_file_name;
|
||||||
String meta_file_name;
|
String meta_file_name;
|
||||||
String compile_output_file_name;
|
String compile_output_file_name;
|
||||||
@@ -77,6 +79,7 @@ struct SharedUnit {
|
|||||||
String pre_path;
|
String pre_path;
|
||||||
String src_file_name;
|
String src_file_name;
|
||||||
String bin_file_name;
|
String bin_file_name;
|
||||||
|
String wasm_file_name;
|
||||||
String pre_file_name;
|
String pre_file_name;
|
||||||
|
|
||||||
void* so_handle = 0;
|
void* so_handle = 0;
|
||||||
@@ -243,26 +246,29 @@ Request* context;
|
|||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
// NB: header templates must be inline — wasm units rely on
|
||||||
|
// -fvisibility-inlines-hidden binding instantiations locally (§6)
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
void print(Ts... args)
|
inline void print(Ts... args)
|
||||||
{
|
{
|
||||||
((*context->ob << args), ...);
|
((*context->ob << args), ...);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
void out(Ts... args)
|
inline void out(Ts... args)
|
||||||
{
|
{
|
||||||
((*context->ob << args), ...);
|
((*context->ob << args), ...);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
String concat(Ts... args)
|
inline String concat(Ts... args)
|
||||||
{
|
{
|
||||||
ByteStream out;
|
ByteStream out;
|
||||||
((out << args), ...);
|
((out << args), ...);
|
||||||
return(out.str());
|
return(out.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef __UCE_WASM_UNIT__
|
||||||
void * operator new(decltype(sizeof(0)) n) noexcept(false)
|
void * operator new(decltype(sizeof(0)) n) noexcept(false)
|
||||||
{
|
{
|
||||||
void* ptr = malloc(n);
|
void* ptr = malloc(n);
|
||||||
@@ -280,3 +286,4 @@ void operator delete(void * p) throw()
|
|||||||
//TO DO: track deallocations
|
//TO DO: track deallocations
|
||||||
free(p);
|
free(p);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -11,9 +11,22 @@
|
|||||||
#include "sys.cpp"
|
#include "sys.cpp"
|
||||||
#include "uri.cpp"
|
#include "uri.cpp"
|
||||||
#include "cli.cpp"
|
#include "cli.cpp"
|
||||||
|
|
||||||
|
#ifdef __UCE_WASM_CORE__
|
||||||
|
// markdown is pure compute (no PCRE/syscalls/regex) — it belongs in the wasm
|
||||||
|
// core so markdown_to_html/markdown_to_ast render in-workspace. compiler.cpp
|
||||||
|
// (which declares component() ahead of markdown in the native build) is carved
|
||||||
|
// out here, and the wasm core defines component() later in src/wasm/core.cpp,
|
||||||
|
// so markdown just needs the forward declaration.
|
||||||
|
String component(String name, DValue props, Request& context);
|
||||||
|
#include "markdown.cpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __UCE_WASM_CORE__
|
||||||
#include "compiler-parser.cpp"
|
#include "compiler-parser.cpp"
|
||||||
#include "compiler.cpp"
|
#include "compiler.cpp"
|
||||||
#include "markdown.cpp"
|
#include "markdown.cpp"
|
||||||
#include "zip.cpp"
|
#include "zip.cpp"
|
||||||
#include "mysql-connector.cpp"
|
#include "mysql-connector.cpp"
|
||||||
#include "sqlite-connector.cpp"
|
#include "sqlite-connector.cpp"
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __UCE_WASM_CORE__
|
||||||
|
extern "C" size_t uce_host_random(char* buf, size_t len);
|
||||||
|
#endif
|
||||||
|
|
||||||
String base64_encode(String raw)
|
String base64_encode(String raw)
|
||||||
{
|
{
|
||||||
static const char* chars =
|
static const char* chars =
|
||||||
@@ -714,6 +718,19 @@ StringMap parse_cookies(String cookie_String)
|
|||||||
|
|
||||||
String session_id_create()
|
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];
|
unsigned char bytes[32];
|
||||||
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
|
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
|
||||||
if(fd == -1)
|
if(fd == -1)
|
||||||
@@ -738,6 +755,7 @@ String session_id_create()
|
|||||||
result.push_back(hex[b & 0x0f]);
|
result.push_back(hex[b & 0x0f]);
|
||||||
}
|
}
|
||||||
return(result);
|
return(result);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_valid_session_id(String session_id)
|
bool is_valid_session_id(String session_id)
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#if defined(__GNUG__)
|
// The demangler is a host-side nicety for trap traces. wasi-libc++ does not
|
||||||
|
// provide __cxa_demangle, and a wasm unit that merely includes uce_lib.h must
|
||||||
|
// not pull it in as an unresolvable import — so it is host-only.
|
||||||
|
#if defined(__GNUG__) && !defined(__wasm__)
|
||||||
|
#define UCE_WASM_TRACE_HAVE_DEMANGLE 1
|
||||||
#include <cxxabi.h>
|
#include <cxxabi.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@ struct WasmTraceSummary
|
|||||||
|
|
||||||
inline std::string wasm_trace_demangle(const std::string& name)
|
inline std::string wasm_trace_demangle(const std::string& name)
|
||||||
{
|
{
|
||||||
#if defined(__GNUG__)
|
#if defined(UCE_WASM_TRACE_HAVE_DEMANGLE)
|
||||||
// frame names look like "module!symbol"; demangle the symbol part
|
// frame names look like "module!symbol"; demangle the symbol part
|
||||||
auto bang = name.find('!');
|
auto bang = name.find('!');
|
||||||
std::string prefix = bang == std::string::npos ? "" : name.substr(0, bang + 1);
|
std::string prefix = bang == std::string::npos ? "" : name.substr(0, bang + 1);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "lib/uce_lib.cpp"
|
#include "lib/uce_lib.cpp"
|
||||||
|
#include "wasm/backend.cpp"
|
||||||
#include <csetjmp>
|
#include <csetjmp>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -940,6 +941,23 @@ int handle_complete(FastCGIRequest& request) {
|
|||||||
compiler_invoke_cli(&request, request.params["SCRIPT_FILENAME"]);
|
compiler_invoke_cli(&request, request.params["SCRIPT_FILENAME"]);
|
||||||
else if(request.params["UCE_SERVE_HTTP"] == "1")
|
else if(request.params["UCE_SERVE_HTTP"] == "1")
|
||||||
compiler_invoke_serve_http(&request, request.params["SCRIPT_FILENAME"], request.params["UCE_SERVE_HTTP_FUNCTION"]);
|
compiler_invoke_serve_http(&request, request.params["SCRIPT_FILENAME"], request.params["UCE_SERVE_HTTP_FUNCTION"]);
|
||||||
|
else if(wasm_backend_should_handle(request, compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"])))
|
||||||
|
{
|
||||||
|
// W4/W5: Wasmtime uses host signals internally to implement guest
|
||||||
|
// traps. The native SIGSEGV/SIGILL request recovery handler must not
|
||||||
|
// intercept those, or clean guest traps become native fatal signals.
|
||||||
|
request_fault_active = 0;
|
||||||
|
restore_request_fault_handlers();
|
||||||
|
String wasm_error = wasm_backend_serve(request, compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"]));
|
||||||
|
install_request_fault_handlers();
|
||||||
|
request_fault_active = 1;
|
||||||
|
if(wasm_error != "")
|
||||||
|
{
|
||||||
|
failure_title = "wasm runtime error during request";
|
||||||
|
failure_details = "";
|
||||||
|
failure_trace = wasm_error;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
compiler_invoke(&request, request.params["SCRIPT_FILENAME"]);
|
compiler_invoke(&request, request.params["SCRIPT_FILENAME"]);
|
||||||
}
|
}
|
||||||
@@ -1023,6 +1041,7 @@ void on_terminate(int sig)
|
|||||||
if(getpid() != parent_pid)
|
if(getpid() != parent_pid)
|
||||||
exit(1);
|
exit(1);
|
||||||
printf("Terminating... PID %i:%i\n", getpid(), parent_pid);
|
printf("Terminating... PID %i:%i\n", getpid(), parent_pid);
|
||||||
|
wasm_backend_shutdown();
|
||||||
server.shutdown();
|
server.shutdown();
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
// W4 — FastCGI backend glue for the W3 wasm workspace runtime.
|
||||||
|
//
|
||||||
|
// Included into the native server TU (src/linux_fastcgi.cpp) after uce_lib.cpp,
|
||||||
|
// so it shares String/DValue/config and the UCEB1 codec. Provides a
|
||||||
|
// config-selectable page-render backend: when WASM_BACKEND_ENABLED and a wasm
|
||||||
|
// artifact exists for the entry unit, the request is served through a
|
||||||
|
// per-request wasm workspace instead of the native dlopen path.
|
||||||
|
//
|
||||||
|
// The seam is narrow on purpose — only the page render branch in
|
||||||
|
// handle_complete() changes. CLI, serve_http, and websocket stay native.
|
||||||
|
|
||||||
|
#include "../lib/wasm_trace.h"
|
||||||
|
// The native server TU has the real connectors (sqlite/mysql) compiled in, so
|
||||||
|
// the worker's host-side connector hostcalls are available here. The W3 CLI
|
||||||
|
// driver does not define this and gets a named-trap stub for those imports.
|
||||||
|
#define UCE_WASM_HOST_CONNECTORS 1
|
||||||
|
#include "worker.cpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
// per forked worker process: one engine + compiled-core cache, one epoch ticker
|
||||||
|
static WasmWorker* g_wasm_worker = 0;
|
||||||
|
static std::thread g_wasm_epoch_ticker;
|
||||||
|
static std::atomic<bool> g_wasm_epoch_running(false);
|
||||||
|
static String g_wasm_init_error;
|
||||||
|
static bool g_wasm_init_attempted = false;
|
||||||
|
|
||||||
|
bool wasm_backend_configured(Request* context)
|
||||||
|
{
|
||||||
|
if(!context || !context->server)
|
||||||
|
return(false);
|
||||||
|
return(config_bool("WASM_BACKEND_ENABLED", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazily bring up the per-process worker on first use inside a forked child
|
||||||
|
// (the engine must not be inherited across fork). Returns "" on success.
|
||||||
|
static String wasm_backend_ensure_started(Request* context)
|
||||||
|
{
|
||||||
|
if(g_wasm_init_attempted)
|
||||||
|
return(g_wasm_init_error);
|
||||||
|
g_wasm_init_attempted = true;
|
||||||
|
|
||||||
|
StringMap& cfg = context->server->config;
|
||||||
|
WasmWorkerConfig wc;
|
||||||
|
wc.core_wasm_path = first(cfg["WASM_CORE_PATH"],
|
||||||
|
path_join(cfg["COMPILER_SYS_PATH"], "bin/wasm/core.wasm"));
|
||||||
|
wc.site_root = path_join(cfg["COMPILER_SYS_PATH"], cfg["SITE_DIRECTORY"]);
|
||||||
|
wc.cache_root = cfg["BIN_DIRECTORY"];
|
||||||
|
// write membrane allowlist: the site tree plus the runtime scratch dirs
|
||||||
|
// pages legitimately write to (matches native reachable write targets).
|
||||||
|
wc.write_roots = { wc.site_root, "/tmp" };
|
||||||
|
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
|
||||||
|
if(cfg[key] != "")
|
||||||
|
wc.write_roots.push_back(cfg[key]);
|
||||||
|
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
|
||||||
|
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
|
||||||
|
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
|
||||||
|
|
||||||
|
g_wasm_worker = new WasmWorker(wc);
|
||||||
|
g_wasm_init_error = g_wasm_worker->init();
|
||||||
|
if(g_wasm_init_error != "")
|
||||||
|
{
|
||||||
|
delete g_wasm_worker;
|
||||||
|
g_wasm_worker = 0;
|
||||||
|
return(g_wasm_init_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_wasm_epoch_running.store(true);
|
||||||
|
WasmWorker* worker = g_wasm_worker;
|
||||||
|
u64 period_ms = config_u64("WASM_EPOCH_PERIOD_MS", 50);
|
||||||
|
g_wasm_epoch_ticker = std::thread([worker, period_ms] {
|
||||||
|
while(g_wasm_epoch_running.load())
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(period_ms));
|
||||||
|
worker->engine.increment_epoch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return("");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool wasm_artifact_exists(Request* context, const String& entry_unit)
|
||||||
|
{
|
||||||
|
if(entry_unit == "")
|
||||||
|
return(false);
|
||||||
|
String wasm_path = context->server->config["BIN_DIRECTORY"] + entry_unit + ".wasm";
|
||||||
|
struct stat wasm_st;
|
||||||
|
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
|
||||||
|
return(false);
|
||||||
|
// The wasm path bypasses the native JIT recompile, so a source edit would
|
||||||
|
// otherwise be served from a stale .wasm. Require the artifact to be newer
|
||||||
|
// than the source; if it is stale, fall back to native — which JIT-rebuilds
|
||||||
|
// both .so and .wasm — so the next request gets a fresh artifact.
|
||||||
|
struct stat src_st;
|
||||||
|
if(stat(entry_unit.c_str(), &src_st) == 0 && wasm_st.st_mtime < src_st.st_mtime)
|
||||||
|
return(false);
|
||||||
|
return(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide, by source inspection, whether a page must stay on the native
|
||||||
|
// backend for now (W5 surfaces not yet behind the membrane).
|
||||||
|
//
|
||||||
|
// Caveats this deliberately accepts:
|
||||||
|
// - ENTRY-UNIT ONLY: a clean entry page that component()s into a native-only
|
||||||
|
// unit is still served by wasm. With signals_based_traps off (see
|
||||||
|
// make_engine) the worst case is a clean wasm error or a stubbed-empty
|
||||||
|
// result, never a worker crash — so this is a best-effort routing hint,
|
||||||
|
// not a guarantee. Full coverage is a W5 concern (un-stub the APIs).
|
||||||
|
// - SUBSTRING match, intentionally conservative: a token like "xml_" also
|
||||||
|
// matches an identifier or doc string containing it. That only ever
|
||||||
|
// over-routes to native (always correct), never the reverse.
|
||||||
|
static bool wasm_backend_native_fallback_uncached(Request* context, const String& entry_unit)
|
||||||
|
{
|
||||||
|
String source = file_get_contents(entry_unit);
|
||||||
|
// Supported by the wasm core, intentionally NOT in this list:
|
||||||
|
// - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in),
|
||||||
|
// - unit_render()/component() (host resolver).
|
||||||
|
// What remains is genuinely host-owned / native-only for now:
|
||||||
|
// - zip, sockets/custom servers, memcache, mysql;
|
||||||
|
// - unit_call + compiler/unit introspection (need the native toolchain).
|
||||||
|
// Background tasks and sleep/usleep are host-owned but now have membrane
|
||||||
|
// hostcalls, so they intentionally do not fallback here.
|
||||||
|
StringList native_only_tokens = {
|
||||||
|
"zip_", "socket_", "server_start_http(", "server_stop(",
|
||||||
|
"memcache_", "mysql_", "unit_call(",
|
||||||
|
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit("
|
||||||
|
};
|
||||||
|
for(auto& token : native_only_tokens)
|
||||||
|
if(source.find(token) != String::npos)
|
||||||
|
return(true);
|
||||||
|
return(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool wasm_backend_native_fallback_needed(Request* context, const String& entry_unit)
|
||||||
|
{
|
||||||
|
if(!context || !context->server || entry_unit == "")
|
||||||
|
return(true);
|
||||||
|
// Cache the verdict per process, keyed on path + mtime: the source scan is
|
||||||
|
// otherwise a file read + 20 substring searches on every page request.
|
||||||
|
struct CachedVerdict { time_t mtime; bool fallback; };
|
||||||
|
static std::map<String, CachedVerdict> cache;
|
||||||
|
struct stat st;
|
||||||
|
time_t mtime = (stat(entry_unit.c_str(), &st) == 0) ? st.st_mtime : 0;
|
||||||
|
auto it = cache.find(entry_unit);
|
||||||
|
if(it != cache.end() && it->second.mtime == mtime)
|
||||||
|
return(it->second.fallback);
|
||||||
|
bool fallback = wasm_backend_native_fallback_uncached(context, entry_unit);
|
||||||
|
cache[entry_unit] = { mtime, fallback };
|
||||||
|
return(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if this request should be served by the wasm backend. Falls through to
|
||||||
|
// native when disabled, when init failed, or when the unit has no wasm artifact
|
||||||
|
// (e.g. units skip-listed for try/catch — automatic, graceful fallback).
|
||||||
|
bool wasm_backend_should_handle(Request& request, const String& entry_unit)
|
||||||
|
{
|
||||||
|
if(!wasm_backend_configured(&request))
|
||||||
|
return(false);
|
||||||
|
if(request.resources.is_cli)
|
||||||
|
return(false);
|
||||||
|
if(wasm_backend_native_fallback_needed(&request, entry_unit))
|
||||||
|
return(false);
|
||||||
|
if(!wasm_artifact_exists(&request, entry_unit))
|
||||||
|
return(false);
|
||||||
|
if(wasm_backend_ensure_started(&request) != "")
|
||||||
|
return(false);
|
||||||
|
return(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the page render through a wasm workspace. Populates the native Request
|
||||||
|
// (status/headers/cookies/session/body) so the existing transport writes the
|
||||||
|
// response unchanged. Returns "" on success, or a collapsed error/trace string
|
||||||
|
// for the caller to route into the configured error page.
|
||||||
|
String wasm_backend_serve(Request& request, const String& entry_unit)
|
||||||
|
{
|
||||||
|
DValue ctx;
|
||||||
|
auto copy_map = [&](const StringMap& source, const char* key) {
|
||||||
|
for(auto& entry : source)
|
||||||
|
ctx[key][entry.first] = entry.second;
|
||||||
|
};
|
||||||
|
copy_map(request.params, "params");
|
||||||
|
copy_map(request.get, "get");
|
||||||
|
copy_map(request.post, "post");
|
||||||
|
copy_map(request.cookies, "cookies");
|
||||||
|
copy_map(request.session, "session");
|
||||||
|
ctx["entry_unit"] = entry_unit;
|
||||||
|
|
||||||
|
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit);
|
||||||
|
if(!response.ok)
|
||||||
|
return(response.error == "" ? String("wasm workspace failed") : response.error);
|
||||||
|
|
||||||
|
// Diagnostic timing headers are opt-in: they leak workspace internals and
|
||||||
|
// belong to the W5 benchmark harness, not public responses.
|
||||||
|
if(config_bool("WASM_BACKEND_VERBOSE", false))
|
||||||
|
{
|
||||||
|
request.header["X-UCE-Backend"] = "wasm";
|
||||||
|
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
|
||||||
|
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
|
||||||
|
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
|
||||||
|
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
|
||||||
|
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// status line: keep the native default unless the unit set one
|
||||||
|
String status = response.meta["status"].to_string();
|
||||||
|
if(status != "")
|
||||||
|
request.response_code = status;
|
||||||
|
// merge headers over the native defaults (so Content-Type survives unless
|
||||||
|
// the unit overrode it); replace cookies/session with the unit's view
|
||||||
|
if(response.meta.key("headers"))
|
||||||
|
response.meta["headers"].each([&](const DValue& value, String name) {
|
||||||
|
request.header[name] = value.to_string();
|
||||||
|
});
|
||||||
|
if(response.meta.key("cookies"))
|
||||||
|
response.meta["cookies"].each([&](const DValue& value, String) {
|
||||||
|
request.set_cookies.push_back(value.to_string());
|
||||||
|
});
|
||||||
|
if(response.meta.key("session"))
|
||||||
|
{
|
||||||
|
request.session.clear();
|
||||||
|
response.meta["session"].each([&](const DValue& value, String name) {
|
||||||
|
request.session[name] = value.to_string();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// body into the request's primary output stream (ob_stack[0]); the
|
||||||
|
// transport's assemble_output_buffer concatenates the stack
|
||||||
|
if(request.ob)
|
||||||
|
request.ob->write(response.body.data(), response.body.size());
|
||||||
|
return("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the ticker before the worker process exits (best-effort; forked workers
|
||||||
|
// are usually killed, but a clean ager-out path should join the thread).
|
||||||
|
void wasm_backend_shutdown()
|
||||||
|
{
|
||||||
|
if(g_wasm_epoch_running.exchange(false) && g_wasm_epoch_ticker.joinable())
|
||||||
|
g_wasm_epoch_ticker.join();
|
||||||
|
}
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
// Production WASM W1 core entrypoint.
|
||||||
|
//
|
||||||
|
// This file deliberately includes the real UCE runtime amalgamation with
|
||||||
|
// __UCE_WASM_CORE__ enabled. Native-only pieces are carved out in the runtime
|
||||||
|
// sources, while the workspace-owned DValue ABI and output plumbing are built
|
||||||
|
// into core.wasm.
|
||||||
|
|
||||||
|
#define __UCE_WASM_CORE__ 1
|
||||||
|
#include "../lib/uce_lib.cpp"
|
||||||
|
#include "../lib/mysql-connector.h"
|
||||||
|
#include "../lib/sqlite-connector.h"
|
||||||
|
|
||||||
|
// ---- W3 connector membrane stubs -------------------------------------------
|
||||||
|
// Generated units reference the connector class surface through uce_lib.h, so
|
||||||
|
// the core must define it. Until the real hostcall connectors land (W5 parity
|
||||||
|
// scope: the starter uses no database), every operation fails cleanly with an
|
||||||
|
// explanatory error instead of trapping.
|
||||||
|
|
||||||
|
static const char* WASM_DB_UNAVAILABLE =
|
||||||
|
"database connectors are not yet available in the wasm workspace";
|
||||||
|
|
||||||
|
bool MySQL::connect(String host, String username, String password)
|
||||||
|
{
|
||||||
|
(void)host; (void)username; (void)password;
|
||||||
|
connection = 0;
|
||||||
|
statement_info = WASM_DB_UNAVAILABLE;
|
||||||
|
return(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MySQL::disconnect() { connection = 0; }
|
||||||
|
String MySQL::error() { return(WASM_DB_UNAVAILABLE); }
|
||||||
|
String MySQL::escape(String raw, char quote_char) { (void)quote_char; return(raw); }
|
||||||
|
String MySQL::parse_query_parameters(String query, StringMap m) { (void)m; return(query); }
|
||||||
|
DValue MySQL::query(String q) { (void)q; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
|
||||||
|
DValue MySQL::query(String q, StringMap params) { (void)q; (void)params; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
|
||||||
|
DValue MySQL::get_pending_result() { return(DValue()); }
|
||||||
|
|
||||||
|
String mysql_escape(String raw, char quote_char) { (void)quote_char; return(raw); }
|
||||||
|
|
||||||
|
// sqlite runs host-side (the host links libsqlite and owns the connections in
|
||||||
|
// a per-workspace handle table). One UCEB1-marshalled hostcall carries
|
||||||
|
// {op,handle,path,query,params} in and {handle,result,insert_id,affected,
|
||||||
|
// error_code,statement_info} out. `connection` holds the host handle (>0).
|
||||||
|
extern "C" size_t uce_host_sqlite(const char* in, size_t in_len, char* out, size_t cap);
|
||||||
|
|
||||||
|
static DValue wasm_sqlite_call(DValue request)
|
||||||
|
{
|
||||||
|
String encoded = ucb_encode(request);
|
||||||
|
size_t need = uce_host_sqlite(encoded.data(), encoded.size(), 0, 0);
|
||||||
|
if(need == 0)
|
||||||
|
return(DValue());
|
||||||
|
String buffer(need, 0);
|
||||||
|
size_t got = uce_host_sqlite(encoded.data(), encoded.size(), &buffer[0], need);
|
||||||
|
if(got == 0 || got > need)
|
||||||
|
return(DValue());
|
||||||
|
DValue response;
|
||||||
|
String error;
|
||||||
|
ucb_decode(String(buffer.data(), got), response, &error);
|
||||||
|
return(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SQLite::set_error(s32 code, String info) { error_code = code; statement_info = info; }
|
||||||
|
|
||||||
|
bool SQLite::connect(String path)
|
||||||
|
{
|
||||||
|
this->path = path;
|
||||||
|
DValue request;
|
||||||
|
request["op"] = "connect";
|
||||||
|
request["path"] = path;
|
||||||
|
DValue response = wasm_sqlite_call(request);
|
||||||
|
u64 handle = response["handle"].to_u64();
|
||||||
|
connection = (void*)(uintptr_t)handle;
|
||||||
|
error_code = (s32)response["error_code"].to_s64();
|
||||||
|
statement_info = response["statement_info"].to_string();
|
||||||
|
return(handle != 0 && error_code == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SQLite::disconnect()
|
||||||
|
{
|
||||||
|
if(connection)
|
||||||
|
{
|
||||||
|
DValue request;
|
||||||
|
request["op"] = "disconnect";
|
||||||
|
request["handle"] = (f64)(uintptr_t)connection;
|
||||||
|
wasm_sqlite_call(request);
|
||||||
|
connection = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String SQLite::error()
|
||||||
|
{
|
||||||
|
return(statement_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
DValue SQLite::query(String q, const StringMap& params)
|
||||||
|
{
|
||||||
|
DValue request;
|
||||||
|
request["op"] = "query";
|
||||||
|
request["handle"] = (f64)(uintptr_t)connection;
|
||||||
|
request["query"] = q;
|
||||||
|
for(auto& entry : params)
|
||||||
|
request["params"][entry.first] = entry.second;
|
||||||
|
DValue response = wasm_sqlite_call(request);
|
||||||
|
insert_id = response["insert_id"].to_u64();
|
||||||
|
affected_rows = (u32)response["affected"].to_u64();
|
||||||
|
error_code = (s32)response["error_code"].to_s64();
|
||||||
|
statement_info = response["statement_info"].to_string();
|
||||||
|
DValue* result = response.key("result");
|
||||||
|
return(result ? *result : DValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
DValue SQLite::query(String q) { return(query(q, StringMap())); }
|
||||||
|
bool SQLite::apply_default_pragmas() { return(true); }
|
||||||
|
bool SQLite::bind_params(void* statement, const StringMap& params) { (void)statement; (void)params; return(true); }
|
||||||
|
DValue SQLite::collect_rows(void* statement) { (void)statement; return(DValue()); }
|
||||||
|
|
||||||
|
SQLite* sqlite_connect(String path)
|
||||||
|
{
|
||||||
|
SQLite* db = new SQLite();
|
||||||
|
db->request_cleanup_delete = true;
|
||||||
|
db->connect(path);
|
||||||
|
return(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sqlite_disconnect(SQLite* db) { if(db) { db->disconnect(); if(db->request_cleanup_delete) delete db; } }
|
||||||
|
String sqlite_error(SQLite* db) { return(db ? db->error() : String(WASM_DB_UNAVAILABLE)); }
|
||||||
|
DValue sqlite_query(SQLite* db, String q) { return(db ? db->query(q) : DValue()); }
|
||||||
|
DValue sqlite_query(SQLite* db, String q, const StringMap& params) { return(db ? db->query(q, params) : DValue()); }
|
||||||
|
u64 sqlite_insert_id(SQLite* db) { return(db ? db->insert_id : 0); }
|
||||||
|
u32 sqlite_affected_rows(SQLite* db) { return(db ? db->affected_rows : 0); }
|
||||||
|
void cleanup_sqlite_connections() { }
|
||||||
|
|
||||||
|
static ServerState wasm_server;
|
||||||
|
static Request wasm_request;
|
||||||
|
static String wasm_output;
|
||||||
|
static String wasm_response_meta;
|
||||||
|
|
||||||
|
// ---- vague-linkage link anchors --------------------------------------------
|
||||||
|
// Units import libc++ template instantiations they use; --export-all only
|
||||||
|
// exports what the core itself instantiated. Some libc++ internals lack the
|
||||||
|
// hide-from-ABI attribute (the Phase 0 lambda finding), so units emit them as
|
||||||
|
// imports rather than binding locally. This function exists purely to make
|
||||||
|
// the core instantiate — and therefore export — the ones the site tree needs.
|
||||||
|
// Extend it when the loader reports "unresolved import env.<libc++ symbol>".
|
||||||
|
extern "C" void uce_wasm_link_anchors()
|
||||||
|
{
|
||||||
|
StringMap string_map;
|
||||||
|
string_map["k"] = "v";
|
||||||
|
string_map.erase(String("k")); // __tree::__erase_unique<String>
|
||||||
|
std::map<String, DValue> dvalue_map;
|
||||||
|
dvalue_map["k"] = DValue();
|
||||||
|
dvalue_map.erase(String("k"));
|
||||||
|
std::vector<String> string_list = { "a", "b" };
|
||||||
|
string_list.erase(string_list.begin());
|
||||||
|
std::set<String> string_set;
|
||||||
|
string_set.insert("k");
|
||||||
|
string_set.erase(String("k"));
|
||||||
|
|
||||||
|
// libc functions units may call that the core itself never references —
|
||||||
|
// taking their address forces them into the link (and --export-all)
|
||||||
|
static void* volatile libc_anchors[] = {
|
||||||
|
(void*)&atof, (void*)&atoi, (void*)&atol, (void*)&atoll,
|
||||||
|
(void*)&strtol, (void*)&strtoul, (void*)&strtoll, (void*)&strtoull,
|
||||||
|
(void*)&strtod, (void*)&strtof,
|
||||||
|
(void*)&qsort, (void*)&bsearch,
|
||||||
|
(void*)&snprintf, (void*)&sscanf,
|
||||||
|
(void*)&memmove, (void*)&strncmp, (void*)&strncpy,
|
||||||
|
// memchr/strchr/strrchr/strstr are C++-overloaded; cast to the C shape
|
||||||
|
(void*)(const void* (*)(const void*, int, size_t))&memchr,
|
||||||
|
(void*)(const char* (*)(const char*, int))&strchr,
|
||||||
|
(void*)(const char* (*)(const char*, int))&strrchr,
|
||||||
|
(void*)(const char* (*)(const char*, const char*))&strstr,
|
||||||
|
// ctype family (int(int)); units use these directly
|
||||||
|
(void*)&isalnum, (void*)&isalpha, (void*)&isblank, (void*)&iscntrl,
|
||||||
|
(void*)&isdigit, (void*)&isgraph, (void*)&islower, (void*)&isprint,
|
||||||
|
(void*)&ispunct, (void*)&isspace, (void*)&isupper, (void*)&isxdigit,
|
||||||
|
(void*)&tolower, (void*)&toupper,
|
||||||
|
};
|
||||||
|
(void)libc_anchors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// W3 membrane: the host resolves component/render targets to funcref-table
|
||||||
|
// slots (loading units lazily) and writes the resolved unit path back so
|
||||||
|
// nested relative component resolution keeps working.
|
||||||
|
extern "C" int32_t uce_host_component_resolve(
|
||||||
|
const char* target, size_t target_len, int32_t kind,
|
||||||
|
const char* current_unit, size_t current_unit_len,
|
||||||
|
char* resolved_buf, size_t resolved_cap);
|
||||||
|
|
||||||
|
// target → table slot, reset per request (workspaces die with the request,
|
||||||
|
// but a single workspace can render the same component many times)
|
||||||
|
static std::map<String, s32> wasm_component_slots;
|
||||||
|
|
||||||
|
// These mirror small page-runtime pieces of compiler.cpp, which is carved
|
||||||
|
// out of the wasm core wholesale (it is the native toolchain: parser, clang
|
||||||
|
// driver, dlopen). Kept byte-identical where possible.
|
||||||
|
String component_normalize_path(String name)
|
||||||
|
{
|
||||||
|
name = trim(name);
|
||||||
|
if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce")
|
||||||
|
return(name);
|
||||||
|
return(name + ".uce");
|
||||||
|
}
|
||||||
|
|
||||||
|
void component_parse_target(String target, String& file_name, String& render_name)
|
||||||
|
{
|
||||||
|
target = trim(target);
|
||||||
|
render_name = "";
|
||||||
|
auto render_split_pos = target.find(":");
|
||||||
|
if(render_split_pos != String::npos)
|
||||||
|
{
|
||||||
|
render_name = trim(target.substr(render_split_pos + 1));
|
||||||
|
target = trim(target.substr(0, render_split_pos));
|
||||||
|
}
|
||||||
|
file_name = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
String component_error_banner(String message)
|
||||||
|
{
|
||||||
|
return("<div class=\"banner\">" + html_escape(message) + "</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RequestPropsScope
|
||||||
|
{
|
||||||
|
Request* context = 0;
|
||||||
|
DValue previous_props;
|
||||||
|
|
||||||
|
RequestPropsScope(Request* context, const DValue& props)
|
||||||
|
{
|
||||||
|
this->context = context;
|
||||||
|
if(this->context)
|
||||||
|
{
|
||||||
|
previous_props = this->context->props;
|
||||||
|
this->context->props = props;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~RequestPropsScope()
|
||||||
|
{
|
||||||
|
if(context)
|
||||||
|
context->props = previous_props;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// kind values shared with the host loader (src/wasm/worker.cpp)
|
||||||
|
enum WasmResolveKind {
|
||||||
|
WASM_RESOLVE_COMPONENT = 0,
|
||||||
|
WASM_RESOLVE_RENDER = 1,
|
||||||
|
WASM_RESOLVE_EXISTS = 2,
|
||||||
|
WASM_RESOLVE_ONCE = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
static s32 wasm_resolve_target(String target, s32 kind, String* resolved_out = 0)
|
||||||
|
{
|
||||||
|
String cache_key = std::to_string(kind) + ":" + target;
|
||||||
|
auto cached = wasm_component_slots.find(cache_key);
|
||||||
|
if(cached != wasm_component_slots.end() && kind != WASM_RESOLVE_EXISTS)
|
||||||
|
return(cached->second);
|
||||||
|
char resolved[512];
|
||||||
|
String current = context ? context->resources.current_unit_file : "";
|
||||||
|
s32 slot = uce_host_component_resolve(
|
||||||
|
target.data(), target.size(), kind,
|
||||||
|
current.data(), current.size(),
|
||||||
|
resolved, sizeof(resolved));
|
||||||
|
if(resolved_out && slot)
|
||||||
|
*resolved_out = String(resolved, strnlen(resolved, sizeof(resolved)));
|
||||||
|
if(kind != WASM_RESOLVE_EXISTS)
|
||||||
|
wasm_component_slots[cache_key] = slot;
|
||||||
|
return(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
String component_resolve(String name)
|
||||||
|
{
|
||||||
|
String resolved;
|
||||||
|
if(wasm_resolve_target(trim(name), WASM_RESOLVE_EXISTS, &resolved))
|
||||||
|
return(resolved);
|
||||||
|
return("");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool component_exists(String name)
|
||||||
|
{
|
||||||
|
return(component_resolve(name) != "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a unit's ONCE() handler at most once per request (native
|
||||||
|
// compiler_run_unit_once_if_needed semantics): dedup on the resolved unit
|
||||||
|
// path via request.once_units. The handler emits head assets, etc.
|
||||||
|
static void wasm_run_once(const String& resolved, Request& request)
|
||||||
|
{
|
||||||
|
if(resolved == "")
|
||||||
|
return;
|
||||||
|
if(request.once_units.find(resolved) != request.once_units.end())
|
||||||
|
return;
|
||||||
|
request.once_units.insert(resolved);
|
||||||
|
s32 once_slot = wasm_resolve_target(resolved, WASM_RESOLVE_ONCE);
|
||||||
|
if(once_slot == 0)
|
||||||
|
return;
|
||||||
|
String previous_unit = request.resources.current_unit_file;
|
||||||
|
request.resources.current_unit_file = resolved;
|
||||||
|
request_ref_handler once_handler = (request_ref_handler)(uintptr_t)once_slot;
|
||||||
|
once_handler(request);
|
||||||
|
request.resources.current_unit_file = previous_unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void component_render(String name, DValue props, Request& request)
|
||||||
|
{
|
||||||
|
String resolved;
|
||||||
|
s32 slot = wasm_resolve_target(trim(name), WASM_RESOLVE_COMPONENT, &resolved);
|
||||||
|
if(!slot)
|
||||||
|
{
|
||||||
|
print(component_error_banner("component not found: " + trim(name)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wasm_run_once(resolved, request);
|
||||||
|
RequestPropsScope props_scope(&request, props);
|
||||||
|
String previous_unit = request.resources.current_unit_file;
|
||||||
|
if(resolved != "")
|
||||||
|
request.resources.current_unit_file = resolved;
|
||||||
|
// a wasm function pointer is its index in the shared funcref table; the
|
||||||
|
// host returned the handler's slot, so this is a plain call_indirect
|
||||||
|
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
|
||||||
|
handler(request);
|
||||||
|
request.resources.current_unit_file = previous_unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void component_render(String name) { DValue props; component_render(name, props, *context); }
|
||||||
|
void component_render(String name, Request& request) { DValue props; component_render(name, props, request); }
|
||||||
|
void component_render(String name, DValue props) { component_render(name, props, *context); }
|
||||||
|
|
||||||
|
String component(String name, DValue props, Request& request)
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
component_render(name, props, request);
|
||||||
|
return(ob_get_close());
|
||||||
|
}
|
||||||
|
|
||||||
|
String component(String name) { DValue props; return(component(name, props, *context)); }
|
||||||
|
String component(String name, Request& request) { DValue props; return(component(name, props, request)); }
|
||||||
|
String component(String name, DValue props) { return(component(name, props, *context)); }
|
||||||
|
|
||||||
|
void unit_render(String file_name, Request& request)
|
||||||
|
{
|
||||||
|
String resolved;
|
||||||
|
s32 slot = wasm_resolve_target(trim(file_name), WASM_RESOLVE_RENDER, &resolved);
|
||||||
|
if(!slot)
|
||||||
|
{
|
||||||
|
print(component_error_banner("unit not found: " + trim(file_name)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wasm_run_once(resolved, request);
|
||||||
|
String previous_unit = request.resources.current_unit_file;
|
||||||
|
if(resolved != "")
|
||||||
|
request.resources.current_unit_file = resolved;
|
||||||
|
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
|
||||||
|
handler(request);
|
||||||
|
request.resources.current_unit_file = previous_unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unit_render(String file_name) { unit_render(file_name, *context); }
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
// Host calls this to render the request's entry unit. Routing through
|
||||||
|
// unit_render gives the entry the same ONCE() + dispatch semantics as
|
||||||
|
// components (the host pre-loaded it, so resolution is a cache hit).
|
||||||
|
void uce_wasm_render_entry(const char* path, size_t len)
|
||||||
|
{
|
||||||
|
// the host always runs uce_wasm_core_init + apply_context before this
|
||||||
|
unit_render(String(path, len), *context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void* uce_alloc(size_t len)
|
||||||
|
{
|
||||||
|
return(malloc(len));
|
||||||
|
}
|
||||||
|
|
||||||
|
void uce_free(void* ptr)
|
||||||
|
{
|
||||||
|
free(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 uce_wasm_core_abi_version()
|
||||||
|
{
|
||||||
|
return(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
int uce_wasm_core_init()
|
||||||
|
{
|
||||||
|
wasm_server.config = default_config();
|
||||||
|
wasm_request.server = &wasm_server;
|
||||||
|
// the primary output stream must live ON ob_stack (native semantics):
|
||||||
|
// ob_get_close()/ob_close() pop and rebalance against the stack, so a
|
||||||
|
// stream outside it would be orphaned by the first component() capture
|
||||||
|
if(wasm_request.ob_stack.empty())
|
||||||
|
wasm_request.ob_start();
|
||||||
|
context = &wasm_request;
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uce_wasm_core_reset_request()
|
||||||
|
{
|
||||||
|
if(context == 0)
|
||||||
|
uce_wasm_core_init();
|
||||||
|
wasm_request.call = DValue();
|
||||||
|
wasm_request.props = DValue();
|
||||||
|
wasm_request.params.clear();
|
||||||
|
wasm_request.get.clear();
|
||||||
|
wasm_request.post.clear();
|
||||||
|
wasm_request.header.clear();
|
||||||
|
wasm_request.set_cookies.clear();
|
||||||
|
wasm_request.response_code = "HTTP/1.1 200 OK";
|
||||||
|
wasm_request.flags = Request::Flags();
|
||||||
|
wasm_request.stats = Request::Stats();
|
||||||
|
for(auto* stream : wasm_request.ob_stack)
|
||||||
|
delete stream;
|
||||||
|
wasm_request.ob_stack.clear();
|
||||||
|
wasm_request.ob_start();
|
||||||
|
wasm_output = "";
|
||||||
|
wasm_response_meta = "";
|
||||||
|
wasm_request.cookies.clear();
|
||||||
|
wasm_request.session.clear();
|
||||||
|
wasm_request.out = "";
|
||||||
|
wasm_request.resources.current_unit_file = "";
|
||||||
|
wasm_component_slots.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host pushes the UCEB1-encoded request context into a guest buffer
|
||||||
|
// (uce_alloc) and applies it here; mirrors the native param population.
|
||||||
|
int uce_wasm_apply_context(const char* buf, size_t len)
|
||||||
|
{
|
||||||
|
if(context == 0)
|
||||||
|
uce_wasm_core_init();
|
||||||
|
DValue decoded;
|
||||||
|
String error;
|
||||||
|
if(!ucb_decode(String(buf, len), decoded, &error))
|
||||||
|
{
|
||||||
|
uce_host_log(3, error.data(), error.size());
|
||||||
|
return(1);
|
||||||
|
}
|
||||||
|
wasm_request.call = decoded;
|
||||||
|
auto apply_map = [](DValue* source, StringMap& dest) {
|
||||||
|
dest.clear();
|
||||||
|
if(source)
|
||||||
|
source->each([&](const DValue& item, String key) {
|
||||||
|
dest[key] = item.to_string();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
apply_map(decoded.key("params"), wasm_request.params);
|
||||||
|
apply_map(decoded.key("get"), wasm_request.get);
|
||||||
|
apply_map(decoded.key("post"), wasm_request.post);
|
||||||
|
apply_map(decoded.key("cookies"), wasm_request.cookies);
|
||||||
|
apply_map(decoded.key("session"), wasm_request.session);
|
||||||
|
DValue* entry = decoded.key("entry_unit");
|
||||||
|
if(entry)
|
||||||
|
wasm_request.resources.current_unit_file = entry->to_string();
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Request* uce_wasm_request()
|
||||||
|
{
|
||||||
|
if(context == 0)
|
||||||
|
uce_wasm_core_init();
|
||||||
|
return(&wasm_request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After render: response metadata (status line, headers, cookies, session)
|
||||||
|
// goes back to the host as UCEB1.
|
||||||
|
void uce_wasm_finish_response_meta()
|
||||||
|
{
|
||||||
|
DValue meta;
|
||||||
|
meta["status"] = wasm_request.response_code;
|
||||||
|
for(auto& header : wasm_request.header)
|
||||||
|
meta["headers"][header.first] = header.second;
|
||||||
|
for(auto& cookie : wasm_request.set_cookies)
|
||||||
|
{
|
||||||
|
DValue cookie_value;
|
||||||
|
cookie_value = cookie;
|
||||||
|
meta["cookies"].push(cookie_value);
|
||||||
|
}
|
||||||
|
for(auto& entry : wasm_request.session)
|
||||||
|
meta["session"][entry.first] = entry.second;
|
||||||
|
wasm_response_meta = ucb_encode(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* uce_wasm_response_meta_data()
|
||||||
|
{
|
||||||
|
return(wasm_response_meta.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t uce_wasm_response_meta_size()
|
||||||
|
{
|
||||||
|
return(wasm_response_meta.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void uce_print_bytes(const char* data, size_t len)
|
||||||
|
{
|
||||||
|
if(context == 0)
|
||||||
|
uce_wasm_core_init();
|
||||||
|
if(context->ob && data && len)
|
||||||
|
context->ob->write(data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uce_wasm_finish_output()
|
||||||
|
{
|
||||||
|
// ob_stack[0] is the request's primary stream; nested captures above it
|
||||||
|
// belong to unbalanced ob_start() calls and are intentionally ignored
|
||||||
|
wasm_output = wasm_request.ob_stack.empty() ? String("") : wasm_request.ob_stack[0]->str();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* uce_wasm_output_data()
|
||||||
|
{
|
||||||
|
return(wasm_output.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t uce_wasm_output_size()
|
||||||
|
{
|
||||||
|
return(wasm_output.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
uce_host_time
|
||||||
|
uce_host_time_precise
|
||||||
|
uce_host_env
|
||||||
|
uce_host_log
|
||||||
|
uce_host_random
|
||||||
|
uce_host_task_spawn
|
||||||
|
uce_host_task_pid
|
||||||
|
uce_host_task_kill
|
||||||
|
uce_host_sleep_us
|
||||||
|
uce_host_component_resolve
|
||||||
|
uce_host_file_exists
|
||||||
|
uce_host_file_read
|
||||||
|
uce_host_file_write
|
||||||
|
uce_host_file_unlink
|
||||||
|
uce_host_regex
|
||||||
|
uce_host_sqlite
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
atof
|
||||||
|
atoi
|
||||||
|
atol
|
||||||
|
atoll
|
||||||
|
strtol
|
||||||
|
strtoul
|
||||||
|
strtoll
|
||||||
|
strtoull
|
||||||
|
strtod
|
||||||
|
strtof
|
||||||
|
qsort
|
||||||
|
bsearch
|
||||||
|
snprintf
|
||||||
|
sscanf
|
||||||
|
memmove
|
||||||
|
memchr
|
||||||
|
strncmp
|
||||||
|
strncpy
|
||||||
|
strchr
|
||||||
|
strrchr
|
||||||
|
strstr
|
||||||
|
isalnum
|
||||||
|
isalpha
|
||||||
|
isblank
|
||||||
|
iscntrl
|
||||||
|
isdigit
|
||||||
|
isgraph
|
||||||
|
islower
|
||||||
|
isprint
|
||||||
|
ispunct
|
||||||
|
isspace
|
||||||
|
isupper
|
||||||
|
isxdigit
|
||||||
|
tolower
|
||||||
|
toupper
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
// W1 smoke driver for the production UCE core.wasm.
|
||||||
|
|
||||||
|
#include <wasm.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
|
||||||
|
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
|
||||||
|
|
||||||
|
static std::vector<uint8_t> read_file(const char* path)
|
||||||
|
{
|
||||||
|
FILE* f = fopen(path, "rb");
|
||||||
|
CHECK(f, "cannot open %s", path);
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long n = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
std::vector<uint8_t> data((size_t)n);
|
||||||
|
CHECK(fread(data.data(), 1, data.size(), f) == data.size(), "short read on %s", path);
|
||||||
|
fclose(f);
|
||||||
|
return(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string wasm_name(const wasm_name_t* name)
|
||||||
|
{
|
||||||
|
std::string s(name->data, name->size);
|
||||||
|
while(!s.empty() && s.back() == '\0') s.pop_back();
|
||||||
|
return(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static wasm_store_t* g_store_for_traps = nullptr;
|
||||||
|
|
||||||
|
static void set_i32(wasm_val_t* result, int32_t value)
|
||||||
|
{
|
||||||
|
result->kind = WASM_I32;
|
||||||
|
result->of.i32 = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_i64(wasm_val_t* result, int64_t value)
|
||||||
|
{
|
||||||
|
result->kind = WASM_I64;
|
||||||
|
result->of.i64 = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_f64(wasm_val_t* result, double value)
|
||||||
|
{
|
||||||
|
result->kind = WASM_F64;
|
||||||
|
result->of.f64 = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static wasm_trap_t* host_time(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
|
||||||
|
{
|
||||||
|
set_i64(&results->data[0], 1700000000);
|
||||||
|
return(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static wasm_trap_t* host_time_precise(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
|
||||||
|
{
|
||||||
|
set_f64(&results->data[0], 1700000000.25);
|
||||||
|
return(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static wasm_trap_t* host_env(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
|
||||||
|
{
|
||||||
|
set_i32(&results->data[0], 0);
|
||||||
|
return(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static wasm_memory_t* g_memory = nullptr;
|
||||||
|
|
||||||
|
static wasm_trap_t* host_random(void*, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
||||||
|
{
|
||||||
|
uint32_t ptr = args->data[0].of.i32;
|
||||||
|
uint32_t len = args->data[1].of.i32;
|
||||||
|
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
||||||
|
size_t mem_size = wasm_memory_data_size(g_memory);
|
||||||
|
if((size_t)ptr + len > mem_size)
|
||||||
|
{
|
||||||
|
set_i32(&results->data[0], 0);
|
||||||
|
return(nullptr);
|
||||||
|
}
|
||||||
|
for(uint32_t i = 0; i < len; ++i)
|
||||||
|
mem[ptr + i] = (uint8_t)(0x5au ^ (i * 29u));
|
||||||
|
set_i32(&results->data[0], len);
|
||||||
|
return(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static wasm_trap_t* host_log(void*, const wasm_val_vec_t*, wasm_val_vec_t*)
|
||||||
|
{
|
||||||
|
return(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t*, wasm_val_vec_t*)
|
||||||
|
{
|
||||||
|
std::string label = (const char*)env;
|
||||||
|
std::string msg = "unexpected import called: " + label;
|
||||||
|
wasm_byte_vec_t message;
|
||||||
|
wasm_byte_vec_new(&message, msg.size(), msg.data());
|
||||||
|
wasm_trap_t* trap = wasm_trap_new(g_store_for_traps, &message);
|
||||||
|
wasm_byte_vec_delete(&message);
|
||||||
|
return(trap);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Instance
|
||||||
|
{
|
||||||
|
wasm_module_t* module = nullptr;
|
||||||
|
wasm_instance_t* instance = nullptr;
|
||||||
|
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
|
||||||
|
std::map<std::string, wasm_extern_t*> by_name;
|
||||||
|
|
||||||
|
void index_exports()
|
||||||
|
{
|
||||||
|
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
|
||||||
|
wasm_module_exports(module, &types);
|
||||||
|
wasm_instance_exports(instance, &exports);
|
||||||
|
CHECK(types.size == exports.size, "export count mismatch");
|
||||||
|
for(size_t i = 0; i < types.size; ++i)
|
||||||
|
by_name[wasm_name(wasm_exporttype_name(types.data[i]))] = exports.data[i];
|
||||||
|
wasm_exporttype_vec_delete(&types);
|
||||||
|
}
|
||||||
|
|
||||||
|
wasm_func_t* func(const char* name)
|
||||||
|
{
|
||||||
|
auto it = by_name.find(name);
|
||||||
|
return(it == by_name.end() ? nullptr : wasm_extern_as_func(it->second));
|
||||||
|
}
|
||||||
|
|
||||||
|
wasm_memory_t* memory()
|
||||||
|
{
|
||||||
|
auto it = by_name.find("memory");
|
||||||
|
return(it == by_name.end() ? nullptr : wasm_extern_as_memory(it->second));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void report_trap(wasm_trap_t* trap, const char* what)
|
||||||
|
{
|
||||||
|
if(!trap) return;
|
||||||
|
wasm_message_t msg;
|
||||||
|
wasm_trap_message(trap, &msg);
|
||||||
|
FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t call_i32(Instance& inst, const char* name, std::vector<int32_t> argv = {})
|
||||||
|
{
|
||||||
|
wasm_func_t* f = inst.func(name);
|
||||||
|
CHECK(f, "missing function %s", name);
|
||||||
|
CHECK(argv.size() <= 4, "too many args for %s", name);
|
||||||
|
wasm_val_t args_buf[4];
|
||||||
|
for(size_t i = 0; i < argv.size(); ++i) args_buf[i] = WASM_I32_VAL(argv[i]);
|
||||||
|
wasm_val_t result_buf[1] = { WASM_INIT_VAL };
|
||||||
|
wasm_val_vec_t args = { argv.size(), args_buf };
|
||||||
|
wasm_val_vec_t results = { 1, result_buf };
|
||||||
|
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
|
||||||
|
wasm_trap_t* trap = wasm_func_call(f, &args, wasm_func_result_arity(f) ? &results : &no_results);
|
||||||
|
report_trap(trap, name);
|
||||||
|
return(wasm_func_result_arity(f) ? result_buf[0].of.i32 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_bytes(wasm_memory_t* memory, uint32_t ptr, const std::string& data)
|
||||||
|
{
|
||||||
|
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||||
|
size_t mem_size = wasm_memory_data_size(memory);
|
||||||
|
CHECK((size_t)ptr + data.size() <= mem_size, "write outside memory");
|
||||||
|
memcpy(mem + ptr, data.data(), data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string read_bytes(wasm_memory_t* memory, uint32_t ptr, uint32_t len)
|
||||||
|
{
|
||||||
|
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||||
|
size_t mem_size = wasm_memory_data_size(memory);
|
||||||
|
CHECK((size_t)ptr + len <= mem_size, "read outside memory");
|
||||||
|
return(std::string((const char*)mem + ptr, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string read_cstr(wasm_memory_t* memory, uint32_t ptr, uint32_t cap = 4096)
|
||||||
|
{
|
||||||
|
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||||
|
size_t mem_size = wasm_memory_data_size(memory);
|
||||||
|
CHECK(ptr < mem_size, "cstr starts outside memory");
|
||||||
|
std::string out;
|
||||||
|
for(uint32_t i = 0; i < cap && (size_t)ptr + i < mem_size; ++i)
|
||||||
|
{
|
||||||
|
if(mem[ptr + i] == 0)
|
||||||
|
return(out);
|
||||||
|
out.push_back((char)mem[ptr + i]);
|
||||||
|
}
|
||||||
|
FAIL("unterminated cstr");
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t read_u32(wasm_memory_t* memory, uint32_t ptr)
|
||||||
|
{
|
||||||
|
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||||
|
size_t mem_size = wasm_memory_data_size(memory);
|
||||||
|
CHECK((size_t)ptr + 4 <= mem_size, "u32 read outside memory");
|
||||||
|
return((uint32_t)mem[ptr] | ((uint32_t)mem[ptr + 1] << 8) | ((uint32_t)mem[ptr + 2] << 16) | ((uint32_t)mem[ptr + 3] << 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-w1/core.wasm";
|
||||||
|
wasm_engine_t* engine = wasm_engine_new();
|
||||||
|
CHECK(engine, "engine");
|
||||||
|
wasm_store_t* store = wasm_store_new(engine);
|
||||||
|
CHECK(store, "store");
|
||||||
|
g_store_for_traps = store;
|
||||||
|
|
||||||
|
std::vector<uint8_t> bytes = read_file(core_path);
|
||||||
|
wasm_byte_vec_t bv;
|
||||||
|
wasm_byte_vec_new(&bv, bytes.size(), (const char*)bytes.data());
|
||||||
|
Instance core;
|
||||||
|
core.module = wasm_module_new(store, &bv);
|
||||||
|
wasm_byte_vec_delete(&bv);
|
||||||
|
CHECK(core.module, "module load");
|
||||||
|
|
||||||
|
wasm_importtype_vec_t imports = WASM_EMPTY_VEC;
|
||||||
|
wasm_module_imports(core.module, &imports);
|
||||||
|
std::vector<wasm_extern_t*> import_externs(imports.size);
|
||||||
|
for(size_t i = 0; i < imports.size; ++i)
|
||||||
|
{
|
||||||
|
std::string mod = wasm_name(wasm_importtype_module(imports.data[i]));
|
||||||
|
std::string name = wasm_name(wasm_importtype_name(imports.data[i]));
|
||||||
|
const wasm_externtype_t* et = wasm_importtype_type(imports.data[i]);
|
||||||
|
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, "unexpected non-func import %s.%s", mod.c_str(), name.c_str());
|
||||||
|
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
|
||||||
|
wasm_func_t* fn = nullptr;
|
||||||
|
if(mod == "env" && name == "uce_host_time") fn = wasm_func_new_with_env(store, ft, host_time, nullptr, nullptr);
|
||||||
|
else if(mod == "env" && name == "uce_host_time_precise") fn = wasm_func_new_with_env(store, ft, host_time_precise, nullptr, nullptr);
|
||||||
|
else if(mod == "env" && name == "uce_host_env") fn = wasm_func_new_with_env(store, ft, host_env, nullptr, nullptr);
|
||||||
|
else if(mod == "env" && name == "uce_host_random") fn = wasm_func_new_with_env(store, ft, host_random, nullptr, nullptr);
|
||||||
|
else if(mod == "env" && name == "uce_host_log") fn = wasm_func_new_with_env(store, ft, host_log, nullptr, nullptr);
|
||||||
|
else if(mod == "wasi_snapshot_preview1")
|
||||||
|
{
|
||||||
|
char* label = strdup((mod + "." + name).c_str());
|
||||||
|
fn = wasm_func_new_with_env(store, ft, stub_callback, label, nullptr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
FAIL("unexpected core import %s.%s", mod.c_str(), name.c_str());
|
||||||
|
CHECK(fn, "import function %s.%s", mod.c_str(), name.c_str());
|
||||||
|
import_externs[i] = wasm_func_as_extern(fn);
|
||||||
|
}
|
||||||
|
wasm_extern_vec_t iv = { import_externs.size(), import_externs.data() };
|
||||||
|
wasm_trap_t* trap = nullptr;
|
||||||
|
core.instance = wasm_instance_new(store, core.module, &iv, &trap);
|
||||||
|
report_trap(trap, "core instantiation");
|
||||||
|
CHECK(core.instance, "core instantiate");
|
||||||
|
core.index_exports();
|
||||||
|
CHECK(core.memory(), "core exports memory");
|
||||||
|
g_memory = core.memory();
|
||||||
|
if(core.func("_initialize")) call_i32(core, "_initialize");
|
||||||
|
|
||||||
|
CHECK(call_i32(core, "uce_wasm_core_init") == 0, "core init failed");
|
||||||
|
call_i32(core, "uce_wasm_core_reset_request");
|
||||||
|
CHECK(call_i32(core, "uce_wasm_core_abi_version") == 6, "unexpected ABI version");
|
||||||
|
|
||||||
|
wasm_memory_t* memory = core.memory();
|
||||||
|
int32_t root = call_i32(core, "uce_dv_root");
|
||||||
|
CHECK(root != 0, "uce_dv_root returned null");
|
||||||
|
std::string key = "message";
|
||||||
|
std::string value = "hello from W1 core";
|
||||||
|
int32_t key_ptr = call_i32(core, "uce_alloc", { (int32_t)key.size() });
|
||||||
|
int32_t value_ptr = call_i32(core, "uce_alloc", { (int32_t)value.size() });
|
||||||
|
write_bytes(memory, key_ptr, key);
|
||||||
|
write_bytes(memory, value_ptr, value);
|
||||||
|
int32_t child = call_i32(core, "uce_dv_get", { root, key_ptr, (int32_t)key.size() });
|
||||||
|
CHECK(child != 0, "uce_dv_get returned null");
|
||||||
|
call_i32(core, "uce_dv_set_value", { child, value_ptr, (int32_t)value.size() });
|
||||||
|
CHECK(call_i32(core, "uce_dv_find", { root, key_ptr, (int32_t)key.size() }) == child, "uce_dv_find mismatch");
|
||||||
|
CHECK(call_i32(core, "uce_dv_count", { root }) == 1, "root count mismatch");
|
||||||
|
CHECK(call_i32(core, "uce_dv_is_list", { root }) == 0, "root unexpectedly list-shaped");
|
||||||
|
int32_t value_len_ptr = call_i32(core, "uce_alloc", { 4 });
|
||||||
|
int32_t value_result_ptr = call_i32(core, "uce_dv_value", { child, value_len_ptr });
|
||||||
|
uint32_t value_result_len = read_u32(memory, value_len_ptr);
|
||||||
|
CHECK(read_bytes(memory, value_result_ptr, value_result_len) == value, "uce_dv_value mismatch");
|
||||||
|
int32_t encoded_len = call_i32(core, "uce_dv_encode", { root, 0, 0 });
|
||||||
|
CHECK(encoded_len > 5, "encoded length too small");
|
||||||
|
int32_t encoded_ptr = call_i32(core, "uce_alloc", { encoded_len });
|
||||||
|
CHECK(call_i32(core, "uce_dv_encode", { root, encoded_ptr, encoded_len }) == encoded_len, "encode length mismatch");
|
||||||
|
std::string encoded = read_bytes(memory, encoded_ptr, encoded_len);
|
||||||
|
CHECK(encoded.rfind("UCEB\x01", 0) == 0, "UCEB1 header missing");
|
||||||
|
int32_t decoded = call_i32(core, "uce_dv_decode", { encoded_ptr, encoded_len });
|
||||||
|
CHECK(decoded != 0, "uce_dv_decode failed");
|
||||||
|
CHECK(call_i32(core, "uce_dv_count", { decoded }) == 1, "decoded root count mismatch");
|
||||||
|
int32_t last_error_ptr = call_i32(core, "uce_dv_last_error");
|
||||||
|
CHECK(last_error_ptr != 0, "uce_dv_last_error returned null");
|
||||||
|
CHECK(read_cstr(memory, last_error_ptr) == "", "last error not clear after successful decode");
|
||||||
|
std::string bad = "bad";
|
||||||
|
int32_t bad_ptr = call_i32(core, "uce_alloc", { (int32_t)bad.size() });
|
||||||
|
write_bytes(memory, bad_ptr, bad);
|
||||||
|
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB1 decode unexpectedly succeeded");
|
||||||
|
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB1 decode did not set error");
|
||||||
|
|
||||||
|
std::string out = "W1 output";
|
||||||
|
int32_t out_ptr = call_i32(core, "uce_alloc", { (int32_t)out.size() });
|
||||||
|
write_bytes(memory, out_ptr, out);
|
||||||
|
call_i32(core, "uce_print_bytes", { out_ptr, (int32_t)out.size() });
|
||||||
|
call_i32(core, "uce_wasm_finish_output");
|
||||||
|
int32_t output_len = call_i32(core, "uce_wasm_output_size");
|
||||||
|
int32_t output_ptr = call_i32(core, "uce_wasm_output_data");
|
||||||
|
CHECK(read_bytes(memory, output_ptr, output_len) == out, "output plumbing mismatch");
|
||||||
|
|
||||||
|
printf("W1 core.wasm smoke: abi=6 encoded=%d output=%d\n", encoded_len, output_len);
|
||||||
|
printf("W1 EXIT CRITERION: PASS\n");
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
// W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3.
|
||||||
|
//
|
||||||
|
// Serves real requests through the production workspace runtime
|
||||||
|
// (src/wasm/worker.cpp): UCEB1 context in → core + lazily loaded real
|
||||||
|
// generated units → body/response-meta out. Each --repeat gets a fresh
|
||||||
|
// workspace, proving birth/drop. An epoch ticker thread enforces the CPU
|
||||||
|
// budget; the store limiter enforces memory; traps come back as collapsed
|
||||||
|
// wasm_trace summaries.
|
||||||
|
//
|
||||||
|
// Amalgamation TU (project style, like uce_lib.cpp): native types + DValue
|
||||||
|
// codec first, then the worker.
|
||||||
|
|
||||||
|
// same order as uce_lib.cpp, minus the native compiler/connector block
|
||||||
|
#include "../lib/types.cpp"
|
||||||
|
#include "../lib/dvalue.cpp"
|
||||||
|
#include "../lib/functionlib.cpp"
|
||||||
|
#include "../lib/hash.cpp"
|
||||||
|
#include "../lib/sys.cpp"
|
||||||
|
#include "../lib/uri.cpp"
|
||||||
|
#include "../lib/cli.cpp"
|
||||||
|
#include "../lib/wasm_trace.h"
|
||||||
|
#include "worker.cpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
static String arg_value(int argc, char** argv, int& i, const char* flag)
|
||||||
|
{
|
||||||
|
if(i + 1 >= argc)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "missing value for %s\n", flag);
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
return(String(argv[++i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String absolute_path(String path)
|
||||||
|
{
|
||||||
|
if(path.rfind("/", 0) == 0)
|
||||||
|
return(path);
|
||||||
|
char buf[4096];
|
||||||
|
if(!getcwd(buf, sizeof(buf)))
|
||||||
|
return(path);
|
||||||
|
return(String(buf) + "/" + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
WasmWorkerConfig cfg;
|
||||||
|
cfg.site_root = absolute_path("site");
|
||||||
|
String page;
|
||||||
|
String route;
|
||||||
|
StringMap params;
|
||||||
|
StringMap get_params;
|
||||||
|
std::vector<String> expects;
|
||||||
|
String expect_status;
|
||||||
|
int repeat = 1;
|
||||||
|
u64 epoch_period_ms = 50;
|
||||||
|
bool show_body = true;
|
||||||
|
|
||||||
|
for(int i = 1; i < argc; i++)
|
||||||
|
{
|
||||||
|
String arg = argv[i];
|
||||||
|
if(arg == "--core") cfg.core_wasm_path = arg_value(argc, argv, i, "--core");
|
||||||
|
else if(arg == "--site") cfg.site_root = absolute_path(arg_value(argc, argv, i, "--site"));
|
||||||
|
else if(arg == "--cache") cfg.cache_root = arg_value(argc, argv, i, "--cache");
|
||||||
|
else if(arg == "--page") page = arg_value(argc, argv, i, "--page");
|
||||||
|
else if(arg == "--route") route = arg_value(argc, argv, i, "--route");
|
||||||
|
else if(arg == "--repeat") repeat = atoi(arg_value(argc, argv, i, "--repeat").c_str());
|
||||||
|
else if(arg == "--expect") expects.push_back(arg_value(argc, argv, i, "--expect"));
|
||||||
|
else if(arg == "--expect-status") expect_status = arg_value(argc, argv, i, "--expect-status");
|
||||||
|
else if(arg == "--epoch-ticks") cfg.epoch_deadline_ticks = strtoull(arg_value(argc, argv, i, "--epoch-ticks").c_str(), 0, 10);
|
||||||
|
else if(arg == "--epoch-ms") epoch_period_ms = strtoull(arg_value(argc, argv, i, "--epoch-ms").c_str(), 0, 10);
|
||||||
|
else if(arg == "--mem-limit") cfg.memory_limit = strtoll(arg_value(argc, argv, i, "--mem-limit").c_str(), 0, 10);
|
||||||
|
else if(arg == "--table-headroom") cfg.table_headroom = (u32)atoi(arg_value(argc, argv, i, "--table-headroom").c_str());
|
||||||
|
else if(arg == "--quiet") show_body = false;
|
||||||
|
else if(arg == "--verbose") cfg.verbose = true;
|
||||||
|
else if(arg == "--param" || arg == "--get")
|
||||||
|
{
|
||||||
|
String pair = arg_value(argc, argv, i, arg.c_str());
|
||||||
|
auto eq = pair.find("=");
|
||||||
|
if(eq == String::npos)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s expects K=V\n", arg.c_str());
|
||||||
|
return(2);
|
||||||
|
}
|
||||||
|
(arg == "--param" ? params : get_params)[pair.substr(0, eq)] = pair.substr(eq + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fprintf(stderr, "unknown argument: %s\n", arg.c_str());
|
||||||
|
return(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(page == "")
|
||||||
|
{
|
||||||
|
fprintf(stderr, "usage: w3_driver --page /demo/hello.uce [--site site] [--cache /tmp/uce/work]\n"
|
||||||
|
" [--core bin/wasm/core.wasm] [--param K=V ...] [--get K=V ...] [--route path]\n"
|
||||||
|
" [--repeat N] [--expect STR ...] [--expect-status STR] [--quiet] [--verbose]\n"
|
||||||
|
" [--epoch-ticks N] [--epoch-ms N] [--mem-limit BYTES]\n");
|
||||||
|
return(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String entry_unit = cfg.site_root + page;
|
||||||
|
WasmWorker worker(cfg);
|
||||||
|
String init_error = worker.init();
|
||||||
|
if(init_error != "")
|
||||||
|
{
|
||||||
|
fprintf(stderr, "FAIL: %s\n", init_error.c_str());
|
||||||
|
return(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the epoch only advances through this ticker; period × deadline-ticks
|
||||||
|
// is the per-request CPU budget
|
||||||
|
std::atomic<bool> running(true);
|
||||||
|
std::thread ticker([&] {
|
||||||
|
while(running.load())
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(epoch_period_ms));
|
||||||
|
worker.engine.increment_epoch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DValue context_tree;
|
||||||
|
for(auto& entry : params)
|
||||||
|
context_tree["params"][entry.first] = entry.second;
|
||||||
|
if(context_tree.key("params") == 0 || !params.count("HTTP_HOST"))
|
||||||
|
context_tree["params"]["HTTP_HOST"] = "w3.test";
|
||||||
|
context_tree["params"]["SCRIPT_URL"] = page;
|
||||||
|
context_tree["params"]["REQUEST_METHOD"] = "GET";
|
||||||
|
for(auto& entry : get_params)
|
||||||
|
context_tree["get"][entry.first] = entry.second;
|
||||||
|
if(route != "")
|
||||||
|
{
|
||||||
|
context_tree["route"]["l_path"] = route;
|
||||||
|
context_tree["params"]["ROUTE_PATH"] = route;
|
||||||
|
}
|
||||||
|
context_tree["entry_unit"] = entry_unit;
|
||||||
|
|
||||||
|
bool all_ok = true;
|
||||||
|
WasmResponse last;
|
||||||
|
for(int request = 0; request < repeat && all_ok; request++)
|
||||||
|
{
|
||||||
|
f64 started = (f64)clock() / CLOCKS_PER_SEC;
|
||||||
|
last = wasm_worker_serve(worker, context_tree, entry_unit);
|
||||||
|
f64 elapsed_ms = ((f64)clock() / CLOCKS_PER_SEC - started) * 1000.0;
|
||||||
|
printf("==== request %d/%d (%.1f ms cpu) ====\n", request + 1, repeat, elapsed_ms);
|
||||||
|
if(!last.ok)
|
||||||
|
{
|
||||||
|
printf("ERROR:\n%s\n", last.error.c_str());
|
||||||
|
all_ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
printf("status: %s\n", last.meta["status"].to_string().c_str());
|
||||||
|
if(last.meta.key("headers"))
|
||||||
|
last.meta["headers"].each([](const DValue& value, String key) {
|
||||||
|
printf("header: %s: %s\n", key.c_str(), value.to_string().c_str());
|
||||||
|
});
|
||||||
|
if(show_body)
|
||||||
|
printf("---- body (%zu bytes) ----\n%.*s\n----\n",
|
||||||
|
last.body.size(), (int)last.body.size(), last.body.data());
|
||||||
|
else
|
||||||
|
printf("body: %zu bytes\n", last.body.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
running.store(false);
|
||||||
|
ticker.join();
|
||||||
|
|
||||||
|
if(all_ok && expect_status != "" && last.meta["status"].to_string().find(expect_status) == String::npos)
|
||||||
|
{
|
||||||
|
printf("FAIL: status %s does not contain %s\n", last.meta["status"].to_string().c_str(), expect_status.c_str());
|
||||||
|
all_ok = false;
|
||||||
|
}
|
||||||
|
for(auto& expect : expects)
|
||||||
|
if(all_ok && last.body.find(expect) == String::npos)
|
||||||
|
{
|
||||||
|
printf("FAIL: body does not contain %s\n", expect.c_str());
|
||||||
|
all_ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(all_ok ? "W3 RESULT: PASS\n" : "W3 RESULT: FAIL\n");
|
||||||
|
return(all_ok ? 0 : 1);
|
||||||
|
}
|
||||||
+1441
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def register(registry):
|
||||||
|
# W4/W5 kill pages are only meaningful with the wasm backend enabled. Native
|
||||||
|
# requests may terminate the worker by design, so keep them out of the normal
|
||||||
|
# native suite unless the W5 harness opts in explicitly.
|
||||||
|
if os.environ.get("UCE_INCLUDE_WASM_KILL") != "1":
|
||||||
|
return
|
||||||
|
|
||||||
|
pages = [
|
||||||
|
# oob is __builtin_trap() → wasm `unreachable`, a signal-delivering trap.
|
||||||
|
# It crashed the worker until signals_based_traps(false) (see make_engine
|
||||||
|
# in src/wasm/worker.cpp); keeping it in the gate guards that fix.
|
||||||
|
("wasm kill trap", "/tests/wasm-kill/oob.uce", "unreachable"),
|
||||||
|
("wasm kill loop", "/tests/wasm-kill/loop.uce", "interrupt"),
|
||||||
|
("wasm kill recurse", "/tests/wasm-kill/recurse.uce", "wasm_kill_recurse"),
|
||||||
|
]
|
||||||
|
for name, path, marker in pages:
|
||||||
|
def make_case(page_path=path, expected_marker=marker):
|
||||||
|
def run(context):
|
||||||
|
response = context.expect_status(page_path, 500)
|
||||||
|
context.expect_body_contains(response, "wasm runtime error during request")
|
||||||
|
context.expect_body_contains(response, expected_marker)
|
||||||
|
# The worker should remain healthy after the trap.
|
||||||
|
health = context.expect_status("/demo/hello.uce", 200)
|
||||||
|
context.expect_body_contains(health, "hello world")
|
||||||
|
return "clean wasm trap page and post-trap health check for %s" % page_path
|
||||||
|
return run
|
||||||
|
registry.case(name, make_case(), tags=["http", "uce", "wasm", "kill", "internal"])
|
||||||
@@ -122,6 +122,8 @@ def main() -> int:
|
|||||||
parser = argparse.ArgumentParser(description="Phase 5 native/wasm benchmark harness")
|
parser = argparse.ArgumentParser(description="Phase 5 native/wasm benchmark harness")
|
||||||
parser.add_argument("--native-base-url", default="http://localhost:80")
|
parser.add_argument("--native-base-url", default="http://localhost:80")
|
||||||
parser.add_argument("--wasm-base-url", default="", help="optional wasm worker URL; omitted until worker exists")
|
parser.add_argument("--wasm-base-url", default="", help="optional wasm worker URL; omitted until worker exists")
|
||||||
|
parser.add_argument("--backend-label", default="native", help="label for --native-base-url measurements when running one backend at a time")
|
||||||
|
parser.add_argument("--compare-native-json", default="", help="optional prior native benchmark.json for budget comparison")
|
||||||
parser.add_argument("--host-header", default="uce.openfu.com")
|
parser.add_argument("--host-header", default="uce.openfu.com")
|
||||||
parser.add_argument("--warmups", type=int, default=2)
|
parser.add_argument("--warmups", type=int, default=2)
|
||||||
parser.add_argument("--samples", type=int, default=20)
|
parser.add_argument("--samples", type=int, default=20)
|
||||||
@@ -130,7 +132,11 @@ def main() -> int:
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
targets = build_targets()
|
targets = build_targets()
|
||||||
results = measure_backend("native", args.native_base_url, args.host_header, targets, args.warmups, args.samples, args.timeout)
|
results: list[Measurement] = []
|
||||||
|
if args.compare_native_json:
|
||||||
|
for row in json.loads(Path(args.compare_native_json).read_text()):
|
||||||
|
results.append(Measurement(**row))
|
||||||
|
results.extend(measure_backend(args.backend_label, args.native_base_url, args.host_header, targets, args.warmups, args.samples, args.timeout))
|
||||||
if args.wasm_base_url:
|
if args.wasm_base_url:
|
||||||
results.extend(measure_backend("wasm", args.wasm_base_url, args.host_header, targets, args.warmups, args.samples, args.timeout))
|
results.extend(measure_backend("wasm", args.wasm_base_url, args.host_header, targets, args.warmups, args.samples, args.timeout))
|
||||||
|
|
||||||
@@ -138,7 +144,7 @@ def main() -> int:
|
|||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
(out_dir / "benchmark.json").write_text(json.dumps([asdict(r) for r in results], indent=2) + "\n")
|
(out_dir / "benchmark.json").write_text(json.dumps([asdict(r) for r in results], indent=2) + "\n")
|
||||||
md_lines = ["# Phase 5 benchmark report", "", *compare(results), ""]
|
md_lines = ["# Phase 5 benchmark report", "", *compare(results), ""]
|
||||||
if not args.wasm_base_url:
|
if not args.wasm_base_url and args.backend_label == "native" and not args.compare_native_json:
|
||||||
md_lines.append("WASM worker URL was not provided; this report is the native baseline that future wasm runs compare against.")
|
md_lines.append("WASM worker URL was not provided; this report is the native baseline that future wasm runs compare against.")
|
||||||
(out_dir / "benchmark.md").write_text("\n".join(md_lines) + "\n")
|
(out_dir / "benchmark.md").write_text("\n".join(md_lines) + "\n")
|
||||||
print("\n".join(md_lines))
|
print("\n".join(md_lines))
|
||||||
@@ -7,7 +7,7 @@ import json
|
|||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
SITE = ROOT / "site"
|
SITE = ROOT / "site"
|
||||||
|
|
||||||
PATTERNS = [
|
PATTERNS = [
|
||||||
Reference in New Issue
Block a user