This commit is contained in:
udo 2026-06-13 16:19:52 +00:00
parent 5a56d4f39e
commit c84fc86e6c
42 changed files with 307 additions and 3137 deletions

View File

@ -6,9 +6,16 @@
`src/wasm/backend.cpp`) are **done**: with `WASM_BACKEND_ENABLED=1` the
real nginx → fastcgi → wasm path serves the starter app (parity 14/14) and
produces clean error pages from an unharmed worker on real kill pages.
Native is still the default backend (full suite 83/83). Current phase:
**W5 — full parity, performance, cutover** (un-stub regex/xml/yaml, add the
remaining membrane hostcalls, then flip the default).
W5 cutover is done: WASM is the default page-render backend with native kept
as the config-selectable reference/fallback; the membrane now covers regex,
xml/yaml, markdown, filesystem read/write, sqlite, background tasks, sleep,
and time formatting. W6 spike cleanup is done (prototypes deleted, gates
re-homed to `tests/`/`docs/`); the native-machinery retirements stay gated
while native still serves the remaining fallback surfaces (zip, sockets,
memcache, mysql, `unit_call`, compiler-introspection). Open items: the
sqlite membrane is ~67× native (per-query double hostcall + UCEB1
marshalling — §11.4 cursor/bulk decision), and CoW snapshot birth for the
workspace-birth budget.
- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so`
`dlopen`) with per-unit WebAssembly modules executed in a per-request,
runtime-linked workspace, exposing the same API surface to page code,
@ -428,7 +435,7 @@ stub) hello-world linked at runtime by a minimal loader, in the chosen
vendored runtime.
> **Status: DONE (2026-06-12).** Exit criterion passed on k-uce; see
> `spikes/wasm-phase0/FINDINGS.md`. Runtime selected: **Wasmtime v45.0.1**
> `docs/wasm-toolchain-findings.md`. Runtime selected: **Wasmtime v45.0.1**
> — WAMR is blocked on the load-bearing requirement (its wasm-c-api ignores
> imported memories/tables; host-side table growth unsupported). wasi-sdk-33
> PIC validated on stubs **and** on real generated units
@ -521,7 +528,7 @@ Exit: the uce-starter renders end-to-end with components loading lazily;
> wrong values and wrote into core memory at low addresses; both spike loaders
> are fixed and the Phase 3 exit gate now asserts every GOT-derived output
> marker (`self-got`/`callback`/`each`/`map`) so a regression cannot pass.
> Details in the `spikes/wasm-phase0/FINDINGS.md` erratum.
> Details in the `docs/wasm-toolchain-findings.md` erratum.
>
> **Production Phase 3 work plan** (ordered by dependency/risk; spike-proven
> mechanics not repeated here):
@ -618,7 +625,7 @@ call cost. Exit: numbers published in this document, all tests and reviews pass.
> measured gate to absorb cold unit-cache timeouts. Informational native medians
> from the 2026-06-12 baseline are: template-heavy doc `313.8 ms`, sqlite page
> `3.4 ms`, starter dashboard `41.1 ms`. A durable snapshot lives in
> `spikes/wasm-phase5/reports/native-baseline-2026-06-12.md`; paired wasm/native
> `docs/wasm-baselines/native-baseline-2026-06-12.md`; paired wasm/native
> gate runs still recompute the native medians for the actual ≤2× comparison.
> True Phase 5 completion still requires passing the same harness against a real
> wasm worker URL and adding worker-internal probes for workspace birth and
@ -846,8 +853,8 @@ reference.
> **Status: DONE (2026-06-13).** W5 flips the page-render default to the WASM
> backend while retaining explicit native fallback for the surfaces still
> genuinely host-owned: zip, sqlite, background tasks, filesystem writes,
> sockets/sleep, and `unit_call`/compiler-introspection (the last need the
> genuinely host-owned or not yet membraned: zip, sockets/custom servers,
> memcache/mysql, and `unit_call`/compiler-introspection (the last need the
> native toolchain). The fallback is selected before workspace creation by
> scanning the entry source for a small native-only token set, so unsupported
> pages neither fail through wasm nor hide as trap regressions.
@ -855,12 +862,27 @@ reference.
> **Promoted onto wasm during W5** (no longer fallback): **regex** runs through
> a single UCEB1-marshalled `uce_host_regex` hostcall against the host's PCRE2;
> **markdown** is compiled into the core (pure compute); **xml/yaml** were
> already in-core; **filesystem writes** (`file_put_contents`/`file_append`) go
> through a policy-gated `uce_host_file_write` hostcall, and the read membrane
> was widened to the same allowlist (site tree + scratch roots: `/tmp`,
> already in-core; **filesystem writes** (`file_put_contents`/`file_append`/
> `file_unlink`) go through policy-gated hostcalls, and the read membrane was
> widened to the same allowlist (site tree + scratch roots: `/tmp`,
> `BIN_DIRECTORY`, `SESSION_PATH`, `TMP_UPLOAD_PATH`) so a page can read back
> what it writes (e.g. the `io.uce` /tmp round-trip). The `/doc/*` blanket
> fallback is gone — doc pages render on wasm. Two wasm-stack/ABI issues surfaced and were fixed: the core stack was
> what it writes (e.g. the `io.uce` /tmp round-trip); **sqlite** runs through a
> `uce_host_sqlite` hostcall against the host's real connector, with
> connections held in a per-workspace handle table that is closed on workspace
> drop — the first concrete realization of the §3.1 "handle-table drop =
> resource cleanup" model; **background tasks** now use a host-managed process
> membrane (`uce_host_task_spawn`/`pid`/`kill`) with a guest callback trampoline
> (`uce_wasm_task_run`) in the forked child, so `task()`, `task_repeat()`,
> `task_pid()`, and `task_kill()` run under the wasm backend; **sleep/usleep**
> use a host sleep call that renews the epoch deadline after blocking. The
> `/doc/*` blanket fallback is gone — doc pages render on wasm.
>
> One subtle bug fixed here: the sized-hostcall convention has the guest call
> twice (buf=0 to learn the length, then to fetch). That re-executed the op —
> harmless for idempotent regex/file-read, but for sqlite it ran every
> `INSERT`/`CREATE` twice and the second pass failed on its own side effects.
> The host now stages the encoded result on the first call (keyed on the exact
> input) and replays it on the fetch, so a side-effecting op runs once. Two wasm-stack/ABI issues surfaced and were fixed: the core stack was
> raised to 8 MB so recursive `ucb_decode_node` reaches the 1024 depth limit
> like native (the core.uce depth-bomb negative test overflowed the small
> default stack), and `wasm_trace`'s demangler is host-only (wasi-libc++ has no
@ -884,8 +906,8 @@ reference.
> starter subset + the native-vs-wasm benchmark, optionally leave the backend
> enabled (`UCE_W5_KEEP_BACKEND=1`). Final k-uce gate: native reference suite
> **83/83**; wasm/default full suite **83/83** warm (starter, docs, core tests,
> markdown, regex all on wasm; zip/sqlite/tasks correctly on native fallback);
> starter subset **14/14**; kill pages (trap/loop/recurse) clean with the worker
> markdown, regex, sqlite, and tasks on wasm; zip and native-only service/
> compiler surfaces routed to native fallback); starter subset **14/14**; kill pages (trap/loop/recurse) clean with the worker
> unharmed. Benchmark medians passed the ≤2× budget: doc singlepage native
> ~324ms vs wasm ~318ms, sqlite 3.7ms vs 3.8ms, starter dashboard 44ms vs wasm
> ~6ms. Worker-internal probe headers (`X-UCE-Backend`,
@ -896,13 +918,14 @@ reference.
> cutover budget). Live `/etc/uce/settings.cfg` left with `WASM_BACKEND_ENABLED=1`;
> native remains in-tree and config-selectable.
>
> **Still genuinely deferred (not W5-blocking):** sqlite/tasks/zip membrane
> hostcalls (host-owned, native fallback works today; zip additionally can't be
> a wasm side module while its `.uce` source uses try/catch), `unit_call` and
> compiler/unit-introspection pages (need the native toolchain — candidates for
> host-serviced hostcalls if native is ever to be fully retired), CoW snapshot
> birth + placement memoization for the workspace-birth budget, and the §3.2
> statics audit follow-through.
> **Remaining native fallback** (the only pages still routed to native): **zip**
> (its `.uce` source uses try/catch, so it can't be a `-fno-exceptions` wasm
> side module regardless of a hostcall), socket/custom-server/memcache/mysql
> service surfaces, and `unit_call` + compiler/unit-introspection
> (`unit-info`/`unit-browser`/`sharedunit` — need the native toolchain;
> candidates for host-serviced hostcalls if native is to be fully retired).
> **Still deferred, not blocking:** CoW snapshot birth + placement memoization
> for the workspace-birth budget, and the §3.2 statics-audit follow-through.
**W6 — cleanup.**
Cutover checklist gate 4, verbatim: re-home the spike-hosted gates
@ -910,6 +933,30 @@ Cutover checklist gate 4, verbatim: re-home the spike-hosted gates
`docs/`), delete `spikes/wasm-phase*`, retire the SIGSEGV recovery, the
tracking `operator new`, and `cleanup_*_connections()`.
> **Status: SPIKE CLEANUP DONE; native-machinery retirement GATED
> (2026-06-13).** Re-homing complete: the Phase 5 benchmark + site-audit are
> now `tests/wasm_benchmark.py` / `tests/wasm_site_audit.py` (with
> `scripts/wasm/run_w5.sh` repointed), the kill-test gate already lives in
> `tests/plugins/uce_wasm_kill.py`, the Phase 0 toolchain findings + GOT
> erratum are `docs/wasm-toolchain-findings.md`, and the pinned native
> baselines are `docs/wasm-baselines/`. **All `spikes/wasm-phase*` deleted**
> (prototypes superseded by `src/wasm/`), and the dead 2022
> `src/lib/_scratchpad.cpp` arena experiment is removed. Build + full suite
> stay green (83/83). The phase-04 status blocks above keep their narrative;
> their `spikes/...` paths are now historical — the artifacts that mattered
> were re-homed, the rest was throwaway scaffolding by design.
>
> **The three native-machinery retirements remain BLOCKED, deliberately:**
> native is still the live backend for pages touching zip, sockets / custom
> HTTP servers, memcache, mysql, `unit_call`, and compiler/unit-introspection
> — everything still in `native_only_tokens`. While *any* page renders on
> native, retiring its SIGSEGV/`sigsetjmp` recovery, the tracking
> `operator new`, or `cleanup_*_connections()` would strip crash protection
> and resource cleanup from live traffic. They retire only once those surfaces
> move behind membrane hostcalls (so nothing falls back) or the native backend
> is formally decommissioned. This is the honest end-state of W6 given the
> current fallback set, not an oversight.
---
## 10. Risks & mitigations

View File

@ -68,20 +68,20 @@ PY
set_backend 0
python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/native-warmup.json" >/dev/null || true
python3 tests/run_network_tests.py --include-internal --json-report "$OUT/native-network.json"
python3 spikes/wasm-phase5/benchmark.py --out-dir "$OUT/native-benchmark" --samples "${UCE_W5_BENCH_SAMPLES:-20}" --timeout 30 >/dev/null
python3 tests/wasm_benchmark.py --out-dir "$OUT/native-benchmark" --samples "${UCE_W5_BENCH_SAMPLES:-20}" --timeout 30 >/dev/null
# WASM default backend with W5 native fallbacks for host-owned surfaces.
set_backend 1
UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/wasm-warmup.json" >/dev/null || true
UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --json-report "$OUT/wasm-network.json"
python3 tests/run_network_tests.py --include-internal --match starter --json-report "$OUT/wasm-starter.json"
python3 spikes/wasm-phase5/benchmark.py \
python3 tests/wasm_benchmark.py \
--out-dir "$OUT/benchmark" \
--backend-label wasm \
--compare-native-json "$OUT/native-benchmark/benchmark.json" \
--samples "${UCE_W5_BENCH_SAMPLES:-20}" \
--timeout 30
python3 spikes/wasm-phase5/audit_site_statics.py --out-dir "$OUT" >/dev/null
python3 tests/wasm_site_audit.py --out-dir "$OUT" >/dev/null
python3 - "$OUT" <<'PY'
import json, sys

View File

@ -1,30 +0,0 @@
# spikes/wasm-phase0 — WASM-PROPOSAL Phase 0 (toolchain & runtime spike)
Validates the dynamic-linking foundation for the WASM unit runtime:
wasi-sdk PIC side modules + a host loader linking a core module and a unit
module at runtime with shared memory/table. **Exit criterion passed; runtime
selected: Wasmtime.** See `FINDINGS.md` for results and `realunit-report.md`
for the real-generated-unit compile log.
Everything builds and runs **on k-uce** (toolchains under `/opt`, artifacts
under `/tmp/uce/wasm-phase0`):
```
bash spikes/wasm-phase0/build_modules.sh # core.wasm + unit.wasm (wasi-sdk)
bash spikes/wasm-phase0/build_loader.sh # loader (Wasmtime C API)
/tmp/uce/wasm-phase0/loader # expect: PHASE0 EXIT CRITERION: PASS
```
Files:
- `core.cpp` — core-module stand-in: owns memory/allocator/libc++, exports
everything; includes the guest-side GOT.func resolver pattern
- `unit.cpp` — unit stand-in: PIC, imports allocator/runtime, exercises GOT,
cross-module C++ objects, function pointers, lambdas
- `loader.cpp` — minimal runtime linker (standard wasm-c-api): dylink.0
parsing, base allocation, GOT resolution, init sequencing
- `wasm_inspect.py` — dumps imports/exports/dylink.0 of a module
- `FINDINGS.md` — toolchain pins, WAMR rejection evidence, flag recipe,
Phase 2/3 implications
- `realunit-report.md``collections.uce.cpp`/`hello.uce.cpp` compiled as
side modules (worker-agent log)

View File

@ -1,17 +0,0 @@
#!/bin/bash
# Phase 0: build the minimal loader against the WAMR build at /opt/wamr.
# Runs on k-uce.
set -e
cd "$(dirname "$0")"
# Runtime: Wasmtime C API. (WAMR was evaluated first per WASM-PROPOSAL §9 but
# its wasm-c-api ignores imported memories/tables — see FINDINGS.md.)
WASMTIME=/opt/wasmtime
OUT=/tmp/uce/wasm-phase0
clang++ -std=c++17 -O1 -g loader.cpp -o "$OUT/loader" \
-I"$WASMTIME/include" \
-L"$WASMTIME/lib" -lwasmtime -Wl,-rpath,"$WASMTIME/lib" \
-lpthread -lm -ldl
echo built: "$OUT/loader"

View File

@ -1,51 +0,0 @@
#!/bin/bash
# Phase 0: build core (non-PIC reactor, libc statically linked, all-exported)
# and unit (-fPIC, wasm-ld -shared, dylink.0) with wasi-sdk.
# Runs on k-uce. Artifacts go to /tmp/uce/wasm-phase0.
set -e
cd "$(dirname "$0")"
SDK=/opt/wasi-sdk
OUT=/tmp/uce/wasm-phase0
mkdir -p "$OUT"
# Core module: reactor (no main, exports _initialize), exceptions off per
# WASM-PROPOSAL §11.1 (error codes at unit boundaries).
# --export-all so unit modules can import libc/libc++/core symbols from it;
# --import-table so the host creates the shared funcref table (sized with
# headroom up front — WAMR cannot grow or write tables from the host side);
# __stack_pointer exported because PIC side modules import it (dylink ABI).
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
-O1 -fno-exceptions \
core.cpp -o "$OUT/core.wasm" \
-Wl,--export-all \
-Wl,--import-table \
-Wl,--export=__stack_pointer \
-Wl,--export=__heap_base \
-Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE
# Unit module: PIC object, linked -shared with no libc/libc++ of its own —
# every undefined symbol must become an import (resolved from core by the
# loader). Inline/template code (std::string etc.) instantiates locally,
# which is fine; the allocator must NOT be defined here (§3.2).
# -fvisibility-inlines-hidden: vague-linkage code (templates, lambdas)
# binds locally instead of being interposable — without it, one libc++
# tree-emplace lambda became a self-import the loader cannot satisfy.
"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \
-fvisibility-inlines-hidden \
-O1 -fno-exceptions \
-c unit.cpp -o "$OUT/unit.o"
# --Bsymbolic: bind weak/vague-linkage symbols the unit defines itself
# (template instantiations, lambdas) locally instead of emitting
# self-imports that the loader would have to lazy-bind.
"$SDK/bin/wasm-ld" -shared --experimental-pic \
--unresolved-symbols=import-dynamic \
--Bsymbolic \
"$OUT/unit.o" -o "$OUT/unit.wasm" \
--export=uce_unit_render
echo "--- core.wasm ---"
ls -la "$OUT/core.wasm"
echo "--- unit.wasm ---"
ls -la "$OUT/unit.wasm"

View File

@ -1,74 +0,0 @@
// WASM-PROPOSAL Phase 0 — core module stub.
//
// Stands in for the future "core module" (uce_lib + libc compiled to wasm):
// owns linear memory, the allocator, and libc/libc++ (statically linked,
// per the §10 mitigation: avoid shared wasi-libc entirely). Built as a
// non-PIC wasm32-wasi reactor with everything exported so unit modules can
// import from it.
//
// Deliberately avoids WASI I/O (no printf-to-fd): output accumulates in a
// buffer the host reads back, so the module can be instantiated through the
// plain wasm-c-api without a WASI context if need be.
#include <string>
#include <functional>
#include <cstdio>
#include <cstring>
static std::string g_output;
extern "C" {
// data symbol referenced from the unit module → must arrive there as a
// GOT.mem import
int core_counter = 7;
void uce_print(const char* s, size_t len)
{
g_output.append(s, len);
}
// heap C++ object created in core, handed to the unit by pointer —
// validates one-heap/one-allocator pointer semantics (§3.4)
std::string* core_make_string(const char* s)
{
return(new std::string(s));
}
void core_append_string(std::string* str, const char* s)
{
str->append(s);
}
// plain function pointer crossing: unit passes its own function, core calls
// it back — validates the shared funcref table
void core_invoke_callback(void (*cb)(int), int arg)
{
cb(arg);
}
// std::function allocated by unit code, executed here — validates fat
// callable objects (lambdas) across module boundaries
void core_invoke_function(std::function<int(int)>* f, int arg)
{
char buf[64];
snprintf(buf, sizeof(buf), "[fn:%d]", (*f)(arg));
g_output.append(buf);
}
// GOT.func resolution helper: taking the address here forces uce_print into
// core's elem segment at link time, and on wasm a function pointer IS its
// table index — so the loader can resolve GOT.func.uce_print with a plain
// call instead of host-side funcref injection (which WAMR does not allow).
// The production core will generalize this into a name → funcptr registry.
intptr_t core_table_index_of_uce_print()
{
return(reinterpret_cast<intptr_t>(uce_print));
}
// host reads the result out of linear memory via these
const char* core_output_data() { return(g_output.data()); }
size_t core_output_size() { return(g_output.size()); }
void core_output_clear() { g_output.clear(); }
}

View File

@ -1,446 +0,0 @@
// WASM-PROPOSAL Phase 0 — minimal runtime loader (exit criterion).
//
// Embeds a wasm runtime through the standard wasm-c-api and links two
// modules at runtime, the way the production loader (§6) will:
//
// 1. instantiate core.wasm (owns memory/table/allocator/libc), stubbing
// its WASI imports with named trap functions (the core stub does no
// real I/O; any stub that actually gets called names itself)
// 2. parse unit.wasm's dylink.0 → data size/align, table slots needed
// 3. allocate __memory_base by calling core's exported malloc, and
// __table_base by growing core's exported funcref table
// 4. build the unit's import vector: env.memory / env.__indirect_function_table /
// env.__stack_pointer straight from core's exports; env.* functions from
// core's exports; GOT.mem.* as mutable i32 globals holding resolved
// addresses (self-resolved from the unit's own exports after
// instantiation when core lacks the symbol — weak data case);
// GOT.func.* as mutable i32 globals holding freshly grown table slots
// pointing at core export funcrefs
// 5. instantiate unit, patch deferred GOT entries, run
// __wasm_apply_data_relocs then __wasm_call_ctors
// 6. call uce_unit_render and read the output back out of linear memory
//
// Everything here is deliberately validation-grade: fail loudly, never guess.
#include <wasm.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
static std::vector<uint8_t> read_file(const char* fn)
{
FILE* f = fopen(fn, "rb");
CHECK(f, "cannot open %s", fn);
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
std::vector<uint8_t> buf(n);
CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn);
fclose(f);
return buf;
}
// ---- minimal dylink.0 parser -------------------------------------------
struct DylinkInfo
{
uint32_t mem_size = 0;
uint32_t mem_align = 0; // power of 2
uint32_t table_size = 0;
uint32_t table_align = 0;
bool found = false;
};
static uint64_t read_uleb(const uint8_t* buf, size_t& pos)
{
uint64_t result = 0;
int shift = 0;
while(true)
{
uint8_t b = buf[pos++];
result |= (uint64_t)(b & 0x7f) << shift;
if(!(b & 0x80))
return result;
shift += 7;
}
}
static DylinkInfo parse_dylink(const std::vector<uint8_t>& wasm)
{
DylinkInfo info;
CHECK(wasm.size() > 8 && !memcmp(wasm.data(), "\0asm", 4), "not a wasm module");
size_t pos = 8;
while(pos < wasm.size())
{
uint8_t sec_id = wasm[pos++];
uint64_t size = read_uleb(wasm.data(), pos);
size_t end = pos + size;
if(sec_id == 0)
{
uint64_t name_len = read_uleb(wasm.data(), pos);
std::string name((const char*)wasm.data() + pos, name_len);
pos += name_len;
if(name == "dylink.0")
{
while(pos < end)
{
uint8_t sub = wasm[pos++];
uint64_t sub_len = read_uleb(wasm.data(), pos);
size_t sub_end = pos + sub_len;
if(sub == 1) // WASM_DYLINK_MEM_INFO
{
info.mem_size = read_uleb(wasm.data(), pos);
info.mem_align = read_uleb(wasm.data(), pos);
info.table_size = read_uleb(wasm.data(), pos);
info.table_align = read_uleb(wasm.data(), pos);
info.found = true;
}
pos = sub_end;
}
}
}
pos = end;
}
return info;
}
// ---- named trap stubs for unsatisfied (WASI) imports --------------------
static wasm_store_t* g_store_for_stubs = nullptr;
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
{
(void)args; (void)results;
fprintf(stderr, "[stub called: %s]\n", (const char*)env);
char msg[256];
snprintf(msg, sizeof(msg), "unimplemented host import called: %s", (const char*)env);
wasm_message_t message;
wasm_byte_vec_new(&message, strlen(msg) + 1, msg);
wasm_trap_t* trap = wasm_trap_new(g_store_for_stubs, &message);
wasm_byte_vec_delete(&message);
return trap;
}
// ---- instance wrapper: name → extern map --------------------------------
struct Instance
{
wasm_module_t* module = nullptr;
wasm_instance_t* instance = nullptr;
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
std::map<std::string, wasm_extern_t*> by_name;
void index_exports()
{
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
wasm_module_exports(module, &types);
wasm_instance_exports(instance, &exports);
CHECK(types.size == exports.size, "export type/extern count mismatch");
for(size_t i = 0; i < types.size; i++)
{
const wasm_name_t* nm = wasm_exporttype_name(types.data[i]);
std::string key(nm->data, nm->size);
// some wasm-c-api impls include the trailing NUL in name size
while(!key.empty() && key.back() == '\0')
key.pop_back();
by_name[key] = exports.data[i];
}
wasm_exporttype_vec_delete(&types);
}
wasm_func_t* func(const char* name)
{
auto it = by_name.find(name);
return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second);
}
wasm_global_t* global(const char* name)
{
auto it = by_name.find(name);
return it == by_name.end() ? nullptr : wasm_extern_as_global(it->second);
}
};
static wasm_store_t* g_store = nullptr;
static void report_trap(wasm_trap_t* trap, const char* what)
{
if(!trap)
return;
wasm_message_t msg;
wasm_trap_message(trap, &msg);
FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data);
}
static int32_t call_i32(Instance& inst, const char* name, std::vector<int32_t> argv = {})
{
wasm_func_t* f = inst.func(name);
CHECK(f, "missing export func %s", name);
wasm_val_t args_buf[4];
for(size_t i = 0; i < argv.size(); i++)
args_buf[i] = WASM_I32_VAL(argv[i]);
wasm_val_t results_buf[1] = { WASM_INIT_VAL };
wasm_val_vec_t args = { argv.size(), args_buf };
wasm_val_vec_t results = { 1, results_buf };
size_t result_arity = wasm_func_result_arity(f);
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
wasm_trap_t* trap = wasm_func_call(f, &args, result_arity ? &results : &no_results);
report_trap(trap, name);
return result_arity ? results_buf[0].of.i32 : 0;
}
static wasm_global_t* make_i32_global(int32_t value, wasm_mutability_t mut)
{
wasm_globaltype_t* gt = wasm_globaltype_new(wasm_valtype_new(WASM_I32), mut);
wasm_val_t val = WASM_I32_VAL(value);
wasm_global_t* g = wasm_global_new(g_store, gt, &val);
CHECK(g, "wasm_global_new failed (host-created globals unsupported?)");
wasm_globaltype_delete(gt);
return g;
}
int main(int argc, char** argv)
{
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase0/core.wasm";
const char* unit_path = argc > 2 ? argv[2] : "/tmp/uce/wasm-phase0/unit.wasm";
wasm_engine_t* engine = wasm_engine_new();
CHECK(engine, "engine");
g_store = wasm_store_new(engine);
CHECK(g_store, "store");
g_store_for_stubs = g_store;
// ---- 1. core module ---------------------------------------------------
std::vector<uint8_t> core_bytes = read_file(core_path);
wasm_byte_vec_t core_bv;
wasm_byte_vec_new(&core_bv, core_bytes.size(), (const char*)core_bytes.data());
Instance core;
core.module = wasm_module_new(g_store, &core_bv);
wasm_byte_vec_delete(&core_bv);
CHECK(core.module, "core module load failed");
// the shared funcref table is host-created (core links with --import-table)
// because WAMR cannot grow a table from the host: size it up front as
// core's declared minimum (= its own elem needs) plus headroom for unit
// module table regions. __table_base allocation bumps from the minimum.
wasm_table_t* table = nullptr;
uint32_t table_next_free = 0;
const uint32_t TABLE_HEADROOM = 2048;
wasm_importtype_vec_t core_imports = WASM_EMPTY_VEC;
wasm_module_imports(core.module, &core_imports);
std::vector<wasm_extern_t*> core_import_externs(core_imports.size);
for(size_t i = 0; i < core_imports.size; i++)
{
const wasm_name_t* mod = wasm_importtype_module(core_imports.data[i]);
const wasm_name_t* nm = wasm_importtype_name(core_imports.data[i]);
const wasm_externtype_t* et = wasm_importtype_type(core_imports.data[i]);
std::string name(nm->data, nm->size);
if(wasm_externtype_kind(et) == WASM_EXTERN_TABLE)
{
CHECK(name.rfind("__indirect_function_table", 0) == 0,
"unexpected core table import %s", name.c_str());
const wasm_tabletype_t* tt = wasm_externtype_as_tabletype_const(et);
uint32_t core_min = wasm_tabletype_limits(tt)->min;
wasm_limits_t limits = { core_min + TABLE_HEADROOM, core_min + TABLE_HEADROOM };
wasm_tabletype_t* host_tt = wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &limits);
table = wasm_table_new(g_store, host_tt, nullptr);
CHECK(table, "wasm_table_new failed (host-created tables unsupported?)");
wasm_tabletype_delete(host_tt);
table_next_free = core_min;
printf("host table created: size=%u, core region=[0,%u)\n", core_min + TABLE_HEADROOM, core_min);
core_import_externs[i] = wasm_table_as_extern(table);
continue;
}
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC,
"core has unexpected non-func import %.*s.%s",
(int)mod->size, mod->data, name.c_str());
// named trap stub; leaks the name string, fine for a spike
char* label = strdup((std::string(mod->data, mod->size) + "." + name).c_str());
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
wasm_func_t* stub = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr);
CHECK(stub, "stub func creation failed for %s", label);
core_import_externs[i] = wasm_func_as_extern(stub);
}
CHECK(table, "core does not import __indirect_function_table — rebuild with --import-table");
wasm_extern_vec_t core_iv = { core_import_externs.size(), core_import_externs.data() };
wasm_trap_t* trap = nullptr;
core.instance = wasm_instance_new(g_store, core.module, &core_iv, &trap);
report_trap(trap, "core instantiation");
CHECK(core.instance, "core instantiation failed");
core.index_exports();
printf("core instantiated: %zu exports\n", core.by_name.size());
// reactor init (runs ctors). Wasmtime does not auto-run _initialize;
// WAMR does — and calling it twice trips wasi-libc's double-init guard
// (__builtin_trap → "unreachable"), which cost us a debugging round.
if(core.func("_initialize"))
call_i32(core, "_initialize");
wasm_memory_t* memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr);
CHECK(memory, "core does not export memory");
// ---- 2./3. dylink + base allocation ------------------------------------
std::vector<uint8_t> unit_bytes = read_file(unit_path);
DylinkInfo dl = parse_dylink(unit_bytes);
CHECK(dl.found, "unit has no dylink.0 mem_info");
printf("dylink.0: memsize=%u memalign=2^%u tablesize=%u\n", dl.mem_size, dl.mem_align, dl.table_size);
uint32_t align = 1u << dl.mem_align;
int32_t raw = call_i32(core, "malloc", { (int32_t)(dl.mem_size + align) });
CHECK(raw, "core malloc returned 0");
int32_t memory_base = (raw + (align - 1)) & ~(int32_t)(align - 1);
uint32_t table_base = table_next_free;
table_next_free += dl.table_size;
CHECK(table_next_free <= wasm_table_size(table), "table headroom exhausted");
printf("bases: __memory_base=%d __table_base=%u (table size %u)\n",
memory_base, table_base, wasm_table_size(table));
// ---- 4. unit import resolution -----------------------------------------
wasm_byte_vec_t unit_bv;
wasm_byte_vec_new(&unit_bv, unit_bytes.size(), (const char*)unit_bytes.data());
Instance unit;
unit.module = wasm_module_new(g_store, &unit_bv);
wasm_byte_vec_delete(&unit_bv);
CHECK(unit.module, "unit module load failed");
wasm_importtype_vec_t unit_imports = WASM_EMPTY_VEC;
wasm_module_imports(unit.module, &unit_imports);
std::vector<wasm_extern_t*> unit_import_externs(unit_imports.size);
// GOT.mem entries that must be self-resolved from the unit's own exports
// after instantiation (weak data the core does not define)
std::vector<std::pair<std::string, wasm_global_t*>> deferred_got;
for(size_t i = 0; i < unit_imports.size; i++)
{
const wasm_name_t* mod_n = wasm_importtype_module(unit_imports.data[i]);
const wasm_name_t* nm_n = wasm_importtype_name(unit_imports.data[i]);
std::string mod(mod_n->data, mod_n->size);
std::string nm(nm_n->data, nm_n->size);
const wasm_externtype_t* et = wasm_importtype_type(unit_imports.data[i]);
wasm_externkind_t kind = wasm_externtype_kind(et);
wasm_extern_t* resolved = nullptr;
if(mod == "env" && nm == "memory")
resolved = wasm_memory_as_extern(memory);
else if(mod == "env" && nm == "__indirect_function_table")
resolved = wasm_table_as_extern(table);
else if(mod == "env" && nm == "__stack_pointer")
{
CHECK(core.global("__stack_pointer"), "core does not export __stack_pointer");
resolved = core.by_name["__stack_pointer"];
}
else if(mod == "env" && nm == "__memory_base")
resolved = wasm_global_as_extern(make_i32_global(memory_base, WASM_CONST));
else if(mod == "env" && nm == "__table_base")
resolved = wasm_global_as_extern(make_i32_global((int32_t)table_base, WASM_CONST));
else if(mod == "env" && kind == WASM_EXTERN_FUNC)
{
auto it = core.by_name.find(nm);
CHECK(it != core.by_name.end(), "unresolved unit func import env.%s", nm.c_str());
resolved = it->second;
}
else if(mod == "GOT.mem")
{
wasm_global_t* src = core.global(nm.c_str());
if(src)
{
wasm_val_t v;
wasm_global_get(src, &v);
resolved = wasm_global_as_extern(make_i32_global(v.of.i32, WASM_VAR));
}
else
{
// provisional 0; patched from the unit's own export post-instantiation
wasm_global_t* g = make_i32_global(0, WASM_VAR);
deferred_got.push_back({ nm, g });
resolved = wasm_global_as_extern(g);
}
}
else if(mod == "GOT.func")
{
// resolved guest-side: the core exports a helper returning
// (intptr_t)&func, which on wasm is the function's table index —
// no host-side funcref injection needed (WAMR forbids it anyway)
std::string helper = "core_table_index_of_" + nm;
CHECK(core.func(helper.c_str()), "no GOT.func resolver %s in core", helper.c_str());
int32_t slot = call_i32(core, helper.c_str());
CHECK(slot > 0, "GOT.func resolver %s returned %d", helper.c_str(), slot);
resolved = wasm_global_as_extern(make_i32_global(slot, WASM_VAR));
}
CHECK(resolved, "unhandled unit import %s.%s (kind %d)", mod.c_str(), nm.c_str(), (int)kind);
unit_import_externs[i] = resolved;
}
// ---- 5. instantiate unit, patch GOT, run init ---------------------------
wasm_extern_vec_t unit_iv = { unit_import_externs.size(), unit_import_externs.data() };
trap = nullptr;
unit.instance = wasm_instance_new(g_store, unit.module, &unit_iv, &trap);
report_trap(trap, "unit instantiation");
CHECK(unit.instance, "unit instantiation failed");
unit.index_exports();
printf("unit instantiated: %zu exports\n", unit.by_name.size());
for(auto& [nm, got] : deferred_got)
{
wasm_global_t* own = unit.global(nm.c_str());
CHECK(own, "GOT.mem.%s defined neither by core nor by unit", nm.c_str());
wasm_val_t v;
wasm_global_get(own, &v);
// dylink ABI: a PIC module's exported data symbols are offsets relative
// to its __memory_base; the linker must add the base when resolving
wasm_val_t nv = WASM_I32_VAL(memory_base + v.of.i32);
wasm_global_set(got, &nv);
printf("self-resolved GOT.mem.%s = %d (offset %d)\n", nm.c_str(), memory_base + v.of.i32, v.of.i32);
}
if(unit.func("__wasm_apply_data_relocs"))
call_i32(unit, "__wasm_apply_data_relocs");
if(unit.func("__wasm_call_ctors"))
call_i32(unit, "__wasm_call_ctors");
// ---- 6. render and read back --------------------------------------------
int32_t counter_addr = 0;
{
wasm_global_t* cc = core.global("core_counter");
CHECK(cc, "core_counter not exported");
wasm_val_t v;
wasm_global_get(cc, &v);
counter_addr = v.of.i32;
}
byte_t* mem = wasm_memory_data(memory);
int32_t counter_before;
memcpy(&counter_before, mem + counter_addr, 4);
call_i32(unit, "uce_unit_render");
int32_t out_ptr = call_i32(core, "core_output_data");
int32_t out_len = call_i32(core, "core_output_size");
mem = wasm_memory_data(memory); // may have moved if memory grew
int32_t counter_after;
memcpy(&counter_after, mem + counter_addr, 4);
printf("---- unit output (%d bytes) ----\n%.*s\n--------------------------------\n",
out_len, out_len, mem + out_ptr);
printf("core_counter (in linear memory): before=%d after=%d\n", counter_before, counter_after);
bool ok = out_len > 0 &&
memmem(mem + out_ptr, out_len, "unit-data-segment-ok", 20) &&
memmem(mem + out_ptr, out_len, "counter=7", 9) &&
memmem(mem + out_ptr, out_len, "mapsum=3", 8) &&
memmem(mem + out_ptr, out_len, "core-string+unit", 16) &&
memmem(mem + out_ptr, out_len, "[cb:42]", 7) &&
memmem(mem + out_ptr, out_len, "[got-func-ok]", 13) &&
memmem(mem + out_ptr, out_len, "[fn:42]", 7) &&
counter_before == 7 && counter_after == 8;
printf(ok ? "PHASE0 EXIT CRITERION: PASS\n" : "PHASE0 EXIT CRITERION: FAIL (see output above)\n");
return ok ? 0 : 1;
}

View File

@ -1,165 +0,0 @@
# UCE real generated unit -> WASM PIC side module report
Remote host: `k-uce`
Working directory/artifacts left in place: `/tmp/uce/wasm-phase0/realunit/`
Repository `/Code/uce.openfu.com/uce` was treated read-only; patched copies/shims only under `/tmp/uce/wasm-phase0/realunit/`.
## Result
Both real generated units compiled and linked as WASM PIC side modules with `dylink.0`:
- `collections.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/collections.wasm`
- `dylink.0`: `memsize=1208 memalign=2^2 tablesize=3 tablealign=2^0`
- imports: 52
- exports: 23
- allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`).
- `hello.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/hello.wasm`
- `dylink.0`: `memsize=5544 memalign=2^2 tablesize=0 tablealign=2^0`
- imports: 38
- exports: 24
- allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`).
Inspector outputs are saved remotely as:
- `/tmp/uce/wasm-phase0/realunit/collections.inspect.txt`
- `/tmp/uce/wasm-phase0/realunit/hello.inspect.txt`
## Final command lines
Run on `k-uce` from `/tmp/uce/wasm-phase0/realunit`.
`collections`:
```sh
/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \
-I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \
-c collections.uce.cpp -o collections.o
/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \
collections.o -o collections.wasm --export=__uce_render
python3 /Code/uce.openfu.com/uce/spikes/wasm-phase0/wasm_inspect.py collections.wasm
```
`hello`:
```sh
/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \
-I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \
-c hello.uce.cpp -o hello.o
/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \
hello.o -o hello.wasm --export=__uce_render
```
Note: the stub-spike command exported `uce_unit_render`, but these generated units define/export `__uce_render` from the repo `RENDER` macro, so the real-unit link exports `__uce_render`.
## Ordered friction points and fixes
1. **WASI sysroot rejects `<signal.h>` by default.**
First compile failed via `/Code/uce.openfu.com/uce/src/lib/sys.h`:
```txt
/share/wasi-sysroot/include/wasm32-wasip1/signal.h:2:2: error:
"wasm lacks signal support; to enable minimal signal emulation, compile with -D_WASI_EMULATED_SIGNAL ..."
```
Fix: added `/tmp/uce/wasm-phase0/realunit/shim/signal.h` before repo/system includes. Key lines:
```c
typedef int sig_atomic_t;
typedef void (*sighandler_t)(int);
#define SIGTERM 15
int raise(int);
sighandler_t signal(int, sighandler_t);
```
This is only enough for declarations in `sys.h`; real signal behavior has no WASI equivalent.
2. **Generated `collections.uce.cpp` includes missing local `demo_guard.h`.**
First compile also failed:
```txt
fatal error: 'demo_guard.h' file not found
```
Fix: added empty include guard shim `/tmp/uce/wasm-phase0/realunit/shim/demo_guard.h`:
```c
#ifndef UCE_WASM_SHIM_DEMO_GUARD_H
#define UCE_WASM_SHIM_DEMO_GUARD_H
#endif
```
3. **Generated source uses an absolute repo include, preventing header interposition.**
Original generated line:
```c++
#include "/Code/uce.openfu.com/uce/src/lib/uce_lib.h"
```
To use patched shim headers without touching `/Code`, copied the generated units into the workdir and changed only that line to:
```c++
#include "uce_lib.h"
```
Copies are `/tmp/uce/wasm-phase0/realunit/collections.uce.cpp` and `hello.uce.cpp`.
4. **`types.h` defines global `operator new/delete` in every unit.**
A straight build linked, but exported allocator definitions (`_Znwm`, `_ZdlPv`), violating the side-module allocator rule. The repo code in `types.h` contains:
```c++
void * operator new(decltype(sizeof(0)) n) noexcept(false) { ... malloc(n) ... }
void operator delete(void * p) throw() { free(p); }
```
Fix: copied `uce_lib.h` and `types.h` to the shim tree, then patched only the copied `shim/types.h` allocator block to declarations:
```c++
// WASM side-module shim: allocator must be provided by the host/core module,
// not defined in each generated unit. Keep declarations only.
void * operator new(decltype(sizeof(0)) n) noexcept(false);
void operator delete(void * p) throw();
```
Verification from `llvm-nm -C collections.o`:
```txt
U operator delete(void*, unsigned long)
U operator new(unsigned long)
```
## Notable imports / GOT entries
`collections.wasm` notable imports:
- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.__stack_pointer`, `env.memory`, `env.__indirect_function_table`
- UCE/runtime functions: `DValue::set`, `DValue::operator[]`, `DValue::push`, `dv_filter`, `dv_map`, `dv_group_by`, `list_unique`, `list_sort`, `replace`, `to_upper`, `html_escape`, `json_encode`, `var_dump`, `time`
- MySQL wrappers are imported even for this unit because included header inline functions reference `MySQL` methods.
- GOT.mem globals:
- `GOT.mem.context`
- `GOT.mem._ZNSt3__25ctypeIcE2idE`
- `GOT.mem._ZTVN10__cxxabiv117__class_type_infoE`
`hello.wasm` notable imports:
- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.memcmp`
- UCE/runtime functions: `time`, `html_escape`, `html_escapey`, `var_dump`
- GOT.mem globals:
- `GOT.mem.context`
- `GOT.mem._ZNSt3__219piecewise_constructE`
- `GOT.mem._ZNSt3__25ctypeIcE2idE`
## Phase 2 land mines
- `src/lib/sys.h` includes `<signal.h>` and declares signals/tasks/socket/process/file-lock APIs. WASI has no normal POSIX signals, fork/exec, traditional sockets, or process model. These need real repo-level `#ifdef __wasm__` / runtime boundary design, not local shims.
- `types.h` defines global allocator operators in a header. For side modules this must move to the core/runtime or be gated out under WASM side-module builds; otherwise each unit defines its own allocator entry points.
- Generated units include `/Code/.../uce_lib.h` by absolute path. That makes cross-target header selection brittle. The generator should emit a logical include (`"uce_lib.h"`) and the build should supply target-specific include order.
- Header-defined helper functions cause many exports from each unit (`to_string`, MySQL wrapper functions, vector slow-path/lambda helpers, globals `parent_pid`, `my_pid`, `context`). For a clean side-module ABI, these should probably be hidden, moved out of headers, marked inline/static where appropriate, or provided by the core module.
- MySQL inline wrappers are pulled into every unit by `uce_lib.h` even when the unit does not use MySQL directly. This is why `MySQL::connect/disconnect/query/error` imports appear in both outputs.
- Kept `-fno-exceptions`; no try/catch blocker was hit for these two generated units.

View File

@ -1,73 +0,0 @@
// WASM-PROPOSAL Phase 0 — unit module stub.
//
// Stands in for a generated UCE unit: compiled with -fPIC and linked with
// wasm-ld -shared into a PIC module carrying a dylink.0 section. Defines no
// allocator and links no libc — operator new/delete, memcpy, and the core
// API all arrive as imports resolved by the loader against the core module
// (§5.4 unit module contract).
//
// Exercises, in one render call:
// - unit-local data segment with relocation (__memory_base placement)
// - GOT.mem read+write of a core-defined global (core_counter)
// - C++ containers (std::string/std::map) on the core's heap
// - heap object created in core, mutated and read from the unit
// - function pointer from unit through core and back (shared table)
// - std::function/lambda handed across the module boundary
#include <string>
#include <functional>
#include <map>
extern "C" {
void uce_print(const char* s, size_t len);
extern int core_counter;
std::string* core_make_string(const char* s);
void core_append_string(std::string* str, const char* s);
void core_invoke_callback(void (*cb)(int), int arg);
void core_invoke_function(std::function<int(int)>* f, int arg);
}
static const char* unit_static_message = "unit-data-segment-ok";
static int unit_state = 41;
static void my_callback(int x)
{
std::string s = "[cb:" + std::to_string(x + unit_state) + "]";
uce_print(s.data(), s.size());
}
extern "C" void uce_unit_render()
{
std::string out = "hello from unit; ";
out += unit_static_message;
out += "; counter=" + std::to_string(core_counter);
uce_print(out.data(), out.size());
std::map<std::string, int> m;
m["a"] = 1;
m["b"] = 2;
int sum = 0;
for(auto& kv : m)
sum += kv.second;
std::string s2 = "; mapsum=" + std::to_string(sum);
uce_print(s2.data(), s2.size());
std::string* cs = core_make_string("; core-string");
core_append_string(cs, "+unit");
uce_print(cs->data(), cs->size());
delete cs;
core_invoke_callback(my_callback, 1);
// address of a core-defined function taken in the unit → GOT.func import;
// the loader must ensure the core function has a funcref table entry.
// volatile so the optimizer cannot fold it back into a direct call.
void (*volatile print_ptr)(const char*, size_t) = uce_print;
print_ptr("[got-func-ok]", 13);
auto* fn = new std::function<int(int)>([](int v) { return(v * 3); });
core_invoke_function(fn, 14);
delete fn;
core_counter++;
}

View File

@ -1,95 +0,0 @@
#!/usr/bin/env python3
"""Phase 0 helper: dump a wasm module's imports, exports, and dylink.0
custom section (memory/table size requirements). Doubles as the reference
for the loader's dylink.0 parsing (§6 step 4)."""
import sys, struct
def uleb(buf, pos):
result = 0
shift = 0
while True:
b = buf[pos]
pos += 1
result |= (b & 0x7f) << shift
if not (b & 0x80):
return result, pos
shift += 7
def name(buf, pos):
n, pos = uleb(buf, pos)
return buf[pos:pos+n].decode("utf-8", "replace"), pos + n
def limits(buf, pos):
flags = buf[pos]; pos += 1
mn, pos = uleb(buf, pos)
mx = None
if flags & 1:
mx, pos = uleb(buf, pos)
return (mn, mx), pos
KIND = {0: "func", 1: "table", 2: "memory", 3: "global"}
VALTYPE = {0x7f: "i32", 0x7e: "i64", 0x7d: "f32", 0x7c: "f64", 0x70: "funcref", 0x6f: "externref"}
def main(fn):
buf = open(fn, "rb").read()
assert buf[:8] == b"\0asm\x01\0\0\0", "not a wasm module"
pos = 8
while pos < len(buf):
sec_id = buf[pos]; pos += 1
size, pos = uleb(buf, pos)
end = pos + size
if sec_id == 0:
sname, p = name(buf, pos)
if sname == "dylink.0":
print("== dylink.0 ==")
while p < end:
sub = buf[p]; p += 1
sublen, p = uleb(buf, p)
subend = p + sublen
if sub == 1: # WASM_DYLINK_MEM_INFO
memsize, p2 = uleb(buf, p)
memalign, p2 = uleb(buf, p2)
tabsize, p2 = uleb(buf, p2)
tabalign, p2 = uleb(buf, p2)
print(f" mem_info: memsize={memsize} memalign=2^{memalign} tablesize={tabsize} tablealign=2^{tabalign}")
else:
print(f" subsection type={sub} len={sublen}")
p = subend
elif sname in ("uce.abi",):
print(f"== custom section {sname} ({end - p} bytes) ==")
elif sec_id == 2:
count, p = uleb(buf, pos)
print(f"== imports ({count}) ==")
for _ in range(count):
mod, p = name(buf, p)
nm, p = name(buf, p)
kind = buf[p]; p += 1
detail = ""
if kind == 0:
_, p = uleb(buf, p)
elif kind == 1:
et = buf[p]; p += 1
lim, p = limits(buf, p)
detail = f" {VALTYPE.get(et, hex(et))} {lim}"
elif kind == 2:
lim, p = limits(buf, p)
detail = f" {lim}"
elif kind == 3:
vt = buf[p]; p += 1
mut = buf[p]; p += 1
detail = f" {VALTYPE.get(vt, hex(vt))}{' mut' if mut else ''}"
print(f" {KIND.get(kind, kind):6} {mod}.{nm}{detail}")
elif sec_id == 7:
count, p = uleb(buf, pos)
print(f"== exports ({count}) ==")
for _ in range(count):
nm, p = name(buf, p)
kind = buf[p]; p += 1
_, p = uleb(buf, p)
print(f" {KIND.get(kind, kind):6} {nm}")
pos = end
if __name__ == "__main__":
for fn in sys.argv[1:]:
print(f"### {fn}")
main(fn)

View File

@ -1,51 +0,0 @@
# spikes/wasm-phase2 — core module + membrane scaffold
Phase 2 validates the next WASM step without the Phase 3 dynamic loader. The
scaffold compiles a native UCE core subset (`Request`, `DValue`, UCEB1, print
buffering) plus one statically linked real `.uce` page into a WASM reactor.
A small Wasmtime C-API host provides the first membrane hostcalls, sends a UCEB1
request context into the guest, invokes render, and reads the response from
linear memory. `uce_host_ctx_read(ptr, cap)` follows a length-query contract:
`ptr == 0` or `cap == 0` returns the required length; a short non-zero buffer
also returns the required length without a partial copy; an adequately sized
buffer receives the full context and returns the copied length.
This is intentionally not the final worker. It proves the Phase 2 membrane path:
core-owned DValue/UCEB1 code runs in WASM and a `.uce` render entry sees a
host-provided request context through the membrane. The checked-in page uses the
same `RENDER(Request&)` entry shape as generated units, but it is included
directly by the scaffold rather than emitted by the UCE preprocessor.
Run on `k-uce`:
```bash
bash spikes/wasm-phase2/build_modules.sh
bash spikes/wasm-phase2/build_loader.sh
/tmp/uce/wasm-phase2/loader
```
Expected final line:
```text
PHASE2 EXIT CRITERION: PASS
```
Files:
- `core.cpp` — WASM reactor core subset and membrane decode/render entry.
- `page.uce` — real UCE page statically linked for Phase 2 only.
- `loader.cpp` — host runner with `uce_host_ctx_read` and `uce_host_log`.
Its small host-side UCEB1 encoder is spike-only; the production UCE server
host should use the native `ucb_encode()` implementation to avoid format drift.
- `build_modules.sh` — wasi-sdk build for `/tmp/uce/wasm-phase2/core.wasm`.
- `build_loader.sh` — Wasmtime C-API build for `/tmp/uce/wasm-phase2/loader`.
Deferred to Phase 3:
- PIC side modules and dylink loader integration.
- Lazy unit/component loading.
- Full `uce_lib` hostcall surface beyond the minimal context/log membrane.
- Generator-emitted units (`.uce` preprocessor output with literal markup,
`__uce_set_current_request`, and generated includes) rendering through this
membrane; Phase 0 only proved those generated units can compile as side
modules.

View File

@ -1,16 +0,0 @@
#!/bin/bash
# Phase 2: build the minimal host/membrane runner against Wasmtime's wasm-c-api.
set -e
cd "$(dirname "$0")"
OUT=/tmp/uce/wasm-phase2
mkdir -p "$OUT"
c++ -std=c++17 -O2 loader.cpp \
-I/opt/wasmtime/include \
-L/opt/wasmtime/lib \
-Wl,-rpath,/opt/wasmtime/lib \
-lwasmtime \
-o "$OUT/loader"
echo "built: $OUT/loader"

View File

@ -1,21 +0,0 @@
#!/bin/bash
# Phase 2: build the native DValue/UCEB1 core subset plus one statically
# linked real .uce page as a WASM reactor. Runs on k-uce; artifacts go under
# /tmp/uce/wasm-phase2.
set -e
cd "$(dirname "$0")"
SDK=/opt/wasi-sdk
OUT=/tmp/uce/wasm-phase2
mkdir -p "$OUT"
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
-O1 -fno-exceptions \
-I../.. -I../../src/lib \
core.cpp -o "$OUT/core.wasm" \
-Wl,--export-all \
-Wl,--export=__heap_base \
-Wl,--allow-undefined-file=hostcalls.syms
echo "--- phase2 core.wasm ---"
ls -la "$OUT/core.wasm"

View File

@ -1,142 +0,0 @@
// WASM-PROPOSAL Phase 2 — native UCE core subset compiled to WASM.
//
// This scaffold validates the Phase 2 membrane without the Phase 3 dynamic
// loader: the core owns memory/libc++/DValue/UCEB1, imports a tiny hostcall
// surface, decodes a host-provided UCEB1 request context, and invokes one
// statically linked real .uce page.
#include <cstdlib>
#include <cstring>
#include "../../src/lib/types.h"
#include "../../src/lib/dvalue.cpp"
extern "C" {
size_t uce_host_ctx_read(char* buf, size_t cap);
void uce_host_log(int level, const char* buf, size_t len);
}
#define RENDER(X) extern "C" void __uce_render(X)
#include "page.uce"
static Request g_request;
static ByteStream g_ob;
static String g_output;
SharedUnit::~SharedUnit() {}
String nibble(String div, String& haystack)
{
auto pos = haystack.find(div);
if(pos == String::npos)
{
auto result = haystack;
haystack.clear();
return(result);
}
auto result = haystack.substr(0, pos);
haystack.erase(0, pos + div.length());
return(result);
}
void Request::ob_start()
{
ob_stack.push_back(new ByteStream());
ob = ob_stack.back();
}
void Request::set_status(s32 code, String reason)
{
if(reason == "")
reason = code == 200 ? "OK" : "Status";
response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason;
}
Request::~Request()
{
for(auto* stream : ob_stack)
delete stream;
ob_stack.clear();
}
static void phase2_clear_ob_stack()
{
for(auto* stream : g_request.ob_stack)
delete stream;
g_request.ob_stack.clear();
}
static void phase2_apply_context(DValue& root)
{
g_request.call = root;
g_request.params.clear();
DValue* params = root.key("params");
if(params)
{
params->each([&](const DValue& item, String key) {
g_request.params[key] = item.to_string();
});
}
}
extern "C" {
void* uce_alloc(size_t len)
{
return(malloc(len));
}
void uce_free(void* ptr)
{
free(ptr);
}
int uce_phase2_render()
{
context = &g_request;
phase2_clear_ob_stack();
g_ob.str("");
g_ob.clear();
g_request.ob = &g_ob;
g_request.out = "";
g_output = "";
size_t ctx_required = uce_host_ctx_read(0, 0);
if(ctx_required == 0)
return(10);
char* ctx_buf = (char*)malloc(ctx_required);
if(ctx_buf == 0)
return(11);
size_t ctx_len = uce_host_ctx_read(ctx_buf, ctx_required);
if(ctx_len != ctx_required)
{
free(ctx_buf);
return(12);
}
DValue decoded;
String error;
bool ok = ucb_decode(String(ctx_buf, ctx_len), decoded, &error);
free(ctx_buf);
if(!ok)
{
uce_host_log(3, error.data(), error.size());
return(20);
}
phase2_apply_context(decoded);
__uce_render(g_request);
g_output = g_ob.str();
return(0);
}
const char* uce_phase2_output_data()
{
return(g_output.data());
}
size_t uce_phase2_output_size()
{
return(g_output.size());
}
}

View File

@ -1,2 +0,0 @@
uce_host_ctx_read
uce_host_log

View File

@ -1,280 +0,0 @@
// WASM-PROPOSAL Phase 2 — minimal membrane host.
// Instantiates the statically linked core/page module, serves a UCEB1 request
// context via hostcall, invokes render, and reads the response from guest
// memory. Dynamic linking is intentionally Phase 3 work.
#include <wasm.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <vector>
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
static wasm_store_t* g_store = nullptr;
static wasm_memory_t* g_memory = nullptr;
static std::string g_context;
static std::vector<uint8_t> read_file(const char* fn)
{
FILE* f = fopen(fn, "rb");
CHECK(f, "cannot open %s", fn);
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
std::vector<uint8_t> buf(n);
CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn);
fclose(f);
return buf;
}
static void append_varuint(std::string& out, uint64_t value)
{
while(value >= 0x80)
{
out.push_back((char)((value & 0x7f) | 0x80));
value >>= 7;
}
out.push_back((char)value);
}
struct Node
{
std::string scalar;
bool is_list = false;
std::vector<std::pair<std::string, Node>> children;
};
static Node scalar(const char* value)
{
Node n;
n.scalar = value;
return n;
}
static void encode_node(std::string& out, const Node& node)
{
out.push_back(node.is_list ? 1 : 0);
append_varuint(out, node.scalar.size());
out.append(node.scalar);
append_varuint(out, node.children.size());
for(const auto& child : node.children)
{
append_varuint(out, child.first.size());
out.append(child.first);
encode_node(out, child.second);
}
}
static std::string make_context()
{
Node params;
params.children.push_back({"HTTP_HOST", scalar("phase2.example.test")});
params.children.push_back({"SCRIPT_URL", scalar("/spikes/wasm-phase2/page.uce")});
Node nested;
nested.children.push_back({"answer", scalar("42")});
Node root;
root.children.push_back({"params", params});
root.children.push_back({"route", scalar("/spikes/wasm-phase2/page.uce")});
root.children.push_back({"nested", nested});
std::string out = "UCEB";
out.push_back((char)1);
encode_node(out, root);
return out;
}
static wasm_trap_t* host_ctx_read(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
{
(void)env;
uint32_t ptr = args->data[0].of.i32;
uint32_t cap = args->data[1].of.i32;
if(ptr == 0 || cap == 0 || cap < g_context.size())
{
results->data[0] = WASM_I32_VAL((int32_t)g_context.size());
return nullptr;
}
CHECK(g_memory, "host ctx_read called before memory export was captured");
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
size_t mem_size = wasm_memory_data_size(g_memory);
CHECK((size_t)ptr <= mem_size, "ctx_read pointer outside memory");
CHECK((size_t)ptr + g_context.size() <= mem_size, "ctx_read buffer outside memory");
memcpy(mem + ptr, g_context.data(), g_context.size());
results->data[0] = WASM_I32_VAL((int32_t)g_context.size());
return nullptr;
}
static wasm_trap_t* host_log(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
{
(void)env; (void)results;
int level = args->data[0].of.i32;
uint32_t ptr = args->data[1].of.i32;
uint32_t len = args->data[2].of.i32;
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
size_t mem_size = wasm_memory_data_size(g_memory);
if((size_t)ptr + len <= mem_size)
fprintf(stderr, "[guest log %d] %.*s\n", level, (int)len, (const char*)mem + ptr);
return nullptr;
}
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
{
(void)args; (void)results;
char msg[256];
snprintf(msg, sizeof(msg), "unimplemented import called: %s", (const char*)env);
wasm_message_t message;
wasm_byte_vec_new(&message, strlen(msg) + 1, msg);
wasm_trap_t* trap = wasm_trap_new(g_store, &message);
wasm_byte_vec_delete(&message);
return trap;
}
static wasm_func_t* make_func(wasm_store_t* store, std::vector<wasm_valkind_t> params, std::vector<wasm_valkind_t> results, wasm_func_callback_with_env_t cb, void* env = nullptr)
{
wasm_valtype_vec_t ps;
wasm_valtype_vec_new_uninitialized(&ps, params.size());
for(size_t i = 0; i < params.size(); i++)
ps.data[i] = wasm_valtype_new(params[i]);
wasm_valtype_vec_t rs;
wasm_valtype_vec_new_uninitialized(&rs, results.size());
for(size_t i = 0; i < results.size(); i++)
rs.data[i] = wasm_valtype_new(results[i]);
wasm_functype_t* ft = wasm_functype_new(&ps, &rs);
wasm_func_t* fn = wasm_func_new_with_env(store, ft, cb, env, nullptr);
wasm_functype_delete(ft);
return fn;
}
struct Instance
{
wasm_module_t* module = nullptr;
wasm_instance_t* instance = nullptr;
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
std::map<std::string, wasm_extern_t*> by_name;
void index_exports()
{
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
wasm_module_exports(module, &types);
wasm_instance_exports(instance, &exports);
for(size_t i = 0; i < types.size; i++)
{
const wasm_name_t* nm = wasm_exporttype_name(types.data[i]);
std::string key(nm->data, nm->size);
while(!key.empty() && key.back() == '\0') key.pop_back();
by_name[key] = exports.data[i];
}
wasm_exporttype_vec_delete(&types);
}
wasm_func_t* func(const char* name)
{
auto it = by_name.find(name);
return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second);
}
};
static int32_t call_i32(Instance& inst, const char* name)
{
wasm_func_t* f = inst.func(name);
CHECK(f, "missing export func %s", name);
wasm_val_t result_buf[1] = { WASM_INIT_VAL };
wasm_val_vec_t args = WASM_EMPTY_VEC;
wasm_val_vec_t results = { wasm_func_result_arity(f), result_buf };
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
wasm_trap_t* trap = wasm_func_call(f, &args, results.size ? &results : &no_results);
if(trap)
{
wasm_message_t msg;
wasm_trap_message(trap, &msg);
FAIL("trap during %s: %.*s", name, (int)msg.size, msg.data);
}
return results.size ? result_buf[0].of.i32 : 0;
}
int main(int argc, char** argv)
{
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase2/core.wasm";
g_context = make_context();
wasm_engine_t* engine = wasm_engine_new();
CHECK(engine, "engine");
g_store = wasm_store_new(engine);
CHECK(g_store, "store");
std::vector<uint8_t> bytes = read_file(core_path);
wasm_byte_vec_t bv;
wasm_byte_vec_new(&bv, bytes.size(), (const char*)bytes.data());
Instance core;
core.module = wasm_module_new(g_store, &bv);
wasm_byte_vec_delete(&bv);
CHECK(core.module, "core module load failed");
wasm_importtype_vec_t imports = WASM_EMPTY_VEC;
wasm_module_imports(core.module, &imports);
std::vector<wasm_extern_t*> externs(imports.size);
std::vector<wasm_func_t*> owned_funcs;
for(size_t i = 0; i < imports.size; i++)
{
const wasm_name_t* mod_n = wasm_importtype_module(imports.data[i]);
const wasm_name_t* name_n = wasm_importtype_name(imports.data[i]);
std::string mod(mod_n->data, mod_n->size);
std::string name(name_n->data, name_n->size);
while(!mod.empty() && mod.back() == '\0') mod.pop_back();
while(!name.empty() && name.back() == '\0') name.pop_back();
const wasm_externtype_t* et = wasm_importtype_type(imports.data[i]);
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, "unexpected non-func import %s.%s", mod.c_str(), name.c_str());
wasm_func_t* fn = nullptr;
if(mod == "env" && name == "uce_host_ctx_read")
fn = make_func(g_store, {WASM_I32, WASM_I32}, {WASM_I32}, host_ctx_read);
else if(mod == "env" && name == "uce_host_log")
fn = make_func(g_store, {WASM_I32, WASM_I32, WASM_I32}, {}, host_log);
else
{
char* label = strdup((mod + "." + name).c_str());
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
fn = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr);
}
CHECK(fn, "failed to create import %s.%s", mod.c_str(), name.c_str());
owned_funcs.push_back(fn);
externs[i] = wasm_func_as_extern(fn);
}
wasm_extern_vec_t iv = { externs.size(), externs.data() };
wasm_trap_t* trap = nullptr;
core.instance = wasm_instance_new(g_store, core.module, &iv, &trap);
if(trap)
{
wasm_message_t msg;
wasm_trap_message(trap, &msg);
FAIL("trap during instantiation: %.*s", (int)msg.size, msg.data);
}
CHECK(core.instance, "instantiation failed");
core.index_exports();
g_memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr);
CHECK(g_memory, "core does not export memory");
if(core.func("_initialize"))
call_i32(core, "_initialize");
int rc = call_i32(core, "uce_phase2_render");
CHECK(rc == 0, "render returned %d", rc);
int32_t data = call_i32(core, "uce_phase2_output_data");
int32_t size = call_i32(core, "uce_phase2_output_size");
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
CHECK((size_t)data + (size_t)size <= wasm_memory_data_size(g_memory), "output outside memory");
std::string output((const char*)mem + data, size);
printf("---- phase2 output (%d bytes) ----\n%s", size, output.c_str());
printf("----------------------------------\n");
CHECK(output.find("PHASE2 PAGE OK") != std::string::npos, "missing page marker");
CHECK(output.find("host=phase2.example.test") != std::string::npos, "missing context param");
CHECK(output.find("answer=42") != std::string::npos, "missing nested context value");
printf("PHASE2 EXIT CRITERION: PASS\n");
return 0;
}

View File

@ -1,12 +0,0 @@
// WASM-PROPOSAL Phase 2 scaffold page.
// This is a real .uce source file with a normal RENDER entry point. The
// Phase 2 core statically links it to validate the host-context membrane
// before the dynamic loader is introduced in Phase 3.
RENDER(Request& context)
{
print("PHASE2 PAGE OK\n");
print("host=", context.params["HTTP_HOST"], "\n");
print("route=", context.call["route"].to_string(), "\n");
print("answer=", context.call["nested"]["answer"].to_string(), "\n");
}

View File

@ -1,63 +0,0 @@
# spikes/wasm-phase3 — dynamic loader + workspace scaffold
Phase 3 combines the Phase 0 dynamic PIC-module loader with the Phase 2 UCEB1
request-context membrane. It is still a spike, not the production wasm worker,
but it validates the load-bearing pieces together:
1. build a core WASM reactor that owns memory, allocator, `Request`, `DValue`,
UCEB1, and output buffering;
2. build a separate PIC side module with `dylink.0` from a generated-shape UCE
page C++ file;
3. parse `dylink.0`, allocate `__memory_base`/`__table_base`, resolve
`env.*`, `GOT.mem.*`, and `GOT.func.*` imports against the core/workspace;
4. instantiate the unit, run relocations/constructors, set its Request pointer,
call `__uce_render`, and read rendered output from core-owned memory.
The fixture intentionally exercises the loader branches that are easiest to
accidentally leave dark: non-zero `table_size` / `__table_base`, `GOT.func`
resolution (`phase3_core_print`), and deferred self-resolution of unit-owned
`GOT.mem` entries. The loader output prints those resolutions before the final
pass marker. Its generated-shape expressions call `html_escape(...)` so the
side-module import surface includes the first ordinary UCE helper dependency
rather than only `types.h`/`DValue`.
Run on `k-uce`:
```bash
bash spikes/wasm-phase3/build_modules.sh
bash spikes/wasm-phase3/build_loader.sh
/tmp/uce/wasm-phase3/loader
```
Expected final line:
```text
PHASE3 EXIT CRITERION: PASS
```
Files:
- `core.cpp` — core/workspace scaffold and UCEB1 membrane decode.
- `page.uce` — source page for the generated-shape fixture.
- `generated/page.uce.cpp` — checked-in UCE-preprocessor-shaped side-module
fixture: `uce_lib.h` include, `__uce_set_current_request`, `__uce_render`,
literal markup lowered to `print(R"(...)")`, expression prints wrapped in
`html_escape(...)`, callback/lambda/table exercises, and self-GOT data.
- `loader.cpp` — dynamic linker/runner adapted from Phase 0.
- `hostcalls.syms` — explicit core hostcall allowlist.
Still deferred to production Phase 3/4 work:
- running the actual UCE preprocessor inside this build script rather than using
a checked-in generated-shape fixture;
- lazy component/path dispatch for a full site tree;
- uce-starter end-to-end under a wasm FastCGI worker;
- productionizing the side-module allocator gate as a real `#ifdef` in
`types.h` instead of this spike's copied-header text patch;
- parser hardening beyond the spike's basic magic/version/bounds/alignment
checks, including fuzzing malformed wasm/dylink sections before accepting
cache or third-party units;
- import-policy hardening beyond this spike's fail-fast resolver;
- workspace birth/drop integration with the real server lifecycle, including
retaining unaligned allocation pointers needed to unload units or drop a
workspace cleanly.

View File

@ -1,16 +0,0 @@
#!/bin/bash
# Phase 3: build the dynamic-loader/membrane runner against Wasmtime's wasm-c-api.
set -e
cd "$(dirname "$0")"
OUT=/tmp/uce/wasm-phase3
mkdir -p "$OUT"
c++ -std=c++17 -O2 loader.cpp \
-I/opt/wasmtime/include \
-L/opt/wasmtime/lib \
-Wl,-rpath,/opt/wasmtime/lib \
-lwasmtime \
-o "$OUT/loader"
echo "built: $OUT/loader"

View File

@ -1,70 +0,0 @@
#!/bin/bash
# Phase 3: build a core reactor plus one generated-shape PIC side module.
# Runs on k-uce; artifacts go under /tmp/uce/wasm-phase3.
set -e
cd "$(dirname "$0")"
SDK=/opt/wasi-sdk
ROOT=$(cd ../.. && pwd)
OUT=/tmp/uce/wasm-phase3
SHIM="$OUT/shim"
mkdir -p "$OUT" "$SHIM"
cat > "$SHIM/uce_lib.h" <<'EOF'
#pragma once
#include "types.h"
String html_escape(String s);
EOF
cp "$ROOT/src/lib/types.h" "$SHIM/types.h"
python3 - <<'PY'
from pathlib import Path
p = Path('/tmp/uce/wasm-phase3/shim/types.h')
s = p.read_text()
start = s.index('void * operator new(decltype(sizeof(0)) n) noexcept(false)')
end = s.index('void operator delete(void * p) throw()')
end = s.index('\n}', end) + 3
replacement = '''// WASM side-module shim: allocator is owned by the core module.\nvoid * operator new(decltype(sizeof(0)) n) noexcept(false);\nvoid operator delete(void * p) throw();\nvoid operator delete(void * p, decltype(sizeof(0)) n) noexcept;\n'''
p.write_text(s[:start] + replacement + s[end:])
PY
cat > "$SHIM/signal.h" <<'EOF'
#pragma once
typedef int sig_atomic_t;
typedef void (*sighandler_t)(int);
#define SIGTERM 15
int raise(int);
sighandler_t signal(int, sighandler_t);
EOF
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
-O1 -fno-exceptions \
-I../.. -I../../src/lib \
core.cpp -o "$OUT/core.wasm" \
-Wl,--export-all \
-Wl,--import-table \
-Wl,--export=__stack_pointer \
-Wl,--export=__heap_base \
-Wl,--allow-undefined-file=hostcalls.syms \
-Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE
"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \
-fvisibility-inlines-hidden \
-O1 -fno-exceptions \
-I"$SHIM" -I"$ROOT/src/lib" \
-c generated/page.uce.cpp -o "$OUT/page.o"
"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \
-fvisibility-inlines-hidden \
-O1 -fno-exceptions \
-I"$SHIM" -I"$ROOT/src/lib" \
-c generated/helper.cpp -o "$OUT/helper.o"
"$SDK/bin/wasm-ld" -shared --experimental-pic \
--unresolved-symbols=import-dynamic \
"$OUT/page.o" "$OUT/helper.o" -o "$OUT/page.wasm" \
--export=__uce_set_current_request \
--export=__uce_render
echo "--- phase3 core.wasm ---"
ls -la "$OUT/core.wasm"
echo "--- phase3 page.wasm ---"
ls -la "$OUT/page.wasm"

View File

@ -1,188 +0,0 @@
// WASM-PROPOSAL Phase 3 — dynamic-loader core scaffold.
//
// This core owns the UCE heap, Request, DValue/UCEB1, and output buffer. The
// Phase 3 loader dynamically links a PIC side module generated in the same
// shape as UCE preprocessor output, sets its Request* context, and invokes its
// __uce_render entry.
#include <cstdlib>
#include <cstring>
#include <functional>
#include "../../src/lib/types.h"
#include "../../src/lib/dvalue.cpp"
extern "C" {
size_t uce_host_ctx_read(char* buf, size_t cap);
void uce_host_log(int level, const char* buf, size_t len);
}
extern "C" void phase3_core_print(const char* data, size_t len)
{
if(context && context->ob)
context->ob->write(data, len);
}
extern "C" void phase3_core_invoke_callback(void (*cb)(int), int value)
{
cb(value);
}
extern "C" void phase3_core_invoke_function(std::function<int(int)>* f, int value)
{
String out = "<p>fn=" + std::to_string((*f)(value)) + "</p>\n";
phase3_core_print(out.data(), out.size());
}
extern "C" intptr_t core_table_index_of_phase3_core_print()
{
return(reinterpret_cast<intptr_t>(phase3_core_print));
}
static Request g_request;
static ByteStream g_ob;
static String g_output;
SharedUnit::~SharedUnit() {}
String html_escape(String s)
{
String result;
for(char c : s)
{
switch(c)
{
case '&': result += "&amp;"; break;
case '<': result += "&lt;"; break;
case '>': result += "&gt;"; break;
case '"': result += "&quot;"; break;
case '\'': result += "&#39;"; break;
default: result.push_back(c); break;
}
}
return(result);
}
String nibble(String div, String& haystack)
{
auto pos = haystack.find(div);
if(pos == String::npos)
{
auto result = haystack;
haystack.clear();
return(result);
}
auto result = haystack.substr(0, pos);
haystack.erase(0, pos + div.length());
return(result);
}
void Request::ob_start()
{
ob_stack.push_back(new ByteStream());
ob = ob_stack.back();
}
void Request::set_status(s32 code, String reason)
{
if(reason == "")
reason = code == 200 ? "OK" : "Status";
response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason;
}
Request::~Request()
{
for(auto* stream : ob_stack)
delete stream;
ob_stack.clear();
}
static void phase3_clear_ob_stack()
{
for(auto* stream : g_request.ob_stack)
delete stream;
g_request.ob_stack.clear();
}
static void phase3_apply_context(DValue& root)
{
g_request.call = root;
g_request.params.clear();
DValue* params = root.key("params");
if(params)
{
params->each([&](const DValue& item, String key) {
g_request.params[key] = item.to_string();
});
}
}
extern "C" {
void* uce_alloc(size_t len)
{
return(malloc(len));
}
void uce_free(void* ptr)
{
free(ptr);
}
int uce_phase3_prepare()
{
context = &g_request;
phase3_clear_ob_stack();
g_ob.str("");
g_ob.clear();
g_request.ob = &g_ob;
g_request.out = "";
g_output = "";
size_t ctx_required = uce_host_ctx_read(0, 0);
if(ctx_required == 0)
return(10);
char* ctx_buf = (char*)malloc(ctx_required);
if(ctx_buf == 0)
return(11);
size_t ctx_len = uce_host_ctx_read(ctx_buf, ctx_required);
if(ctx_len != ctx_required)
{
free(ctx_buf);
return(12);
}
DValue decoded;
String error;
bool ok = ucb_decode(String(ctx_buf, ctx_len), decoded, &error);
free(ctx_buf);
if(!ok)
{
uce_host_log(3, error.data(), error.size());
return(20);
}
phase3_apply_context(decoded);
return(0);
}
Request* uce_phase3_request()
{
return(&g_request);
}
void uce_phase3_finish()
{
g_output = g_ob.str();
}
const char* uce_phase3_output_data()
{
return(g_output.data());
}
size_t uce_phase3_output_size()
{
return(g_output.size());
}
}

View File

@ -1,6 +0,0 @@
extern int phase3_unit_state;
extern "C" int phase3_helper_state_plus(int value)
{
return(phase3_unit_state + value);
}

View File

@ -1,72 +0,0 @@
#include "uce_lib.h"
#include <functional>
#include <map>
#include <tuple>
extern "C" void phase3_core_print(const char* data, size_t len);
extern "C" void phase3_core_invoke_callback(void (*cb)(int), int value);
extern "C" void phase3_core_invoke_function(std::function<int(int)>* f, int value);
extern "C" int phase3_helper_state_plus(int value);
int phase3_unit_state = 40;
int phase3_weak_data __attribute__((weak)) = 0;
static void phase3_unit_callback(int value)
{
String out = "<p>callback=" + std::to_string(value + phase3_unit_state) + "</p>\n";
print(out);
}
#ifndef UCE_SET_CURRENT_REQUEST_DEFINED
#define UCE_SET_CURRENT_REQUEST_DEFINED
/*load_declarations*/
extern "C" void __uce_set_current_request(Request* _request)
{
context = _request;
/*load_units*/
}
#endif
#line 4 "spikes/wasm-phase3/page.uce"
extern "C" void __uce_render(Request& context)
{
print(R"(<section class="phase3">
<h1>PHASE3 PAGE OK</h1>
<p>host=)");
print(html_escape(context.params["HTTP_HOST"]));
print(R"(</p>
<p>route=)");
print(html_escape(context.call["route"].to_string()));
print(R"(</p>
<p>answer=)");
print(html_escape(context.call["nested"]["answer"].to_string()));
print(R"(</p>
)");
context.call["nested"].each([&](const DValue& value, String key) {
print(R"( <p>each=)");
print(html_escape(key + ":" + value.to_string()));
print(R"(</p>
)");
});
print(R"( <p>self-got=)");
phase3_weak_data = 7;
print(std::to_string(phase3_helper_state_plus(2) + phase3_weak_data - 7));
print(R"(</p>
)");
std::map<String, String> phase3_map;
phase3_map.emplace(std::piecewise_construct, std::forward_as_tuple("piece"), std::forward_as_tuple("wise"));
print(R"( <p>map=)");
print(html_escape(phase3_map["piece"]));
print(R"(</p>
)");
phase3_core_invoke_callback(phase3_unit_callback, 2);
void (*volatile print_ptr)(const char*, size_t) = phase3_core_print;
print_ptr("<p>got-func-ok</p>\n", 19);
auto* fn = new std::function<int(int)>([](int value) { return(value * 3); });
phase3_core_invoke_function(fn, 14);
delete fn;
print(R"( </section>
)");
}

View File

@ -1,2 +0,0 @@
uce_host_ctx_read
uce_host_log

View File

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

View File

@ -1,12 +0,0 @@
// Phase 3 generated-unit fixture source. The checked-in generated/page.uce.cpp
// is the UCE-preprocessor-shaped output used by the wasm side-module build.
RENDER(Request& context)
{
<><section class="phase3">
<h1>PHASE3 PAGE OK</h1>
<p>host=<?= context.params["HTTP_HOST"] ?></p>
<p>route=<?= context.call["route"].to_string() ?></p>
<p>answer=<?= context.call["nested"]["answer"].to_string() ?></p>
</section>
}

View File

@ -1,77 +0,0 @@
# spikes/wasm-phase4 — production mechanics kill-test spike
Phase 4 validates the operational mechanics needed before a WASM worker can be
trusted in production. This spike is intentionally smaller than the future UCE
worker integration: it uses tiny WAT modules instead of generated UCE units, but
it exercises the Wasmtime controls and workspace cleanup paths the worker will
use. It is also deliberately the first spike on the Wasmtime-specific C++ API
(`wasmtime.hh`) — fuel, epochs, store limiters, and snapshots do not exist in
the portable `wasm.h` surface, so runtime-agnosticism ends here by design.
Run on `k-uce`:
```bash
bash spikes/wasm-phase4/build_runner.sh
/tmp/uce/wasm-phase4/runner
```
Expected final line:
```text
PHASE4 EXIT CRITERION: PASS
```
What this proves:
- **Reusable compiled artifact / snapshot proxy:** modules are compiled once and
then instantiated in fresh stores. Not OS CoW yet, but it validates the shape
of "shared core artifact + per-request workspace birth".
- **Unharmed worker:** after all six kill cases, the same engine serves a
healthy request that completes with the expected value. This — not merely
"didn't crash" — is the worker half of the Phase 4 exit criterion.
- **CPU limits, both mechanisms:** fuel (deterministic, per-instruction cost)
and epoch interruption (near-zero overhead, ticker thread, traps as
`interrupt`). Production default is **epoch** per the Phase 0 findings; fuel
is validated as the deterministic fallback. Note: the engine epoch only
advances, so every store must set its own deadline.
- **Memory limit, actually load-bearing:** the OOM fixture grows by 10 pages —
within its declared max (100) — so only the store limiter (2 pages) can deny
the growth. The denial is observed by the guest (`memory.grow` → -1) and
converted to a trap.
- **Trap-to-error-page data path:** every kill case's gate asserts the captured
message contains a `wasm backtrace` and the expected cause. Fixture frames
show `<unknown>` — readable production traces require units to keep their
name section (or a symbolication side-file in the artifact cache).
- **Trace summarizer (`src/lib/wasm_trace.h`):** trap messages are rendered
through the production collapse facility (repeated frames → `×N` lines,
mangled symbols demangled, cause/detail split out). The stack-exhaustion
case gates it on live trap output here; `site/tests/core.uce` gates the
parsing on canned messages in the native suite.
- **Handle cleanup with falsifiable checks:** closers must run exactly once per
handle and *while the store is still alive* (production closers may flush
guest-resident state); destructor-only cleanup fails the ordering check.
Kill fixtures (all genuinely trap):
- `unreachable`: `__builtin_trap` analog.
- `oob-access`: wild pointer; load at 128 KiB from a 64 KiB memory.
- `stack-exhaustion`: runaway recursion → `call stack exhausted`.
- `infinite-loop-fuel` / `infinite-loop-epoch`: same loop, both CPU limits.
- `oom-limiter`: limiter-denied `memory.grow` → guest converts -1 to a trap.
**A literal C++ null-pointer dereference is deliberately absent:** address 0 is
valid wasm linear memory, so `*(int*)nullptr` does not trap — it silently
writes inside the workspace, which is then dropped at request end (still
strictly better than a native SIGSEGV). See WASM-PROPOSAL §10 for the recorded
risk/policy.
Still deferred to production Phase 4:
- real core snapshot + OS-level CoW birth;
- wiring `wasm_trace.h` summaries into the UCE error-page UI (the summarizer
itself is done and gated); name-section policy for unit artifacts;
- wiring these traps into `linux_fastcgi.cpp` / the WASM worker backend;
- real runtime handle-table cleanup for sqlite/mysql/sockets/tasks;
- artifact ABI versioning end-to-end;
- memory-limit policy for guest allocators that return failure instead of
trapping (bump allocator vs dlmalloc behavior under the limiter).

View File

@ -1,16 +0,0 @@
#!/bin/bash
# Phase 4: build the Wasmtime kill-test runner.
set -e
cd "$(dirname "$0")"
OUT=/tmp/uce/wasm-phase4
mkdir -p "$OUT"
c++ -std=c++17 -O2 -pthread runner.cpp \
-I/opt/wasmtime/include \
-L/opt/wasmtime/lib \
-Wl,-rpath,/opt/wasmtime/lib \
-lwasmtime \
-o "$OUT/runner"
echo "built: $OUT/runner"

View File

@ -1,298 +0,0 @@
// WASM-PROPOSAL Phase 4 — production-mechanics kill-test spike.
//
// Uses Wasmtime's C++ API directly (this spike is deliberately where the
// portable wasm.h surface ends: fuel, epochs, store limiters, and snapshots
// are Wasmtime-specific by nature; the runtime was selected in Phase 0).
//
// Validates, per kill case, that: the guest trap is captured with a wasm
// backtrace, the workspace handle closers run exactly once and while the
// store is still alive, and — after all kills — the same engine still serves
// a healthy request (the actual meaning of "unharmed worker").
//
// Note: a literal C++ null-pointer dereference does NOT trap in wasm —
// address 0 is valid linear memory; the damage stays inside the dropped
// workspace (see WASM-PROPOSAL §10). The kill fixtures below therefore use
// faults that genuinely trap: unreachable (__builtin_trap analog), an
// out-of-bounds access (wild pointer), stack exhaustion (runaway recursion),
// fuel/epoch exhaustion (infinite loop), and a limiter-denied memory.grow.
#include <wasmtime.hh>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include "../../src/lib/wasm_trace.h"
using namespace wasmtime;
// Handle-table proxy. The checks here are deliberately falsifiable: closers
// must run exactly once per handle, and they must run while the store (and
// thus guest memory) is still alive — production closers may need to flush
// guest-resident state. A destructor-only cleanup fails the ordering check.
struct Workspace
{
int handles_open = 0;
int handles_closed = 0;
bool cleanup_ran = false;
bool closed_while_store_alive = false;
void open_handles(int count)
{
handles_open = count;
}
void cleanup(bool store_alive)
{
if(cleanup_ran)
return;
cleanup_ran = true;
closed_while_store_alive = store_alive;
for(int i = 0; i < handles_open; i++)
handles_closed++;
}
~Workspace()
{
cleanup(false);
}
bool ok() const
{
return(cleanup_ran && handles_closed == handles_open && closed_while_store_alive);
}
};
static Module compile_wat(Engine& engine, const std::string& wat)
{
auto module = Module::compile(engine, wat);
if(!module)
{
std::cerr << "compile failed: " << module.err_ref().message() << "\n";
exit(1);
}
return module.ok();
}
struct RunResult
{
bool trapped = false;
std::string message;
bool cleanup_ok = false;
bool has_value = false;
int32_t value = 0;
};
static RunResult run_case(Engine& engine, const Module& module, uint64_t fuel, int64_t memory_limit, bool epoch_ticker = false)
{
RunResult result;
Workspace workspace;
workspace.open_handles(2);
{
Store store(engine);
store.limiter(memory_limit, -1, -1, -1, -1);
Store::Context cx(store);
// epoch_interruption is enabled engine-wide and the engine epoch only
// advances; every store needs a deadline beyond the current epoch or
// it traps immediately. The epoch kill case gets a deadline of 1 tick
// and a ticker thread; everything else gets effectively-unbounded.
cx.set_epoch_deadline(epoch_ticker ? 1 : 1'000'000'000);
auto fuel_result = cx.set_fuel(fuel);
if(!fuel_result)
{
result.trapped = true;
result.message = fuel_result.err_ref().message();
workspace.cleanup(true);
result.cleanup_ok = workspace.ok();
return result;
}
auto instance = Instance::create(cx, module, {});
if(!instance)
{
result.trapped = true;
result.message = instance.err_ref().message();
workspace.cleanup(true);
result.cleanup_ok = workspace.ok();
return result;
}
auto run_export = instance.ok_ref().get(cx, "run");
if(!run_export || !std::get_if<Func>(&*run_export))
{
result.trapped = true;
result.message = "missing run export";
workspace.cleanup(true);
result.cleanup_ok = workspace.ok();
return result;
}
std::thread ticker;
if(epoch_ticker)
ticker = std::thread([&engine] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
engine.increment_epoch();
});
Func run = *std::get_if<Func>(&*run_export);
auto call = run.call(cx, std::vector<Val>{});
if(ticker.joinable())
ticker.join();
if(!call)
{
result.trapped = true;
result.message = call.err_ref().message();
}
else
{
result.message = "completed without trap";
auto values = call.ok();
if(!values.empty())
{
result.has_value = true;
result.value = values[0].i32();
}
}
workspace.cleanup(true);
result.cleanup_ok = workspace.ok();
}
return result;
}
static void print_case(const char* label, const RunResult& result)
{
std::cout << "--- " << label << " ---\n";
std::cout << "trapped=" << (result.trapped ? "yes" : "no") << "\n";
if(result.trapped)
std::cout << "summary:\n" << wasm_trace_collapse(result.message) << "\n";
else
std::cout << "message=" << result.message << "\n";
std::cout << "cleanup=" << (result.cleanup_ok ? "ok" : "failed") << "\n";
}
static void check_kill(const char* label, const RunResult& result, const char* expect_in_message)
{
print_case(label, result);
bool has_trace = result.message.find("wasm backtrace") != std::string::npos;
bool has_cause = result.message.find(expect_in_message) != std::string::npos;
if(!result.trapped || !result.cleanup_ok || !has_trace || !has_cause)
{
std::cerr << "PHASE4 EXIT CRITERION: FAIL at " << label
<< (result.trapped ? "" : " (no trap)")
<< (result.cleanup_ok ? "" : " (cleanup)")
<< (has_trace ? "" : " (no backtrace)")
<< (has_cause ? "" : " (wrong cause)") << "\n";
exit(1);
}
}
static void check_healthy(const char* label, const RunResult& result)
{
print_case(label, result);
if(result.trapped || !result.cleanup_ok || !result.has_value || result.value != 42)
{
std::cerr << "PHASE4 EXIT CRITERION: FAIL at " << label << " (worker harmed)\n";
exit(1);
}
}
int main()
{
Config config;
config.consume_fuel(true);
config.epoch_interruption(true);
Engine engine(std::move(config));
// Compiled modules are reused across stores: a deliberately small proxy
// for the future core snapshot — per-request workspaces (stores) are
// born fresh while compilation work is shared.
// __builtin_trap analog; a real C++ *nullptr does not trap (see header)
Module unreachable_module = compile_wat(engine, R"wat(
(module
(func (export "run")
unreachable))
)wat");
// wild/OOB pointer: load at 128 KiB from a 64 KiB memory
Module oob_module = compile_wat(engine, R"wat(
(module
(memory 1 1)
(func (export "run")
(drop (i32.load (i32.const 131072)))))
)wat");
// runaway recursion → call stack exhausted
Module stack_module = compile_wat(engine, R"wat(
(module
(func $f (export "run")
(call $f)))
)wat");
Module loop_module = compile_wat(engine, R"wat(
(module
(func (export "run")
(loop br 0)))
)wat");
// grows by 10 pages: within the module's own declared max (100), so only
// the store limiter (2 pages) can deny it — this proves the limiter is
// load-bearing, not the declared max
Module oom_module = compile_wat(engine, R"wat(
(module
(memory 1 100)
(func (export "run")
i32.const 10
memory.grow
i32.const -1
i32.eq
(if (then unreachable))))
)wat");
Module healthy_module = compile_wat(engine, R"wat(
(module
(func (export "run") (result i32)
i32.const 42))
)wat");
const int64_t MEM_LIMIT = 2 * 65536;
const uint64_t FUEL = 10'000;
const uint64_t FUEL_PLENTY = 1'000'000'000'000;
check_kill("unreachable", run_case(engine, unreachable_module, FUEL, MEM_LIMIT), "unreachable");
check_kill("oob-access", run_case(engine, oob_module, FUEL, MEM_LIMIT), "out of bounds");
RunResult stack_result = run_case(engine, stack_module, FUEL_PLENTY, MEM_LIMIT);
check_kill("stack-exhaustion", stack_result, "call stack exhausted");
// the trace formatter (src/lib/wasm_trace.h) is part of the gate: the
// recursion frames (Wasmtime caps the displayed backtrace at 20) must
// collapse to a bounded summary
WasmTraceSummary stack_summary = wasm_trace_summarize(stack_result.message);
if(!stack_summary.parsed || stack_summary.total_frames < 10
|| stack_summary.frames.size() > 12
|| stack_summary.cause.find("call stack exhausted") == std::string::npos
|| wasm_trace_format(stack_summary).find("×") == std::string::npos)
{
std::cerr << "PHASE4 EXIT CRITERION: FAIL at trace-collapse (parsed="
<< stack_summary.parsed << " total=" << stack_summary.total_frames
<< " lines=" << stack_summary.frames.size()
<< " cause=" << stack_summary.cause << ")\n";
exit(1);
}
std::cout << "--- trace-collapse ---\n" << stack_summary.total_frames
<< " raw frames -> " << stack_summary.frames.size() << " summary lines\n";
check_kill("infinite-loop-fuel", run_case(engine, loop_module, FUEL, MEM_LIMIT), "fuel");
// Wasmtime reports epoch-deadline traps as "interrupt"
check_kill("infinite-loop-epoch", run_case(engine, loop_module, FUEL_PLENTY, MEM_LIMIT, true), "interrupt");
check_kill("oom-limiter", run_case(engine, oom_module, FUEL, MEM_LIMIT), "unreachable");
// after every kill above, the same engine must still serve a request —
// this is the "unharmed worker" half of the Phase 4 exit criterion
check_healthy("healthy-after-kills", run_case(engine, healthy_module, FUEL, MEM_LIMIT));
std::cout << "PHASE4 EXIT CRITERION: PASS\n";
return 0;
}

View File

@ -1,62 +0,0 @@
# spikes/wasm-phase5 — parity, audit, and performance harness
Phase 5's production exit requires the full network suite to pass on the WASM
worker and performance numbers to be published. The production worker is not in
this branch yet, so this spike builds the Phase 5 harness and records the native
baseline that the WASM worker must match.
Run on `k-uce`:
```bash
bash spikes/wasm-phase5/run_phase5.sh
```
Expected final line:
```text
PHASE5 HARNESS: PASS
```
The harness first runs a throwaway warmup pass to populate the unit cache after
binary rebuilds. The warmup excludes the stateful `site tests tasks` case so it
cannot perturb the measured task-lifecycle check. The measured full-suite and
starter-subset passes then become the gate.
Artifacts are written under `/tmp/uce/wasm-phase5/`:
- `native-network-warmup.json` — throwaway warmup result; allowed to fail on cold
compile timeouts.
- `native-network.json` — measured full native network suite result. The harness
fails if fewer than 80 cases run, so an empty or broken suite cannot pass.
- `native-starter.json` — measured starter-focused parity subset. The harness
fails if fewer than 10 cases run, which protects against a vacuous
`--match starter` exit gate.
- `site-static-audit.{json,md}` — candidate cross-request/static-state risks in
`site/` for the §3.2 semantic change audit. By default it scans code-like
`.uce` and `.h` files only; pass `--include-doc-text` to include `.txt` docs
as `documentation` severity findings.
- `benchmark.{json,md}` — warmed native baseline for the three Phase 5 budget
pages. The default is 20 samples per target; use a higher `--samples` value for
noisy shared-host gate runs if needed:
- `template-heavy-doc`: `/doc/singlepage.uce`
- `sqlite-page`: `/demo/sqlite.uce`
- `component-heavy-starter`: `/examples/uce-starter/?dashboard`
`benchmark.py` also accepts `--wasm-base-url` when native and WASM are served
from separate endpoints. For the W5 in-place cutover, use
`--backend-label wasm --compare-native-json <native benchmark.json>` after
switching `/etc/uce/settings.cfg` to the WASM backend; this compares WASM
medians against the Phase 5 budget of ≤2× native page latency. W5 worker builds
emit `X-UCE-Wasm-Workspace-Birth-Us` and component resolve probe headers on
WASM-served responses.
A durable informational baseline snapshot is kept in `reports/`. Paired
native/WASM runs still recompute native medians for the actual budget decision.
Current scope:
- Native parity and baseline collection are automated.
- WASM parity/performance comparison is ready but blocked on the production WASM
worker endpoint.
- The static-state audit is heuristic and intentionally conservative; findings
must be reviewed by a human before migration work is scheduled.

View File

@ -1,62 +0,0 @@
#!/bin/bash
# Phase 5 harness: native parity, site static audit, and native perf baseline.
# Run on k-uce from repo root.
set -euo pipefail
cd "$(dirname "$0")/../.."
OUT=/tmp/uce/wasm-phase5
mkdir -p "$OUT"
# The network suite can show transient cold-compile timeouts immediately after a
# binary rebuild. Run a throwaway pass first so the measured gate is a warmed
# parity signal rather than a compiler-cache signal. The warmup is allowed to
# fail; the measured pass below is not.
python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/native-network-warmup.json" || true
network_status=0
starter_status=0
benchmark_status=0
python3 tests/run_network_tests.py --include-internal --json-report "$OUT/native-network.json" || network_status=$?
python3 tests/run_network_tests.py --include-internal --match starter --json-report "$OUT/native-starter.json" || starter_status=$?
python3 spikes/wasm-phase5/audit_site_statics.py --out-dir "$OUT"
python3 spikes/wasm-phase5/benchmark.py --out-dir "$OUT" || benchmark_status=$?
NETWORK_STATUS=$network_status STARTER_STATUS=$starter_status BENCHMARK_STATUS=$benchmark_status python3 - <<'PY'
import json
import os
from pathlib import Path
MIN_NETWORK_CASES = 80
MIN_STARTER_CASES = 10
MIN_BENCHMARKS = 3
out = Path('/tmp/uce/wasm-phase5')
network = json.loads((out / 'native-network.json').read_text())
starter = json.loads((out / 'native-starter.json').read_text())
bench = json.loads((out / 'benchmark.json').read_text())
failures = [r for r in network if not r['ok']] + [r for r in starter if not r['ok']] + [r for r in bench if not r['ok']]
structural_failures = []
if len(network) < MIN_NETWORK_CASES:
structural_failures.append(f"network suite ran {len(network)} cases, expected at least {MIN_NETWORK_CASES}")
if len(starter) < MIN_STARTER_CASES:
structural_failures.append(f"starter subset ran {len(starter)} cases, expected at least {MIN_STARTER_CASES}")
if len(bench) < MIN_BENCHMARKS:
structural_failures.append(f"benchmark ran {len(bench)} rows, expected at least {MIN_BENCHMARKS}")
nonzero = {
'network_status': int(os.environ['NETWORK_STATUS']),
'starter_status': int(os.environ['STARTER_STATUS']),
'benchmark_status': int(os.environ['BENCHMARK_STATUS']),
}
for name, status in nonzero.items():
if status != 0:
structural_failures.append(f"{name} exited {status}")
if failures or structural_failures:
print('PHASE5 HARNESS: FAIL')
for failure in structural_failures:
print(failure)
for failure in failures:
print(failure)
raise SystemExit(1)
print('PHASE5 HARNESS: PASS')
print(f"network_cases={len(network)} starter_cases={len(starter)} benchmarks={len(bench)}")
print(f"reports={out}")
PY

View File

@ -1,90 +0,0 @@
#define NO_GLOBAL_ARENA_ALLOCATOR
struct MemoryArena {
u8* data;
u64 size = 0;
u64 capacity = 0;
String name = "unnamed";
MemoryArena(u64 cap, String _name = "unnamed")
{
name = _name;
capacity = cap;
printf("(i) memory arena '%s' created with capacity of %llu bytes\n", name.c_str(), capacity);
data = (u8*)malloc(cap);
}
~MemoryArena()
{
free(data);
}
void clear()
{
#ifdef DEBUG_MEMORY
printf("(i) memory arena '%s' cleared after high mark of %llu bytes\n", name.c_str(), size);
#endif
size = 0;
}
void* get(u64 size_needed)
{
u64 size_aligned = 8 + (8 * ((size_needed) / 8));
u8* result = data + size;
if(size_aligned + size >= capacity)
{
printf("(!) memory arena '%s' capacity (%llu) exceeded %llu/%llu + %llu >= %llu\n",
name.c_str(), capacity, size_needed, size_aligned, size, capacity);
return(0);
}
size += size_aligned;
#ifdef DEBUG_MEMORY_DETAILED
printf("(i) memory arena '%s' [+%llu]:%p alloc %llu/%llu bytes\n", name.c_str(), size, result, size_needed, size_aligned);
#endif
return(result);
}
};
MemoryArena* current_memory_arena = 0;
void switch_to_system_alloc()
{
#ifdef GLOBAL_ARENA_ALLOCATOR
current_memory_arena = 0;
#endif
}
void switch_to_arena(MemoryArena* a)
{
#ifdef GLOBAL_ARENA_ALLOCATOR
current_memory_arena = a;
#endif
}
#ifdef GLOBAL_ARENA_ALLOCATOR
void * operator new(decltype(sizeof(0)) n) noexcept(false)
{
if(current_memory_arena)
{
return(current_memory_arena->get(n));
}
else
{
return(malloc(n));
}
}
void operator delete(void * p) throw()
{
if(current_memory_arena)
{
}
else
{
free(p);
}
}
#endif

View File

@ -1,4 +1,5 @@
#ifdef __UCE_WASM_CORE__
#include <cmath>
#include "types.h"
#include "functionlib.h"
#include "sys.h"
@ -16,6 +17,10 @@ int uce_host_file_exists(const char* path, size_t path_len, const char* current,
size_t uce_host_file_read(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap);
int uce_host_file_write(const char* path, size_t path_len, const char* current, size_t current_len, const char* content, size_t content_len, int append);
void uce_host_file_unlink(const char* path, size_t path_len, const char* current, size_t current_len);
int uce_host_task_spawn(const char* key, size_t key_len, uint64_t callback_id, double interval, uint64_t timeout, int repeat);
int uce_host_task_pid(const char* key, size_t key_len);
int uce_host_task_kill(int pid, int sig);
unsigned int uce_host_sleep_us(uint64_t usec);
}
static String wasm_current_unit_file()
@ -82,9 +87,55 @@ f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(conte
bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); }
f64 time_precise() { return(uce_host_time_precise()); }
u64 time() { return(uce_host_time()); }
String time_format_local(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); }
String time_format_utc(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); }
String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) { (void)format_very_recent; (void)medium_recency_seconds; (void)format_medium_recent; (void)not_recent_seconds; (void)format_not_recent; return(std::to_string(timestamp)); }
// The native build shells out to `date`; the wasm core has no shell, so it
// formats with wasi-libc strftime (no TZ data → local == UTC, acceptable).
static String wasm_time_strftime(String format, u64 timestamp, bool utc)
{
if(timestamp == 0)
timestamp = time();
time_t t = (time_t)timestamp;
struct tm tmv;
if(utc)
gmtime_r(&t, &tmv);
else
localtime_r(&t, &tmv);
char buffer[512];
size_t n = strftime(buffer, sizeof(buffer), format.c_str(), &tmv);
return(String(buffer, n));
}
String time_format_local(String format, u64 timestamp) { return(wasm_time_strftime(format, timestamp, false)); }
String time_format_utc(String format, u64 timestamp)
{
if(format == "RFC1123")
format = "%a, %d %b %Y %T GMT";
return(wasm_time_strftime(format, timestamp, true));
}
static String wasm_time_expand_delta(String format, u64 timestamp, u64 now_timestamp)
{
u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0;
format = replace(format, "%deltaY", std::to_string(delta_seconds / (60 * 60 * 24 * 365)));
format = replace(format, "%deltam", std::to_string(delta_seconds / (60 * 60 * 24 * 30)));
format = replace(format, "%deltad", std::to_string(delta_seconds / (60 * 60 * 24)));
format = replace(format, "%deltaH", std::to_string(delta_seconds / (60 * 60)));
format = replace(format, "%deltaM", std::to_string(delta_seconds / 60));
format = replace(format, "%deltaS", std::to_string(delta_seconds));
return(format);
}
String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent)
{
u64 now_timestamp = time();
u64 delta_seconds = now_timestamp > timestamp ? now_timestamp - timestamp : 0;
format_very_recent = first(format_very_recent, "just now");
medium_recency_seconds = medium_recency_seconds > 0 ? medium_recency_seconds : 90;
format_medium_recent = first(format_medium_recent, "%deltaM minutes ago");
not_recent_seconds = not_recent_seconds > 0 ? not_recent_seconds : 90 * 60;
format_not_recent = first(format_not_recent, "%deltaH hours ago");
if(delta_seconds < medium_recency_seconds)
return(wasm_time_expand_delta(format_very_recent, timestamp, now_timestamp));
if(delta_seconds < not_recent_seconds)
return(wasm_time_expand_delta(format_medium_recent, timestamp, now_timestamp));
return(wasm_time_expand_delta(format_not_recent, timestamp, now_timestamp));
}
u64 time_parse(String time_String) { char* end = 0; unsigned long long v = strtoull(time_String.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : 0); }
u64 socket_connect(String host, short port) { (void)host; (void)port; return(0); }
void socket_close(u64 sockfd) { (void)sockfd; }
@ -112,11 +163,38 @@ bool memcache_delete(u64 connection, String key) { (void)connection; (void)key;
String memcache_get(u64 connection, String key, String default_value) { (void)connection; (void)key; return(default_value); }
StringMap memcache_get_multiple(u64 connection, StringList keys) { (void)connection; (void)keys; return(StringMap()); }
void on_segfault(int sig) { (void)sig; }
int task_kill(pid_t pid, int sig) { (void)pid; (void)sig; return(-1); }
static u64 wasm_next_task_callback_id = 1;
static std::map<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) { (void)key; (void)exec_after_spawn; (void)timeout; return(0); }
pid_t task_repeat(String key, f64 interval, std::function<void()> exec_after_spawn, u64 timeout) { (void)key; (void)interval; (void)exec_after_spawn; (void)timeout; return(0); }
pid_t task_pid(String key) { (void)key; return(0); }
pid_t task(String key, std::function<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()

View File

@ -117,13 +117,14 @@ static bool wasm_backend_native_fallback_uncached(Request* context, const String
// - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in),
// - unit_render()/component() (host resolver).
// What remains is genuinely host-owned / native-only for now:
// - zip, sqlite, background tasks, filesystem writes, sleeps;
// - zip, sockets/custom servers, memcache, mysql;
// - unit_call + compiler/unit introspection (need the native toolchain).
// Background tasks and sleep/usleep are host-owned but now have membrane
// hostcalls, so they intentionally do not fallback here.
StringList native_only_tokens = {
"zip_", "task(", "task_repeat(",
"task_pid(", "task_kill(", "unit_call(",
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(",
"usleep(", "sleep("
"zip_", "socket_", "server_start_http(", "server_stop(",
"memcache_", "mysql_", "unit_call(",
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit("
};
for(auto& token : native_only_tokens)
if(source.find(token) != String::npos)

View File

@ -3,6 +3,10 @@ uce_host_time_precise
uce_host_env
uce_host_log
uce_host_random
uce_host_task_spawn
uce_host_task_pid
uce_host_task_kill
uce_host_sleep_us
uce_host_component_resolve
uce_host_file_exists
uce_host_file_read

View File

@ -27,6 +27,7 @@
#include <wasmtime.hh>
#include <chrono>
#include <cmath>
#include <cstring>
#include <ctime>
#include <fstream>
@ -352,6 +353,27 @@ public:
}
#endif
// The guest calls a sized hostcall twice (buf=0 to learn the length, then
// to fetch). For side-effecting ops (sqlite) re-executing on the fetch is
// wrong, so the result is staged on the first call (keyed on the exact
// input bytes) and replayed on the second without re-running the op.
String staged_hostcall_input;
String staged_hostcall_result;
bool hostcall_staged(const String& input, String& out)
{
if(!staged_hostcall_input.empty() && input == staged_hostcall_input)
{
out = staged_hostcall_result;
return(true);
}
return(false);
}
void hostcall_stage(const String& input, const String& out)
{
staged_hostcall_input = input;
staged_hostcall_result = out;
}
// resolve-kind values shared with the guest core (src/wasm/core.cpp)
// must match WasmResolveKind in src/wasm/core.cpp
enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2, RESOLVE_ONCE = 3 };
@ -1054,6 +1076,17 @@ private:
return((int32_t)slot);
}
String run_task_callback(u64 callback_id)
{
auto runner = core_func("uce_wasm_task_run");
if(!runner)
return("core does not export uce_wasm_task_run");
auto result = runner->call(ctx(), { wasmtime::Val((int64_t)callback_id) });
if(!result)
return(trap_text(result.err()));
return("");
}
// ---- host imports for the core -----------------------------------------
wasmtime::Extern make_host_import(wasmtime::Store::Context cx, const String& mod, const String& name, const wasmtime::FuncType& func_type)
@ -1168,52 +1201,58 @@ private:
// handle table (handle = 1-based index).
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
DValue request, response, decode_err_unused;
String decode_error;
if(ucb_decode(encoded, request, &decode_error))
String out;
// run the op once across the length-query + fetch pair
if(!self->hostcall_staged(encoded, out))
{
String op = request["op"].to_string();
if(op == "connect")
DValue request, response;
String decode_error;
if(ucb_decode(encoded, request, &decode_error))
{
SQLite* db = new SQLite();
db->connect(request["path"].to_string());
u64 handle = 0;
if(db->connection)
String op = request["op"].to_string();
if(op == "connect")
{
self->sqlite_handles.push_back(db);
handle = self->sqlite_handles.size();
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->error_code;
response["statement_info"] = db->error();
if(handle == 0)
delete db;
}
else
{
u64 handle = request["handle"].to_u64();
SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size())
? self->sqlite_handles[(size_t)handle - 1] : 0;
if(op == "query" && db)
{
StringMap params;
DValue* p = request.key("params");
if(p)
p->each([&](const DValue& value, String key) { params[key] = value.to_string(); });
response["result"] = db->query(request["query"].to_string(), params);
response["insert_id"] = (f64)db->insert_id;
response["affected"] = (f64)db->affected_rows;
SQLite* db = new SQLite();
db->connect(request["path"].to_string());
u64 handle = 0;
if(db->connection)
{
self->sqlite_handles.push_back(db);
handle = self->sqlite_handles.size();
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->error_code;
response["statement_info"] = db->error();
if(handle == 0)
delete db;
}
else if(op == "disconnect" && db)
else
{
delete db;
self->sqlite_handles[(size_t)handle - 1] = 0;
u64 handle = request["handle"].to_u64();
SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size())
? self->sqlite_handles[(size_t)handle - 1] : 0;
if(op == "query" && db)
{
StringMap params;
DValue* p = request.key("params");
if(p)
p->each([&](const DValue& value, String key) { params[key] = value.to_string(); });
response["result"] = db->query(request["query"].to_string(), params);
response["insert_id"] = (f64)db->insert_id;
response["affected"] = (f64)db->affected_rows;
response["error_code"] = (f64)db->error_code;
response["statement_info"] = db->error();
}
else if(op == "disconnect" && db)
{
delete db;
self->sqlite_handles[(size_t)handle - 1] = 0;
}
}
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
String out = ucb_encode(response);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
@ -1232,6 +1271,70 @@ private:
::unlink(resolved.c_str());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_task_spawn")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String key;
self->hostcall_read(args[0].i32(), args[1].i32(), key);
u64 callback_id = (u64)args[2].i64();
f64 interval = args[3].f64();
u64 timeout = (u64)args[4].i64();
bool repeat = args[5].i32() != 0;
auto run_callback = [self, callback_id]() {
String error = self->run_task_callback(callback_id);
if(error != "")
fprintf(stderr, "[wasm task] callback failed: %s\n", error.c_str());
};
pid_t pid = 0;
try
{
if(!repeat || (interval > 0 && std::isfinite(interval)))
pid = repeat
? ::task_repeat(key, interval, run_callback, timeout)
: ::task(key, run_callback, timeout);
}
catch(const std::exception& e)
{
fprintf(stderr, "[wasm task] spawn failed for key '%s': %s\n", key.c_str(), e.what());
}
catch(...)
{
fprintf(stderr, "[wasm task] spawn failed for key '%s'\n", key.c_str());
}
results[0] = Val((int32_t)pid);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_task_pid")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String key;
self->hostcall_read(args[0].i32(), args[1].i32(), key);
results[0] = Val((int32_t)::task_pid(key));
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_task_kill")
return(add([](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
results[0] = Val((int32_t)::task_kill((pid_t)args[0].i32(), args[1].i32()));
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_sleep_us")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 usec = (u64)args[0].i64();
while(usec >= 1000000ull)
{
unsigned int remaining = ::sleep((unsigned int)(usec / 1000000ull));
if(remaining != 0)
{
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)remaining);
return(std::monostate());
}
usec %= 1000000ull;
}
if(usec > 0)
::usleep((useconds_t)usec);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_regex")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
// {op,pattern,subject,flags,replacement} in (UCEB1) → result out.

View File

@ -7,7 +7,7 @@ import json
from dataclasses import asdict, dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "site"
PATTERNS = [