This commit is contained in:
udo 2026-06-13 02:07:38 +00:00
parent eb8f303f94
commit 577aae076e
27 changed files with 2937 additions and 21 deletions

View File

@ -1,10 +1,13 @@
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE # WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
- **Status:** design guide; Phase 04 spikes complete and gated (2026-06-12). - **Status:** building the production worker per §9.1 (W-phases). W1 (core
Next: production implementation, starting with the Phase 3 work plan module), W2 (wasm unit compile target), and W3 (workspace runtime +
(item 1, real `uce_lib` core compile); the starter parity bar membrane, `src/wasm/worker.cpp`) are **done**: real pages — including the
(`tests/run_network_tests.py --match starter`, 14 cases) is already green starter views — render through per-request wasm workspaces with lazy
against the native backend. 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` - **Scope:** replace the native unit pipeline (generated C++ → clang → `.so`
`dlopen`) with per-unit WebAssembly modules executed in a per-request, `dlopen`) with per-unit WebAssembly modules executed in a per-request,
runtime-linked workspace, exposing the same API surface to page code, runtime-linked workspace, exposing the same API surface to page code,
@ -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 but we switch over to the wasm backend as soon as it's available and test
only on that; both backends share the Phase 1 C ABI. only on that; both backends share the Phase 1 C ABI.
**Cutover & cleanup checklist (gated, in order — no step before the gate
above it is green).**
1. **Build** — production Phase 3/4 work plan items 17: 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 W2W4 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 23 — 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 ## 10. Risks & mitigations

View File

@ -21,6 +21,10 @@ SITE_DIRECTORY=site
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT # ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
JIT_COMPILE_ON_REQUEST=1 JIT_COMPILE_ON_REQUEST=1
# OPTIONAL W2 WEBASSEMBLY SIDE-MODULE COMPILATION BESIDE NATIVE .so UNITS
COMPILE_WASM_UNITS=0
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP # ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
PROACTIVE_COMPILE_ENABLED=1 PROACTIVE_COMPILE_ENABLED=1

30
scripts/build_core_wasm.sh Executable file
View 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

View File

@ -26,7 +26,7 @@ OPT_FLAG="O0"
COMPILER="clang++" COMPILER="clang++"
#COMPILER="g++" #COMPILER="g++"
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC" FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC -Isrc/lib"
LIBS="-ldl -lm -lpthread" LIBS="-ldl -lm -lpthread"
SRCFLAGS="-D PLATFORM_NAME=\"linux\"" SRCFLAGS="-D PLATFORM_NAME=\"linux\""

93
scripts/compile_wasm_unit Executable file
View 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
View 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
View 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"

View 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
View 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())

View File

@ -32,7 +32,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
## Pipeline ## Pipeline
- The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`. - The generated file starts by including the logical runtime header `uce_lib.h`; native and WASM compile scripts provide the include path.
- It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`. - It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`.
- It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file. - It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file.
- Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content. - Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content.

View File

@ -494,7 +494,7 @@ String compiler_rewrite_named_render_syntax(String content)
String compiler_preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String content) String compiler_preprocess_shared_unit_char_wise(Request* context, SharedUnit* su, String content)
{ {
String parsed_content = String parsed_content =
("#include \"")+context->server->config["COMPILER_SYS_PATH"] +"/src/lib/uce_lib.h\" \n"+ "#include \"uce_lib.h\" \n"+
file_get_contents( file_get_contents(
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"] context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]
)+ )+

View File

@ -132,6 +132,23 @@ String compiler_unit_metadata_text(Request* context, SharedUnit* su)
); );
} }
bool compiler_wasm_unit_compile_enabled(Request* context)
{
if(!context || !context->server)
return(false);
return(config_bool("COMPILE_WASM_UNITS", false));
}
String compiler_wasm_compile_script(Request* context)
{
String script = "scripts/compile_wasm_unit";
if(context && context->server)
script = first(context->server->config["WASM_COMPILE_SCRIPT"], script);
if(script != "" && script[0] != '/' && context && context->server)
script = path_join(context->server->config["COMPILER_SYS_PATH"], script);
return(script);
}
SharedUnitCompileCheck shared_unit_compile_check(const SharedUnitFilesystemState& state) SharedUnitCompileCheck shared_unit_compile_check(const SharedUnitFilesystemState& state)
{ {
SharedUnitCompileCheck result; SharedUnitCompileCheck result;
@ -647,9 +664,12 @@ void setup_unit_paths(Request* context, SharedUnit* su, String file_name)
su->src_file_name = basename(file_name); su->src_file_name = basename(file_name);
su->bin_file_name = su->src_file_name + ".so"; su->bin_file_name = su->src_file_name + ".so";
su->wasm_file_name = su->src_file_name + ".wasm";
su->pre_file_name = su->src_file_name + ".cpp"; su->pre_file_name = su->src_file_name + ".cpp";
su->so_name = su->bin_path + "/" + su->bin_file_name; su->so_name = su->bin_path + "/" + su->bin_file_name;
su->wasm_name = su->bin_path + "/" + su->wasm_file_name;
su->wasm_check_file_name = su->bin_path + "/" + su->src_file_name + ".wasm-check.txt";
su->api_file_name = su->bin_path + "/" + su->src_file_name + ".exports.txt"; su->api_file_name = su->bin_path + "/" + su->src_file_name + ".exports.txt";
su->meta_file_name = su->bin_path + "/" + su->src_file_name + ".meta.txt"; su->meta_file_name = su->bin_path + "/" + su->src_file_name + ".meta.txt";
su->compile_output_file_name = su->bin_path + "/" + su->src_file_name + ".compile.txt"; su->compile_output_file_name = su->bin_path + "/" + su->src_file_name + ".compile.txt";
@ -740,7 +760,7 @@ void load_shared_unit(Request* context, SharedUnit* su)
String result = String result =
String("#ifndef UCE_LIB_INCLUDED\n") + String("#ifndef UCE_LIB_INCLUDED\n") +
"#define UCE_LIB_INCLUDED\n" + "#define UCE_LIB_INCLUDED\n" +
("#include \"")+context->server->config["COMPILER_SYS_PATH"] +"/src/lib/uce_lib.h\" \n"+ "#include \"uce_lib.h\" \n"+
file_get_contents( file_get_contents(
context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]) + context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]) +
"#endif \n"; "#endif \n";
@ -839,6 +859,15 @@ void compile_shared_unit(Request* context, SharedUnit* su)
shell_escape(su->bin_file_name) shell_escape(su->bin_file_name)
)); ));
if(su->compiler_messages.length() == 0 && !su->opt_so_optional && compiler_wasm_unit_compile_enabled(context))
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) if(su->compiler_messages.length() > 0)
{ {
String raw_messages = su->compiler_messages; String raw_messages = su->compiler_messages;
@ -1171,6 +1200,8 @@ DValue unit_info(String path)
info["bin_file_name"] = su->bin_file_name; info["bin_file_name"] = su->bin_file_name;
info["pre_file_name"] = su->pre_file_name; info["pre_file_name"] = su->pre_file_name;
info["so_name"] = su->so_name; info["so_name"] = su->so_name;
info["wasm_name"] = su->wasm_name;
info["wasm_exists"].set_bool(file_exists(su->wasm_name));
info["api_file_name"] = su->api_file_name; info["api_file_name"] = su->api_file_name;
info["meta_file_name"] = su->meta_file_name; info["meta_file_name"] = su->meta_file_name;
info["compile_output_file_name"] = su->compile_output_file_name; info["compile_output_file_name"] = su->compile_output_file_name;

View File

@ -1,7 +1,9 @@
#include "functionlib.h" #include "functionlib.h"
#ifndef __UCE_WASM_CORE__
#define PCRE2_CODE_UNIT_WIDTH 8 #define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h> #include <pcre2.h>
#endif
#include <cctype> #include <cctype>
#include <stdexcept> #include <stdexcept>
#include <algorithm> #include <algorithm>
@ -296,6 +298,7 @@ String replace(String s, String search, String replace_with)
return(result); return(result);
} }
#ifndef __UCE_WASM_CORE__
namespace { namespace {
String regex_flags_label(String flags) String regex_flags_label(String flags)
@ -637,6 +640,47 @@ StringList regex_split(String pattern, String subject, String flags)
return(result); 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) String trim(String raw)
{ {
s64 len = raw.length(); s64 len = raw.length();
@ -1323,7 +1367,12 @@ String xml_decode_text(String s)
void xml_throw(String message) void xml_throw(String message)
{ {
#ifdef __UCE_WASM_CORE__
(void)message;
__builtin_trap();
#else
throw std::runtime_error("xml_decode(): " + message); throw std::runtime_error("xml_decode(): " + message);
#endif
} }
struct XmlParser struct XmlParser
@ -1856,7 +1905,12 @@ String yaml_decode_quoted(String raw)
void yaml_throw(String message) void yaml_throw(String message)
{ {
#ifdef __UCE_WASM_CORE__
(void)message;
__builtin_trap();
#else
throw std::runtime_error("yaml_decode(): " + message); throw std::runtime_error("yaml_decode(): " + message);
#endif
} }
struct YamlParser struct YamlParser

View File

@ -55,8 +55,11 @@ String to_string(SharedUnit* u) {
return(result); return(result);
} }
// NB: header-defined function templates must be inline — wasm side modules
// compile with -fvisibility-inlines-hidden so instantiations bind locally
// instead of becoming unresolvable self-imports (WASM-PROPOSAL §6)
template <typename ITYPE> template <typename ITYPE>
String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1) inline String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
{ {
static const char* digits = "0123456789ABCDEF"; static const char* digits = "0123456789ABCDEF";
String rc(hex_len,'0'); String rc(hex_len,'0');
@ -66,7 +69,7 @@ String to_hex(ITYPE w, size_t hex_len = sizeof(ITYPE)<<1)
} }
template<typename T, typename F> template<typename T, typename F>
std::vector<T> filter(std::vector<T> items, F f) inline std::vector<T> filter(std::vector<T> items, F f)
{ {
std::vector<T> new_items; std::vector<T> new_items;
for(auto item : items) for(auto item : items)
@ -78,7 +81,7 @@ std::vector<T> filter(std::vector<T> items, F f)
} }
template<typename T, typename F> template<typename T, typename F>
auto map(std::vector<T> items, F f) inline auto map(std::vector<T> items, F f)
{ {
using ResultType = decltype(f(items[0])); using ResultType = decltype(f(items[0]));
std::vector<ResultType> new_items; std::vector<ResultType> new_items;
@ -101,7 +104,7 @@ DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f);
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f); DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f);
template <class ...Args> template <class ...Args>
String first(Args... args) inline String first(Args... args)
{ {
std::vector<String> vec = {args...}; std::vector<String> vec = {args...};
for(auto s : vec) for(auto s : vec)

View File

@ -19,6 +19,11 @@ A million repetitions of "a"
/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */ /* #define LITTLE_ENDIAN * This should be #define'd already, if true. */
/* #define SHA1HANDSOFF * Copies data before messing with it. */ /* #define SHA1HANDSOFF * Copies data before messing with it. */
#ifdef __UCE_WASM_CORE__
#include <stdint.h>
typedef uint32_t u_int32_t;
#endif
typedef struct { typedef struct {
u_int32_t state[5]; u_int32_t state[5];
u_int32_t count[2]; u_int32_t count[2];

View File

@ -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 <string.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/socket.h> #include <sys/socket.h>
@ -942,6 +1058,8 @@ StringMap make_server_settings()
cfg["BIN_DIRECTORY"] = "/tmp/uce/work"; cfg["BIN_DIRECTORY"] = "/tmp/uce/work";
cfg["COMPILE_SCRIPT"] = "scripts/compile"; cfg["COMPILE_SCRIPT"] = "scripts/compile";
cfg["WASM_COMPILE_SCRIPT"] = "scripts/compile_wasm_unit";
cfg["COMPILE_WASM_UNITS"] = "0";
cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template"; cfg["SETUP_TEMPLATE"] = "scripts/setup.h.template";
cfg["LIT_ESC"] = "3d5b5_1"; cfg["LIT_ESC"] = "3d5b5_1";
cfg["CONTENT_TYPE"] = "text/html; charset=utf-8"; cfg["CONTENT_TYPE"] = "text/html; charset=utf-8";
@ -999,3 +1117,4 @@ StringMap make_server_settings()
return(cfg); return(cfg);
} }
#endif

View File

@ -1,7 +1,28 @@
#pragma once #pragma once
#if defined(__UCE_WASM_CORE__) || defined(__UCE_WASM_UNIT__)
#ifndef LOCK_SH
#define LOCK_SH 1
#define LOCK_EX 2
#define LOCK_UN 8
#endif
#ifndef SIGABRT
#define SIGABRT 6
#endif
#ifndef SIGSEGV
#define SIGSEGV 11
#endif
typedef int pid_t;
extern "C" {
int raise(int);
unsigned int sleep(unsigned int seconds);
int usleep(unsigned int usec);
}
#else
#include <sys/file.h> #include <sys/file.h>
#include <signal.h> #include <signal.h>
#endif
#include <ctime>
#include <sstream> #include <sstream>
String shell_exec(String cmd); String shell_exec(String cmd);
@ -22,7 +43,7 @@ String file_get_contents(String file_name);
bool file_put_contents(String file_name, String content); bool file_put_contents(String file_name, String content);
bool file_append_contents(String file_name, String content); bool file_append_contents(String file_name, String content);
template <typename... Ts> template <typename... Ts>
bool file_append(String file_name, Ts... args) inline bool file_append(String file_name, Ts... args)
{ {
std::ostringstream out; std::ostringstream out;
((out << args), ...); ((out << args), ...);

View File

@ -1,16 +1,17 @@
#ifndef __UCE_WASM_CORE__
#include <sys/file.h> #include <sys/file.h>
#include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h>
#include <dlfcn.h>
#endif
#include <stdlib.h>
#include <iostream> #include <iostream>
#include <filesystem> #include <filesystem>
#include <ctype.h> #include <ctype.h>
#include <fstream> #include <fstream>
#include <sys/stat.h>
#include <ctime> #include <ctime>
#include <dlfcn.h>
#include <limits.h> #include <limits.h>
#include <algorithm> #include <algorithm>
#include <sys/stat.h>
#include <iostream> #include <iostream>
#include "types.h" #include "types.h"
@ -52,10 +53,12 @@ String http_status_reason(s32 code)
SharedUnit::~SharedUnit() SharedUnit::~SharedUnit()
{ {
#ifndef __UCE_WASM_CORE__
if(so_handle) if(so_handle)
{ {
dlclose(so_handle); dlclose(so_handle);
} }
#endif
} }
String nibble(String div, String& haystack) String nibble(String div, String& haystack)
@ -98,6 +101,8 @@ Request::~Request()
delete stream; delete stream;
ob_stack.clear(); ob_stack.clear();
ob = 0; ob = 0;
#ifndef __UCE_WASM_CORE__
for(auto& sockfd : resources.sockets) for(auto& sockfd : resources.sockets)
close(sockfd); close(sockfd);
#endif
} }

View File

@ -65,6 +65,8 @@ struct SharedUnit {
String file_name; String file_name;
String so_name; String so_name;
String wasm_name;
String wasm_check_file_name;
String api_file_name; String api_file_name;
String meta_file_name; String meta_file_name;
String compile_output_file_name; String compile_output_file_name;
@ -77,6 +79,7 @@ struct SharedUnit {
String pre_path; String pre_path;
String src_file_name; String src_file_name;
String bin_file_name; String bin_file_name;
String wasm_file_name;
String pre_file_name; String pre_file_name;
void* so_handle = 0; void* so_handle = 0;
@ -243,26 +246,29 @@ Request* context;
#include <iostream> #include <iostream>
// NB: header templates must be inline — wasm units rely on
// -fvisibility-inlines-hidden binding instantiations locally (§6)
template <typename... Ts> template <typename... Ts>
void print(Ts... args) inline void print(Ts... args)
{ {
((*context->ob << args), ...); ((*context->ob << args), ...);
} }
template <typename... Ts> template <typename... Ts>
void out(Ts... args) inline void out(Ts... args)
{ {
((*context->ob << args), ...); ((*context->ob << args), ...);
} }
template <typename... Ts> template <typename... Ts>
String concat(Ts... args) inline String concat(Ts... args)
{ {
ByteStream out; ByteStream out;
((out << args), ...); ((out << args), ...);
return(out.str()); return(out.str());
} }
#ifndef __UCE_WASM_UNIT__
void * operator new(decltype(sizeof(0)) n) noexcept(false) void * operator new(decltype(sizeof(0)) n) noexcept(false)
{ {
void* ptr = malloc(n); void* ptr = malloc(n);
@ -280,3 +286,4 @@ void operator delete(void * p) throw()
//TO DO: track deallocations //TO DO: track deallocations
free(p); free(p);
} }
#endif

View File

@ -11,9 +11,12 @@
#include "sys.cpp" #include "sys.cpp"
#include "uri.cpp" #include "uri.cpp"
#include "cli.cpp" #include "cli.cpp"
#ifndef __UCE_WASM_CORE__
#include "compiler-parser.cpp" #include "compiler-parser.cpp"
#include "compiler.cpp" #include "compiler.cpp"
#include "markdown.cpp" #include "markdown.cpp"
#include "zip.cpp" #include "zip.cpp"
#include "mysql-connector.cpp" #include "mysql-connector.cpp"
#include "sqlite-connector.cpp" #include "sqlite-connector.cpp"
#endif

View File

@ -4,6 +4,10 @@
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#ifdef __UCE_WASM_CORE__
extern "C" size_t uce_host_random(char* buf, size_t len);
#endif
String base64_encode(String raw) String base64_encode(String raw)
{ {
static const char* chars = static const char* chars =
@ -714,6 +718,19 @@ StringMap parse_cookies(String cookie_String)
String session_id_create() String session_id_create()
{ {
#ifdef __UCE_WASM_CORE__
unsigned char bytes[32];
if(uce_host_random((char*)bytes, sizeof(bytes)) != sizeof(bytes))
__builtin_trap();
String result;
static const char* hex = "0123456789abcdef";
for(unsigned char b : bytes)
{
result.push_back(hex[b >> 4]);
result.push_back(hex[b & 0x0f]);
}
return(result);
#else
unsigned char bytes[32]; unsigned char bytes[32];
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if(fd == -1) if(fd == -1)
@ -738,6 +755,7 @@ String session_id_create()
result.push_back(hex[b & 0x0f]); result.push_back(hex[b & 0x0f]);
} }
return(result); return(result);
#endif
} }
bool is_valid_session_id(String session_id) bool is_valid_session_id(String session_id)

412
src/wasm/core.cpp Normal file
View 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());
}
}

View 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

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff