From 7e2faf1472c84f302ce5b7f9674091a1a2b3930f Mon Sep 17 00:00:00 2001 From: udo Date: Fri, 12 Jun 2026 11:40:26 +0000 Subject: [PATCH] spike: validate WASM phase 0 --- WASM-PROPOSAL.md | 11 + spikes/wasm-phase0/FINDINGS.md | 188 +++++++++++ spikes/wasm-phase0/README.md | 30 ++ spikes/wasm-phase0/build_loader.sh | 17 + spikes/wasm-phase0/build_modules.sh | 51 +++ spikes/wasm-phase0/core.cpp | 74 +++++ spikes/wasm-phase0/loader.cpp | 444 ++++++++++++++++++++++++++ spikes/wasm-phase0/realunit-report.md | 165 ++++++++++ spikes/wasm-phase0/unit.cpp | 73 +++++ spikes/wasm-phase0/wasm_inspect.py | 95 ++++++ 10 files changed, 1148 insertions(+) create mode 100644 spikes/wasm-phase0/FINDINGS.md create mode 100644 spikes/wasm-phase0/README.md create mode 100644 spikes/wasm-phase0/build_loader.sh create mode 100644 spikes/wasm-phase0/build_modules.sh create mode 100644 spikes/wasm-phase0/core.cpp create mode 100644 spikes/wasm-phase0/loader.cpp create mode 100644 spikes/wasm-phase0/realunit-report.md create mode 100644 spikes/wasm-phase0/unit.cpp create mode 100644 spikes/wasm-phase0/wasm_inspect.py diff --git a/WASM-PROPOSAL.md b/WASM-PROPOSAL.md index 3c32287..8f228dd 100644 --- a/WASM-PROPOSAL.md +++ b/WASM-PROPOSAL.md @@ -417,6 +417,17 @@ AOT artifact quality, patchability. Exit: a two-module (core stub + unit 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** +> — 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 +> (`collections.uce.cpp`, `hello.uce.cpp` → side modules, no allocator +> definitions). Exceptions: `-fno-exceptions` confirmed across all of it +> (§11.1 stands). Cross-module C++ (containers, heap objects, function +> pointers, `std::function` lambdas, GOT.mem/GOT.func) all proven through +> the spike loader. + **Phase 1 — DValue C ABI in the native runtime.** Introduce `uce_dv_*` (§5.2) and the UCEB1 codec in `src/lib/`, used natively. Zero wasm dependency; immediately testable; freezes the contract diff --git a/spikes/wasm-phase0/FINDINGS.md b/spikes/wasm-phase0/FINDINGS.md new file mode 100644 index 0000000..7805086 --- /dev/null +++ b/spikes/wasm-phase0/FINDINGS.md @@ -0,0 +1,188 @@ +# Phase 0 findings — toolchain & runtime spike + +- **Status: EXIT CRITERION PASSED** (2026-06-12, on k-uce) +- **Runtime selected: Wasmtime** (v45.0.1, C API). WAMR rejected — evidence below. +- Exceptions decision (§11.1, error codes / `-fno-exceptions`) **confirmed viable**: + both the stubs and two real generated units compile with `-fno-exceptions`, + no try/catch blocker anywhere. + +The exit criterion ran end-to-end: a core stub (libc/libc++ statically linked, +owns memory/allocator) and a unit stub (PIC, `dylink.0`) were linked **at +runtime** by `loader.cpp` and produced: + +``` +hello from unit; unit-data-segment-ok; counter=7; mapsum=3; core-string+unit[cb:42][got-func-ok][fn:42] +core_counter (in linear memory): before=7 after=8 +PHASE0 EXIT CRITERION: PASS +``` + +which validates, in one render call: unit data-segment relocation +(`__memory_base`), GOT.mem read **and write** of a core global, `std::string` +/`std::map` in unit code on the core's heap, a heap C++ object created in core +and mutated/read by the unit, function pointers crossing unit→core→unit +through the shared table, GOT.func resolution, and a `std::function` lambda +allocated in the unit and invoked by core. That is the §3.4 contract +("DValue inside the workspace: no serialization, ever") demonstrated at the +ABI level. + +## Toolchain pins + +| What | Version | Where on k-uce | +|---|---|---| +| wasi-sdk | 33 (clang 22.1.0-wasi-sdk) | `/opt/wasi-sdk` | +| target triple | `wasm32-wasip1` (`wasm32-wasi` is deprecated) | — | +| Wasmtime C API | v45.0.1 (prebuilt x86_64-linux release) | `/opt/wasmtime` | +| WAMR (rejected) | WAMR-2.4.4, built from source | `/opt/wamr` | +| cmake / ninja | 3.31.6 / 1.12.1 (apt) | — | + +## Runtime selection: why not WAMR + +WAMR was tried first per §9 ("preferred ... use Wasmtime only if blocked"). +We are blocked, on the load-bearing requirement itself: + +1. **WAMR's wasm-c-api ignores imported memories and tables.** At unit + instantiation it logs `"doesn't support import memories and tables for + now, ignore them"` (`wasm_c_api.c`) and gives the instance its own + memory/table — which silently destroys the shared-workspace model. +2. **Host-side `wasm_table_grow` / `wasm_memory_grow` are explicitly + unsupported** ("Only allow growing a table via the opcode table.grow"). +3. Its build banner lists *Import/Export of Mutable Globals* as unsupported — + the dylink ABI imports `__stack_pointer` and every `GOT.*` entry as a + mutable global. + +Its multi-module feature is name-based auto-resolution, not host-orchestrated +dylink (no host-computed `__memory_base`/`__table_base`, no GOT). Making WAMR +fit means implementing import binding through the c-api layer and runtime +internals — a runtime-development project, not a patch. + +**Wasmtime v45.0.1 passed everything on the first run** through the standard +`wasm.h` C API: host-created funcref table shared by both instances, exported +memory imported by the unit, host-created (mutable) globals, cross-instance +export→import wiring. The remaining §9 criteria also favor it: AOT artifacts +(`.cwasm` precompilation) for the unit cache, epoch interruption for CPU +limits, and built-in copy-on-write memory-image instantiation for the §6 core +snapshot (machinery we'd have had to build ourselves on WAMR). + +Trade-off accepted (was already in §10): Rust codebase, heavier to +vendor/patch. Pin the release artifact (lib + headers, checksummed) the way +sqlite is vendored; building from source stays possible but is not the +default path. The C API .so is ~27 MB. + +## Module build recipe (what `build_modules.sh` settled on) + +Core (non-PIC reactor, owns libc/libc++/allocator): +``` +clang++ --target=wasm32-wasip1 -mexec-model=reactor -O1 -fno-exceptions \ + core.cpp -o core.wasm \ + -Wl,--export-all -Wl,--import-table \ + -Wl,--export=__stack_pointer -Wl,--export=__heap_base \ + -Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE +``` + +Unit (PIC side module): +``` +clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default \ + -fvisibility-inlines-hidden -O1 -fno-exceptions -c unit.cpp +wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \ + --Bsymbolic unit.o -o unit.wasm --export= +``` + +Hard-won flag findings: + +1. **`--unresolved-symbols=import-dynamic`** is required for the side-module + link; undefined symbols then become `env.*` function imports and `GOT.*` + globals exactly per the Emscripten dylink ABI. +2. **`-fvisibility-inlines-hidden` is mandatory.** Without it one libc++ + vague-linkage lambda (`std::map` tree-emplace internals, missing libc++'s + usual hide-from-ABI attribute) is emitted as *both* an export and an + import of the unit — a self-import the loader cannot satisfy at + instantiation time without lazy-binding trampolines. `--Bsymbolic` alone + did **not** bind it. +3. **Core symbol closure**: `--export-all` only exports what got *linked*. + The unit needed `__cxxabiv1::__class_type_info`'s vtable (RTTI machinery + behind `std::function`), which the core never references — forced in with + `--undefined=`. The production core needs a closure strategy: + `--whole-archive` for libc/libc++/libc++abi, or a curated keep-list. The + loader also implements the complementary fallback (resolve `GOT.mem` of + weak data from the unit's *own* exports post-instantiation, patching the + provisional GOT global). +4. **`--import-table` on the core** + a **host-created table** is the right + shape (see loader notes); `--export-table`/`--growable-table` was the + first attempt and died on WAMR's host-grow limitation, but host-created + stays the better design under Wasmtime too: the loader picks table size + (core's declared minimum + headroom) before any instantiation. +5. `wasm32-wasi` triple is deprecated in wasi-sdk 33 → use `wasm32-wasip1`. + +## Loader notes (`loader.cpp`, ~450 lines, standard wasm-c-api) + +Sequence proven: instantiate core (45 WASI imports satisfied with named trap +stubs — none was ever called) → parse `dylink.0` (`mem_info`: memsize/align, +tablesize) → `__memory_base` = call core's exported `malloc` → `__table_base` += bump pointer starting at core's table-import minimum → build the unit's +import vector (memory/table/`__stack_pointer` shared from core; `env.*` +functions from core exports; `GOT.mem.*` as host mutable i32 globals holding +addresses read from core's exported data-symbol globals) → instantiate → +patch deferred GOT entries → `__wasm_apply_data_relocs` → `__wasm_call_ctors` +→ call the entry export. + +- **GOT.func is resolved guest-side**: the core exports a helper returning + `(intptr_t)&func` — taking the address forces a link-time elem entry and a + wasm function pointer *is* its table index. No host-side funcref injection + is needed at all (it was WAMR-unsupported; under Wasmtime it would work but + the guest-side registry is simpler and runtime-agnostic). The production + core should carry a name→funcptr registry (dlsym-shaped) for its API + surface. +- **wasi-libc gotcha**: `_initialize` has a double-init guard ending in + `__builtin_trap()`. WAMR runs `_initialize` automatically at instantiation + (so calling it again traps "unreachable"); Wasmtime does not (so you must + call it). Cost one debugging round; recorded here so it never costs another. +- Export-name `wasm_name_t` may include the trailing NUL in `size` (WAMR + did); trim when indexing exports by name. + +## Real generated units (delegated grind — full log in `realunit-report.md`) + +`site/demo/collections.uce.cpp` and `hello.uce.cpp` (taken verbatim from the +live unit cache at `/tmp/uce/work/...`) both compile and link as PIC side +modules with `dylink.0`, **no allocator definitions**, with only shim-level +intervention. `collections.wasm`: 42 KB, 52 imports — including exactly the +predicted `GOT.mem.context` for the global `Request*`. Friction points that +become Phase 2 work items: + +1. **`types.h` defines global `operator new/delete` in every unit** — must be + gated (`#ifdef`) out of side-module builds; allocator ownership belongs to + the core (§3.2: "the one fatal misconfiguration"). +2. **`sys.h` includes ``** → wasi needs `-D_WASI_EMULATED_SIGNAL` + (+ `-lwasi-emulated-signal` in the core) or an `#ifdef __wasm__` carve-out; + signals/fork/exec/sockets in `sys.h` have no wasi equivalent and move + behind hostcalls anyway (§5.1). +3. **Generated units include `uce_lib.h` by absolute path** — the + preprocessor should emit a logical include so the wasm build can supply + its own include order. +4. **Header-inline connector wrappers (MySQL etc.) get pulled into every + unit** regardless of use; the §3.3 membrane split (thin wasm-side shim, + host-side implementation) resolves this and shrinks unit import lists. +5. The real-unit compile used the pre-`-fvisibility-inlines-hidden` flag set + (parallel work); the final unit flag set above should be used from + Phase 2 on. + +## Implications for the next phases + +- **Phase 1 (DValue C ABI, native)**: unaffected by any of this; proceed as + written. +- **Phase 2 (core module + membrane)**: add the closure strategy + (whole-archive), the GOT.func name→funcptr registry, the `types.h` + allocator gate, the signal emulation define, and the preprocessor include + change. Compile `uce_lib` with the core recipe above. +- **Phase 3 (loader)**: `loader.cpp` here is the skeleton — dylink parsing, + base allocation, GOT resolution, and init sequencing are all proven; what + remains is the registry/dispatch layer, ABI stamping, and multi-unit + placement. +- **Phase 4**: use Wasmtime's epoch interruption for CPU limits and its + memory-image/CoW instantiation for the core snapshot rather than building + either by hand. + +## Artifacts (on k-uce, not in git) + +- `/tmp/uce/wasm-phase0/{core.wasm,unit.wasm,loader}` — exit-criterion run +- `/tmp/uce/wasm-phase0/realunit/` — real-unit compiles + shim tree + inspector dumps +- `/opt/wasi-sdk`, `/opt/wasmtime`, `/opt/wamr` — toolchains/runtimes diff --git a/spikes/wasm-phase0/README.md b/spikes/wasm-phase0/README.md new file mode 100644 index 0000000..9e89603 --- /dev/null +++ b/spikes/wasm-phase0/README.md @@ -0,0 +1,30 @@ +# spikes/wasm-phase0 — WASM-PROPOSAL Phase 0 (toolchain & runtime spike) + +Validates the dynamic-linking foundation for the WASM unit runtime: +wasi-sdk PIC side modules + a host loader linking a core module and a unit +module at runtime with shared memory/table. **Exit criterion passed; runtime +selected: Wasmtime.** See `FINDINGS.md` for results and `realunit-report.md` +for the real-generated-unit compile log. + +Everything builds and runs **on k-uce** (toolchains under `/opt`, artifacts +under `/tmp/uce/wasm-phase0`): + +``` +bash spikes/wasm-phase0/build_modules.sh # core.wasm + unit.wasm (wasi-sdk) +bash spikes/wasm-phase0/build_loader.sh # loader (Wasmtime C API) +/tmp/uce/wasm-phase0/loader # expect: PHASE0 EXIT CRITERION: PASS +``` + +Files: + +- `core.cpp` — core-module stand-in: owns memory/allocator/libc++, exports + everything; includes the guest-side GOT.func resolver pattern +- `unit.cpp` — unit stand-in: PIC, imports allocator/runtime, exercises GOT, + cross-module C++ objects, function pointers, lambdas +- `loader.cpp` — minimal runtime linker (standard wasm-c-api): dylink.0 + parsing, base allocation, GOT resolution, init sequencing +- `wasm_inspect.py` — dumps imports/exports/dylink.0 of a module +- `FINDINGS.md` — toolchain pins, WAMR rejection evidence, flag recipe, + Phase 2/3 implications +- `realunit-report.md` — `collections.uce.cpp`/`hello.uce.cpp` compiled as + side modules (worker-agent log) diff --git a/spikes/wasm-phase0/build_loader.sh b/spikes/wasm-phase0/build_loader.sh new file mode 100644 index 0000000..a1ae08e --- /dev/null +++ b/spikes/wasm-phase0/build_loader.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Phase 0: build the minimal loader against the WAMR build at /opt/wamr. +# Runs on k-uce. +set -e +cd "$(dirname "$0")" + +# Runtime: Wasmtime C API. (WAMR was evaluated first per WASM-PROPOSAL §9 but +# its wasm-c-api ignores imported memories/tables — see FINDINGS.md.) +WASMTIME=/opt/wasmtime +OUT=/tmp/uce/wasm-phase0 + +clang++ -std=c++17 -O1 -g loader.cpp -o "$OUT/loader" \ + -I"$WASMTIME/include" \ + -L"$WASMTIME/lib" -lwasmtime -Wl,-rpath,"$WASMTIME/lib" \ + -lpthread -lm -ldl + +echo built: "$OUT/loader" diff --git a/spikes/wasm-phase0/build_modules.sh b/spikes/wasm-phase0/build_modules.sh new file mode 100644 index 0000000..cc25a30 --- /dev/null +++ b/spikes/wasm-phase0/build_modules.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Phase 0: build core (non-PIC reactor, libc statically linked, all-exported) +# and unit (-fPIC, wasm-ld -shared, dylink.0) with wasi-sdk. +# Runs on k-uce. Artifacts go to /tmp/uce/wasm-phase0. +set -e +cd "$(dirname "$0")" + +SDK=/opt/wasi-sdk +OUT=/tmp/uce/wasm-phase0 +mkdir -p "$OUT" + +# Core module: reactor (no main, exports _initialize), exceptions off per +# WASM-PROPOSAL §11.1 (error codes at unit boundaries). +# --export-all so unit modules can import libc/libc++/core symbols from it; +# --import-table so the host creates the shared funcref table (sized with +# headroom up front — WAMR cannot grow or write tables from the host side); +# __stack_pointer exported because PIC side modules import it (dylink ABI). +"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \ + -O1 -fno-exceptions \ + core.cpp -o "$OUT/core.wasm" \ + -Wl,--export-all \ + -Wl,--import-table \ + -Wl,--export=__stack_pointer \ + -Wl,--export=__heap_base \ + -Wl,--undefined=_ZTVN10__cxxabiv117__class_type_infoE + +# Unit module: PIC object, linked -shared with no libc/libc++ of its own — +# every undefined symbol must become an import (resolved from core by the +# loader). Inline/template code (std::string etc.) instantiates locally, +# which is fine; the allocator must NOT be defined here (§3.2). +# -fvisibility-inlines-hidden: vague-linkage code (templates, lambdas) +# binds locally instead of being interposable — without it, one libc++ +# tree-emplace lambda became a self-import the loader cannot satisfy. +"$SDK/bin/clang++" --target=wasm32-wasip1 -fPIC -fvisibility=default \ + -fvisibility-inlines-hidden \ + -O1 -fno-exceptions \ + -c unit.cpp -o "$OUT/unit.o" + +# --Bsymbolic: bind weak/vague-linkage symbols the unit defines itself +# (template instantiations, lambdas) locally instead of emitting +# self-imports that the loader would have to lazy-bind. +"$SDK/bin/wasm-ld" -shared --experimental-pic \ + --unresolved-symbols=import-dynamic \ + --Bsymbolic \ + "$OUT/unit.o" -o "$OUT/unit.wasm" \ + --export=uce_unit_render + +echo "--- core.wasm ---" +ls -la "$OUT/core.wasm" +echo "--- unit.wasm ---" +ls -la "$OUT/unit.wasm" diff --git a/spikes/wasm-phase0/core.cpp b/spikes/wasm-phase0/core.cpp new file mode 100644 index 0000000..d72b58b --- /dev/null +++ b/spikes/wasm-phase0/core.cpp @@ -0,0 +1,74 @@ +// WASM-PROPOSAL Phase 0 — core module stub. +// +// Stands in for the future "core module" (uce_lib + libc compiled to wasm): +// owns linear memory, the allocator, and libc/libc++ (statically linked, +// per the §10 mitigation: avoid shared wasi-libc entirely). Built as a +// non-PIC wasm32-wasi reactor with everything exported so unit modules can +// import from it. +// +// Deliberately avoids WASI I/O (no printf-to-fd): output accumulates in a +// buffer the host reads back, so the module can be instantiated through the +// plain wasm-c-api without a WASI context if need be. + +#include +#include +#include +#include + +static std::string g_output; + +extern "C" { + +// data symbol referenced from the unit module → must arrive there as a +// GOT.mem import +int core_counter = 7; + +void uce_print(const char* s, size_t len) +{ + g_output.append(s, len); +} + +// heap C++ object created in core, handed to the unit by pointer — +// validates one-heap/one-allocator pointer semantics (§3.4) +std::string* core_make_string(const char* s) +{ + return(new std::string(s)); +} + +void core_append_string(std::string* str, const char* s) +{ + str->append(s); +} + +// plain function pointer crossing: unit passes its own function, core calls +// it back — validates the shared funcref table +void core_invoke_callback(void (*cb)(int), int arg) +{ + cb(arg); +} + +// std::function allocated by unit code, executed here — validates fat +// callable objects (lambdas) across module boundaries +void core_invoke_function(std::function* f, int arg) +{ + char buf[64]; + snprintf(buf, sizeof(buf), "[fn:%d]", (*f)(arg)); + g_output.append(buf); +} + +// GOT.func resolution helper: taking the address here forces uce_print into +// core's elem segment at link time, and on wasm a function pointer IS its +// table index — so the loader can resolve GOT.func.uce_print with a plain +// call instead of host-side funcref injection (which WAMR does not allow). +// The production core will generalize this into a name → funcptr registry. +intptr_t core_table_index_of_uce_print() +{ + return(reinterpret_cast(uce_print)); +} + +// host reads the result out of linear memory via these +const char* core_output_data() { return(g_output.data()); } +size_t core_output_size() { return(g_output.size()); } +void core_output_clear() { g_output.clear(); } + +} diff --git a/spikes/wasm-phase0/loader.cpp b/spikes/wasm-phase0/loader.cpp new file mode 100644 index 0000000..902216f --- /dev/null +++ b/spikes/wasm-phase0/loader.cpp @@ -0,0 +1,444 @@ +// WASM-PROPOSAL Phase 0 — minimal runtime loader (exit criterion). +// +// Embeds a wasm runtime through the standard wasm-c-api and links two +// modules at runtime, the way the production loader (§6) will: +// +// 1. instantiate core.wasm (owns memory/table/allocator/libc), stubbing +// its WASI imports with named trap functions (the core stub does no +// real I/O; any stub that actually gets called names itself) +// 2. parse unit.wasm's dylink.0 → data size/align, table slots needed +// 3. allocate __memory_base by calling core's exported malloc, and +// __table_base by growing core's exported funcref table +// 4. build the unit's import vector: env.memory / env.__indirect_function_table / +// env.__stack_pointer straight from core's exports; env.* functions from +// core's exports; GOT.mem.* as mutable i32 globals holding resolved +// addresses (self-resolved from the unit's own exports after +// instantiation when core lacks the symbol — weak data case); +// GOT.func.* as mutable i32 globals holding freshly grown table slots +// pointing at core export funcrefs +// 5. instantiate unit, patch deferred GOT entries, run +// __wasm_apply_data_relocs then __wasm_call_ctors +// 6. call uce_unit_render and read the output back out of linear memory +// +// Everything here is deliberately validation-grade: fail loudly, never guess. + +#include + +#include +#include +#include +#include +#include +#include + +#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0) +#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0) + +static std::vector read_file(const char* fn) +{ + FILE* f = fopen(fn, "rb"); + CHECK(f, "cannot open %s", fn); + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + std::vector buf(n); + CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn); + fclose(f); + return buf; +} + +// ---- minimal dylink.0 parser ------------------------------------------- + +struct DylinkInfo +{ + uint32_t mem_size = 0; + uint32_t mem_align = 0; // power of 2 + uint32_t table_size = 0; + uint32_t table_align = 0; + bool found = false; +}; + +static uint64_t read_uleb(const uint8_t* buf, size_t& pos) +{ + uint64_t result = 0; + int shift = 0; + while(true) + { + uint8_t b = buf[pos++]; + result |= (uint64_t)(b & 0x7f) << shift; + if(!(b & 0x80)) + return result; + shift += 7; + } +} + +static DylinkInfo parse_dylink(const std::vector& wasm) +{ + DylinkInfo info; + CHECK(wasm.size() > 8 && !memcmp(wasm.data(), "\0asm", 4), "not a wasm module"); + size_t pos = 8; + while(pos < wasm.size()) + { + uint8_t sec_id = wasm[pos++]; + uint64_t size = read_uleb(wasm.data(), pos); + size_t end = pos + size; + if(sec_id == 0) + { + uint64_t name_len = read_uleb(wasm.data(), pos); + std::string name((const char*)wasm.data() + pos, name_len); + pos += name_len; + if(name == "dylink.0") + { + while(pos < end) + { + uint8_t sub = wasm[pos++]; + uint64_t sub_len = read_uleb(wasm.data(), pos); + size_t sub_end = pos + sub_len; + if(sub == 1) // WASM_DYLINK_MEM_INFO + { + info.mem_size = read_uleb(wasm.data(), pos); + info.mem_align = read_uleb(wasm.data(), pos); + info.table_size = read_uleb(wasm.data(), pos); + info.table_align = read_uleb(wasm.data(), pos); + info.found = true; + } + pos = sub_end; + } + } + } + pos = end; + } + return info; +} + +// ---- named trap stubs for unsatisfied (WASI) imports -------------------- + +static wasm_store_t* g_store_for_stubs = nullptr; + +static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results) +{ + (void)args; (void)results; + fprintf(stderr, "[stub called: %s]\n", (const char*)env); + char msg[256]; + snprintf(msg, sizeof(msg), "unimplemented host import called: %s", (const char*)env); + wasm_message_t message; + wasm_byte_vec_new(&message, strlen(msg) + 1, msg); + wasm_trap_t* trap = wasm_trap_new(g_store_for_stubs, &message); + wasm_byte_vec_delete(&message); + return trap; +} + +// ---- instance wrapper: name → extern map -------------------------------- + +struct Instance +{ + wasm_module_t* module = nullptr; + wasm_instance_t* instance = nullptr; + wasm_extern_vec_t exports = WASM_EMPTY_VEC; + std::map by_name; + + void index_exports() + { + wasm_exporttype_vec_t types = WASM_EMPTY_VEC; + wasm_module_exports(module, &types); + wasm_instance_exports(instance, &exports); + CHECK(types.size == exports.size, "export type/extern count mismatch"); + for(size_t i = 0; i < types.size; i++) + { + const wasm_name_t* nm = wasm_exporttype_name(types.data[i]); + std::string key(nm->data, nm->size); + // some wasm-c-api impls include the trailing NUL in name size + while(!key.empty() && key.back() == '\0') + key.pop_back(); + by_name[key] = exports.data[i]; + } + wasm_exporttype_vec_delete(&types); + } + + wasm_func_t* func(const char* name) + { + auto it = by_name.find(name); + return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second); + } + wasm_global_t* global(const char* name) + { + auto it = by_name.find(name); + return it == by_name.end() ? nullptr : wasm_extern_as_global(it->second); + } +}; + +static wasm_store_t* g_store = nullptr; + +static void report_trap(wasm_trap_t* trap, const char* what) +{ + if(!trap) + return; + wasm_message_t msg; + wasm_trap_message(trap, &msg); + FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data); +} + +static int32_t call_i32(Instance& inst, const char* name, std::vector argv = {}) +{ + wasm_func_t* f = inst.func(name); + CHECK(f, "missing export func %s", name); + wasm_val_t args_buf[4]; + for(size_t i = 0; i < argv.size(); i++) + args_buf[i] = WASM_I32_VAL(argv[i]); + wasm_val_t results_buf[1] = { WASM_INIT_VAL }; + wasm_val_vec_t args = { argv.size(), args_buf }; + wasm_val_vec_t results = { 1, results_buf }; + size_t result_arity = wasm_func_result_arity(f); + wasm_val_vec_t no_results = WASM_EMPTY_VEC; + wasm_trap_t* trap = wasm_func_call(f, &args, result_arity ? &results : &no_results); + report_trap(trap, name); + return result_arity ? results_buf[0].of.i32 : 0; +} + +static wasm_global_t* make_i32_global(int32_t value, wasm_mutability_t mut) +{ + wasm_globaltype_t* gt = wasm_globaltype_new(wasm_valtype_new(WASM_I32), mut); + wasm_val_t val = WASM_I32_VAL(value); + wasm_global_t* g = wasm_global_new(g_store, gt, &val); + CHECK(g, "wasm_global_new failed (host-created globals unsupported?)"); + wasm_globaltype_delete(gt); + return g; +} + +int main(int argc, char** argv) +{ + const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase0/core.wasm"; + const char* unit_path = argc > 2 ? argv[2] : "/tmp/uce/wasm-phase0/unit.wasm"; + + wasm_engine_t* engine = wasm_engine_new(); + CHECK(engine, "engine"); + g_store = wasm_store_new(engine); + CHECK(g_store, "store"); + g_store_for_stubs = g_store; + + // ---- 1. core module --------------------------------------------------- + std::vector core_bytes = read_file(core_path); + wasm_byte_vec_t core_bv; + wasm_byte_vec_new(&core_bv, core_bytes.size(), (const char*)core_bytes.data()); + Instance core; + core.module = wasm_module_new(g_store, &core_bv); + wasm_byte_vec_delete(&core_bv); + CHECK(core.module, "core module load failed"); + + // the shared funcref table is host-created (core links with --import-table) + // because WAMR cannot grow a table from the host: size it up front as + // core's declared minimum (= its own elem needs) plus headroom for unit + // module table regions. __table_base allocation bumps from the minimum. + wasm_table_t* table = nullptr; + uint32_t table_next_free = 0; + const uint32_t TABLE_HEADROOM = 2048; + + wasm_importtype_vec_t core_imports = WASM_EMPTY_VEC; + wasm_module_imports(core.module, &core_imports); + std::vector core_import_externs(core_imports.size); + for(size_t i = 0; i < core_imports.size; i++) + { + const wasm_name_t* mod = wasm_importtype_module(core_imports.data[i]); + const wasm_name_t* nm = wasm_importtype_name(core_imports.data[i]); + const wasm_externtype_t* et = wasm_importtype_type(core_imports.data[i]); + std::string name(nm->data, nm->size); + if(wasm_externtype_kind(et) == WASM_EXTERN_TABLE) + { + CHECK(name.rfind("__indirect_function_table", 0) == 0, + "unexpected core table import %s", name.c_str()); + const wasm_tabletype_t* tt = wasm_externtype_as_tabletype_const(et); + uint32_t core_min = wasm_tabletype_limits(tt)->min; + wasm_limits_t limits = { core_min + TABLE_HEADROOM, core_min + TABLE_HEADROOM }; + wasm_tabletype_t* host_tt = wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &limits); + table = wasm_table_new(g_store, host_tt, nullptr); + CHECK(table, "wasm_table_new failed (host-created tables unsupported?)"); + wasm_tabletype_delete(host_tt); + table_next_free = core_min; + printf("host table created: size=%u, core region=[0,%u)\n", core_min + TABLE_HEADROOM, core_min); + core_import_externs[i] = wasm_table_as_extern(table); + continue; + } + CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, + "core has unexpected non-func import %.*s.%s", + (int)mod->size, mod->data, name.c_str()); + // named trap stub; leaks the name string, fine for a spike + char* label = strdup((std::string(mod->data, mod->size) + "." + name).c_str()); + const wasm_functype_t* ft = wasm_externtype_as_functype_const(et); + wasm_func_t* stub = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr); + CHECK(stub, "stub func creation failed for %s", label); + core_import_externs[i] = wasm_func_as_extern(stub); + } + CHECK(table, "core does not import __indirect_function_table — rebuild with --import-table"); + wasm_extern_vec_t core_iv = { core_import_externs.size(), core_import_externs.data() }; + wasm_trap_t* trap = nullptr; + core.instance = wasm_instance_new(g_store, core.module, &core_iv, &trap); + report_trap(trap, "core instantiation"); + CHECK(core.instance, "core instantiation failed"); + core.index_exports(); + printf("core instantiated: %zu exports\n", core.by_name.size()); + + // reactor init (runs ctors). Wasmtime does not auto-run _initialize; + // WAMR does — and calling it twice trips wasi-libc's double-init guard + // (__builtin_trap → "unreachable"), which cost us a debugging round. + if(core.func("_initialize")) + call_i32(core, "_initialize"); + + wasm_memory_t* memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr); + CHECK(memory, "core does not export memory"); + + // ---- 2./3. dylink + base allocation ------------------------------------ + std::vector unit_bytes = read_file(unit_path); + DylinkInfo dl = parse_dylink(unit_bytes); + CHECK(dl.found, "unit has no dylink.0 mem_info"); + printf("dylink.0: memsize=%u memalign=2^%u tablesize=%u\n", dl.mem_size, dl.mem_align, dl.table_size); + + uint32_t align = 1u << dl.mem_align; + int32_t raw = call_i32(core, "malloc", { (int32_t)(dl.mem_size + align) }); + CHECK(raw, "core malloc returned 0"); + int32_t memory_base = (raw + (align - 1)) & ~(int32_t)(align - 1); + + uint32_t table_base = table_next_free; + table_next_free += dl.table_size; + CHECK(table_next_free <= wasm_table_size(table), "table headroom exhausted"); + printf("bases: __memory_base=%d __table_base=%u (table size %u)\n", + memory_base, table_base, wasm_table_size(table)); + + // ---- 4. unit import resolution ----------------------------------------- + wasm_byte_vec_t unit_bv; + wasm_byte_vec_new(&unit_bv, unit_bytes.size(), (const char*)unit_bytes.data()); + Instance unit; + unit.module = wasm_module_new(g_store, &unit_bv); + wasm_byte_vec_delete(&unit_bv); + CHECK(unit.module, "unit module load failed"); + + wasm_importtype_vec_t unit_imports = WASM_EMPTY_VEC; + wasm_module_imports(unit.module, &unit_imports); + std::vector unit_import_externs(unit_imports.size); + // GOT.mem entries that must be self-resolved from the unit's own exports + // after instantiation (weak data the core does not define) + std::vector> deferred_got; + + for(size_t i = 0; i < unit_imports.size; i++) + { + const wasm_name_t* mod_n = wasm_importtype_module(unit_imports.data[i]); + const wasm_name_t* nm_n = wasm_importtype_name(unit_imports.data[i]); + std::string mod(mod_n->data, mod_n->size); + std::string nm(nm_n->data, nm_n->size); + const wasm_externtype_t* et = wasm_importtype_type(unit_imports.data[i]); + wasm_externkind_t kind = wasm_externtype_kind(et); + wasm_extern_t* resolved = nullptr; + + if(mod == "env" && nm == "memory") + resolved = wasm_memory_as_extern(memory); + else if(mod == "env" && nm == "__indirect_function_table") + resolved = wasm_table_as_extern(table); + else if(mod == "env" && nm == "__stack_pointer") + { + CHECK(core.global("__stack_pointer"), "core does not export __stack_pointer"); + resolved = core.by_name["__stack_pointer"]; + } + else if(mod == "env" && nm == "__memory_base") + resolved = wasm_global_as_extern(make_i32_global(memory_base, WASM_CONST)); + else if(mod == "env" && nm == "__table_base") + resolved = wasm_global_as_extern(make_i32_global((int32_t)table_base, WASM_CONST)); + else if(mod == "env" && kind == WASM_EXTERN_FUNC) + { + auto it = core.by_name.find(nm); + CHECK(it != core.by_name.end(), "unresolved unit func import env.%s", nm.c_str()); + resolved = it->second; + } + else if(mod == "GOT.mem") + { + wasm_global_t* src = core.global(nm.c_str()); + if(src) + { + wasm_val_t v; + wasm_global_get(src, &v); + resolved = wasm_global_as_extern(make_i32_global(v.of.i32, WASM_VAR)); + } + else + { + // provisional 0; patched from the unit's own export post-instantiation + wasm_global_t* g = make_i32_global(0, WASM_VAR); + deferred_got.push_back({ nm, g }); + resolved = wasm_global_as_extern(g); + } + } + else if(mod == "GOT.func") + { + // resolved guest-side: the core exports a helper returning + // (intptr_t)&func, which on wasm is the function's table index — + // no host-side funcref injection needed (WAMR forbids it anyway) + std::string helper = "core_table_index_of_" + nm; + CHECK(core.func(helper.c_str()), "no GOT.func resolver %s in core", helper.c_str()); + int32_t slot = call_i32(core, helper.c_str()); + CHECK(slot > 0, "GOT.func resolver %s returned %d", helper.c_str(), slot); + resolved = wasm_global_as_extern(make_i32_global(slot, WASM_VAR)); + } + CHECK(resolved, "unhandled unit import %s.%s (kind %d)", mod.c_str(), nm.c_str(), (int)kind); + unit_import_externs[i] = resolved; + } + + // ---- 5. instantiate unit, patch GOT, run init --------------------------- + wasm_extern_vec_t unit_iv = { unit_import_externs.size(), unit_import_externs.data() }; + trap = nullptr; + unit.instance = wasm_instance_new(g_store, unit.module, &unit_iv, &trap); + report_trap(trap, "unit instantiation"); + CHECK(unit.instance, "unit instantiation failed"); + unit.index_exports(); + printf("unit instantiated: %zu exports\n", unit.by_name.size()); + + for(auto& [nm, got] : deferred_got) + { + wasm_global_t* own = unit.global(nm.c_str()); + CHECK(own, "GOT.mem.%s defined neither by core nor by unit", nm.c_str()); + wasm_val_t v; + wasm_global_get(own, &v); + wasm_val_t nv = WASM_I32_VAL(v.of.i32); + wasm_global_set(got, &nv); + printf("self-resolved GOT.mem.%s = %d\n", nm.c_str(), v.of.i32); + } + + if(unit.func("__wasm_apply_data_relocs")) + call_i32(unit, "__wasm_apply_data_relocs"); + if(unit.func("__wasm_call_ctors")) + call_i32(unit, "__wasm_call_ctors"); + + // ---- 6. render and read back -------------------------------------------- + int32_t counter_addr = 0; + { + wasm_global_t* cc = core.global("core_counter"); + CHECK(cc, "core_counter not exported"); + wasm_val_t v; + wasm_global_get(cc, &v); + counter_addr = v.of.i32; + } + byte_t* mem = wasm_memory_data(memory); + int32_t counter_before; + memcpy(&counter_before, mem + counter_addr, 4); + + call_i32(unit, "uce_unit_render"); + + int32_t out_ptr = call_i32(core, "core_output_data"); + int32_t out_len = call_i32(core, "core_output_size"); + mem = wasm_memory_data(memory); // may have moved if memory grew + int32_t counter_after; + memcpy(&counter_after, mem + counter_addr, 4); + + printf("---- unit output (%d bytes) ----\n%.*s\n--------------------------------\n", + out_len, out_len, mem + out_ptr); + printf("core_counter (in linear memory): before=%d after=%d\n", counter_before, counter_after); + + bool ok = out_len > 0 && + memmem(mem + out_ptr, out_len, "unit-data-segment-ok", 20) && + memmem(mem + out_ptr, out_len, "counter=7", 9) && + memmem(mem + out_ptr, out_len, "mapsum=3", 8) && + memmem(mem + out_ptr, out_len, "core-string+unit", 16) && + memmem(mem + out_ptr, out_len, "[cb:42]", 7) && + memmem(mem + out_ptr, out_len, "[got-func-ok]", 13) && + memmem(mem + out_ptr, out_len, "[fn:42]", 7) && + counter_before == 7 && counter_after == 8; + + printf(ok ? "PHASE0 EXIT CRITERION: PASS\n" : "PHASE0 EXIT CRITERION: FAIL (see output above)\n"); + return ok ? 0 : 1; +} diff --git a/spikes/wasm-phase0/realunit-report.md b/spikes/wasm-phase0/realunit-report.md new file mode 100644 index 0000000..265f493 --- /dev/null +++ b/spikes/wasm-phase0/realunit-report.md @@ -0,0 +1,165 @@ +# UCE real generated unit -> WASM PIC side module report + +Remote host: `k-uce` +Working directory/artifacts left in place: `/tmp/uce/wasm-phase0/realunit/` +Repository `/Code/uce.openfu.com/uce` was treated read-only; patched copies/shims only under `/tmp/uce/wasm-phase0/realunit/`. + +## Result + +Both real generated units compiled and linked as WASM PIC side modules with `dylink.0`: + +- `collections.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/collections.wasm` + - `dylink.0`: `memsize=1208 memalign=2^2 tablesize=3 tablealign=2^0` + - imports: 52 + - exports: 23 + - allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`). +- `hello.uce.cpp` -> `/tmp/uce/wasm-phase0/realunit/hello.wasm` + - `dylink.0`: `memsize=5544 memalign=2^2 tablesize=0 tablealign=2^0` + - imports: 38 + - exports: 24 + - allocator status: no exported/defined `malloc`, `free`, or `operator new`; `operator new`/sized delete are imports (`env._Znwm`, `env._ZdlPvm`). + +Inspector outputs are saved remotely as: + +- `/tmp/uce/wasm-phase0/realunit/collections.inspect.txt` +- `/tmp/uce/wasm-phase0/realunit/hello.inspect.txt` + +## Final command lines + +Run on `k-uce` from `/tmp/uce/wasm-phase0/realunit`. + +`collections`: + +```sh +/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \ + -I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \ + -c collections.uce.cpp -o collections.o + +/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \ + collections.o -o collections.wasm --export=__uce_render + +python3 /Code/uce.openfu.com/uce/spikes/wasm-phase0/wasm_inspect.py collections.wasm +``` + +`hello`: + +```sh +/opt/wasi-sdk/bin/clang++ --target=wasm32-wasip1 -fPIC -fvisibility=default -O1 -fno-exceptions \ + -I/tmp/uce/wasm-phase0/realunit/shim -I/Code/uce.openfu.com/uce/src/lib \ + -c hello.uce.cpp -o hello.o + +/opt/wasi-sdk/bin/wasm-ld -shared --experimental-pic --unresolved-symbols=import-dynamic \ + hello.o -o hello.wasm --export=__uce_render +``` + +Note: the stub-spike command exported `uce_unit_render`, but these generated units define/export `__uce_render` from the repo `RENDER` macro, so the real-unit link exports `__uce_render`. + +## Ordered friction points and fixes + +1. **WASI sysroot rejects `` by default.** + + First compile failed via `/Code/uce.openfu.com/uce/src/lib/sys.h`: + + ```txt + /share/wasi-sysroot/include/wasm32-wasip1/signal.h:2:2: error: + "wasm lacks signal support; to enable minimal signal emulation, compile with -D_WASI_EMULATED_SIGNAL ..." + ``` + + Fix: added `/tmp/uce/wasm-phase0/realunit/shim/signal.h` before repo/system includes. Key lines: + + ```c + typedef int sig_atomic_t; + typedef void (*sighandler_t)(int); + #define SIGTERM 15 + int raise(int); + sighandler_t signal(int, sighandler_t); + ``` + + This is only enough for declarations in `sys.h`; real signal behavior has no WASI equivalent. + +2. **Generated `collections.uce.cpp` includes missing local `demo_guard.h`.** + + First compile also failed: + + ```txt + fatal error: 'demo_guard.h' file not found + ``` + + Fix: added empty include guard shim `/tmp/uce/wasm-phase0/realunit/shim/demo_guard.h`: + + ```c + #ifndef UCE_WASM_SHIM_DEMO_GUARD_H + #define UCE_WASM_SHIM_DEMO_GUARD_H + #endif + ``` + +3. **Generated source uses an absolute repo include, preventing header interposition.** + + Original generated line: + + ```c++ + #include "/Code/uce.openfu.com/uce/src/lib/uce_lib.h" + ``` + + To use patched shim headers without touching `/Code`, copied the generated units into the workdir and changed only that line to: + + ```c++ + #include "uce_lib.h" + ``` + + Copies are `/tmp/uce/wasm-phase0/realunit/collections.uce.cpp` and `hello.uce.cpp`. + +4. **`types.h` defines global `operator new/delete` in every unit.** + + A straight build linked, but exported allocator definitions (`_Znwm`, `_ZdlPv`), violating the side-module allocator rule. The repo code in `types.h` contains: + + ```c++ + void * operator new(decltype(sizeof(0)) n) noexcept(false) { ... malloc(n) ... } + void operator delete(void * p) throw() { free(p); } + ``` + + Fix: copied `uce_lib.h` and `types.h` to the shim tree, then patched only the copied `shim/types.h` allocator block to declarations: + + ```c++ + // WASM side-module shim: allocator must be provided by the host/core module, + // not defined in each generated unit. Keep declarations only. + void * operator new(decltype(sizeof(0)) n) noexcept(false); + void operator delete(void * p) throw(); + ``` + + Verification from `llvm-nm -C collections.o`: + + ```txt + U operator delete(void*, unsigned long) + U operator new(unsigned long) + ``` + +## Notable imports / GOT entries + +`collections.wasm` notable imports: + +- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.__stack_pointer`, `env.memory`, `env.__indirect_function_table` +- UCE/runtime functions: `DValue::set`, `DValue::operator[]`, `DValue::push`, `dv_filter`, `dv_map`, `dv_group_by`, `list_unique`, `list_sort`, `replace`, `to_upper`, `html_escape`, `json_encode`, `var_dump`, `time` +- MySQL wrappers are imported even for this unit because included header inline functions reference `MySQL` methods. +- GOT.mem globals: + - `GOT.mem.context` + - `GOT.mem._ZNSt3__25ctypeIcE2idE` + - `GOT.mem._ZTVN10__cxxabiv117__class_type_infoE` + +`hello.wasm` notable imports: + +- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.memcmp` +- UCE/runtime functions: `time`, `html_escape`, `html_escapey`, `var_dump` +- GOT.mem globals: + - `GOT.mem.context` + - `GOT.mem._ZNSt3__219piecewise_constructE` + - `GOT.mem._ZNSt3__25ctypeIcE2idE` + +## Phase 2 land mines + +- `src/lib/sys.h` includes `` and declares signals/tasks/socket/process/file-lock APIs. WASI has no normal POSIX signals, fork/exec, traditional sockets, or process model. These need real repo-level `#ifdef __wasm__` / runtime boundary design, not local shims. +- `types.h` defines global allocator operators in a header. For side modules this must move to the core/runtime or be gated out under WASM side-module builds; otherwise each unit defines its own allocator entry points. +- Generated units include `/Code/.../uce_lib.h` by absolute path. That makes cross-target header selection brittle. The generator should emit a logical include (`"uce_lib.h"`) and the build should supply target-specific include order. +- Header-defined helper functions cause many exports from each unit (`to_string`, MySQL wrapper functions, vector slow-path/lambda helpers, globals `parent_pid`, `my_pid`, `context`). For a clean side-module ABI, these should probably be hidden, moved out of headers, marked inline/static where appropriate, or provided by the core module. +- MySQL inline wrappers are pulled into every unit by `uce_lib.h` even when the unit does not use MySQL directly. This is why `MySQL::connect/disconnect/query/error` imports appear in both outputs. +- Kept `-fno-exceptions`; no try/catch blocker was hit for these two generated units. diff --git a/spikes/wasm-phase0/unit.cpp b/spikes/wasm-phase0/unit.cpp new file mode 100644 index 0000000..e33edd4 --- /dev/null +++ b/spikes/wasm-phase0/unit.cpp @@ -0,0 +1,73 @@ +// WASM-PROPOSAL Phase 0 — unit module stub. +// +// Stands in for a generated UCE unit: compiled with -fPIC and linked with +// wasm-ld -shared into a PIC module carrying a dylink.0 section. Defines no +// allocator and links no libc — operator new/delete, memcpy, and the core +// API all arrive as imports resolved by the loader against the core module +// (§5.4 unit module contract). +// +// Exercises, in one render call: +// - unit-local data segment with relocation (__memory_base placement) +// - GOT.mem read+write of a core-defined global (core_counter) +// - C++ containers (std::string/std::map) on the core's heap +// - heap object created in core, mutated and read from the unit +// - function pointer from unit through core and back (shared table) +// - std::function/lambda handed across the module boundary + +#include +#include +#include + +extern "C" { +void uce_print(const char* s, size_t len); +extern int core_counter; +std::string* core_make_string(const char* s); +void core_append_string(std::string* str, const char* s); +void core_invoke_callback(void (*cb)(int), int arg); +void core_invoke_function(std::function* f, int arg); +} + +static const char* unit_static_message = "unit-data-segment-ok"; +static int unit_state = 41; + +static void my_callback(int x) +{ + std::string s = "[cb:" + std::to_string(x + unit_state) + "]"; + uce_print(s.data(), s.size()); +} + +extern "C" void uce_unit_render() +{ + std::string out = "hello from unit; "; + out += unit_static_message; + out += "; counter=" + std::to_string(core_counter); + uce_print(out.data(), out.size()); + + std::map m; + m["a"] = 1; + m["b"] = 2; + int sum = 0; + for(auto& kv : m) + sum += kv.second; + std::string s2 = "; mapsum=" + std::to_string(sum); + uce_print(s2.data(), s2.size()); + + std::string* cs = core_make_string("; core-string"); + core_append_string(cs, "+unit"); + uce_print(cs->data(), cs->size()); + delete cs; + + core_invoke_callback(my_callback, 1); + + // address of a core-defined function taken in the unit → GOT.func import; + // the loader must ensure the core function has a funcref table entry. + // volatile so the optimizer cannot fold it back into a direct call. + void (*volatile print_ptr)(const char*, size_t) = uce_print; + print_ptr("[got-func-ok]", 13); + + auto* fn = new std::function([](int v) { return(v * 3); }); + core_invoke_function(fn, 14); + delete fn; + + core_counter++; +} diff --git a/spikes/wasm-phase0/wasm_inspect.py b/spikes/wasm-phase0/wasm_inspect.py new file mode 100644 index 0000000..efa344d --- /dev/null +++ b/spikes/wasm-phase0/wasm_inspect.py @@ -0,0 +1,95 @@ +#!/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)