W3
This commit is contained in:
parent
eb8f303f94
commit
577aae076e
193
WASM-PROPOSAL.md
193
WASM-PROPOSAL.md
@ -1,10 +1,13 @@
|
||||
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
|
||||
|
||||
- **Status:** design guide; Phase 0–4 spikes complete and gated (2026-06-12).
|
||||
Next: production implementation, starting with the Phase 3 work plan
|
||||
(item 1, real `uce_lib` core compile); the starter parity bar
|
||||
(`tests/run_network_tests.py --match starter`, 14 cases) is already green
|
||||
against the native backend.
|
||||
- **Status:** building the production worker per §9.1 (W-phases). W1 (core
|
||||
module), W2 (wasm unit compile target), and W3 (workspace runtime +
|
||||
membrane, `src/wasm/worker.cpp`) are **done**: real pages — including the
|
||||
starter views — render through per-request wasm workspaces with lazy
|
||||
mid-request component loading, epoch/memory limits, and trap summaries.
|
||||
Current phase: **W4 — the FastCGI worker backend.** The cutover bar
|
||||
(`--match starter` + full suite + kill-tests + budgets) is already
|
||||
executable and green against the native backend.
|
||||
- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` →
|
||||
`dlopen`) with per-unit WebAssembly modules executed in a per-request,
|
||||
runtime-linked workspace, exposing the same API surface to page code,
|
||||
@ -630,6 +633,186 @@ The native `.so` backend remains in-tree (as a reference) and selectable by conf
|
||||
but we switch over to the wasm backend as soon as it's available and test
|
||||
only on that; both backends share the Phase 1 C ABI.
|
||||
|
||||
**Cutover & cleanup checklist (gated, in order — no step before the gate
|
||||
above it is green).**
|
||||
|
||||
1. **Build** — production Phase 3/4 work plan items 1–7: real `uce_lib`
|
||||
`core.wasm`, WASI decision recorded, generator changes, production loader
|
||||
in `src/`, starter-scoped hostcalls, `component_resolve` + lazy dispatch,
|
||||
wasm worker backend selectable by config in `linux_fastcgi.cpp`.
|
||||
2. **Prove** — the Phase 5 harness against the wasm worker URL: full network
|
||||
suite green (≥ 80 cases), starter parity (≥ 10), benchmarks within the
|
||||
≤2× paired-run budget; the Phase 4 kill-tests reproduced against the real
|
||||
worker (OOB page, stack-exhaustion page, infinite loop, OOM) rendering
|
||||
clean error pages from `wasm_trace.h` summaries.
|
||||
3. **Switch** — config default flips to the wasm backend; the native backend
|
||||
stays in-tree as reference per the paragraph above (archival is a separate,
|
||||
later decision, not part of cutover).
|
||||
4. **Clean up** — only after 3, and gates must be re-homed before their
|
||||
spikes are deleted: the Phase 5 harness moves from `spikes/` into `tests/`,
|
||||
the Phase 4 kill cases become worker integration tests, and the Phase 0
|
||||
FINDINGS/erratum content folds into `docs/` — then `spikes/wasm-phase*`
|
||||
can go. Native-only machinery that cutover obsoletes is retired in the
|
||||
same pass: the SIGSEGV `sigsetjmp`/`siglongjmp` recovery, the tracking
|
||||
`operator new` in `types.h`, the per-connector `cleanup_*_connections()`
|
||||
pattern (§3.1/§3.2 supersede all three).
|
||||
|
||||
Status against this checklist (2026-06-12): W1 has landed enough `src/` /
|
||||
`scripts/` integration to build and smoke-test `core.wasm`; the remaining gate
|
||||
1 work is W2–W4 plus the residual W1 wasi-libc import closure noted below. The
|
||||
native backend is still the only server backend and serves the entire green
|
||||
suite. The single cleanup item safe today is `src/lib/_scratchpad.cpp` (the
|
||||
failed arena experiment, unreferenced by any build since 2022; §1 cites it as
|
||||
history only).
|
||||
|
||||
### 9.1 Production build plan — the worker (W-phases)
|
||||
|
||||
The spike sequence above is closed; every architectural risk it could retire
|
||||
is retired. The W-phases build the production worker. Ground rules: the
|
||||
`.uce → C++` preprocessor does not change; every phase lands production code
|
||||
in `src/`/`scripts/` gated by the existing network suite; no further
|
||||
throwaway scaffolding. Dependency order is W1 → W3 → W4 → W5 → W6, with W2
|
||||
parallelizable once W1's shim headers and ABI stamp exist.
|
||||
|
||||
**W1 — the core module, for real.**
|
||||
Carve `uce_lib` so it compiles as `core.wasm` with the Phase 0 recipe:
|
||||
gate the global allocator in `types.h` behind an `#ifdef` (core owns it,
|
||||
units import it — replacing the spike's copied-header text patch); `#ifdef
|
||||
__wasm__` carve-outs in `sys.h` (signals/fork/exec/sockets move behind
|
||||
hostcalls); split the connectors — `mysql-connector`/`sqlite-connector`
|
||||
keep their `.uce`-visible signatures but forward to membrane hostcalls in
|
||||
the wasm build while native implementations stay host-side. Zero-WASI core
|
||||
(record the decision in §11): time/random/env are `uce_host_*` hostcalls,
|
||||
no `wasi_snapshot_preview1` imports. The core exports the full DValue C ABI
|
||||
(§5.2), a name→funcptr symbol registry for `GOT.func` (replacing per-symbol
|
||||
helpers), `uce_alloc`/`uce_free`, and output plumbing.
|
||||
`scripts/build_core_wasm.sh` joins the normal build.
|
||||
Exit: `core.wasm` builds reproducibly from the real `uce_lib`; a small
|
||||
smoke driver instantiates it, runs `_initialize`, and exercises `uce_dv_*`;
|
||||
the native build and suite remain untouched and green.
|
||||
|
||||
> **Status: DONE (2026-06-12).** `scripts/build_core_wasm.sh` builds
|
||||
> `src/wasm/core.cpp` into `core.wasm` from the real `uce_lib` carve-out. The
|
||||
> W1 carve-out keeps native builds unchanged while `__UCE_WASM_CORE__` removes
|
||||
> native-only compiler/connector code from the core, provides WASM stubs for
|
||||
> process/socket/task/file surfaces, gates generated-unit allocator definitions
|
||||
> behind `__UCE_WASM_UNIT__`, and leaves the workspace-owned DValue C ABI in the
|
||||
> core. `scripts/wasm/build_w1_smoke.sh` builds `src/wasm/w1_smoke.cpp`; the
|
||||
> smoke driver instantiates `core.wasm`, runs `_initialize`, initializes the UCE
|
||||
> request context, exercises `uce_dv_root/get/find/set_value/value/count/is_list`
|
||||
> plus UCEB1 encode/decode, verifies output plumbing, and passes with
|
||||
> `W1 EXIT CRITERION: PASS`. Native validation after the W1 carve-out: rebuilt
|
||||
> and restarted `uce.service`; warm full network suite passed `83/83`. Remaining
|
||||
> W1 follow-up before W3: close the residual wasi-libc/libc++
|
||||
> `wasi_snapshot_preview1.*` imports in the produced binary; UCE's own time/env
|
||||
> calls are routed through `uce_host_*`, but the smoke driver still supplies
|
||||
> trap stubs for unused libc WASI imports.
|
||||
|
||||
**W2 — the compiler outputs wasm units.**
|
||||
Preprocessor output unchanged. The unit compile path in `compiler.cpp`
|
||||
gains the wasm target beside the `.so` target: `clang
|
||||
--target=wasm32-wasip1 -fPIC` + `wasm-ld -shared` (Phase 0 unit recipe),
|
||||
logical `uce_lib.h` include instead of the absolute path, a `uce.abi`
|
||||
custom-section stamp (ABI version + toolchain id), and per-unit `.wasm`
|
||||
artifacts in the same cache with the same invalidation as `.so`.
|
||||
Exit: a batch compile of every unit the suite touches produces valid PIC
|
||||
modules — `dylink.0` present, `uce.abi` stamped, no allocator definitions,
|
||||
import shapes verified by a check tool — and compile-on-miss works for the
|
||||
wasm target.
|
||||
|
||||
> **Status: DONE (2026-06-12).** Generated units now include the logical
|
||||
> `#include "uce_lib.h"`; native `scripts/compile` supplies `-Isrc/lib`, and
|
||||
> `scripts/compile_wasm_unit` builds PIC side modules with `__UCE_WASM_UNIT__`,
|
||||
> `wasm-ld -shared --experimental-pic`, and an `llvm-objcopy`-inserted
|
||||
> `uce.abi` custom section. `scripts/wasm/check_unit_wasm.py` validates wasm v1
|
||||
> structure, `dylink.0` mem_info, `uce.abi` ABI/toolchain stamp, import policy,
|
||||
> required PIC imports, and absence of allocator definitions. The compiler can
|
||||
> optionally build the wasm artifact beside the native `.so` via
|
||||
> `COMPILE_WASM_UNITS=1`; native remains default. Batch validation built/checked
|
||||
> all 128 known/generated suite units, and a temporary `unit_compile()`
|
||||
> compile-on-miss page produced and verified a fresh `.wasm`. Reused-artifact
|
||||
> batch validation was tightened after the first slow full rebuild: unchanged
|
||||
> units are rechecked instead of rebuilt, reducing a 128-unit no-op pass from
|
||||
> several minutes to about 5 seconds. `scripts/compile_wasm_unit` now also uses
|
||||
> a keyed Clang PCH for the stable `uce_lib.h` unit header by default
|
||||
> (`UCE_WASM_UNIT_PCH=0` disables it); the key includes ABI version, clang
|
||||
> version, common compile flags, and `src/lib/*.h` content. Spot checks: warm
|
||||
> `hello.uce` wasm compile dropped from about 2.3s to 0.65s, `core.uce` from
|
||||
> about 5.7s to 4.1s, and a full 128-unit rebuild with PCH took about 163s.
|
||||
> Native validation stayed green (`83/83`).
|
||||
|
||||
**W3 — workspace runtime + membrane (the worker core).**
|
||||
Productionize the loader into `src/wasm/` per §6, from the Phase 3 spike
|
||||
plus everything it deferred: symbol registry, dispatch map, ABI stamp
|
||||
verification, import discipline (reject units defining the allocator),
|
||||
the `__memory_base`-relative data-export rule, multi-unit placement,
|
||||
hardened binary parsing, an explicit export name-collision policy.
|
||||
Workspace lifecycle: core snapshot born by memcpy (CoW is W5), dropped per
|
||||
request; host handle table with closers. Starter-scoped hostcall set
|
||||
(~12, the §5.1 subset): `ctx_read`, `respond`, `stream_write`, `log`,
|
||||
`time`, `random`, `env`, `session_get`/`set`, `http_request`,
|
||||
`component_resolve`, `last_error`.
|
||||
Exit: a real request — UCEB1 context in, render, response out — served
|
||||
end-to-end through a workspace running the real core and real generated
|
||||
units, driven by a CLI test driver; epoch CPU limit and memory limiter
|
||||
active; traps produce `wasm_trace.h` summaries.
|
||||
|
||||
> **Status: DONE (2026-06-13).** `src/wasm/worker.cpp` is the production
|
||||
> workspace runtime (wasmtime.hh; per-request store, hardened dylink/uce.abi
|
||||
> parsing, import discipline, GOT.func via host funcref placement, the
|
||||
> `__memory_base` data-export rule, core-first/first-unit-wins symbol
|
||||
> registry, per-worker compiled-module cache) and `src/wasm/w3_driver.cpp`
|
||||
> is the CLI gate. Membrane so far: time/time_precise/env/random/log,
|
||||
> `component_resolve` (lazy mid-request loading), and a policy-gated
|
||||
> read-only file membrane (`file_exists`/`file_read`, current-unit-relative,
|
||||
> site-tree-contained). The exit gate passed on k-uce: `/demo/hello.uce` and
|
||||
> `/demo/components.uce` render **byte-identical to native** (incl. nested
|
||||
> and named `COMPONENT:X` handlers through `ob_*` capture); starter
|
||||
> dashboard/gauges/features/workspace/page1 all render `200` with ~12 units
|
||||
> lazily loaded mid-request (≈700 ms first request — dominated by per-request
|
||||
> Wasmtime module compilation, the W5 AOT/snapshot target — ≈2 ms warm);
|
||||
> epoch kill mid-render traps as `interrupt` with a symbolicated
|
||||
> `wasm_trace` summary and a clean workspace drop. Carve findings recorded
|
||||
> on the way: header function templates must be `inline` (self-import rule,
|
||||
> now commented in `types.h`/`functionlib.h`), the core needs vague-linkage
|
||||
> and libc link anchors (`uce_wasm_link_anchors()` +
|
||||
> `core_libc_exports.syms`), units build `-fno-rtti`/`-fno-exceptions` to
|
||||
> match the core ABI, and connector classes have explicit fail-clean stubs
|
||||
> until the W5 hostcall connectors. Deferred to W4 as planned: `respond`/
|
||||
> `stream_write`/`session`/`http_request` hostcalls (response metadata
|
||||
> currently returns as UCEB1 via `uce_wasm_response_meta`), kill-pages, and
|
||||
> the FastCGI backend; `error-reporting.uce` and `tests/zip.uce` are
|
||||
> native-only pending the trap error path and a zip hostcall.
|
||||
|
||||
**W4 — the FastCGI worker.**
|
||||
Wire W3 into `linux_fastcgi.cpp` as a config-selectable backend (native
|
||||
stays default until W5): the §7 lifecycle, lazy mid-request
|
||||
`component_resolve` → load → `call_indirect`, path dispatch, trap →
|
||||
configured UCE error pages with collapsed guest traces, handle-table
|
||||
cleanup on workspace drop.
|
||||
Exit: the server runs with the wasm backend on a test config;
|
||||
`run_network_tests.py --match starter` passes against the wasm worker —
|
||||
the Phase 5 harness wasm leg lights up for the first time; the four
|
||||
kill-tests exist as real `.uce` pages and produce clean error pages from
|
||||
an unharmed worker.
|
||||
|
||||
**W5 — parity, performance, cutover.**
|
||||
Full network suite green on the wasm backend; §3.2 statics-audit findings
|
||||
(50 code candidates) fixed as differential native-vs-wasm runs surface
|
||||
them; CoW snapshot birth and placement memoization as needed to meet the
|
||||
budgets (≤2× page latency against the pinned baseline shape, workspace
|
||||
birth ≤100µs, component-call overhead ≤10×) with worker-internal probes
|
||||
for the latter two.
|
||||
Exit: cutover checklist gates 2–3 — harness fully green against the wasm
|
||||
worker URL, config default flips to wasm, native stays in-tree as
|
||||
reference.
|
||||
|
||||
**W6 — cleanup.**
|
||||
Cutover checklist gate 4, verbatim: re-home the spike-hosted gates
|
||||
(Phase 5 harness → `tests/`, kill cases → worker tests, FINDINGS/erratum →
|
||||
`docs/`), delete `spikes/wasm-phase*`, retire the SIGSEGV recovery, the
|
||||
tracking `operator new`, and `cleanup_*_connections()`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Risks & mitigations
|
||||
|
||||
@ -21,6 +21,10 @@ SITE_DIRECTORY=site
|
||||
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
|
||||
JIT_COMPILE_ON_REQUEST=1
|
||||
|
||||
# OPTIONAL W2 WEBASSEMBLY SIDE-MODULE COMPILATION BESIDE NATIVE .so UNITS
|
||||
COMPILE_WASM_UNITS=0
|
||||
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
|
||||
|
||||
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
|
||||
PROACTIVE_COMPILE_ENABLED=1
|
||||
|
||||
|
||||
30
scripts/build_core_wasm.sh
Executable file
30
scripts/build_core_wasm.sh
Executable file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Build the production W1 UCE WASM core from the real runtime carve-out.
|
||||
# Run on k-uce from any working directory.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
||||
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
|
||||
mkdir -p "$OUT" bin/wasm
|
||||
|
||||
if [ ! -x "$SDK/bin/clang++" ]; then
|
||||
echo "wasi-sdk clang++ not found; set WASI_SDK" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
|
||||
-O1 -g -std=c++20 -fno-exceptions -fno-rtti \
|
||||
-D__UCE_WASM_CORE__ \
|
||||
-I. -Isrc/lib \
|
||||
src/wasm/core.cpp -o "$OUT/core.wasm" \
|
||||
-Wl,--export-all \
|
||||
-Wl,--export=__heap_base \
|
||||
-Wl,--export=__stack_pointer \
|
||||
-Wl,--import-table \
|
||||
-Wl,--allow-undefined-file=src/wasm/core_hostcalls.syms \
|
||||
-Wl,--no-entry \
|
||||
$(sed "s/^/-Wl,--export-if-defined=/" src/wasm/core_libc_exports.syms | tr "\n" " ")
|
||||
|
||||
cp "$OUT/core.wasm" bin/wasm/core.wasm
|
||||
ls -lh "$OUT/core.wasm" bin/wasm/core.wasm
|
||||
@ -26,7 +26,7 @@ OPT_FLAG="O0"
|
||||
|
||||
COMPILER="clang++"
|
||||
#COMPILER="g++"
|
||||
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC"
|
||||
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC -Isrc/lib"
|
||||
|
||||
LIBS="-ldl -lm -lpthread"
|
||||
SRCFLAGS="-D PLATFORM_NAME=\"linux\""
|
||||
|
||||
93
scripts/compile_wasm_unit
Executable file
93
scripts/compile_wasm_unit
Executable file
@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# Compile a preprocessed UCE unit into a PIC WebAssembly side module.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
cd ..
|
||||
|
||||
SRC_DIR="$1"
|
||||
DEST_DIR="$2"
|
||||
SRC_FN="$3"
|
||||
PP_FN="$4"
|
||||
WASM_FN="$5"
|
||||
|
||||
SDK=${WASI_SDK:-/opt/wasi-sdk}
|
||||
ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
|
||||
ROOT=$(pwd)
|
||||
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
|
||||
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
|
||||
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
|
||||
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
|
||||
COMMON_FLAGS=(
|
||||
--target=wasm32-wasip1
|
||||
-fPIC -fvisibility=default -fvisibility-inlines-hidden
|
||||
-O1 -g -std=c++20
|
||||
# must match the core build ABI: units with RTTI/EH enabled import
|
||||
# typeinfo/unwind symbols the -fno-rtti/-fno-exceptions core cannot provide
|
||||
-fno-exceptions -fno-rtti
|
||||
-D__UCE_WASM_UNIT__
|
||||
-DPLATFORM_NAME=\"wasm32-wasip1\"
|
||||
)
|
||||
|
||||
if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ]; then
|
||||
echo "wasi-sdk tools not found; set WASI_SDK" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOOLCHAIN_ID=$(${SDK}/bin/clang++ --version | head -n 1)
|
||||
HEADER_HASH=$(find src/lib -maxdepth 1 -name '*.h' -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | cut -c1-16)
|
||||
FLAGS_HASH=$(printf '%s\0' "${COMMON_FLAGS[@]}" -Isrc/lib | sha1sum | cut -c1-16)
|
||||
PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH" "$FLAGS_HASH" | sha1sum | cut -c1-16)
|
||||
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
|
||||
|
||||
mkdir -p "$DEST_DIR" >/dev/null 2>&1
|
||||
|
||||
build_pch_if_needed() {
|
||||
if [ "$PCH_ENABLED" = "0" ]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$PCH_DIR"
|
||||
if [ -s "$PCH_FN" ]; then
|
||||
return 0
|
||||
fi
|
||||
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
||||
-Isrc/lib \
|
||||
-x c++-header src/lib/uce_lib.h -o "$PCH_FN.tmp"
|
||||
mv "$PCH_FN.tmp" "$PCH_FN"
|
||||
}
|
||||
|
||||
cat > "$ABI_TMP" <<EOF
|
||||
format=uce-wasm-unit-abi-v1
|
||||
unit_abi_version=$ABI_VERSION
|
||||
toolchain=$TOOLCHAIN_ID
|
||||
source=$SRC_FN
|
||||
EOF
|
||||
|
||||
build_pch_if_needed
|
||||
PCH_FLAGS=()
|
||||
if [ "$PCH_ENABLED" != "0" ]; then
|
||||
PCH_FLAGS=(-include-pch "$PCH_FN")
|
||||
fi
|
||||
|
||||
"$SDK/bin/clang++" "${COMMON_FLAGS[@]}" \
|
||||
-I"$SRC_DIR" -I"$ROOT/src/lib" \
|
||||
"${PCH_FLAGS[@]}" \
|
||||
-c "$DEST_DIR/$PP_FN" -o "$OBJ_FN"
|
||||
|
||||
"$SDK/bin/wasm-ld" -shared --experimental-pic \
|
||||
--unresolved-symbols=import-dynamic \
|
||||
--Bsymbolic \
|
||||
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
|
||||
--export-if-defined=__uce_set_current_request \
|
||||
--export-if-defined=__uce_render \
|
||||
--export-if-defined=__uce_component \
|
||||
--export-if-defined=__uce_websocket \
|
||||
--export-if-defined=__uce_cli \
|
||||
--export-if-defined=__uce_serve_http \
|
||||
--export-if-defined=__uce_once \
|
||||
--export-if-defined=__uce_init
|
||||
|
||||
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
|
||||
|
||||
python3 scripts/wasm/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
|
||||
|
||||
rm -f "$OBJ_FN" "$ABI_TMP"
|
||||
25
scripts/wasm/build_w1_smoke.sh
Executable file
25
scripts/wasm/build_w1_smoke.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Build the W1 host smoke driver. Run on k-uce.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w1}
|
||||
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
|
||||
WASMTIME_INCLUDE=${WASMTIME_INCLUDE:-$WASMTIME_HOME/include}
|
||||
WASMTIME_LIB=${WASMTIME_LIB:-$WASMTIME_HOME/lib}
|
||||
mkdir -p "$OUT"
|
||||
|
||||
if [ ! -d "$WASMTIME_INCLUDE" ] || [ ! -d "$WASMTIME_LIB" ]; then
|
||||
echo "Wasmtime C API not found; set WASMTIME_HOME or WASMTIME_INCLUDE/WASMTIME_LIB" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
g++ -std=c++17 -O2 -Wall -Wextra \
|
||||
-I"$WASMTIME_INCLUDE" \
|
||||
src/wasm/w1_smoke.cpp \
|
||||
-L"$WASMTIME_LIB" \
|
||||
-Wl,-rpath,"$WASMTIME_LIB" \
|
||||
-lwasmtime \
|
||||
-o "$OUT/w1_smoke"
|
||||
|
||||
ls -lh "$OUT/w1_smoke"
|
||||
62
scripts/wasm/build_w2_units.sh
Executable file
62
scripts/wasm/build_w2_units.sh
Executable file
@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Batch-build W2 wasm side modules for already-known/generated UCE units.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
BIN_DIR=${UCE_BIN_DIRECTORY:-/tmp/uce/work}
|
||||
KNOWN_FILE=${UCE_KNOWN_UNITS_FILE:-$BIN_DIR/known-uce-files.txt}
|
||||
MIN_UNITS=${UCE_W2_MIN_UNITS:-1}
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
UNITS=("$@")
|
||||
else
|
||||
if [ ! -f "$KNOWN_FILE" ]; then
|
||||
echo "known unit registry not found: $KNOWN_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
mapfile -t UNITS < <(grep -v '^[[:space:]]*$' "$KNOWN_FILE")
|
||||
fi
|
||||
|
||||
count=0
|
||||
checked=0
|
||||
skipped=0
|
||||
# Native-only units that cannot be wasm side modules (yet).
|
||||
# - error-reporting.uce deliberately throws to exercise the native exception
|
||||
# path; the wasm backend replaces that machinery with traps (§11.1).
|
||||
# - tests/zip.uce uses try/catch around the zip library, which is carved out
|
||||
# of the wasm core until it moves behind a hostcall (W4+ membrane work).
|
||||
SKIP_PATTERN=${UCE_W2_SKIP:-(error-reporting|tests/zip)\.uce$}
|
||||
|
||||
for unit in "${UNITS[@]}"; do
|
||||
case "$unit" in
|
||||
*.uce|*.ws.uce) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
if [[ "$unit" =~ $SKIP_PATTERN ]]; then
|
||||
continue
|
||||
fi
|
||||
src_dir=$(dirname "$unit")
|
||||
base=$(basename "$unit")
|
||||
dest_dir="$BIN_DIR$src_dir"
|
||||
pp_fn="$base.cpp"
|
||||
wasm_fn="$base.wasm"
|
||||
if [ ! -f "$dest_dir/$pp_fn" ]; then
|
||||
echo "missing preprocessed unit: $dest_dir/$pp_fn" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -s "$dest_dir/$wasm_fn" ] && [ "$dest_dir/$wasm_fn" -nt "$dest_dir/$pp_fn" ] && [ "$dest_dir/$wasm_fn" -nt "$unit" ]; then
|
||||
scripts/wasm/check_unit_wasm.py "$dest_dir/$wasm_fn"
|
||||
skipped=$((skipped + 1))
|
||||
else
|
||||
scripts/compile_wasm_unit "$src_dir" "$dest_dir" "$unit" "$pp_fn" "$wasm_fn"
|
||||
count=$((count + 1))
|
||||
fi
|
||||
checked=$((checked + 1))
|
||||
done
|
||||
|
||||
if [ "$checked" -lt "$MIN_UNITS" ]; then
|
||||
echo "checked only $checked wasm units, expected at least $MIN_UNITS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "W2 batch wasm units: checked=$checked compiled=$count reused=$skipped"
|
||||
19
scripts/wasm/build_w3_driver.sh
Normal file
19
scripts/wasm/build_w3_driver.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Build the W3 workspace-runtime CLI driver. Run on k-uce.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
OUT=${UCE_WASM_OUT:-/tmp/uce/wasm-w3}
|
||||
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
|
||||
mkdir -p "$OUT"
|
||||
|
||||
g++ -std=c++20 -O1 -g -w \
|
||||
-Isrc/lib -Isrc/wasm \
|
||||
-I"$WASMTIME_HOME/include" \
|
||||
src/wasm/w3_driver.cpp \
|
||||
-L"$WASMTIME_HOME/lib" \
|
||||
-Wl,-rpath,"$WASMTIME_HOME/lib" \
|
||||
-lwasmtime -lpcre2-8 -lpthread -ldl \
|
||||
-o "$OUT/w3_driver"
|
||||
|
||||
ls -lh "$OUT/w3_driver"
|
||||
227
scripts/wasm/check_unit_wasm.py
Executable file
227
scripts/wasm/check_unit_wasm.py
Executable file
@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a W2 UCE PIC unit wasm artifact.
|
||||
|
||||
Checks intentionally stay small and explicit: section walk for dylink.0,
|
||||
uce.abi, imports/exports, plus llvm-nm for allocator definitions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_u32leb(data: bytes, pos: int) -> tuple[int, int]:
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
if pos >= len(data):
|
||||
raise ValueError("truncated leb128")
|
||||
b = data[pos]
|
||||
pos += 1
|
||||
result |= (b & 0x7F) << shift
|
||||
if (b & 0x80) == 0:
|
||||
return result, pos
|
||||
shift += 7
|
||||
if shift > 35:
|
||||
raise ValueError("oversized leb128")
|
||||
|
||||
|
||||
def read_name(data: bytes, pos: int) -> tuple[str, int]:
|
||||
n, pos = read_u32leb(data, pos)
|
||||
end = pos + n
|
||||
if end > len(data):
|
||||
raise ValueError("truncated name")
|
||||
return data[pos:end].decode("utf-8", "replace"), end
|
||||
|
||||
|
||||
def walk_sections(data: bytes):
|
||||
if not data.startswith(b"\0asm\x01\0\0\0"):
|
||||
raise ValueError("not a wasm v1 module")
|
||||
pos = 8
|
||||
while pos < len(data):
|
||||
section_id = data[pos]
|
||||
pos += 1
|
||||
size, pos = read_u32leb(data, pos)
|
||||
end = pos + size
|
||||
if end > len(data):
|
||||
raise ValueError("section extends past EOF")
|
||||
payload = data[pos:end]
|
||||
yield section_id, payload
|
||||
pos = end
|
||||
|
||||
|
||||
def parse_imports(payload: bytes):
|
||||
pos = 0
|
||||
count, pos = read_u32leb(payload, pos)
|
||||
imports = []
|
||||
for _ in range(count):
|
||||
module, pos = read_name(payload, pos)
|
||||
name, pos = read_name(payload, pos)
|
||||
if pos >= len(payload):
|
||||
raise ValueError("truncated import kind")
|
||||
kind = payload[pos]
|
||||
pos += 1
|
||||
# Skip type descriptors. We only need module/name/kind for W2 policy.
|
||||
if kind == 0: # func type index
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
elif kind == 1: # table
|
||||
if pos >= len(payload): raise ValueError("truncated table import")
|
||||
pos += 1
|
||||
flags, pos = read_u32leb(payload, pos)
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
if flags & 1: _, pos = read_u32leb(payload, pos)
|
||||
elif kind == 2: # memory
|
||||
flags, pos = read_u32leb(payload, pos)
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
if flags & 1: _, pos = read_u32leb(payload, pos)
|
||||
elif kind == 3: # global
|
||||
pos += 2
|
||||
else:
|
||||
raise ValueError(f"unknown import kind {kind}")
|
||||
imports.append((module, name, kind))
|
||||
return imports
|
||||
|
||||
|
||||
def parse_exports(payload: bytes):
|
||||
pos = 0
|
||||
count, pos = read_u32leb(payload, pos)
|
||||
exports = []
|
||||
for _ in range(count):
|
||||
name, pos = read_name(payload, pos)
|
||||
if pos >= len(payload):
|
||||
raise ValueError("truncated export kind")
|
||||
kind = payload[pos]
|
||||
pos += 1
|
||||
_, pos = read_u32leb(payload, pos)
|
||||
exports.append((name, kind))
|
||||
return exports
|
||||
|
||||
|
||||
def collect(path: Path):
|
||||
data = path.read_bytes()
|
||||
customs: dict[str, list[bytes]] = {}
|
||||
imports = []
|
||||
exports = []
|
||||
for section_id, payload in walk_sections(data):
|
||||
if section_id == 0:
|
||||
name, pos = read_name(payload, 0)
|
||||
customs.setdefault(name, []).append(payload[pos:])
|
||||
elif section_id == 2:
|
||||
imports = parse_imports(payload)
|
||||
elif section_id == 7:
|
||||
exports = parse_exports(payload)
|
||||
return customs, imports, exports
|
||||
|
||||
|
||||
def dylink_has_valid_mem_info(payload: bytes) -> bool:
|
||||
pos = 0
|
||||
while pos < len(payload):
|
||||
subsection_id = payload[pos]
|
||||
pos += 1
|
||||
size, pos = read_u32leb(payload, pos)
|
||||
end = pos + size
|
||||
if end > len(payload):
|
||||
raise ValueError("dylink.0 subsection extends past section")
|
||||
if subsection_id == 1:
|
||||
mem_size, p = read_u32leb(payload, pos)
|
||||
mem_align, p = read_u32leb(payload, p)
|
||||
table_size, p = read_u32leb(payload, p)
|
||||
table_align, p = read_u32leb(payload, p)
|
||||
if p > end:
|
||||
raise ValueError("truncated dylink.0 mem_info")
|
||||
return mem_align < 32 and table_align < 32 and mem_size < (1 << 31) and table_size < (1 << 31)
|
||||
pos = end
|
||||
return False
|
||||
|
||||
|
||||
def defined_symbols(path: Path, llvm_nm: str) -> list[str]:
|
||||
proc = subprocess.run([llvm_nm, "--defined-only", str(path)], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(proc.stderr.strip() or "llvm-nm failed")
|
||||
symbols = []
|
||||
for line in proc.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if parts:
|
||||
symbols.append(parts[-1])
|
||||
return symbols
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("wasm", type=Path)
|
||||
ap.add_argument("--abi-version", default="6")
|
||||
ap.add_argument("--llvm-nm", default=None)
|
||||
ap.add_argument("--verbose", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
customs, imports, exports = collect(args.wasm)
|
||||
errors = []
|
||||
dylink_payloads = customs.get("dylink.0", [])
|
||||
if not dylink_payloads:
|
||||
errors.append("missing dylink.0 custom section")
|
||||
elif not any(dylink_has_valid_mem_info(payload) for payload in dylink_payloads):
|
||||
errors.append("dylink.0 missing valid mem_info subsection")
|
||||
abi_payloads = customs.get("uce.abi", [])
|
||||
if not abi_payloads:
|
||||
errors.append("missing uce.abi custom section")
|
||||
else:
|
||||
abi_text = abi_payloads[-1].decode("utf-8", "replace")
|
||||
required = ["format=uce-wasm-unit-abi-v1", f"unit_abi_version={args.abi_version}", "toolchain="]
|
||||
for needle in required:
|
||||
if needle not in abi_text:
|
||||
errors.append(f"uce.abi missing {needle!r}")
|
||||
export_names = {name for name, _ in exports}
|
||||
forbidden_exports = {"uce_alloc", "uce_free"}
|
||||
for name in sorted(export_names & forbidden_exports):
|
||||
errors.append(f"forbidden allocator export {name}")
|
||||
import_map = {(module, name): kind for module, name, kind in imports}
|
||||
required_imports = {
|
||||
("env", "memory"): 2,
|
||||
("env", "__memory_base"): 3,
|
||||
}
|
||||
# units without indirect calls / stack spills / table needs
|
||||
# legitimately omit these; if present, the kind must be right
|
||||
optional_imports = {
|
||||
("env", "__indirect_function_table"): 1,
|
||||
("env", "__stack_pointer"): 3,
|
||||
("env", "__table_base"): 3,
|
||||
}
|
||||
for key, kind in required_imports.items():
|
||||
if import_map.get(key) != kind:
|
||||
errors.append(f"missing required import {key[0]}.{key[1]}")
|
||||
for key, kind in optional_imports.items():
|
||||
if key in import_map and import_map[key] != kind:
|
||||
errors.append(f"wrong kind for import {key[0]}.{key[1]}")
|
||||
for module, name, kind in imports:
|
||||
if module.startswith("wasi_") or module == "wasi_snapshot_preview1":
|
||||
errors.append(f"forbidden WASI import {module}.{name}")
|
||||
if module not in {"env", "GOT.mem", "GOT.func"} and not module.startswith("GOT."):
|
||||
errors.append(f"unexpected import module {module}.{name}")
|
||||
if module.startswith("GOT.") and kind != 3:
|
||||
errors.append(f"GOT import is not a global: {module}.{name}")
|
||||
llvm_nm = args.llvm_nm or shutil.which("llvm-nm") or "/opt/wasi-sdk/bin/llvm-nm"
|
||||
if Path(llvm_nm).exists():
|
||||
bad_prefixes = ("_Znwm", "_Znam", "_ZdlPv", "_ZdaPv", "_ZdlPvm", "_ZdaPvm")
|
||||
for sym in defined_symbols(args.wasm, llvm_nm):
|
||||
if sym in {"uce_alloc", "uce_free"} or sym.startswith(bad_prefixes):
|
||||
errors.append(f"forbidden allocator definition {sym}")
|
||||
else:
|
||||
errors.append("llvm-nm not found; cannot verify allocator definitions")
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
return 1
|
||||
if args.verbose:
|
||||
print(f"UCE W2 unit check PASS: {args.wasm}")
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -32,7 +32,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
|
||||
|
||||
## Pipeline
|
||||
|
||||
- The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`.
|
||||
- The generated file starts by including the logical runtime header `uce_lib.h`; native and WASM compile scripts provide the include path.
|
||||
- It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`.
|
||||
- It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file.
|
||||
- Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content.
|
||||
|
||||
@ -494,7 +494,7 @@ String compiler_rewrite_named_render_syntax(String content)
|
||||
String compiler_preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String content)
|
||||
{
|
||||
String parsed_content =
|
||||
("#include \"")+context->server->config["COMPILER_SYS_PATH"] +"/src/lib/uce_lib.h\" \n"+
|
||||
"#include \"uce_lib.h\" \n"+
|
||||
file_get_contents(
|
||||
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]
|
||||
)+
|
||||
|
||||
@ -132,6 +132,23 @@ String compiler_unit_metadata_text(Request* context, SharedUnit* su)
|
||||
);
|
||||
}
|
||||
|
||||
bool compiler_wasm_unit_compile_enabled(Request* context)
|
||||
{
|
||||
if(!context || !context->server)
|
||||
return(false);
|
||||
return(config_bool("COMPILE_WASM_UNITS", false));
|
||||
}
|
||||
|
||||
String compiler_wasm_compile_script(Request* context)
|
||||
{
|
||||
String script = "scripts/compile_wasm_unit";
|
||||
if(context && context->server)
|
||||
script = first(context->server->config["WASM_COMPILE_SCRIPT"], script);
|
||||
if(script != "" && script[0] != '/' && context && context->server)
|
||||
script = path_join(context->server->config["COMPILER_SYS_PATH"], script);
|
||||
return(script);
|
||||
}
|
||||
|
||||
SharedUnitCompileCheck shared_unit_compile_check(const SharedUnitFilesystemState& state)
|
||||
{
|
||||
SharedUnitCompileCheck result;
|
||||
@ -647,9 +664,12 @@ void setup_unit_paths(Request* context, SharedUnit* su, String file_name)
|
||||
|
||||
su->src_file_name = basename(file_name);
|
||||
su->bin_file_name = su->src_file_name + ".so";
|
||||
su->wasm_file_name = su->src_file_name + ".wasm";
|
||||
su->pre_file_name = su->src_file_name + ".cpp";
|
||||
|
||||
su->so_name = su->bin_path + "/" + su->bin_file_name;
|
||||
su->wasm_name = su->bin_path + "/" + su->wasm_file_name;
|
||||
su->wasm_check_file_name = su->bin_path + "/" + su->src_file_name + ".wasm-check.txt";
|
||||
su->api_file_name = su->bin_path + "/" + su->src_file_name + ".exports.txt";
|
||||
su->meta_file_name = su->bin_path + "/" + su->src_file_name + ".meta.txt";
|
||||
su->compile_output_file_name = su->bin_path + "/" + su->src_file_name + ".compile.txt";
|
||||
@ -740,7 +760,7 @@ void load_shared_unit(Request* context, SharedUnit* su)
|
||||
String result =
|
||||
String("#ifndef UCE_LIB_INCLUDED\n") +
|
||||
"#define UCE_LIB_INCLUDED\n" +
|
||||
("#include \"")+context->server->config["COMPILER_SYS_PATH"] +"/src/lib/uce_lib.h\" \n"+
|
||||
"#include \"uce_lib.h\" \n"+
|
||||
file_get_contents(
|
||||
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]) +
|
||||
"#endif \n";
|
||||
@ -839,6 +859,15 @@ void compile_shared_unit(Request* context, SharedUnit* su)
|
||||
shell_escape(su->bin_file_name)
|
||||
));
|
||||
|
||||
if(su->compiler_messages.length() == 0 && !su->opt_so_optional && compiler_wasm_unit_compile_enabled(context))
|
||||
su->compiler_messages = trim(shell_exec(shell_escape(compiler_wasm_compile_script(context))+" "+
|
||||
shell_escape(su->src_path)+" "+
|
||||
shell_escape(su->bin_path)+" "+
|
||||
shell_escape(su->file_name)+" "+
|
||||
shell_escape(su->pre_file_name)+" "+
|
||||
shell_escape(su->wasm_file_name)
|
||||
));
|
||||
|
||||
if(su->compiler_messages.length() > 0)
|
||||
{
|
||||
String raw_messages = su->compiler_messages;
|
||||
@ -1171,6 +1200,8 @@ DValue unit_info(String path)
|
||||
info["bin_file_name"] = su->bin_file_name;
|
||||
info["pre_file_name"] = su->pre_file_name;
|
||||
info["so_name"] = su->so_name;
|
||||
info["wasm_name"] = su->wasm_name;
|
||||
info["wasm_exists"].set_bool(file_exists(su->wasm_name));
|
||||
info["api_file_name"] = su->api_file_name;
|
||||
info["meta_file_name"] = su->meta_file_name;
|
||||
info["compile_output_file_name"] = su->compile_output_file_name;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
#include "functionlib.h"
|
||||
|
||||
#ifndef __UCE_WASM_CORE__
|
||||
#define PCRE2_CODE_UNIT_WIDTH 8
|
||||
#include <pcre2.h>
|
||||
#endif
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
@ -296,6 +298,7 @@ String replace(String s, String search, String replace_with)
|
||||
return(result);
|
||||
}
|
||||
|
||||
#ifndef __UCE_WASM_CORE__
|
||||
namespace {
|
||||
|
||||
String regex_flags_label(String flags)
|
||||
@ -637,6 +640,47 @@ StringList regex_split(String pattern, String subject, String flags)
|
||||
return(result);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool regex_match(String pattern, String subject, String flags)
|
||||
{
|
||||
(void)pattern; (void)subject; (void)flags;
|
||||
return(false);
|
||||
}
|
||||
|
||||
DValue regex_search(String pattern, String subject, String flags)
|
||||
{
|
||||
DValue result;
|
||||
result["matched"].set_bool(false);
|
||||
result["pattern"] = pattern;
|
||||
result["flags"] = flags == "" ? "default" : flags;
|
||||
return(result);
|
||||
}
|
||||
|
||||
DValue regex_search_all(String pattern, String subject, String flags)
|
||||
{
|
||||
DValue result;
|
||||
result["matched"].set_bool(false);
|
||||
result["pattern"] = pattern;
|
||||
result["flags"] = flags == "" ? "default" : flags;
|
||||
result["count"] = (f64)0;
|
||||
return(result);
|
||||
}
|
||||
|
||||
String regex_replace(String pattern, String replacement, String subject, String flags)
|
||||
{
|
||||
(void)pattern; (void)replacement; (void)flags;
|
||||
return(subject);
|
||||
}
|
||||
|
||||
StringList regex_split(String pattern, String subject, String flags)
|
||||
{
|
||||
(void)pattern; (void)flags;
|
||||
return(StringList{ subject });
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
String trim(String raw)
|
||||
{
|
||||
s64 len = raw.length();
|
||||
@ -1323,7 +1367,12 @@ String xml_decode_text(String s)
|
||||
|
||||
void xml_throw(String message)
|
||||
{
|
||||
#ifdef __UCE_WASM_CORE__
|
||||
(void)message;
|
||||
__builtin_trap();
|
||||
#else
|
||||
throw std::runtime_error("xml_decode(): " + message);
|
||||
#endif
|
||||
}
|
||||
|
||||
struct XmlParser
|
||||
@ -1856,7 +1905,12 @@ String yaml_decode_quoted(String raw)
|
||||
|
||||
void yaml_throw(String message)
|
||||
{
|
||||
#ifdef __UCE_WASM_CORE__
|
||||
(void)message;
|
||||
__builtin_trap();
|
||||
#else
|
||||
throw std::runtime_error("yaml_decode(): " + message);
|
||||
#endif
|
||||
}
|
||||
|
||||
struct YamlParser
|
||||
|
||||
@ -55,8 +55,11 @@ String to_string(SharedUnit* u) {
|
||||
return(result);
|
||||
}
|
||||
|
||||
// NB: header-defined function templates must be inline — wasm side modules
|
||||
// compile with -fvisibility-inlines-hidden so instantiations bind locally
|
||||
// instead of becoming unresolvable self-imports (WASM-PROPOSAL §6)
|
||||
template <typename ITYPE>
|
||||
String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
|
||||
inline String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
|
||||
{
|
||||
static const char* digits = "0123456789ABCDEF";
|
||||
String rc(hex_len,'0');
|
||||
@ -66,7 +69,7 @@ String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
|
||||
}
|
||||
|
||||
template<typename T, typename F>
|
||||
std::vector<T> filter(std::vector<T> items, F f)
|
||||
inline std::vector<T> filter(std::vector<T> items, F f)
|
||||
{
|
||||
std::vector<T> new_items;
|
||||
for(auto item : items)
|
||||
@ -78,7 +81,7 @@ std::vector<T> filter(std::vector<T> items, F f)
|
||||
}
|
||||
|
||||
template<typename T, typename F>
|
||||
auto map(std::vector<T> items, F f)
|
||||
inline auto map(std::vector<T> items, F f)
|
||||
{
|
||||
using ResultType = decltype(f(items[0]));
|
||||
std::vector<ResultType> new_items;
|
||||
@ -101,7 +104,7 @@ DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f);
|
||||
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f);
|
||||
|
||||
template <class ...Args>
|
||||
String first(Args... args)
|
||||
inline String first(Args... args)
|
||||
{
|
||||
std::vector<String> vec = {args...};
|
||||
for(auto s : vec)
|
||||
|
||||
@ -19,6 +19,11 @@ A million repetitions of "a"
|
||||
/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
|
||||
/* #define SHA1HANDSOFF * Copies data before messing with it. */
|
||||
|
||||
#ifdef __UCE_WASM_CORE__
|
||||
#include <stdint.h>
|
||||
typedef uint32_t u_int32_t;
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
u_int32_t state[5];
|
||||
u_int32_t count[2];
|
||||
|
||||
119
src/lib/sys.cpp
119
src/lib/sys.cpp
@ -1,3 +1,119 @@
|
||||
#ifdef __UCE_WASM_CORE__
|
||||
#include "types.h"
|
||||
#include "functionlib.h"
|
||||
#include "sys.h"
|
||||
|
||||
extern "C" {
|
||||
uint64_t uce_host_time(void);
|
||||
double uce_host_time_precise(void);
|
||||
size_t uce_host_env(const char* key, size_t key_len, char* buf, size_t cap);
|
||||
size_t uce_host_random(char* buf, size_t len);
|
||||
void uce_host_log(int level, const char* buf, size_t len);
|
||||
// read-only file membrane, policy-gated host-side to the site tree;
|
||||
// relative paths resolve against the current unit's directory (the native
|
||||
// cwd convention); file_read uses the length-query convention
|
||||
int uce_host_file_exists(const char* path, size_t path_len, const char* current, size_t current_len);
|
||||
size_t uce_host_file_read(const char* path, size_t path_len, const char* current, size_t current_len, char* buf, size_t cap);
|
||||
}
|
||||
|
||||
static String wasm_current_unit_file()
|
||||
{
|
||||
return(context ? context->resources.current_unit_file : String(""));
|
||||
}
|
||||
|
||||
String shell_exec(String cmd) { (void)cmd; return(""); }
|
||||
String shell_escape(String raw) { return(raw); }
|
||||
String basename(String fn) { while(fn.find("/") != String::npos) fn = fn.substr(fn.find("/") + 1); return(fn); }
|
||||
String dirname(String fn) { auto pos = fn.find_last_of('/'); return(pos == String::npos ? "" : fn.substr(0, pos)); }
|
||||
String path_join(String base, String child) { if(base == "") return(child); if(child == "") return(base); if(child[0] == '/') return(child); return(base + (base.back() == '/' ? "" : "/") + child); }
|
||||
String path_real(String path) { return(path); }
|
||||
bool path_is_within(String path, String root) { return(str_starts_with(path, root)); }
|
||||
bool mkdir(String path) { (void)path; return(false); }
|
||||
bool file_exists(String path)
|
||||
{
|
||||
String current = wasm_current_unit_file();
|
||||
return(uce_host_file_exists(path.data(), path.size(), current.data(), current.size()) != 0);
|
||||
}
|
||||
int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode, f64 wait_timeout_seconds, String purpose) { (void)file_name; (void)open_flags; (void)lock_type; (void)create_mode; (void)wait_timeout_seconds; (void)purpose; return(-1); }
|
||||
void file_close_locked(int fd) { (void)fd; }
|
||||
void file_release_process_locks(String reason) { (void)reason; }
|
||||
String file_get_contents_locked_fd(int fd) { (void)fd; return(""); }
|
||||
bool file_put_contents_locked_fd(int fd, String content) { (void)fd; (void)content; return(false); }
|
||||
String file_get_contents(String file_name)
|
||||
{
|
||||
String current = wasm_current_unit_file();
|
||||
size_t required = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), 0, 0);
|
||||
if(required == 0)
|
||||
return("");
|
||||
String content(required, 0);
|
||||
size_t got = uce_host_file_read(file_name.data(), file_name.size(), current.data(), current.size(), &content[0], required);
|
||||
content.resize(got <= required ? got : 0);
|
||||
return(content);
|
||||
}
|
||||
bool file_put_contents(String file_name, String content) { (void)file_name; (void)content; return(false); }
|
||||
bool file_append_contents(String file_name, String content) { (void)file_name; (void)content; return(false); }
|
||||
String cwd_get() { return("/"); }
|
||||
void cwd_set(String path) { (void)path; }
|
||||
String process_start_directory() { return("/"); }
|
||||
time_t file_mtime(String file_name) { (void)file_name; return(0); }
|
||||
void file_unlink(String file_name) { (void)file_name; }
|
||||
String expand_path(String path, String relative_to_path) { return(path_join(relative_to_path, path)); }
|
||||
StringList ls(String dir) { (void)dir; return(StringList()); }
|
||||
u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); }
|
||||
f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); }
|
||||
bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); }
|
||||
bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); }
|
||||
u64 config_u64(String key, u64 fallback) { return(context ? config_map_u64(context->server->config, key, fallback) : fallback); }
|
||||
f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(context->server->config, key, fallback) : fallback); }
|
||||
bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); }
|
||||
f64 time_precise() { return(uce_host_time_precise()); }
|
||||
u64 time() { return(uce_host_time()); }
|
||||
String time_format_local(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); }
|
||||
String time_format_utc(String format, u64 timestamp) { (void)format; return(std::to_string(timestamp ? timestamp : time())); }
|
||||
String time_format_relative(u64 timestamp, String format_very_recent, u64 medium_recency_seconds, String format_medium_recent, u64 not_recent_seconds, String format_not_recent) { (void)format_very_recent; (void)medium_recency_seconds; (void)format_medium_recent; (void)not_recent_seconds; (void)format_not_recent; return(std::to_string(timestamp)); }
|
||||
u64 time_parse(String time_String) { char* end = 0; unsigned long long v = strtoull(time_String.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : 0); }
|
||||
u64 socket_connect(String host, short port) { (void)host; (void)port; return(0); }
|
||||
void socket_close(u64 sockfd) { (void)sockfd; }
|
||||
bool socket_write(u64 sockfd, String data) { (void)sockfd; (void)data; return(false); }
|
||||
String socket_read(u64 sockfd, u32 max_length, u32 timeout) { (void)sockfd; (void)max_length; (void)timeout; return(""); }
|
||||
String ws_message() { return(context ? context->in : ""); }
|
||||
String ws_connection_id() { return(context ? context->resources.websocket_connection_id : ""); }
|
||||
String ws_scope() { return(context ? context->resources.websocket_scope : ""); }
|
||||
u8 ws_opcode() { return(context ? context->resources.websocket_opcode : 0); }
|
||||
bool ws_is_binary() { return(context && context->resources.websocket_is_binary); }
|
||||
StringList ws_connections(String scope) { (void)scope; return(StringList()); }
|
||||
u64 ws_connection_count(String scope) { (void)scope; return(0); }
|
||||
bool ws_send(String message, bool binary, String scope) { (void)message; (void)binary; (void)scope; return(false); }
|
||||
bool ws_send_to(String connection_id, String message, bool binary) { (void)connection_id; (void)message; (void)binary; return(false); }
|
||||
bool ws_close(String connection_id) { (void)connection_id; return(false); }
|
||||
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames) { (void)frames; (void)size; (void)skip_frames; return(""); }
|
||||
String capture_backtrace_string(u32 max_frames, u32 skip_frames) { (void)max_frames; (void)skip_frames; return(""); }
|
||||
String signal_name(int sig) { (void)sig; return(""); }
|
||||
String memcache_escape_key(String key) { return(key); }
|
||||
StringList memcache_escape_keys(StringList keys) { return(keys); }
|
||||
u64 memcache_connect(String host, short port) { (void)host; (void)port; return(0); }
|
||||
String memcache_command(u64 connection, String command) { (void)connection; (void)command; return(""); }
|
||||
bool memcache_set(u64 connection, String key, String value, u64 expires_in) { (void)connection; (void)key; (void)value; (void)expires_in; return(false); }
|
||||
bool memcache_delete(u64 connection, String key) { (void)connection; (void)key; return(false); }
|
||||
String memcache_get(u64 connection, String key, String default_value) { (void)connection; (void)key; return(default_value); }
|
||||
StringMap memcache_get_multiple(u64 connection, StringList keys) { (void)connection; (void)keys; return(StringMap()); }
|
||||
void on_segfault(int sig) { (void)sig; }
|
||||
int task_kill(pid_t pid, int sig) { (void)pid; (void)sig; return(-1); }
|
||||
String runtime_safe_key(String key, String label) { (void)label; return(key); }
|
||||
pid_t task(String key, std::function<void()> exec_after_spawn, u64 timeout) { (void)key; (void)exec_after_spawn; (void)timeout; return(0); }
|
||||
pid_t task_repeat(String key, f64 interval, std::function<void()> exec_after_spawn, u64 timeout) { (void)key; (void)interval; (void)exec_after_spawn; (void)timeout; return(0); }
|
||||
pid_t task_pid(String key) { (void)key; return(0); }
|
||||
pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_filename, String call_function) { (void)key; (void)socket_fn_or_port; (void)call_uce_filename; (void)call_function; return(0); }
|
||||
bool server_stop(String key) { (void)key; return(false); }
|
||||
StringMap default_config()
|
||||
{
|
||||
StringMap cfg;
|
||||
cfg["SESSION_TIME"] = std::to_string(60*60*24*30);
|
||||
cfg["MAX_MEMORY"] = std::to_string(1024*1024*16);
|
||||
return(cfg);
|
||||
}
|
||||
|
||||
#else
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
@ -942,6 +1058,8 @@ StringMap make_server_settings()
|
||||
|
||||
cfg["BIN_DIRECTORY"] = "/tmp/uce/work";
|
||||
cfg["COMPILE_SCRIPT"] = "scripts/compile";
|
||||
cfg["WASM_COMPILE_SCRIPT"] = "scripts/compile_wasm_unit";
|
||||
cfg["COMPILE_WASM_UNITS"] = "0";
|
||||
cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template";
|
||||
cfg["LIT_ESC"] = "3d5b5_1";
|
||||
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
|
||||
@ -999,3 +1117,4 @@ StringMap make_server_settings()
|
||||
|
||||
return(cfg);
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1,7 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(__UCE_WASM_CORE__) || defined(__UCE_WASM_UNIT__)
|
||||
#ifndef LOCK_SH
|
||||
#define LOCK_SH 1
|
||||
#define LOCK_EX 2
|
||||
#define LOCK_UN 8
|
||||
#endif
|
||||
#ifndef SIGABRT
|
||||
#define SIGABRT 6
|
||||
#endif
|
||||
#ifndef SIGSEGV
|
||||
#define SIGSEGV 11
|
||||
#endif
|
||||
typedef int pid_t;
|
||||
extern "C" {
|
||||
int raise(int);
|
||||
unsigned int sleep(unsigned int seconds);
|
||||
int usleep(unsigned int usec);
|
||||
}
|
||||
#else
|
||||
#include <sys/file.h>
|
||||
#include <signal.h>
|
||||
#endif
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
|
||||
String shell_exec(String cmd);
|
||||
@ -22,7 +43,7 @@ String file_get_contents(String file_name);
|
||||
bool file_put_contents(String file_name, String content);
|
||||
bool file_append_contents(String file_name, String content);
|
||||
template <typename... Ts>
|
||||
bool file_append(String file_name, Ts... args)
|
||||
inline bool file_append(String file_name, Ts... args)
|
||||
{
|
||||
std::ostringstream out;
|
||||
((out << args), ...);
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
#ifndef __UCE_WASM_CORE__
|
||||
#include <sys/file.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <ctype.h>
|
||||
#include <fstream>
|
||||
#include <sys/stat.h>
|
||||
#include <ctime>
|
||||
#include <dlfcn.h>
|
||||
#include <limits.h>
|
||||
#include <algorithm>
|
||||
#include <sys/stat.h>
|
||||
#include <iostream>
|
||||
|
||||
#include "types.h"
|
||||
@ -52,10 +53,12 @@ String http_status_reason(s32 code)
|
||||
|
||||
SharedUnit::~SharedUnit()
|
||||
{
|
||||
#ifndef __UCE_WASM_CORE__
|
||||
if(so_handle)
|
||||
{
|
||||
dlclose(so_handle);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
String nibble(String div, String& haystack)
|
||||
@ -98,6 +101,8 @@ Request::~Request()
|
||||
delete stream;
|
||||
ob_stack.clear();
|
||||
ob = 0;
|
||||
#ifndef __UCE_WASM_CORE__
|
||||
for(auto& sockfd : resources.sockets)
|
||||
close(sockfd);
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -65,6 +65,8 @@ struct SharedUnit {
|
||||
|
||||
String file_name;
|
||||
String so_name;
|
||||
String wasm_name;
|
||||
String wasm_check_file_name;
|
||||
String api_file_name;
|
||||
String meta_file_name;
|
||||
String compile_output_file_name;
|
||||
@ -77,6 +79,7 @@ struct SharedUnit {
|
||||
String pre_path;
|
||||
String src_file_name;
|
||||
String bin_file_name;
|
||||
String wasm_file_name;
|
||||
String pre_file_name;
|
||||
|
||||
void* so_handle = 0;
|
||||
@ -243,26 +246,29 @@ Request* context;
|
||||
|
||||
#include <iostream>
|
||||
|
||||
// NB: header templates must be inline — wasm units rely on
|
||||
// -fvisibility-inlines-hidden binding instantiations locally (§6)
|
||||
template <typename... Ts>
|
||||
void print(Ts... args)
|
||||
inline void print(Ts... args)
|
||||
{
|
||||
((*context->ob << args), ...);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
void out(Ts... args)
|
||||
inline void out(Ts... args)
|
||||
{
|
||||
((*context->ob << args), ...);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
String concat(Ts... args)
|
||||
inline String concat(Ts... args)
|
||||
{
|
||||
ByteStream out;
|
||||
((out << args), ...);
|
||||
return(out.str());
|
||||
}
|
||||
|
||||
#ifndef __UCE_WASM_UNIT__
|
||||
void * operator new(decltype(sizeof(0)) n) noexcept(false)
|
||||
{
|
||||
void* ptr = malloc(n);
|
||||
@ -280,3 +286,4 @@ void operator delete(void * p) throw()
|
||||
//TO DO: track deallocations
|
||||
free(p);
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -11,9 +11,12 @@
|
||||
#include "sys.cpp"
|
||||
#include "uri.cpp"
|
||||
#include "cli.cpp"
|
||||
|
||||
#ifndef __UCE_WASM_CORE__
|
||||
#include "compiler-parser.cpp"
|
||||
#include "compiler.cpp"
|
||||
#include "markdown.cpp"
|
||||
#include "zip.cpp"
|
||||
#include "mysql-connector.cpp"
|
||||
#include "sqlite-connector.cpp"
|
||||
#endif
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __UCE_WASM_CORE__
|
||||
extern "C" size_t uce_host_random(char* buf, size_t len);
|
||||
#endif
|
||||
|
||||
String base64_encode(String raw)
|
||||
{
|
||||
static const char* chars =
|
||||
@ -714,6 +718,19 @@ StringMap parse_cookies(String cookie_String)
|
||||
|
||||
String session_id_create()
|
||||
{
|
||||
#ifdef __UCE_WASM_CORE__
|
||||
unsigned char bytes[32];
|
||||
if(uce_host_random((char*)bytes, sizeof(bytes)) != sizeof(bytes))
|
||||
__builtin_trap();
|
||||
String result;
|
||||
static const char* hex = "0123456789abcdef";
|
||||
for(unsigned char b : bytes)
|
||||
{
|
||||
result.push_back(hex[b >> 4]);
|
||||
result.push_back(hex[b & 0x0f]);
|
||||
}
|
||||
return(result);
|
||||
#else
|
||||
unsigned char bytes[32];
|
||||
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
|
||||
if(fd == -1)
|
||||
@ -738,6 +755,7 @@ String session_id_create()
|
||||
result.push_back(hex[b & 0x0f]);
|
||||
}
|
||||
return(result);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool is_valid_session_id(String session_id)
|
||||
|
||||
412
src/wasm/core.cpp
Normal file
412
src/wasm/core.cpp
Normal file
@ -0,0 +1,412 @@
|
||||
// Production WASM W1 core entrypoint.
|
||||
//
|
||||
// This file deliberately includes the real UCE runtime amalgamation with
|
||||
// __UCE_WASM_CORE__ enabled. Native-only pieces are carved out in the runtime
|
||||
// sources, while the workspace-owned DValue ABI and output plumbing are built
|
||||
// into core.wasm.
|
||||
|
||||
#define __UCE_WASM_CORE__ 1
|
||||
#include "../lib/uce_lib.cpp"
|
||||
#include "../lib/mysql-connector.h"
|
||||
#include "../lib/sqlite-connector.h"
|
||||
|
||||
// ---- W3 connector membrane stubs -------------------------------------------
|
||||
// Generated units reference the connector class surface through uce_lib.h, so
|
||||
// the core must define it. Until the real hostcall connectors land (W5 parity
|
||||
// scope: the starter uses no database), every operation fails cleanly with an
|
||||
// explanatory error instead of trapping.
|
||||
|
||||
static const char* WASM_DB_UNAVAILABLE =
|
||||
"database connectors are not yet available in the wasm workspace";
|
||||
|
||||
bool MySQL::connect(String host, String username, String password)
|
||||
{
|
||||
(void)host; (void)username; (void)password;
|
||||
connection = 0;
|
||||
statement_info = WASM_DB_UNAVAILABLE;
|
||||
return(false);
|
||||
}
|
||||
|
||||
void MySQL::disconnect() { connection = 0; }
|
||||
String MySQL::error() { return(WASM_DB_UNAVAILABLE); }
|
||||
String MySQL::escape(String raw, char quote_char) { (void)quote_char; return(raw); }
|
||||
String MySQL::parse_query_parameters(String query, StringMap m) { (void)m; return(query); }
|
||||
DValue MySQL::query(String q) { (void)q; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
|
||||
DValue MySQL::query(String q, StringMap params) { (void)q; (void)params; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
|
||||
DValue MySQL::get_pending_result() { return(DValue()); }
|
||||
|
||||
String mysql_escape(String raw, char quote_char) { (void)quote_char; return(raw); }
|
||||
|
||||
bool SQLite::connect(String path) { this->path = path; connection = 0; set_error(1, WASM_DB_UNAVAILABLE); return(false); }
|
||||
void SQLite::disconnect() { connection = 0; }
|
||||
String SQLite::error() { return(error_code ? String(WASM_DB_UNAVAILABLE) : String("")); }
|
||||
DValue SQLite::query(String q) { (void)q; set_error(1, WASM_DB_UNAVAILABLE); return(DValue()); }
|
||||
DValue SQLite::query(String q, const StringMap& params) { (void)q; (void)params; set_error(1, WASM_DB_UNAVAILABLE); return(DValue()); }
|
||||
void SQLite::set_error(s32 code, String info) { error_code = code; statement_info = info; }
|
||||
bool SQLite::apply_default_pragmas() { return(false); }
|
||||
bool SQLite::bind_params(void* statement, const StringMap& params) { (void)statement; (void)params; return(false); }
|
||||
DValue SQLite::collect_rows(void* statement) { (void)statement; return(DValue()); }
|
||||
|
||||
SQLite* sqlite_connect(String path)
|
||||
{
|
||||
SQLite* db = new SQLite();
|
||||
db->request_cleanup_delete = true;
|
||||
db->connect(path);
|
||||
return(db);
|
||||
}
|
||||
|
||||
void sqlite_disconnect(SQLite* db) { if(db) { db->disconnect(); if(db->request_cleanup_delete) delete db; } }
|
||||
String sqlite_error(SQLite* db) { return(db ? db->error() : String(WASM_DB_UNAVAILABLE)); }
|
||||
DValue sqlite_query(SQLite* db, String q) { return(db ? db->query(q) : DValue()); }
|
||||
DValue sqlite_query(SQLite* db, String q, const StringMap& params) { return(db ? db->query(q, params) : DValue()); }
|
||||
u64 sqlite_insert_id(SQLite* db) { return(db ? db->insert_id : 0); }
|
||||
u32 sqlite_affected_rows(SQLite* db) { return(db ? db->affected_rows : 0); }
|
||||
void cleanup_sqlite_connections() { }
|
||||
|
||||
static ServerState wasm_server;
|
||||
static Request wasm_request;
|
||||
static String wasm_output;
|
||||
static String wasm_response_meta;
|
||||
|
||||
// ---- vague-linkage link anchors --------------------------------------------
|
||||
// Units import libc++ template instantiations they use; --export-all only
|
||||
// exports what the core itself instantiated. Some libc++ internals lack the
|
||||
// hide-from-ABI attribute (the Phase 0 lambda finding), so units emit them as
|
||||
// imports rather than binding locally. This function exists purely to make
|
||||
// the core instantiate — and therefore export — the ones the site tree needs.
|
||||
// Extend it when the loader reports "unresolved import env.<libc++ symbol>".
|
||||
extern "C" void uce_wasm_link_anchors()
|
||||
{
|
||||
StringMap string_map;
|
||||
string_map["k"] = "v";
|
||||
string_map.erase(String("k")); // __tree::__erase_unique<String>
|
||||
std::map<String, DValue> dvalue_map;
|
||||
dvalue_map["k"] = DValue();
|
||||
dvalue_map.erase(String("k"));
|
||||
std::vector<String> string_list = { "a", "b" };
|
||||
string_list.erase(string_list.begin());
|
||||
std::set<String> string_set;
|
||||
string_set.insert("k");
|
||||
string_set.erase(String("k"));
|
||||
|
||||
// libc functions units may call that the core itself never references —
|
||||
// taking their address forces them into the link (and --export-all)
|
||||
static void* volatile libc_anchors[] = {
|
||||
(void*)&atof, (void*)&atoi, (void*)&atol, (void*)&atoll,
|
||||
(void*)&strtol, (void*)&strtoul, (void*)&strtoll, (void*)&strtoull,
|
||||
(void*)&strtod, (void*)&strtof,
|
||||
(void*)&qsort, (void*)&bsearch,
|
||||
(void*)&snprintf, (void*)&sscanf,
|
||||
(void*)&memmove, (void*)&strncmp, (void*)&strncpy,
|
||||
// memchr/strchr/strrchr/strstr are C++-overloaded; cast to the C shape
|
||||
(void*)(const void* (*)(const void*, int, size_t))&memchr,
|
||||
(void*)(const char* (*)(const char*, int))&strchr,
|
||||
(void*)(const char* (*)(const char*, int))&strrchr,
|
||||
(void*)(const char* (*)(const char*, const char*))&strstr,
|
||||
};
|
||||
(void)libc_anchors;
|
||||
}
|
||||
|
||||
// W3 membrane: the host resolves component/render targets to funcref-table
|
||||
// slots (loading units lazily) and writes the resolved unit path back so
|
||||
// nested relative component resolution keeps working.
|
||||
extern "C" int32_t uce_host_component_resolve(
|
||||
const char* target, size_t target_len, int32_t kind,
|
||||
const char* current_unit, size_t current_unit_len,
|
||||
char* resolved_buf, size_t resolved_cap);
|
||||
|
||||
// target → table slot, reset per request (workspaces die with the request,
|
||||
// but a single workspace can render the same component many times)
|
||||
static std::map<String, s32> wasm_component_slots;
|
||||
|
||||
// These mirror small page-runtime pieces of compiler.cpp, which is carved
|
||||
// out of the wasm core wholesale (it is the native toolchain: parser, clang
|
||||
// driver, dlopen). Kept byte-identical where possible.
|
||||
String component_normalize_path(String name)
|
||||
{
|
||||
name = trim(name);
|
||||
if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce")
|
||||
return(name);
|
||||
return(name + ".uce");
|
||||
}
|
||||
|
||||
void component_parse_target(String target, String& file_name, String& render_name)
|
||||
{
|
||||
target = trim(target);
|
||||
render_name = "";
|
||||
auto render_split_pos = target.find(":");
|
||||
if(render_split_pos != String::npos)
|
||||
{
|
||||
render_name = trim(target.substr(render_split_pos + 1));
|
||||
target = trim(target.substr(0, render_split_pos));
|
||||
}
|
||||
file_name = target;
|
||||
}
|
||||
|
||||
String component_error_banner(String message)
|
||||
{
|
||||
return("<div class=\"banner\">" + html_escape(message) + "</div>");
|
||||
}
|
||||
|
||||
struct RequestPropsScope
|
||||
{
|
||||
Request* context = 0;
|
||||
DValue previous_props;
|
||||
|
||||
RequestPropsScope(Request* context, const DValue& props)
|
||||
{
|
||||
this->context = context;
|
||||
if(this->context)
|
||||
{
|
||||
previous_props = this->context->props;
|
||||
this->context->props = props;
|
||||
}
|
||||
}
|
||||
|
||||
~RequestPropsScope()
|
||||
{
|
||||
if(context)
|
||||
context->props = previous_props;
|
||||
}
|
||||
};
|
||||
|
||||
// kind values shared with the host loader (see src/wasm/worker.h)
|
||||
enum WasmResolveKind { WASM_RESOLVE_COMPONENT = 0, WASM_RESOLVE_RENDER = 1, WASM_RESOLVE_EXISTS = 2 };
|
||||
|
||||
static s32 wasm_resolve_target(String target, s32 kind, String* resolved_out = 0)
|
||||
{
|
||||
String cache_key = std::to_string(kind) + ":" + target;
|
||||
auto cached = wasm_component_slots.find(cache_key);
|
||||
if(cached != wasm_component_slots.end() && kind != WASM_RESOLVE_EXISTS)
|
||||
return(cached->second);
|
||||
char resolved[512];
|
||||
String current = context ? context->resources.current_unit_file : "";
|
||||
s32 slot = uce_host_component_resolve(
|
||||
target.data(), target.size(), kind,
|
||||
current.data(), current.size(),
|
||||
resolved, sizeof(resolved));
|
||||
if(resolved_out && slot)
|
||||
*resolved_out = String(resolved, strnlen(resolved, sizeof(resolved)));
|
||||
if(kind != WASM_RESOLVE_EXISTS)
|
||||
wasm_component_slots[cache_key] = slot;
|
||||
return(slot);
|
||||
}
|
||||
|
||||
String component_resolve(String name)
|
||||
{
|
||||
String resolved;
|
||||
if(wasm_resolve_target(trim(name), WASM_RESOLVE_EXISTS, &resolved))
|
||||
return(resolved);
|
||||
return("");
|
||||
}
|
||||
|
||||
bool component_exists(String name)
|
||||
{
|
||||
return(component_resolve(name) != "");
|
||||
}
|
||||
|
||||
void component_render(String name, DValue props, Request& request)
|
||||
{
|
||||
String resolved;
|
||||
s32 slot = wasm_resolve_target(trim(name), WASM_RESOLVE_COMPONENT, &resolved);
|
||||
if(!slot)
|
||||
{
|
||||
print(component_error_banner("component not found: " + trim(name)));
|
||||
return;
|
||||
}
|
||||
RequestPropsScope props_scope(&request, props);
|
||||
String previous_unit = request.resources.current_unit_file;
|
||||
if(resolved != "")
|
||||
request.resources.current_unit_file = resolved;
|
||||
// a wasm function pointer is its index in the shared funcref table; the
|
||||
// host returned the handler's slot, so this is a plain call_indirect
|
||||
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
|
||||
handler(request);
|
||||
request.resources.current_unit_file = previous_unit;
|
||||
}
|
||||
|
||||
void component_render(String name) { DValue props; component_render(name, props, *context); }
|
||||
void component_render(String name, Request& request) { DValue props; component_render(name, props, request); }
|
||||
void component_render(String name, DValue props) { component_render(name, props, *context); }
|
||||
|
||||
String component(String name, DValue props, Request& request)
|
||||
{
|
||||
ob_start();
|
||||
component_render(name, props, request);
|
||||
return(ob_get_close());
|
||||
}
|
||||
|
||||
String component(String name) { DValue props; return(component(name, props, *context)); }
|
||||
String component(String name, Request& request) { DValue props; return(component(name, props, request)); }
|
||||
String component(String name, DValue props) { return(component(name, props, *context)); }
|
||||
|
||||
void unit_render(String file_name, Request& request)
|
||||
{
|
||||
String resolved;
|
||||
s32 slot = wasm_resolve_target(trim(file_name), WASM_RESOLVE_RENDER, &resolved);
|
||||
if(!slot)
|
||||
{
|
||||
print(component_error_banner("unit not found: " + trim(file_name)));
|
||||
return;
|
||||
}
|
||||
String previous_unit = request.resources.current_unit_file;
|
||||
if(resolved != "")
|
||||
request.resources.current_unit_file = resolved;
|
||||
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
|
||||
handler(request);
|
||||
request.resources.current_unit_file = previous_unit;
|
||||
}
|
||||
|
||||
void unit_render(String file_name) { unit_render(file_name, *context); }
|
||||
|
||||
extern "C" {
|
||||
|
||||
void* uce_alloc(size_t len)
|
||||
{
|
||||
return(malloc(len));
|
||||
}
|
||||
|
||||
void uce_free(void* ptr)
|
||||
{
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
u32 uce_wasm_core_abi_version()
|
||||
{
|
||||
return(6);
|
||||
}
|
||||
|
||||
int uce_wasm_core_init()
|
||||
{
|
||||
wasm_server.config = default_config();
|
||||
wasm_request.server = &wasm_server;
|
||||
// the primary output stream must live ON ob_stack (native semantics):
|
||||
// ob_get_close()/ob_close() pop and rebalance against the stack, so a
|
||||
// stream outside it would be orphaned by the first component() capture
|
||||
if(wasm_request.ob_stack.empty())
|
||||
wasm_request.ob_start();
|
||||
context = &wasm_request;
|
||||
return(0);
|
||||
}
|
||||
|
||||
void uce_wasm_core_reset_request()
|
||||
{
|
||||
if(context == 0)
|
||||
uce_wasm_core_init();
|
||||
wasm_request.call = DValue();
|
||||
wasm_request.props = DValue();
|
||||
wasm_request.params.clear();
|
||||
wasm_request.get.clear();
|
||||
wasm_request.post.clear();
|
||||
wasm_request.header.clear();
|
||||
wasm_request.set_cookies.clear();
|
||||
wasm_request.response_code = "HTTP/1.1 200 OK";
|
||||
wasm_request.flags = Request::Flags();
|
||||
wasm_request.stats = Request::Stats();
|
||||
for(auto* stream : wasm_request.ob_stack)
|
||||
delete stream;
|
||||
wasm_request.ob_stack.clear();
|
||||
wasm_request.ob_start();
|
||||
wasm_output = "";
|
||||
wasm_response_meta = "";
|
||||
wasm_request.cookies.clear();
|
||||
wasm_request.session.clear();
|
||||
wasm_request.out = "";
|
||||
wasm_request.resources.current_unit_file = "";
|
||||
wasm_component_slots.clear();
|
||||
}
|
||||
|
||||
// Host pushes the UCEB1-encoded request context into a guest buffer
|
||||
// (uce_alloc) and applies it here; mirrors the native param population.
|
||||
int uce_wasm_apply_context(const char* buf, size_t len)
|
||||
{
|
||||
if(context == 0)
|
||||
uce_wasm_core_init();
|
||||
DValue decoded;
|
||||
String error;
|
||||
if(!ucb_decode(String(buf, len), decoded, &error))
|
||||
{
|
||||
uce_host_log(3, error.data(), error.size());
|
||||
return(1);
|
||||
}
|
||||
wasm_request.call = decoded;
|
||||
auto apply_map = [](DValue* source, StringMap& dest) {
|
||||
dest.clear();
|
||||
if(source)
|
||||
source->each([&](const DValue& item, String key) {
|
||||
dest[key] = item.to_string();
|
||||
});
|
||||
};
|
||||
apply_map(decoded.key("params"), wasm_request.params);
|
||||
apply_map(decoded.key("get"), wasm_request.get);
|
||||
apply_map(decoded.key("post"), wasm_request.post);
|
||||
apply_map(decoded.key("cookies"), wasm_request.cookies);
|
||||
apply_map(decoded.key("session"), wasm_request.session);
|
||||
DValue* entry = decoded.key("entry_unit");
|
||||
if(entry)
|
||||
wasm_request.resources.current_unit_file = entry->to_string();
|
||||
return(0);
|
||||
}
|
||||
|
||||
Request* uce_wasm_request()
|
||||
{
|
||||
if(context == 0)
|
||||
uce_wasm_core_init();
|
||||
return(&wasm_request);
|
||||
}
|
||||
|
||||
// After render: response metadata (status line, headers, cookies, session)
|
||||
// goes back to the host as UCEB1.
|
||||
void uce_wasm_finish_response_meta()
|
||||
{
|
||||
DValue meta;
|
||||
meta["status"] = wasm_request.response_code;
|
||||
for(auto& header : wasm_request.header)
|
||||
meta["headers"][header.first] = header.second;
|
||||
for(auto& cookie : wasm_request.set_cookies)
|
||||
{
|
||||
DValue cookie_value;
|
||||
cookie_value = cookie;
|
||||
meta["cookies"].push(cookie_value);
|
||||
}
|
||||
for(auto& entry : wasm_request.session)
|
||||
meta["session"][entry.first] = entry.second;
|
||||
wasm_response_meta = ucb_encode(meta);
|
||||
}
|
||||
|
||||
const char* uce_wasm_response_meta_data()
|
||||
{
|
||||
return(wasm_response_meta.data());
|
||||
}
|
||||
|
||||
size_t uce_wasm_response_meta_size()
|
||||
{
|
||||
return(wasm_response_meta.size());
|
||||
}
|
||||
|
||||
void uce_print_bytes(const char* data, size_t len)
|
||||
{
|
||||
if(context == 0)
|
||||
uce_wasm_core_init();
|
||||
if(context->ob && data && len)
|
||||
context->ob->write(data, len);
|
||||
}
|
||||
|
||||
void uce_wasm_finish_output()
|
||||
{
|
||||
// ob_stack[0] is the request's primary stream; nested captures above it
|
||||
// belong to unbalanced ob_start() calls and are intentionally ignored
|
||||
wasm_output = wasm_request.ob_stack.empty() ? String("") : wasm_request.ob_stack[0]->str();
|
||||
}
|
||||
|
||||
const char* uce_wasm_output_data()
|
||||
{
|
||||
return(wasm_output.data());
|
||||
}
|
||||
|
||||
size_t uce_wasm_output_size()
|
||||
{
|
||||
return(wasm_output.size());
|
||||
}
|
||||
|
||||
}
|
||||
8
src/wasm/core_hostcalls.syms
Normal file
8
src/wasm/core_hostcalls.syms
Normal file
@ -0,0 +1,8 @@
|
||||
uce_host_time
|
||||
uce_host_time_precise
|
||||
uce_host_env
|
||||
uce_host_log
|
||||
uce_host_random
|
||||
uce_host_component_resolve
|
||||
uce_host_file_exists
|
||||
uce_host_file_read
|
||||
21
src/wasm/core_libc_exports.syms
Normal file
21
src/wasm/core_libc_exports.syms
Normal file
@ -0,0 +1,21 @@
|
||||
atof
|
||||
atoi
|
||||
atol
|
||||
atoll
|
||||
strtol
|
||||
strtoul
|
||||
strtoll
|
||||
strtoull
|
||||
strtod
|
||||
strtof
|
||||
qsort
|
||||
bsearch
|
||||
snprintf
|
||||
sscanf
|
||||
memmove
|
||||
memchr
|
||||
strncmp
|
||||
strncpy
|
||||
strchr
|
||||
strrchr
|
||||
strstr
|
||||
308
src/wasm/w1_smoke.cpp
Normal file
308
src/wasm/w1_smoke.cpp
Normal file
@ -0,0 +1,308 @@
|
||||
// W1 smoke driver for the production UCE core.wasm.
|
||||
|
||||
#include <wasm.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
|
||||
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
|
||||
|
||||
static std::vector<uint8_t> read_file(const char* path)
|
||||
{
|
||||
FILE* f = fopen(path, "rb");
|
||||
CHECK(f, "cannot open %s", path);
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
std::vector<uint8_t> data((size_t)n);
|
||||
CHECK(fread(data.data(), 1, data.size(), f) == data.size(), "short read on %s", path);
|
||||
fclose(f);
|
||||
return(data);
|
||||
}
|
||||
|
||||
static std::string wasm_name(const wasm_name_t* name)
|
||||
{
|
||||
std::string s(name->data, name->size);
|
||||
while(!s.empty() && s.back() == '\0') s.pop_back();
|
||||
return(s);
|
||||
}
|
||||
|
||||
static wasm_store_t* g_store_for_traps = nullptr;
|
||||
|
||||
static void set_i32(wasm_val_t* result, int32_t value)
|
||||
{
|
||||
result->kind = WASM_I32;
|
||||
result->of.i32 = value;
|
||||
}
|
||||
|
||||
static void set_i64(wasm_val_t* result, int64_t value)
|
||||
{
|
||||
result->kind = WASM_I64;
|
||||
result->of.i64 = value;
|
||||
}
|
||||
|
||||
static void set_f64(wasm_val_t* result, double value)
|
||||
{
|
||||
result->kind = WASM_F64;
|
||||
result->of.f64 = value;
|
||||
}
|
||||
|
||||
static wasm_trap_t* host_time(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
|
||||
{
|
||||
set_i64(&results->data[0], 1700000000);
|
||||
return(nullptr);
|
||||
}
|
||||
|
||||
static wasm_trap_t* host_time_precise(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
|
||||
{
|
||||
set_f64(&results->data[0], 1700000000.25);
|
||||
return(nullptr);
|
||||
}
|
||||
|
||||
static wasm_trap_t* host_env(void*, const wasm_val_vec_t*, wasm_val_vec_t* results)
|
||||
{
|
||||
set_i32(&results->data[0], 0);
|
||||
return(nullptr);
|
||||
}
|
||||
|
||||
static wasm_memory_t* g_memory = nullptr;
|
||||
|
||||
static wasm_trap_t* host_random(void*, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
||||
{
|
||||
uint32_t ptr = args->data[0].of.i32;
|
||||
uint32_t len = args->data[1].of.i32;
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
||||
size_t mem_size = wasm_memory_data_size(g_memory);
|
||||
if((size_t)ptr + len > mem_size)
|
||||
{
|
||||
set_i32(&results->data[0], 0);
|
||||
return(nullptr);
|
||||
}
|
||||
for(uint32_t i = 0; i < len; ++i)
|
||||
mem[ptr + i] = (uint8_t)(0x5au ^ (i * 29u));
|
||||
set_i32(&results->data[0], len);
|
||||
return(nullptr);
|
||||
}
|
||||
|
||||
static wasm_trap_t* host_log(void*, const wasm_val_vec_t*, wasm_val_vec_t*)
|
||||
{
|
||||
return(nullptr);
|
||||
}
|
||||
|
||||
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t*, wasm_val_vec_t*)
|
||||
{
|
||||
std::string label = (const char*)env;
|
||||
std::string msg = "unexpected import called: " + label;
|
||||
wasm_byte_vec_t message;
|
||||
wasm_byte_vec_new(&message, msg.size(), msg.data());
|
||||
wasm_trap_t* trap = wasm_trap_new(g_store_for_traps, &message);
|
||||
wasm_byte_vec_delete(&message);
|
||||
return(trap);
|
||||
}
|
||||
|
||||
struct Instance
|
||||
{
|
||||
wasm_module_t* module = nullptr;
|
||||
wasm_instance_t* instance = nullptr;
|
||||
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
|
||||
std::map<std::string, wasm_extern_t*> by_name;
|
||||
|
||||
void index_exports()
|
||||
{
|
||||
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
|
||||
wasm_module_exports(module, &types);
|
||||
wasm_instance_exports(instance, &exports);
|
||||
CHECK(types.size == exports.size, "export count mismatch");
|
||||
for(size_t i = 0; i < types.size; ++i)
|
||||
by_name[wasm_name(wasm_exporttype_name(types.data[i]))] = exports.data[i];
|
||||
wasm_exporttype_vec_delete(&types);
|
||||
}
|
||||
|
||||
wasm_func_t* func(const char* name)
|
||||
{
|
||||
auto it = by_name.find(name);
|
||||
return(it == by_name.end() ? nullptr : wasm_extern_as_func(it->second));
|
||||
}
|
||||
|
||||
wasm_memory_t* memory()
|
||||
{
|
||||
auto it = by_name.find("memory");
|
||||
return(it == by_name.end() ? nullptr : wasm_extern_as_memory(it->second));
|
||||
}
|
||||
};
|
||||
|
||||
static void report_trap(wasm_trap_t* trap, const char* what)
|
||||
{
|
||||
if(!trap) return;
|
||||
wasm_message_t msg;
|
||||
wasm_trap_message(trap, &msg);
|
||||
FAIL("trap during %s: %.*s", what, (int)msg.size, msg.data);
|
||||
}
|
||||
|
||||
static int32_t call_i32(Instance& inst, const char* name, std::vector<int32_t> argv = {})
|
||||
{
|
||||
wasm_func_t* f = inst.func(name);
|
||||
CHECK(f, "missing function %s", name);
|
||||
CHECK(argv.size() <= 4, "too many args for %s", name);
|
||||
wasm_val_t args_buf[4];
|
||||
for(size_t i = 0; i < argv.size(); ++i) args_buf[i] = WASM_I32_VAL(argv[i]);
|
||||
wasm_val_t result_buf[1] = { WASM_INIT_VAL };
|
||||
wasm_val_vec_t args = { argv.size(), args_buf };
|
||||
wasm_val_vec_t results = { 1, result_buf };
|
||||
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
|
||||
wasm_trap_t* trap = wasm_func_call(f, &args, wasm_func_result_arity(f) ? &results : &no_results);
|
||||
report_trap(trap, name);
|
||||
return(wasm_func_result_arity(f) ? result_buf[0].of.i32 : 0);
|
||||
}
|
||||
|
||||
static void write_bytes(wasm_memory_t* memory, uint32_t ptr, const std::string& data)
|
||||
{
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||
size_t mem_size = wasm_memory_data_size(memory);
|
||||
CHECK((size_t)ptr + data.size() <= mem_size, "write outside memory");
|
||||
memcpy(mem + ptr, data.data(), data.size());
|
||||
}
|
||||
|
||||
static std::string read_bytes(wasm_memory_t* memory, uint32_t ptr, uint32_t len)
|
||||
{
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||
size_t mem_size = wasm_memory_data_size(memory);
|
||||
CHECK((size_t)ptr + len <= mem_size, "read outside memory");
|
||||
return(std::string((const char*)mem + ptr, len));
|
||||
}
|
||||
|
||||
static std::string read_cstr(wasm_memory_t* memory, uint32_t ptr, uint32_t cap = 4096)
|
||||
{
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||
size_t mem_size = wasm_memory_data_size(memory);
|
||||
CHECK(ptr < mem_size, "cstr starts outside memory");
|
||||
std::string out;
|
||||
for(uint32_t i = 0; i < cap && (size_t)ptr + i < mem_size; ++i)
|
||||
{
|
||||
if(mem[ptr + i] == 0)
|
||||
return(out);
|
||||
out.push_back((char)mem[ptr + i]);
|
||||
}
|
||||
FAIL("unterminated cstr");
|
||||
}
|
||||
|
||||
static uint32_t read_u32(wasm_memory_t* memory, uint32_t ptr)
|
||||
{
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(memory);
|
||||
size_t mem_size = wasm_memory_data_size(memory);
|
||||
CHECK((size_t)ptr + 4 <= mem_size, "u32 read outside memory");
|
||||
return((uint32_t)mem[ptr] | ((uint32_t)mem[ptr + 1] << 8) | ((uint32_t)mem[ptr + 2] << 16) | ((uint32_t)mem[ptr + 3] << 24));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-w1/core.wasm";
|
||||
wasm_engine_t* engine = wasm_engine_new();
|
||||
CHECK(engine, "engine");
|
||||
wasm_store_t* store = wasm_store_new(engine);
|
||||
CHECK(store, "store");
|
||||
g_store_for_traps = store;
|
||||
|
||||
std::vector<uint8_t> bytes = read_file(core_path);
|
||||
wasm_byte_vec_t bv;
|
||||
wasm_byte_vec_new(&bv, bytes.size(), (const char*)bytes.data());
|
||||
Instance core;
|
||||
core.module = wasm_module_new(store, &bv);
|
||||
wasm_byte_vec_delete(&bv);
|
||||
CHECK(core.module, "module load");
|
||||
|
||||
wasm_importtype_vec_t imports = WASM_EMPTY_VEC;
|
||||
wasm_module_imports(core.module, &imports);
|
||||
std::vector<wasm_extern_t*> import_externs(imports.size);
|
||||
for(size_t i = 0; i < imports.size; ++i)
|
||||
{
|
||||
std::string mod = wasm_name(wasm_importtype_module(imports.data[i]));
|
||||
std::string name = wasm_name(wasm_importtype_name(imports.data[i]));
|
||||
const wasm_externtype_t* et = wasm_importtype_type(imports.data[i]);
|
||||
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, "unexpected non-func import %s.%s", mod.c_str(), name.c_str());
|
||||
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
|
||||
wasm_func_t* fn = nullptr;
|
||||
if(mod == "env" && name == "uce_host_time") fn = wasm_func_new_with_env(store, ft, host_time, nullptr, nullptr);
|
||||
else if(mod == "env" && name == "uce_host_time_precise") fn = wasm_func_new_with_env(store, ft, host_time_precise, nullptr, nullptr);
|
||||
else if(mod == "env" && name == "uce_host_env") fn = wasm_func_new_with_env(store, ft, host_env, nullptr, nullptr);
|
||||
else if(mod == "env" && name == "uce_host_random") fn = wasm_func_new_with_env(store, ft, host_random, nullptr, nullptr);
|
||||
else if(mod == "env" && name == "uce_host_log") fn = wasm_func_new_with_env(store, ft, host_log, nullptr, nullptr);
|
||||
else if(mod == "wasi_snapshot_preview1")
|
||||
{
|
||||
char* label = strdup((mod + "." + name).c_str());
|
||||
fn = wasm_func_new_with_env(store, ft, stub_callback, label, nullptr);
|
||||
}
|
||||
else
|
||||
FAIL("unexpected core import %s.%s", mod.c_str(), name.c_str());
|
||||
CHECK(fn, "import function %s.%s", mod.c_str(), name.c_str());
|
||||
import_externs[i] = wasm_func_as_extern(fn);
|
||||
}
|
||||
wasm_extern_vec_t iv = { import_externs.size(), import_externs.data() };
|
||||
wasm_trap_t* trap = nullptr;
|
||||
core.instance = wasm_instance_new(store, core.module, &iv, &trap);
|
||||
report_trap(trap, "core instantiation");
|
||||
CHECK(core.instance, "core instantiate");
|
||||
core.index_exports();
|
||||
CHECK(core.memory(), "core exports memory");
|
||||
g_memory = core.memory();
|
||||
if(core.func("_initialize")) call_i32(core, "_initialize");
|
||||
|
||||
CHECK(call_i32(core, "uce_wasm_core_init") == 0, "core init failed");
|
||||
call_i32(core, "uce_wasm_core_reset_request");
|
||||
CHECK(call_i32(core, "uce_wasm_core_abi_version") == 6, "unexpected ABI version");
|
||||
|
||||
wasm_memory_t* memory = core.memory();
|
||||
int32_t root = call_i32(core, "uce_dv_root");
|
||||
CHECK(root != 0, "uce_dv_root returned null");
|
||||
std::string key = "message";
|
||||
std::string value = "hello from W1 core";
|
||||
int32_t key_ptr = call_i32(core, "uce_alloc", { (int32_t)key.size() });
|
||||
int32_t value_ptr = call_i32(core, "uce_alloc", { (int32_t)value.size() });
|
||||
write_bytes(memory, key_ptr, key);
|
||||
write_bytes(memory, value_ptr, value);
|
||||
int32_t child = call_i32(core, "uce_dv_get", { root, key_ptr, (int32_t)key.size() });
|
||||
CHECK(child != 0, "uce_dv_get returned null");
|
||||
call_i32(core, "uce_dv_set_value", { child, value_ptr, (int32_t)value.size() });
|
||||
CHECK(call_i32(core, "uce_dv_find", { root, key_ptr, (int32_t)key.size() }) == child, "uce_dv_find mismatch");
|
||||
CHECK(call_i32(core, "uce_dv_count", { root }) == 1, "root count mismatch");
|
||||
CHECK(call_i32(core, "uce_dv_is_list", { root }) == 0, "root unexpectedly list-shaped");
|
||||
int32_t value_len_ptr = call_i32(core, "uce_alloc", { 4 });
|
||||
int32_t value_result_ptr = call_i32(core, "uce_dv_value", { child, value_len_ptr });
|
||||
uint32_t value_result_len = read_u32(memory, value_len_ptr);
|
||||
CHECK(read_bytes(memory, value_result_ptr, value_result_len) == value, "uce_dv_value mismatch");
|
||||
int32_t encoded_len = call_i32(core, "uce_dv_encode", { root, 0, 0 });
|
||||
CHECK(encoded_len > 5, "encoded length too small");
|
||||
int32_t encoded_ptr = call_i32(core, "uce_alloc", { encoded_len });
|
||||
CHECK(call_i32(core, "uce_dv_encode", { root, encoded_ptr, encoded_len }) == encoded_len, "encode length mismatch");
|
||||
std::string encoded = read_bytes(memory, encoded_ptr, encoded_len);
|
||||
CHECK(encoded.rfind("UCEB\x01", 0) == 0, "UCEB1 header missing");
|
||||
int32_t decoded = call_i32(core, "uce_dv_decode", { encoded_ptr, encoded_len });
|
||||
CHECK(decoded != 0, "uce_dv_decode failed");
|
||||
CHECK(call_i32(core, "uce_dv_count", { decoded }) == 1, "decoded root count mismatch");
|
||||
int32_t last_error_ptr = call_i32(core, "uce_dv_last_error");
|
||||
CHECK(last_error_ptr != 0, "uce_dv_last_error returned null");
|
||||
CHECK(read_cstr(memory, last_error_ptr) == "", "last error not clear after successful decode");
|
||||
std::string bad = "bad";
|
||||
int32_t bad_ptr = call_i32(core, "uce_alloc", { (int32_t)bad.size() });
|
||||
write_bytes(memory, bad_ptr, bad);
|
||||
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB1 decode unexpectedly succeeded");
|
||||
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB1 decode did not set error");
|
||||
|
||||
std::string out = "W1 output";
|
||||
int32_t out_ptr = call_i32(core, "uce_alloc", { (int32_t)out.size() });
|
||||
write_bytes(memory, out_ptr, out);
|
||||
call_i32(core, "uce_print_bytes", { out_ptr, (int32_t)out.size() });
|
||||
call_i32(core, "uce_wasm_finish_output");
|
||||
int32_t output_len = call_i32(core, "uce_wasm_output_size");
|
||||
int32_t output_ptr = call_i32(core, "uce_wasm_output_data");
|
||||
CHECK(read_bytes(memory, output_ptr, output_len) == out, "output plumbing mismatch");
|
||||
|
||||
printf("W1 core.wasm smoke: abi=6 encoded=%d output=%d\n", encoded_len, output_len);
|
||||
printf("W1 EXIT CRITERION: PASS\n");
|
||||
return(0);
|
||||
}
|
||||
184
src/wasm/w3_driver.cpp
Normal file
184
src/wasm/w3_driver.cpp
Normal file
@ -0,0 +1,184 @@
|
||||
// W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3.
|
||||
//
|
||||
// Serves real requests through the production workspace runtime
|
||||
// (src/wasm/worker.cpp): UCEB1 context in → core + lazily loaded real
|
||||
// generated units → body/response-meta out. Each --repeat gets a fresh
|
||||
// workspace, proving birth/drop. An epoch ticker thread enforces the CPU
|
||||
// budget; the store limiter enforces memory; traps come back as collapsed
|
||||
// wasm_trace summaries.
|
||||
//
|
||||
// Amalgamation TU (project style, like uce_lib.cpp): native types + DValue
|
||||
// codec first, then the worker.
|
||||
|
||||
// same order as uce_lib.cpp, minus the native compiler/connector block
|
||||
#include "../lib/types.cpp"
|
||||
#include "../lib/dvalue.cpp"
|
||||
#include "../lib/functionlib.cpp"
|
||||
#include "../lib/hash.cpp"
|
||||
#include "../lib/sys.cpp"
|
||||
#include "../lib/uri.cpp"
|
||||
#include "../lib/cli.cpp"
|
||||
#include "../lib/wasm_trace.h"
|
||||
#include "worker.cpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
static String arg_value(int argc, char** argv, int& i, const char* flag)
|
||||
{
|
||||
if(i + 1 >= argc)
|
||||
{
|
||||
fprintf(stderr, "missing value for %s\n", flag);
|
||||
exit(2);
|
||||
}
|
||||
return(String(argv[++i]));
|
||||
}
|
||||
|
||||
static String absolute_path(String path)
|
||||
{
|
||||
if(path.rfind("/", 0) == 0)
|
||||
return(path);
|
||||
char buf[4096];
|
||||
if(!getcwd(buf, sizeof(buf)))
|
||||
return(path);
|
||||
return(String(buf) + "/" + path);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
WasmWorkerConfig cfg;
|
||||
cfg.site_root = absolute_path("site");
|
||||
String page;
|
||||
String route;
|
||||
StringMap params;
|
||||
StringMap get_params;
|
||||
std::vector<String> expects;
|
||||
String expect_status;
|
||||
int repeat = 1;
|
||||
u64 epoch_period_ms = 50;
|
||||
bool show_body = true;
|
||||
|
||||
for(int i = 1; i < argc; i++)
|
||||
{
|
||||
String arg = argv[i];
|
||||
if(arg == "--core") cfg.core_wasm_path = arg_value(argc, argv, i, "--core");
|
||||
else if(arg == "--site") cfg.site_root = absolute_path(arg_value(argc, argv, i, "--site"));
|
||||
else if(arg == "--cache") cfg.cache_root = arg_value(argc, argv, i, "--cache");
|
||||
else if(arg == "--page") page = arg_value(argc, argv, i, "--page");
|
||||
else if(arg == "--route") route = arg_value(argc, argv, i, "--route");
|
||||
else if(arg == "--repeat") repeat = atoi(arg_value(argc, argv, i, "--repeat").c_str());
|
||||
else if(arg == "--expect") expects.push_back(arg_value(argc, argv, i, "--expect"));
|
||||
else if(arg == "--expect-status") expect_status = arg_value(argc, argv, i, "--expect-status");
|
||||
else if(arg == "--epoch-ticks") cfg.epoch_deadline_ticks = strtoull(arg_value(argc, argv, i, "--epoch-ticks").c_str(), 0, 10);
|
||||
else if(arg == "--epoch-ms") epoch_period_ms = strtoull(arg_value(argc, argv, i, "--epoch-ms").c_str(), 0, 10);
|
||||
else if(arg == "--mem-limit") cfg.memory_limit = strtoll(arg_value(argc, argv, i, "--mem-limit").c_str(), 0, 10);
|
||||
else if(arg == "--table-headroom") cfg.table_headroom = (u32)atoi(arg_value(argc, argv, i, "--table-headroom").c_str());
|
||||
else if(arg == "--quiet") show_body = false;
|
||||
else if(arg == "--verbose") cfg.verbose = true;
|
||||
else if(arg == "--param" || arg == "--get")
|
||||
{
|
||||
String pair = arg_value(argc, argv, i, arg.c_str());
|
||||
auto eq = pair.find("=");
|
||||
if(eq == String::npos)
|
||||
{
|
||||
fprintf(stderr, "%s expects K=V\n", arg.c_str());
|
||||
return(2);
|
||||
}
|
||||
(arg == "--param" ? params : get_params)[pair.substr(0, eq)] = pair.substr(eq + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
fprintf(stderr, "unknown argument: %s\n", arg.c_str());
|
||||
return(2);
|
||||
}
|
||||
}
|
||||
|
||||
if(page == "")
|
||||
{
|
||||
fprintf(stderr, "usage: w3_driver --page /demo/hello.uce [--site site] [--cache /tmp/uce/work]\n"
|
||||
" [--core bin/wasm/core.wasm] [--param K=V ...] [--get K=V ...] [--route path]\n"
|
||||
" [--repeat N] [--expect STR ...] [--expect-status STR] [--quiet] [--verbose]\n"
|
||||
" [--epoch-ticks N] [--epoch-ms N] [--mem-limit BYTES]\n");
|
||||
return(2);
|
||||
}
|
||||
|
||||
String entry_unit = cfg.site_root + page;
|
||||
WasmWorker worker(cfg);
|
||||
String init_error = worker.init();
|
||||
if(init_error != "")
|
||||
{
|
||||
fprintf(stderr, "FAIL: %s\n", init_error.c_str());
|
||||
return(1);
|
||||
}
|
||||
|
||||
// the epoch only advances through this ticker; period × deadline-ticks
|
||||
// is the per-request CPU budget
|
||||
std::atomic<bool> running(true);
|
||||
std::thread ticker([&] {
|
||||
while(running.load())
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(epoch_period_ms));
|
||||
worker.engine.increment_epoch();
|
||||
}
|
||||
});
|
||||
|
||||
DValue context_tree;
|
||||
for(auto& entry : params)
|
||||
context_tree["params"][entry.first] = entry.second;
|
||||
if(context_tree.key("params") == 0 || !params.count("HTTP_HOST"))
|
||||
context_tree["params"]["HTTP_HOST"] = "w3.test";
|
||||
context_tree["params"]["SCRIPT_URL"] = page;
|
||||
context_tree["params"]["REQUEST_METHOD"] = "GET";
|
||||
for(auto& entry : get_params)
|
||||
context_tree["get"][entry.first] = entry.second;
|
||||
if(route != "")
|
||||
{
|
||||
context_tree["route"]["l_path"] = route;
|
||||
context_tree["params"]["ROUTE_PATH"] = route;
|
||||
}
|
||||
context_tree["entry_unit"] = entry_unit;
|
||||
|
||||
bool all_ok = true;
|
||||
WasmResponse last;
|
||||
for(int request = 0; request < repeat && all_ok; request++)
|
||||
{
|
||||
f64 started = (f64)clock() / CLOCKS_PER_SEC;
|
||||
last = wasm_worker_serve(worker, context_tree, entry_unit);
|
||||
f64 elapsed_ms = ((f64)clock() / CLOCKS_PER_SEC - started) * 1000.0;
|
||||
printf("==== request %d/%d (%.1f ms cpu) ====\n", request + 1, repeat, elapsed_ms);
|
||||
if(!last.ok)
|
||||
{
|
||||
printf("ERROR:\n%s\n", last.error.c_str());
|
||||
all_ok = false;
|
||||
break;
|
||||
}
|
||||
printf("status: %s\n", last.meta["status"].to_string().c_str());
|
||||
if(last.meta.key("headers"))
|
||||
last.meta["headers"].each([](const DValue& value, String key) {
|
||||
printf("header: %s: %s\n", key.c_str(), value.to_string().c_str());
|
||||
});
|
||||
if(show_body)
|
||||
printf("---- body (%zu bytes) ----\n%.*s\n----\n",
|
||||
last.body.size(), (int)last.body.size(), last.body.data());
|
||||
else
|
||||
printf("body: %zu bytes\n", last.body.size());
|
||||
}
|
||||
|
||||
running.store(false);
|
||||
ticker.join();
|
||||
|
||||
if(all_ok && expect_status != "" && last.meta["status"].to_string().find(expect_status) == String::npos)
|
||||
{
|
||||
printf("FAIL: status %s does not contain %s\n", last.meta["status"].to_string().c_str(), expect_status.c_str());
|
||||
all_ok = false;
|
||||
}
|
||||
for(auto& expect : expects)
|
||||
if(all_ok && last.body.find(expect) == String::npos)
|
||||
{
|
||||
printf("FAIL: body does not contain %s\n", expect.c_str());
|
||||
all_ok = false;
|
||||
}
|
||||
|
||||
printf(all_ok ? "W3 RESULT: PASS\n" : "W3 RESULT: FAIL\n");
|
||||
return(all_ok ? 0 : 1);
|
||||
}
|
||||
1074
src/wasm/worker.cpp
Normal file
1074
src/wasm/worker.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user