spike: validate WASM phase 0
This commit is contained in:
parent
7066da3cde
commit
7e2faf1472
@ -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
|
stub) hello-world linked at runtime by a minimal loader, in the chosen
|
||||||
vendored runtime.
|
vendored runtime.
|
||||||
|
|
||||||
|
> **Status: DONE (2026-06-12).** Exit criterion passed on k-uce; see
|
||||||
|
> `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.**
|
**Phase 1 — DValue C ABI in the native runtime.**
|
||||||
Introduce `uce_dv_*` (§5.2) and the UCEB1 codec in `src/lib/`, used
|
Introduce `uce_dv_*` (§5.2) and the UCEB1 codec in `src/lib/`, used
|
||||||
natively. Zero wasm dependency; immediately testable; freezes the contract
|
natively. Zero wasm dependency; immediately testable; freezes the contract
|
||||||
|
|||||||
188
spikes/wasm-phase0/FINDINGS.md
Normal file
188
spikes/wasm-phase0/FINDINGS.md
Normal file
@ -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=<entry>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 `<signal.h>`** → 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
|
||||||
30
spikes/wasm-phase0/README.md
Normal file
30
spikes/wasm-phase0/README.md
Normal file
@ -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)
|
||||||
17
spikes/wasm-phase0/build_loader.sh
Normal file
17
spikes/wasm-phase0/build_loader.sh
Normal file
@ -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"
|
||||||
51
spikes/wasm-phase0/build_modules.sh
Normal file
51
spikes/wasm-phase0/build_modules.sh
Normal file
@ -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"
|
||||||
74
spikes/wasm-phase0/core.cpp
Normal file
74
spikes/wasm-phase0/core.cpp
Normal file
@ -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 <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(); }
|
||||||
|
|
||||||
|
}
|
||||||
444
spikes/wasm-phase0/loader.cpp
Normal file
444
spikes/wasm-phase0/loader.cpp
Normal file
@ -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 <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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
165
spikes/wasm-phase0/realunit-report.md
Normal file
165
spikes/wasm-phase0/realunit-report.md
Normal file
@ -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 `<signal.h>` by default.**
|
||||||
|
|
||||||
|
First compile failed via `/Code/uce.openfu.com/uce/src/lib/sys.h`:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
/share/wasi-sysroot/include/wasm32-wasip1/signal.h:2:2: error:
|
||||||
|
"wasm lacks signal support; to enable minimal signal emulation, compile with -D_WASI_EMULATED_SIGNAL ..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix: added `/tmp/uce/wasm-phase0/realunit/shim/signal.h` before repo/system includes. Key lines:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef int sig_atomic_t;
|
||||||
|
typedef void (*sighandler_t)(int);
|
||||||
|
#define SIGTERM 15
|
||||||
|
int raise(int);
|
||||||
|
sighandler_t signal(int, sighandler_t);
|
||||||
|
```
|
||||||
|
|
||||||
|
This is only enough for declarations in `sys.h`; real signal behavior has no WASI equivalent.
|
||||||
|
|
||||||
|
2. **Generated `collections.uce.cpp` includes missing local `demo_guard.h`.**
|
||||||
|
|
||||||
|
First compile also failed:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
fatal error: 'demo_guard.h' file not found
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix: added empty include guard shim `/tmp/uce/wasm-phase0/realunit/shim/demo_guard.h`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef UCE_WASM_SHIM_DEMO_GUARD_H
|
||||||
|
#define UCE_WASM_SHIM_DEMO_GUARD_H
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Generated source uses an absolute repo include, preventing header interposition.**
|
||||||
|
|
||||||
|
Original generated line:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
#include "/Code/uce.openfu.com/uce/src/lib/uce_lib.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
To use patched shim headers without touching `/Code`, copied the generated units into the workdir and changed only that line to:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
#include "uce_lib.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
Copies are `/tmp/uce/wasm-phase0/realunit/collections.uce.cpp` and `hello.uce.cpp`.
|
||||||
|
|
||||||
|
4. **`types.h` defines global `operator new/delete` in every unit.**
|
||||||
|
|
||||||
|
A straight build linked, but exported allocator definitions (`_Znwm`, `_ZdlPv`), violating the side-module allocator rule. The repo code in `types.h` contains:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
void * operator new(decltype(sizeof(0)) n) noexcept(false) { ... malloc(n) ... }
|
||||||
|
void operator delete(void * p) throw() { free(p); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Fix: copied `uce_lib.h` and `types.h` to the shim tree, then patched only the copied `shim/types.h` allocator block to declarations:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// WASM side-module shim: allocator must be provided by the host/core module,
|
||||||
|
// not defined in each generated unit. Keep declarations only.
|
||||||
|
void * operator new(decltype(sizeof(0)) n) noexcept(false);
|
||||||
|
void operator delete(void * p) throw();
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification from `llvm-nm -C collections.o`:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
U operator delete(void*, unsigned long)
|
||||||
|
U operator new(unsigned long)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notable imports / GOT entries
|
||||||
|
|
||||||
|
`collections.wasm` notable imports:
|
||||||
|
|
||||||
|
- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.__stack_pointer`, `env.memory`, `env.__indirect_function_table`
|
||||||
|
- UCE/runtime functions: `DValue::set`, `DValue::operator[]`, `DValue::push`, `dv_filter`, `dv_map`, `dv_group_by`, `list_unique`, `list_sort`, `replace`, `to_upper`, `html_escape`, `json_encode`, `var_dump`, `time`
|
||||||
|
- MySQL wrappers are imported even for this unit because included header inline functions reference `MySQL` methods.
|
||||||
|
- GOT.mem globals:
|
||||||
|
- `GOT.mem.context`
|
||||||
|
- `GOT.mem._ZNSt3__25ctypeIcE2idE`
|
||||||
|
- `GOT.mem._ZTVN10__cxxabiv117__class_type_infoE`
|
||||||
|
|
||||||
|
`hello.wasm` notable imports:
|
||||||
|
|
||||||
|
- allocator/runtime: `env._Znwm`, `env._ZdlPvm`, libc++ string/iostream helpers, `env.memcmp`
|
||||||
|
- UCE/runtime functions: `time`, `html_escape`, `html_escapey`, `var_dump`
|
||||||
|
- GOT.mem globals:
|
||||||
|
- `GOT.mem.context`
|
||||||
|
- `GOT.mem._ZNSt3__219piecewise_constructE`
|
||||||
|
- `GOT.mem._ZNSt3__25ctypeIcE2idE`
|
||||||
|
|
||||||
|
## Phase 2 land mines
|
||||||
|
|
||||||
|
- `src/lib/sys.h` includes `<signal.h>` and declares signals/tasks/socket/process/file-lock APIs. WASI has no normal POSIX signals, fork/exec, traditional sockets, or process model. These need real repo-level `#ifdef __wasm__` / runtime boundary design, not local shims.
|
||||||
|
- `types.h` defines global allocator operators in a header. For side modules this must move to the core/runtime or be gated out under WASM side-module builds; otherwise each unit defines its own allocator entry points.
|
||||||
|
- Generated units include `/Code/.../uce_lib.h` by absolute path. That makes cross-target header selection brittle. The generator should emit a logical include (`"uce_lib.h"`) and the build should supply target-specific include order.
|
||||||
|
- Header-defined helper functions cause many exports from each unit (`to_string`, MySQL wrapper functions, vector slow-path/lambda helpers, globals `parent_pid`, `my_pid`, `context`). For a clean side-module ABI, these should probably be hidden, moved out of headers, marked inline/static where appropriate, or provided by the core module.
|
||||||
|
- MySQL inline wrappers are pulled into every unit by `uce_lib.h` even when the unit does not use MySQL directly. This is why `MySQL::connect/disconnect/query/error` imports appear in both outputs.
|
||||||
|
- Kept `-fno-exceptions`; no try/catch blocker was hit for these two generated units.
|
||||||
73
spikes/wasm-phase0/unit.cpp
Normal file
73
spikes/wasm-phase0/unit.cpp
Normal file
@ -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 <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void uce_print(const char* s, size_t len);
|
||||||
|
extern int core_counter;
|
||||||
|
std::string* core_make_string(const char* s);
|
||||||
|
void core_append_string(std::string* str, const char* s);
|
||||||
|
void core_invoke_callback(void (*cb)(int), int arg);
|
||||||
|
void core_invoke_function(std::function<int(int)>* f, int arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* unit_static_message = "unit-data-segment-ok";
|
||||||
|
static int unit_state = 41;
|
||||||
|
|
||||||
|
static void my_callback(int x)
|
||||||
|
{
|
||||||
|
std::string s = "[cb:" + std::to_string(x + unit_state) + "]";
|
||||||
|
uce_print(s.data(), s.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void uce_unit_render()
|
||||||
|
{
|
||||||
|
std::string out = "hello from unit; ";
|
||||||
|
out += unit_static_message;
|
||||||
|
out += "; counter=" + std::to_string(core_counter);
|
||||||
|
uce_print(out.data(), out.size());
|
||||||
|
|
||||||
|
std::map<std::string, int> m;
|
||||||
|
m["a"] = 1;
|
||||||
|
m["b"] = 2;
|
||||||
|
int sum = 0;
|
||||||
|
for(auto& kv : m)
|
||||||
|
sum += kv.second;
|
||||||
|
std::string s2 = "; mapsum=" + std::to_string(sum);
|
||||||
|
uce_print(s2.data(), s2.size());
|
||||||
|
|
||||||
|
std::string* cs = core_make_string("; core-string");
|
||||||
|
core_append_string(cs, "+unit");
|
||||||
|
uce_print(cs->data(), cs->size());
|
||||||
|
delete cs;
|
||||||
|
|
||||||
|
core_invoke_callback(my_callback, 1);
|
||||||
|
|
||||||
|
// address of a core-defined function taken in the unit → GOT.func import;
|
||||||
|
// the loader must ensure the core function has a funcref table entry.
|
||||||
|
// volatile so the optimizer cannot fold it back into a direct call.
|
||||||
|
void (*volatile print_ptr)(const char*, size_t) = uce_print;
|
||||||
|
print_ptr("[got-func-ok]", 13);
|
||||||
|
|
||||||
|
auto* fn = new std::function<int(int)>([](int v) { return(v * 3); });
|
||||||
|
core_invoke_function(fn, 14);
|
||||||
|
delete fn;
|
||||||
|
|
||||||
|
core_counter++;
|
||||||
|
}
|
||||||
95
spikes/wasm-phase0/wasm_inspect.py
Normal file
95
spikes/wasm-phase0/wasm_inspect.py
Normal file
@ -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)
|
||||||
Loading…
x
Reference in New Issue
Block a user