Compare commits

..
40 Commits
Author SHA1 Message Date
udo 34a97e2577 chore: use version file for package builds 2026-06-15 16:19:55 +00:00
root 6be92ef93e smolstuf 2026-06-15 15:45:52 +00:00
root 5ee257565c smolstuf 2026-06-15 15:40:24 +00:00
root 5f430155b7 deb update 2026-06-15 13:16:39 +00:00
root 762b586242 post-W7 cleanup 2026-06-15 12:31:33 +00:00
root 1a5c6547b9 post-W7 cleanup 2026-06-15 12:00:46 +00:00
root 75bccb778c W7 done done 2026-06-15 11:14:03 +00:00
root ad2ba7b632 W7 done done 2026-06-15 11:08:44 +00:00
root abcb66717e W7 done done 2026-06-15 11:06:35 +00:00
root 1743c51e46 W7 done done 2026-06-15 11:04:16 +00:00
root 04745f39a8 W7 done done 2026-06-15 10:53:36 +00:00
root f5637bb587 W7 done done 2026-06-15 10:28:23 +00:00
root b1a0df9c93 W7 done done 2026-06-15 10:19:06 +00:00
rootandClaude Opus 4.8 4f84ac544d feat: request_perf() worker-side timing hostcall; restore demo System Info
Units run in the wasm sandbox, so my_pid/parent_pid/context.server->request_count
read as sandbox stubs — the demo System Info counters were broken, and there was
no authoritative server-side request timing available to unit code (client-side
measurement cannot see queue/dispatch latency).

Add a request_perf() unit API backed by a new uce_host_request_perf hostcall.
The native worker answers it live, returning a DValue:
  worker_pid, parent_pid, request_count,
  accept_us  = (time_start - time_init)*1e6   (entry -> dispatch wait),
  running_us = (now - time_start)*1e6         (since dispatch, live),
  total_us   = (now - time_init)*1e6          (since the request entered UCE),
  workspace_birth_us.
time_init is captured at request entry (handle_request, with a handle_complete
fallback); a RequestPerfSnapshot {pids, request_count, time_init, time_start} is
threaded from wasm_backend_serve through wasm_worker_serve onto the workspace,
and the hostcall computes the live deltas at call time. Wired like uce_host_units
(sized DValue hostcall): core_hostcalls.syms + sys.cpp/sys.h request_perf().

site/demo/index.uce System Info now uses request_perf() and shows the real worker
PID, an incrementing per-worker request count, and the timing counters.

Implemented via the pi agent (gpt-5.3-codex-spark); a review of the live numbers
caught accept_us mistakenly computed as (now - time_init) (== total_us), fixed to
the dispatch wait (time_start - time_init). Independently verified on the host:
System Info shows non-zero PIDs, incrementing count, accept_us ~50us << total_us
~2.4ms with accept+running==total; run_cli_tests --include-wasm-kill => 87 passed,
0 failed, 0 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:18:04 +00:00
rootandClaude Opus 4.8 83ab9e10f7 perf: extend the compiled-module disk cache to per-unit modules
Follow-up to 560290c. Per-unit .wasm modules were still Cranelift-compiled per
worker on first use (~40-70ms each), so with 8-call worker recycling fresh
workers re-JIT every unit they touch.

Refactor the core cache logic into a shared WasmWorker helper:
- cached_wasm_path(p): maps <...>.wasm -> <...>.cwasm.
- load_or_compile_cached_module(engine, cached, wasm, bytes, err): deserialize_file
  the .cwasm when it is newer than the .wasm; otherwise Module::compile + serialize,
  written atomically (temp+rename); deserialize failure falls back to compile.
Both the core module load and unit_module() now go through this helper, so unit
artifacts get the same <unit>.uce.cwasm cache the core got.

Independently verified on the host: fresh-worker first-hit unit latency ~40-70ms
-> ~3-21ms; full suite wall-clock 20.5s -> 6.5s (and ~40s before any caching);
run_cli_tests --include-wasm-kill => 87 passed, 0 failed, 0 skipped.

Implemented via the pi agent (gpt-5.3-codex-spark sub-model).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:16:41 +00:00
rootandClaude Opus 4.8 560290ca1d perf: cache the compiled core module so fresh workers deserialize, not recompile
Workers recycle every 8 requests (calls_until_termination=8). Each fresh worker
ran wasmtime::Module::compile() on the 6.8MB bin/wasm/core.wasm — a ~1.3s
Cranelift JIT — on its first request, so every ~8th request spiked to ~1.3s and
dominated suite wall-clock.

Cache the compiled artifact: on worker core-module load, if bin/wasm/core.cwasm
exists and is newer than core.wasm, load it via Module::deserialize_file() (mmap,
~ms); otherwise Module::compile() as before and atomically (temp+rename) write
the serialized artifact for the next worker. Deserialize failure / stale cache
falls back to a normal compile, so it is self-healing; rebuilding core.wasm
(newer mtime) invalidates the cache. Engine config (epoch_interruption,
signals_based_traps(false)) is unchanged, which the serialized format requires.

Implemented via the pi agent (delegated to a gpt-5.3-codex-spark sub-model).
Independently verified on the host: worker-startup request latency drops from
~1.3s to ~23ms (warm 12x /demo/hello.uce: all <=0.023s, no spikes); full suite
wall-clock ~40s -> 20.5s; run_cli_tests --include-wasm-kill => 87 passed, 0
failed, 0 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:02:22 +00:00
rootandClaude Opus 4.8 bfd6d33829 W7f: sweep dead/legacy/fallback leftovers after native-pipeline removal
Post-deletion cleanup (units run only on wasm):
- types.h/compiler.cpp: drop the native-era SharedUnit fields so_name,
  bin_file_name, and the opt_so_optional cache-mode plumbing (no native
  optional .so path remains). The per-unit compile lock is re-keyed from
  so_name+.lock to wasm_name+.lock (still per-unit).
- unit_info() and to_string(SharedUnit*) no longer expose .so artifact fields.
- backend.h: drop the stale "+ fallback-token gate" comment.
- Docs/comments corrected to wasm-only reality: README, tests/README,
  site/doc C++ preprocessor + error_pages + unit_info pages, site/info intro,
  site/demo/unit-browser artifact card; the Phase-5 native-vs-wasm benchmark
  harness (tests/wasm_benchmark.py) reframed for the wasm-only backend.

Audit confirmed no live references remain to so_handle, load_shared_unit,
compiler_load_shared_unit, compiler_invoke*/_cli/_websocket/_serve_http,
COMPILE_SCRIPT/COMPILE_WASM_UNITS, or the native export-symbol constants;
request_ref_handler/dv_call_handler are kept (live wasm funcref casts).

Swept via the pi agent (delegated to a gpt-5.3-codex-spark sub-model);
independently re-verified on the host: run_cli_tests --include-wasm-kill =>
87 passed, 0 failed, 0 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:43:16 +00:00
rootandClaude Opus 4.8 cf51336873 W7e: delete the native unit pipeline (.so compile + dlopen execution)
Units now run exclusively on wasm; the native generated-C++ -> clang -> .so ->
dlopen path and its request-time fallback are removed.

- Dispatch (linux_fastcgi.cpp): the 4 handle_complete branches + the CLI-socket
  path route every request through wasm (wasm_ready compiles cold/stale on
  demand); a wasm-unavailable unit now yields a clean error page
  (fail_wasm_unavailable / render_request_failure) instead of native execution.
  compiler_invoke / _cli / _websocket / _serve_http deleted.
- compiler.cpp (-1274): removed the native .so compile (COMPILE_SCRIPT),
  load_shared_unit, dlopen/dlsym/dlclose, compiler_load_shared_unit, and the
  SharedUnit .so function-pointer fields (on_setup/on_render/on_component/
  on_websocket/on_cli/on_once/on_init) in types.h/types.cpp. compile_shared_unit
  now builds only the .wasm side-module; the .uce preprocessor/parser front-end
  is kept (it emits the C++ the wasm compile consumes).
- unit_call()/component()/once/init now resolve across units through the wasm
  host component resolver (uce_host_component_resolve) instead of native dlsym;
  configured runtime error pages render through the wasm backend.
- Dropped the WASM_BACKEND_ENABLED feature flag and dead COMPILE_SCRIPT /
  COMPILE_WASM_UNITS config; unit ABI freshness tied to UCE_UNIT_ABI_VERSION;
  guard against serving a stale .wasm for a deleted source; retired the obsolete
  W5 native-vs-wasm toggle script. Docs updated.

Implemented via the pi agent (with 3 delegated sub-reviews); independently
re-verified on the build host: run_cli_tests --include-wasm-kill => 87 passed,
0 failed, 0 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:20:50 +00:00
rootandClaude Opus 4.8 fb728d63bc W7e stage B: factor the .wasm artifact into unit compile-freshness
inspect_shared_unit_filesystem() tracked only the .so mtime, so a missing or
stale .wasm with a current .so never triggered a rebuild and the unit fell to
native indefinitely (the in-process cache also never invalidated). Account for
su->wasm_name when wasm unit compilation is enabled: a unit counts as compiled
only as of the OLDER of the two artifacts; a missing .wasm forces a recompile.
Closes the cached-vanished-.wasm gap noted in the stage A review.

Verified independently on the build host: run_cli_tests --include-wasm-kill =>
87 passed, 0 failed; delete-.wasm-then-request rebuilds the artifact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:50:31 +00:00
rootandClaude Opus 4.8 2debd33804 W7e: wasm-preferred dispatch + compile-on-demand; harden unit ABI validator
- Dispatch (linux_fastcgi.cpp): route every request through wasm. On a
  cold/stale artifact, compile the unit on demand (get_shared_unit, forced) and
  serve wasm; native compiler_invoke* remains only as a fallback when wasm
  cannot be made ready (compile failure / backend disabled). Applies to the 4
  handle_complete branches and the CLI socket path.
- backend.cpp: delete the now-vestigial native-only fallback token gate
  (wasm_backend_native_fallback_*), empty since W7d; should_handle now gates on
  config + current artifact + healthy worker only.
- check_unit_wasm.py: skip the defense-in-depth llvm-nm allocator scan when
  llvm-nm SIGSEGVs on a degenerate-but-valid module (e.g. a unit with no
  exported handlers). Fixes site/demo/empty.uce, the last unit that could not
  produce a .wasm; forbidden allocator *exports* are still rejected.

A pi-assisted review caught that the compile-freshness check keys off the .so
mtime only, so force_recompile is required to rebuild a missing/stale .wasm; a
unit already cached in-process whose .wasm later vanished still uses native
fallback (closed in W7e stage B). Native execution is otherwise bypassed for
all real traffic.

Verified: scripts/run_cli_tests.sh --include-wasm-kill -> 87 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:20:12 +00:00
rootandClaude Opus 4.8 8587fbc5aa wasm runtime: central WS broker, unified handlers, W7d holdouts, membrane completeness
- WS: a dedicated broker process owns HTTP_PORT + every connection; it forwards
  renders to the worker pool over uce.sock (non-blocking) and applies ws_*
  command batches flushed back at workspace teardown. Removes the now-dead
  per-worker websocket executor (-509 lines).
- Dispatch: unify CLI / WebSocket / serve_http / page render through one
  serve_via_wasm(entry_unit, handler) path; handler string -> __uce_<handler>
  export symbol.
- W7d: rewrite zip.uce to the membrane return-value error contract (no C++
  try/catch), error-reporting.uce to genuine wasm traps instead of throw, and
  sharedunit.uce to unit_info(); empty the native-only token gate.
- Membrane: wire ls / mkdir / file_mtime through new uce_host_file_list /
  uce_host_file_mkdir / uce_host_file_mtime hostcalls (resolve_guest_file gains
  directory support). Fixes /doc/index.uce listing nothing; adds a regression
  assertion that the index enumerates items.
- Docs: add docs/wasm-runtime-architecture.md; record the W7e staged native-
  deletion plan in WASM-PROPOSAL.md.

Verified: scripts/run_cli_tests.sh --include-wasm-kill -> 87 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:51:51 +00:00
root 15e8d092bc W7+ 2026-06-14 01:37:39 +00:00
udo 6bb4f7f0ad test: port network suite to uce cli runner 2026-06-13 22:58:35 +00:00
udo b1856c1725 chore: narrow wasm backend entrypoint API 2026-06-13 22:32:02 +00:00
udo af6500d134 fix: harden wasm w7 entrypoint paths 2026-06-13 22:08:52 +00:00
root d6421cb8f3 feat: extend wasm backend entrypoints 2026-06-13 21:50:19 +00:00
root fe83c52411 feat: membrane remaining wasm host surfaces 2026-06-13 20:10:00 +00:00
udo c84fc86e6c W6 2026-06-13 16:19:52 +00:00
udo 5a56d4f39e W5 2026-06-13 15:10:42 +00:00
udo afaa4dd7c0 feat: cut over to WASM backend 2026-06-13 08:42:31 +00:00
udo 577aae076e W3 2026-06-13 02:07:38 +00:00
udo eb8f303f94 docs: remove unsupported second-plane language roadmap 2026-06-12 20:36:57 +00:00
udo 89b5499c8f fix: harden WASM phase 5 harness 2026-06-12 20:13:07 +00:00
udo 961df2d542 phase 5 2026-06-12 19:55:35 +00:00
udo 80285b7fb4 feat: implement WASM phase 1 DValue ABI 2026-06-12 12:42:56 +00:00
udo 7e2faf1472 spike: validate WASM phase 0 2026-06-12 11:40:26 +00:00
udo 7066da3cde refactor: rename DTree to DValue 2026-06-12 11:05:52 +00:00
udo 941f5aea08 feat: add configurable UCE error pages 2026-06-12 08:59:09 +00:00
udo b957a2373b chore: ignore Python cache artifacts 2026-06-11 23:06:03 +00:00
udo 20db669589 fix: stabilize runtime follow-up regressions 2026-06-11 23:03:07 +00:00
313 changed files with 12508 additions and 5639 deletions
+14
View File
@@ -36,4 +36,18 @@
tmp/*
bin/*
pkg/*
dist/*
# Python cache artifacts
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
# Editor swap artifacts
*.swp
*.swo
+51 -48
View File
@@ -2,19 +2,19 @@
## Current State
This is in the early stages of development. Don't use this for anything important (or at all)!
This is in the early stages of development. Don't use this for anything important!
## Overview
UCE is a PHP-inspired server-side runtime that lets you build web pages and handlers in C++ using a small `.uce` preprocessor plus a FastCGI application server.
- `.uce` pages compile to shared objects on demand
- `.uce` pages compile to WebAssembly side modules on demand
- normal HTTP pages expose `RENDER(Request& context)`
- WebSocket pages can additionally expose `WS(Request& context)`
- local CLI/admin/test entrypoints can expose `CLI(Request& context)` and are invoked through the Unix CLI socket
- sub-rendering and components pass structured data through `context.props`
- nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener
- the nginx-published application tree lives under `site/`
- nginx can forward normal `.uce` requests to the FastCGI socket, while WebSocket upgrade requests for `.uce` endpoints go to the built-in HTTP/WebSocket listener
- the example application tree lives under `site/`; deployments should publish app files to a normal web root such as `/var/www/html`
- you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed
- the preprocessor has two jobs:
- allow for inline HTML within C++ and the use of templating tags inside of that HTML
@@ -29,7 +29,7 @@ RENDER(Request& context)
}
```
The runtime is still experimental.
*The runtime is still experimental. This is not production-ready. Use at your own risk!*
## Build
@@ -45,8 +45,10 @@ The current build expects:
- `mysql_config`
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support
- Wasmtime C API / C++ headers, defaulting to `/opt/wasmtime` or `WASMTIME_HOME`
- WASI SDK tools, defaulting to `/opt/wasi-sdk` or `WASI_SDK`, for `scripts/build_core_wasm.sh` and unit compilation
SQLite is vendored under `src/3rdparty/sqlite/` and compiled by `scripts/build_linux.sh`; no system SQLite package is required.
SQLite and miniz are vendored under `src/3rdparty/`; no system SQLite or zlib package is required for those helpers.
The binary is written to:
@@ -76,9 +78,9 @@ Useful related runtime patterns:
Useful helpers for that data model include:
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
- `DTree::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values
- `DValue::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
- `DValue::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
- `DValue::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values
- `json_encode(String)` for emitting JavaScript-safe string literals directly
- `ascii_safe_name(String)` for conservative ASCII identifier normalization
- `path_join(base, child)` for filesystem-style path assembly
@@ -86,7 +88,7 @@ Useful helpers for that data model include:
- `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows
- `gz_compress()` and `gz_uncompress()` for gzip-format byte strings
- `server_start_http()` / `server_stop()` for runtime-managed custom HTTP listeners backed by `SERVE_HTTP` handlers
- `map()`, `filter()`, `list_unique()`, `dtree_filter()`, `dtree_map()`, `dtree_pick()`, and related helpers for route/menu/card data shaping near render code
- `map()`, `filter()`, `list_unique()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
Named component handlers are also supported:
@@ -103,7 +105,7 @@ Those are intended for sub-rendering through helpers such as `component("compone
Additional lifecycle hooks are also available on ordinary `.uce` units:
- `INIT(Request& context)` runs once when a worker loads that unit's shared object into memory
- `INIT(Request& context)` runs once when a worker instantiates that unit's wasm module
- `ONCE(Request& context)` runs once per request before the first `RENDER()`, `CLI()`, or `COMPONENT...` entrypoint from that file
CLI units can be invoked locally with the convenience wrapper or directly over HTTP-over-Unix:
@@ -184,9 +186,9 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate
- `ws_send_to(connection_id, message[, binary])`
- `ws_close([connection_id])`
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint.
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.uce` endpoint.
Each live WebSocket connection owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
Each live WebSocket connection owns a broker-side `DValue` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`.
@@ -248,8 +250,8 @@ Representative test pages:
The intended production shape is:
- nginx serves static files directly
- nginx forwards `.uce` requests and ordinary `.ws.uce` page loads to the UCE FastCGI Unix socket
- nginx proxies actual WebSocket upgrade requests for `.ws.uce` endpoints to the runtime's built-in HTTP/WebSocket listener
- nginx forwards ordinary `.uce` page loads to the UCE FastCGI Unix socket
- nginx proxies WebSocket upgrade requests for `.uce` endpoints to the runtime's built-in HTTP/WebSocket listener
- systemd keeps the runtime built, started, and restarted on failure
The repository ships the pieces used for this:
@@ -264,7 +266,7 @@ On a Debian or Ubuntu host, start with the packages needed to build and run UCE
```bash
apt update
apt install -y nginx clang mariadb-client libmariadb-dev libpcre2-dev build-essential
apt install -y nginx clang mariadb-client libmariadb-dev libpcre2-dev build-essential curl rsync ca-certificates
```
The exact package names may vary by distro. The important requirements are:
@@ -274,16 +276,26 @@ The exact package names may vary by distro. The important requirements are:
- `mysql_config`
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
- normal Linux development headers for threads, sockets, `dl`, and backtrace support
- Wasmtime C API / C++ headers installed at `/opt/wasmtime` or configured with `WASMTIME_HOME`
- WASI SDK installed at `/opt/wasi-sdk` or configured with `WASI_SDK`
### 2. Put the repo on the server
This README assumes the repository lives at:
```bash
/Code/uce.openfu.com/uce
/opt/uce
```
That is what the shipped `scripts/systemd/uce.service` file currently uses as its `WorkingDirectory` and build path. If you deploy somewhere else, update that unit file before enabling the service.
The examples below use that path for the runtime. Publish public application files under the normal web root, for example:
```bash
cd /opt/uce
mkdir -p /var/www/html
rsync -a site/ /var/www/html/
```
If you deploy somewhere else, update the systemd unit's `WorkingDirectory`, build path, and `ExecStart` path before enabling the service.
### 3. Configure `/etc/uce/settings.cfg`
@@ -293,7 +305,7 @@ The runtime reads its server settings from:
/etc/uce/settings.cfg
```
The shipped example contains the important filesystem and FastCGI settings:
The example contains the filesystem and FastCGI settings:
```ini
BIN_DIRECTORY=/var/cache/uce/work
@@ -304,7 +316,7 @@ FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
FCGI_PORT=9993
PRECOMPILE_FILES_IN=
SITE_DIRECTORY=site
SITE_DIRECTORY=/var/www/html
PROACTIVE_COMPILE_CHECK_INTERVAL=60
WORKER_COUNT=4
@@ -328,9 +340,9 @@ HTTP_PORT=8080
Proactive compilation settings:
- `SITE_DIRECTORY=site` tells the runtime which tree to scan on startup for `.uce` files when `PRECOMPILE_FILES_IN` is left empty.
- `SITE_DIRECTORY=/var/www/html` tells the runtime which public web tree to scan on startup for `.uce` files when `PRECOMPILE_FILES_IN` is left empty.
- `PRECOMPILE_FILES_IN=` can override that startup scan root with a different absolute or runtime-relative directory.
- `PROACTIVE_COMPILE_CHECK_INTERVAL=60` controls how often the low-priority background compiler rechecks known `.uce` files for stale or missing shared objects.
- `PROACTIVE_COMPILE_CHECK_INTERVAL=60` controls how often the low-priority background compiler rechecks known `.uce` files for stale or missing wasm modules.
The runtime keeps a shared known-file registry under `BIN_DIRECTORY` and updates it as request handling discovers new `.uce` files, so proactive recompiles are not limited to the initial startup scan.
@@ -395,12 +407,14 @@ That script:
- writes Debian maintainer scripts for systemd reload/enable handling
- follows a more PHP-like/FHS deployment shape with immutable runtime files under `/usr/lib`, config under `/etc`, cache/state under `/var`, and the FastCGI socket under `/run/uce/`
### 5. Configure nginx for `.uce` and `.ws.uce`
### 5. Configure nginx for `.uce` and WebSocket upgrades
You need two nginx paths for `.ws.uce` endpoints:
Any `.uce` unit can expose `WS(Request& context)`. WebSocket upgrade requests for `.uce` paths should be routed to the runtime's HTTP/WebSocket listener.
- FastCGI for ordinary `.uce` requests and plain `.ws.uce` page renders
- HTTP proxying only for actual WebSocket upgrade traffic on `.ws.uce` endpoints
You need two transport paths for `.uce` endpoints:
- FastCGI for ordinary `.uce` page renders
- HTTP proxying only for WebSocket upgrade traffic on `.uce` endpoints
If you use WebSockets, add this `map` in the nginx `http` block:
@@ -417,7 +431,7 @@ Then use a server block along these lines:
server {
listen 80;
server_name example.com;
root /Code/uce.openfu.com/uce/site;
root /var/www/html;
index index.uce index.html;
@@ -426,16 +440,6 @@ server {
}
location ~ \.uce$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param DOCUMENT_URI $uri;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_pass unix:/run/uce/fastcgi.sock;
}
location ~ \.ws\.uce$ {
error_page 418 = @uce_websocket;
if ($http_upgrade = "websocket") {
return 418;
@@ -453,7 +457,6 @@ server {
location @uce_websocket {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
@@ -465,19 +468,19 @@ server {
Important details:
- `.ws.uce` must be matched before the more general `.uce` rule
- `fastcgi_pass` should point at the same socket path as `FCGI_SOCKET_PATH`
- `proxy_pass` should point at the runtime's `HTTP_PORT`
- ordinary `GET /page.ws.uce` page renders should stay on FastCGI
- only upgrade requests for `/page.ws.uce` should go through the HTTP/WebSocket listener
- `SCRIPT_FILENAME` should resolve to the actual `.uce` file on disk
- ordinary `GET /page.uce` page renders should stay on FastCGI
- only upgrade requests for `/page.uce` should go through the HTTP/WebSocket listener
- `SCRIPT_FILENAME` should resolve to the requested `.uce` file on disk
- `proxy_http_version 1.1` and the `Upgrade` / `Connection` headers are required for WebSockets
- socket-capable pages are ordinary `.uce` units; route client WebSocket upgrade requests to the HTTP/WebSocket listener
The `location /` block above is intentionally conservative and only serves real files from `site/`. If your app uses a front-controller pattern such as routing everything through `/index.uce`, change that block accordingly.
The `location /` block only serves files from `/var/www/html`. If your app uses a front-controller pattern such as routing everything through `/index.uce`, change that block accordingly.
### 6. Think about document root and private files
Point nginx at `site/`, not the repository root. The repo still contains source, scripts, packaging files, and operational assets that are not meant to be public.
Point nginx at `/var/www/html`, not the runtime repository root. The repo still contains source, scripts, packaging files, and operational assets that are not meant to be public.
At minimum, explicitly block internal directories that should never be served directly. For example:
@@ -487,7 +490,7 @@ location ~ ^/(src|scripts|etc|bin|work|dist|pkg)/ {
}
```
If nginx is rooted at `site/`, most of those paths will not be reachable anyway, which is the preferred setup.
If nginx is rooted at `/var/www/html`, most of those paths will not be reachable anyway, which is the preferred setup.
### 7. Reload nginx and verify the deployment
@@ -506,7 +509,7 @@ curl -i http://127.0.0.1/test/index.uce
curl -i http://127.0.0.1/doc/index.uce
```
If WebSockets are enabled, also verify a `.ws.uce` endpoint through nginx rather than talking to the runtime directly.
If WebSockets are enabled, also verify a `.uce` endpoint that defines `WS(Request& context)` through nginx rather than talking to the runtime directly.
### 8. Troubleshooting
@@ -515,7 +518,7 @@ Common failure modes:
- `502 Bad Gateway`
Usually means `uce.service` is down, the Unix socket path does not match, or the request crashed before sending a valid response.
- WebSocket upgrade fails
Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost.
Check that nginx is routing WebSocket upgrade requests to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost.
- Requests compile but immediately crash
Check `journalctl -u uce.service`. Generated units carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
- nginx serves raw source or internal files
@@ -537,4 +540,4 @@ For up-to-date usage, prefer:
## AI Disclosure
This project is largely human-made, with all the typical idiosyncracies of my projects clearly visible. However, OpenAI Codex was used for code review and documentation. Claude Opus was used for UI design work, and I used VS Code's git commit message generator.
This project is largely human-made, with all the typical idiosyncracies of my projects clearly visible. However, OpenAI Codex was used for code review, debugging, API integration work, and documentation. Claude Opus was used for UI design work, and I used VS Code's git commit message generator.
-492
View File
@@ -1,492 +0,0 @@
# UCE Code Review — Full Findings (2026-06-11)
Scope: the pending working-tree changes (73 modified tracked files plus new
untracked sources — notably `src/lib/sqlite-connector.cpp/.h` and the
uce-starter theme/router rework).
Method: seven independent review angles (line-by-line diff scan,
removed-behavior audit, cross-file call tracing, reuse, simplification,
efficiency, altitude), followed by a verification pass on the correctness
candidates. Status legend:
- **Confirmed** — verified against the working tree, decisive lines quoted.
- **Plausible** — surfaced by a review angle, not individually re-verified.
- **Refuted** — investigated and found not to be an issue (kept here so
nobody re-flags it).
---
## Part 1 — Correctness (all confirmed)
### 1.1 XSS: island props break out of single-quoted attribute
**File:** `site/examples/uce-starter/components/theme/web_affordances.uce:12`
The island component emits JSON props inside a single-quoted attribute
(`data-props='<?: html_escape(payload) ?>'`), but `html_escape()`
(`src/lib/functionlib.cpp:936`) escapes only `& < > "`, and `json_encode` /
`json_escape` (`src/lib/dtree.cpp:794`) never escape apostrophes. Any `'` in a
prop value terminates the attribute.
**Failure:** a view passes user-influenced prop text containing an apostrophe,
e.g. `x' autofocus onfocus='alert(1)` — the attribute terminates early and
attacker-controlled attributes/event handlers land on the div. Even benign
values like `Don't` silently truncate the payload.
**Fix:** use a double-quoted attribute (html_escape covers `"`), or extend
`html_escape` to escape `'` as `&#39;`.
**Status:** fixed, but fix not verified yet.
### 1.2 Crash: positional `?` placeholders in sqlite_query
**File:** `src/lib/sqlite-connector.cpp:86`
`bind_params` constructs a `String` directly from
`sqlite3_bind_parameter_name(stmt, i)`, which returns NULL for nameless
positional `?` parameters. `std::string(nullptr)` is UB and crashes before the
`if(name == "") continue;` guard on the next line can run.
**Failure:** any page calling
`sqlite_query(db, "select * from notes where id = ?", params)` with a bare `?`
instead of `:name` segfaults the worker (500 recovery page).
**Fix:** remove support for positional placeholders from sqlite and mysql API in favor of named placeholders, adjust the documentation accordingly.
**Status:** fixed, but fix not verified yet.
### 1.3 HTTP parsing: split_http_headers assumes lines[0] is the request line
**File:** `src/lib/functionlib.cpp:754`
The rewritten parser unconditionally treats the first line as the HTTP request
line. The old parser located the first colon-free line, which tolerated a
leading CRLF (RFC 7230 §3.5) and header-only input. Two failure modes:
- The direct-HTTP caller (`src/fastcgi/src/fcgicc.cc:693`) passes the raw
buffer unstripped — a client sending a stray leading CRLF before
`GET /x.uce HTTP/1.1` gets the request line silently dropped (empty
`REQUEST_METHOD`, request fails).
- Header-only input (`Host: example.test\nX-Token: abc`, e.g. page code
parsing a raw header block) parses `Host:` as `REQUEST_METHOD` and loses
`HTTP_HOST` entirely. The function is directly callable from .uce pages.
**Fix:** skip leading empty lines before consuming the request line, and only
treat a colon-free first line as the request line.
**Status:** fixed, but fix not verified yet.
### 1.4 Silent data loss: multi-statement SQL drops everything after the first `;`
**File:** `src/lib/sqlite-connector.cpp:178`
`SQLite::query` passes `0` for `pzTail` to `sqlite3_prepare_v2` and never
checks for remaining SQL, so multi-statement strings execute only the first
statement and still report `ok`.
**Failure:** a migration page runs
`sqlite_query(db, "create table t(id integer); insert into t values(1);")` —
only the CREATE executes, the INSERT is silently dropped, `sqlite_error()`
says `ok`, leaving the database half-migrated with no error signal.
**Fix:** capture `pzTail` and either loop over remaining statements or raise
an error when trailing SQL is present.
**Status:** fixed, but fix not verified yet.
### 1.5 Diagnostics regression: fault backtrace captured after siglongjmp
**File:** `src/linux_fastcgi.cpp:895`
The in-handler `capture_backtrace_string` call (`request_fault_trace`) was
deleted; the trace is now captured after `siglongjmp` back in
`handle_complete`, where the faulting stack has already been unwound. No
capture remains in `on_request_fault_signal`.
**Failure:** a `.uce` page null-derefs → SIGSEGV → the error page's Trace
shows only `handle_complete`/`main` frames. The faulting unit's frames are
gone, making crash reports undiagnosable beyond the signal number (the README
still advertises a native backtrace of the failure).
**Fix:** restore the capture inside `on_request_fault_signal` (on the faulting
stack) and hand the result across the longjmp, as the previous code did.
**Status:** fixed, but fix not verified yet.
### 1.7 Memory leak: SQLite wrapper objects never freed by request cleanup
**File:** `src/lib/sqlite-connector.cpp:248`
`cleanup_sqlite_connections()` closes the raw `sqlite3` handles tracked in
`resources.sqlite_connections` but never deletes the heap-allocated `SQLite`
wrappers from `sqlite_connect` (`new` without `delete`; no arena allocator is
compiled in). `sqlite_disconnect` itself is correct (`delete db` after
unregistering). Note: this mirrors a pre-existing identical leak in the MySQL
connector (`cleanup_mysql_connections`, `mysql-connector.cpp:298-304`).
**Failure:** a page calls `sqlite_connect()` per request and relies on
end-of-request cleanup instead of `sqlite_disconnect()` — exactly what the
cleanup path exists for. One wrapper leaks per request; unbounded RSS growth
in the long-lived FastCGI worker.
**Fix:** track the wrapper objects (not raw handles) in
`resources.sqlite_connections` and delete them in cleanup; fix the MySQL
connector the same way, or factor a shared registry (see 5.1).
**Status:** fixed, but fix not verified yet.
### 1.8 Asset shims emit stylesheets mid-`<body>`
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
(also `theme_assets.uce`, `gauges/assets.uce`)
The new ONCE-based asset shims fire inside the view's `ob_start()` capture in
`index.uce` (lines 67-85: view is captured into `fragments["main"]`, then
`themes/page` renders), so their `<link rel="stylesheet">` output is baked
into the main fragment and spliced into the content div — inside `<body>`,
not `<head>`.
**Failure:** every marketing/theme/gauges page ships its stylesheet mid-body
(FOUC, invalid-ish markup). The deleted `page_shell` asset registry rendered
these in `<head>`. While this behavior is often okay in practice, we should
improve on this to encourage cleaner output.
**Fix:** Part A: restore a registration mechanism: record component and asset output
that's intended as once per page in context.call["fragments"]["once"] by default
and the page template component can then explicitly slot this in where appropriate.
Part B: Modify the preprocessor so directives support attributes like this:
ONCE(Request& context)
@fragment my-fragment-name
{
...
}
Which will then automatically (in this example) slot the output into
context.call["fragments"]["my-fragment-name"] instead of the default slot name "once".
In the future we'll introduce more attributes with this syntax. Using this flexible mechanism for the fragment slot
name, we can leave it up to the page template where to slot in what. ONCE, COMPONENT,
and RENDER should support the fragment attribute.
**Status:** fixed, but fix not verified yet.
### 1.9 Error page "Generated C++" hint prints a nonexistent path
**File:** `src/linux_fastcgi.cpp:79`
`render_request_failure` computes the hint as
`path_join(BIN_DIRECTORY, SCRIPT_FILENAME) + ".cpp"`, but `path_join` returns
an absolute child unchanged (`src/lib/sys.cpp:179`), while the compiler builds
the real path by plain concatenation `BIN_DIRECTORY + src_path`
(`src/lib/compiler.cpp:631-636`). SCRIPT_FILENAME is always absolute in the
FastCGI path.
**Failure:** every runtime failure page prints e.g.
`Generated C++: /var/www/site/page.uce.cpp` — the BIN_DIRECTORY prefix is
silently dropped and the path never exists (real file:
`/var/cache/uce/work/var/www/site/page.uce.cpp`).
**Fix:** export one helper from `compiler.cpp` that maps a source file to its
generated artifact (`su->pre_path + "/" + su->pre_file_name` — the new
`compiler_format_compile_failure` in this same changeset already computes it
correctly) and use it in both places.
**Status:** fixed, but fix not verified yet.
### 1.10 O(n²) and per-row deep copies in dtree_map / dtree_filter
**File:** `src/lib/functionlib.cpp:182`
Both helpers call `tree.is_list()` inside the per-element callback —
`is_list()` (`src/lib/dtree.cpp:165`) walks the entire map, making the
operation O(n²) — even though both already compute `is_list()` once before
the loop. Additionally, `DTree::each` (`dtree.h:22`) takes the callback
element by value (`DTree t`), deep-copying every subtree per iteration.
**Failure:** mapping over a 1000-row `sqlite_query` result does ~1M
key-validation checks plus a full deep copy of each row tree.
**Fix:** hoist `is_list()` into a `bool` local reused in the callback; change
`each()` to pass `const DTree&`.
**Status:** fixed, but fix not verified yet.
### 1.11 Proactive compiler child can std::terminate on lock failure
**File:** `src/lib/compiler.cpp:288`
`compiler_with_registry_lock` now throws `std::runtime_error` when the
registry lock file can't be opened (previously: warning + proceed unlocked).
`run_proactive_compiler` (`src/linux_fastcgi.cpp:1270-1372`) calls
`compiler_list_known_units` / `compiler_set_known_units` with no try/catch
anywhere in the chain, so a transient failure (fd exhaustion, disk full)
aborts the forked child via `std::terminate`.
**Blast radius is small:** the proactive compiler runs in a forked child that
the main loop respawns each iteration, and request-path callers are protected
by the try/catch in `handle_complete`. Still, a catch-and-log around the
proactive scan is cheap insurance.
**Status:** fixed, but fix not verified yet.
---
## Part 2 — Reuse / duplication (plausible unless noted)
### 2.1 request_query_path() duplicates request_query_route()
**File:** `src/lib/uri.cpp:311`
`request_query_path()` copy-pastes `request_query_route()`'s (line 325)
first-keyless-segment scan verbatim — two identical "find first &-part
without =" loops plus `route_path_sanitize` calls that must be kept in sync.
It also has **zero callers** in `src/` or `site/` (only a doc page references
it).
**Fix:** implement as
`return request_query_route(context, default_path)["l_path"].to_string();`.
**Status:** fixed, but fix not verified yet.
### 2.2 list_filter / list_map re-implement the generic filter<T> / map templates
**File:** `src/lib/functionlib.cpp:71`
`StringList` is `typedef std::vector<String>` (`types.h:52`), so the existing
`filter<T>` template (`functionlib.h:68`, declared ~10 lines above the new
`list_*` declarations) already does exactly what `list_filter` does. Two
filter implementations now live in the same module; behavior fixes must be
applied twice.
**Fix:** drop it and point the doc page at `filter`); same consideration for `list_map`.
**Status:** fixed, but fix not verified yet.
### 2.3 Hand-rolled redirects instead of the redirect() helper
**Files:** `site/examples/uce-starter/views/account/login.uce:13`,
`logout.uce:6`, `profile.uce:8`, `site/demo/sqlite.uce:21-22`
Four call sites inline `context.set_status(302, "Found");
context.header["Location"] = ...` instead of calling the existing runtime
helper `redirect(String url, s32 code = 302)` (`src/lib/uri.cpp:414`). This
replaced the single `app_redirect()` helper the changeset deleted.
**Note — security aspect refuted (see 6.1):** headers are sanitized centrally
on write-out, so this is a pure reuse nit, not a vulnerability.
**Fix:** use `redirect(app_link("account/profile", context))` or restore one
`app_redirect` wrapper.
**Status:** fixed, but fix not verified yet.
### 2.5 Asset tag boilerplate repeated across ~a dozen files
**Files:** `components/example/marketing_assets.uce:5`,
`components/example/theme_assets.uce`, `components/gauges/assets.uce`,
plus inline `<link rel="stylesheet" href="<?= app_asset_url(...) ?>" />`
blocks in `components/theme/head.uce`, `components/data/widgets.uce`,
`components/workspace/primitives.uce`, `views/dashboard.uce`
About a dozen hand-written copies of the stylesheet/script tag pattern around
`app_asset_url()`; a versioning or attribute change (defer/integrity) touches
every file. The marketing and theme shims differ only in one CSS path.
**Fix:** collapse the three shims into one parameterizable assets component.
**Status:** fixed, but fix not verified yet.
---
## Part 3 — Simplification (plausible)
### 3.1 request_query_route() emits a derivable "rejected" flag
**File:** `src/lib/uri.cpp:347`
The route tree contains both `route["valid"]` and `route["rejected"]` where
`rejected` is exactly `!valid`, and nothing in the codebase reads
`"rejected"`. Redundant derivable state doubles the invariant surface — a
future path that sets one flag without the other produces a route tree that
lies.
**Fix:** drop the `"rejected"` key; callers needing it can write
`!route["valid"].to_bool()`.
**Status:** fixed, but fix not verified yet.
### 3.2 Starter router carries write-only diagnostic state
**File:** `site/examples/uce-starter/index.uce:38`
The router rewrite adds candidate `kind`/`matched` fields and
`context.call["route"]["candidates"]/["resolved"]/["missed"]`, none of which
is read by any component, theme, or test. It also uses `dtree_filter` to
`file_exists`-stat every candidate after the first match is already found.
~55 lines plus a builder helper replace what the deleted `app_resolve_view`
did with three early-return `file_exists` checks.
**Fix:** remove unused parts.
**Status:** fixed, but fix not verified yet.
### 3.4 Single-link shim components where a direct ONCE block suffices
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
(and `theme_assets.uce`)
These are single-`<link>` shim files with empty COMPONENT bodies, invoked via
`print(component(...))` of an empty string. Routed views are mutually
exclusive per request, so the cross-unit dedup the shims provide can never
trigger; they exist only to host one ONCE line at the cost of two extra files
and an indirection on every view. `dashboard.uce:3-6` already demonstrates
the simpler form (ONCE block directly in the view). `gauges/assets.uce` is
the justified case (multiple sibling components per page) and can stay.
**Status:** fixed, but fix not verified yet.
### 3.5 Compile-failure artifact file is self-referential
**File:** `src/lib/compiler.cpp:847`
`compile_shared_unit()` overwrites `su->compiler_messages` with the formatted
failure report, then writes that report into `compile_output_file_name` — the
same artifact the report's own "Compile output:" line
(`compiler_format_compile_failure`, line 800) points readers at. No copy of
the raw, unformatted compiler output survives for tooling to parse.
**Fix:** keep raw messages as the stored/recorded artifact content and format
only at the print/response boundary.
**Status:** fixed, but fix not verified yet.
---
## Part 4 — Efficiency (plausible unless noted)
### 4.1 QUERY_STRING parsed twice per request
**File:** `src/linux_fastcgi.cpp:744`
`prepare_request_body_maps` calls `request_populate_context_params` (which
splits QUERY_STRING on `&` + uri-decodes inside `request_query_route`) and
then `parse_query(QUERY_STRING)` re-splits and re-decodes the identical
string on the next line. Runs on every HTTP request, CLI invocation, and
websocket event. Route params are also computed eagerly for requests that
never read `ROUTE_*`/`BASE_URL`.
**Fix:** parse QUERY_STRING once into `request.get` first and derive the
route token from that single pass, or populate the route params lazily.
**Status:** fixed, but fix not verified yet.
### 4.2 Repeated normalization in request_populate_context_params
**File:** `src/lib/uri.cpp:351`
Per request: `route_path_normalize` runs three times on the same path
(directly, inside `route_path_sanitize`, and inside `route_path_is_safe`),
`request_script_url` is computed twice (for SCRIPT_URL and again inside
`request_base_url`), and `route_path_normalize` (line 255) strips slashes via
`path = path.substr(1)` in a while loop — O(n²) copies per slash run.
**Fix:** normalize once and pass the normalized string down; compute
script_url into a local used by both params; replace the substr loops with
`find_first_not_of("/")` / `find_last_not_of("/")` and a single substr.
**Status:** fixed, but fix not verified yet.
### 4.4 collect_rows rebuilds column-name strings for every row
**File:** `src/lib/sqlite-connector.cpp:118`
For R rows × C columns the row loop does R×C `sqlite3_column_name` calls plus
R×C String heap constructions and map inserts keyed on the fresh string,
though column names are invariant across rows. A 10k-row, 8-column result
builds 80k redundant name strings per query.
**Fix:** build a `std::vector<String> names(column_count)` once before the
step loop and index it inside (`row[names[i]]`).
**Status:** fixed, but fix not verified yet.
### 4.5 bind_params copies the params map twice and binds SQLITE_TRANSIENT
**File:** `src/lib/sqlite-connector.cpp:95`
`SQLite::query` takes `StringMap` by value and passes it by value again to
`bind_params`, then binds with `SQLITE_TRANSIENT`, forcing SQLite to memcpy
each value a third time — even though the copied map outlives the statement
(it lives until after `sqlite3_finalize`).
**Fix:** pass `const StringMap&` through `query()`/`bind_params` and bind
with `SQLITE_STATIC` (or at least drop the two by-value map copies).
**Status:** fixed, but fix not verified yet.
### 4.6 Per-request connection opens re-run pragmas; busy_timeout set twice
**File:** `src/lib/sqlite-connector.cpp:15`
`connect()` calls `sqlite3_busy_timeout(5000)` and then
`apply_default_pragmas` runs `PRAGMA busy_timeout = 5000` again (pure
duplicate). `PRAGMA journal_mode = WAL` is persistent in the database file
but re-issued on every open. Because cleanup closes all handles at request
end, a page like `site/demo/sqlite.uce` pays `sqlite3_open_v2` + 4 pragmas +
`CREATE TABLE IF NOT EXISTS` on every request.
**Fix:** drop the redundant busy_timeout pragma; cache open
connections per worker keyed by path across requests (resetting state at
request end) so pragmas and schema checks run once per worker.
**Status:** fixed, but fix not verified yet.
---
## Part 5 — Altitude / design (plausible unless noted)
### 5.3 Test suites hand-registered in three parallel lists — already drifting
**Files:** `site/tests/index.uce:15`, `tests/plugins/uce_site_suite.py`
A test page must be registered in three places: the `site/tests/*.uce` file
itself, a `site_tests_card()` line in `index.uce`, and a tuple in
`uce_site_suite.py`. The new `sqlite.uce` was added to all three by hand. The
lists have already drifted: `site/tests/call_helpers.uce` exists on disk but
appears in neither `index.uce` nor any plugin list, and
`security_headers.uce` is covered only by the security-smoke plugin, not the
index cards — new suites can silently fall out of the dashboard and/or CI.
**Fix:** enumerate `site/tests/*.uce` with ls()/glob in both `index.uce` and
`uce_site_suite.py`, with title/tags metadata declared once (in the test page
or a single shared manifest).
**Status:** fixed, but fix not verified yet.
### 5.4 compiler_developer_hints() couples the runtime to clang's English message text
**File:** `src/lib/compiler.cpp:778`
The hint table pattern-matches hardcoded English clang diagnostic substrings
("no member named", "expected ';'", ...). A clang version bump, a switch to
gcc (COMPILE_SCRIPT is user-configurable server config), or localized
diagnostics silently degrades every hint to the generic fallback, and each
new error class means hand-extending an if-chain in runtime C++.
**Fix:** the excerpt mechanism added in the same change
(`compiler_format_compile_failure`'s source/generated excerpts + artifact
paths) already carries the diagnostic value; make the hints a data-driven
table or drop them in favor of the excerpts.
**Status:** fixed, but fix not verified yet.
### 5.5 Generated-artifact path computed by hand in two places
**File:** `src/linux_fastcgi.cpp:79` vs `src/lib/compiler.cpp:631-636`
The deeper-fix framing of 1.9: the moment the compiler's artifact layout or
naming changes (hashing, per-config subdirs), any hand-recomputed path goes
stale. One exported source-file → artifact-path helper in `compiler.cpp`,
used by both the compile-failure formatter and the runtime failure page.
**Status:** fixed, but fix not verified yet.
-496
View File
@@ -1,496 +0,0 @@
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
- **Status:** proposal / design draft (2026-06-11)
- **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.
- **Origin:** design discussion 2026-06-11; incorporates the post-mortem of the
earlier per-invocation arena attempt (preserved in
`src/lib/_scratchpad.cpp`).
---
## 1. Motivation
Two long-standing structural problems and one strategic opportunity share a
single root cause: **request code shares an address space and an allocator
with the runtime.**
1. **The arena attempt failed for a structural reason.** The
`GLOBAL_ARENA_ALLOCATOR` design in `_scratchpad.cpp` swapped the global
`operator new`/`delete` against a `current_memory_arena`. A single global
allocator cannot distinguish request-lifetime allocations from
process-lifetime ones: during a request, server-lifetime structures
(compile registry, sessions, config trees, unit statics) also allocate.
Under the arena those dangle on reset; with `delete` as a no-op,
system-allocated objects released mid-request leak. The lifetime
distinction lives in the type system and call graph, not at the allocator
boundary — fixing it in-process means threading PMR allocators through
`String`, `DTree`, and every container.
2. **Fault recovery is best-effort, not sound.** The current
SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips
destructors, can resume over corrupted worker state, and (see
RECOMMENDATIONS.md 1.5) cannot reliably produce a useful backtrace.
A faulting unit *can* have scribbled on runtime memory before the signal.
3. **UCE can only ever host fully-trusted code.** A `.uce` page is arbitrary
native code with full process privileges. Multi-tenant or user-supplied
page hosting is structurally impossible in the native model.
A WASM execution model deletes the shared-fate fact itself rather than
patching around it:
- **Arena by construction:** each request runs in its own linear memory,
dropped wholesale at request end. Request-lifetime memory physically cannot
outlive the request; server-lifetime state physically cannot live inside
it. The `_scratchpad.cpp` design becomes correct because the boundary is
structural, not typological.
- **Sound recovery:** a trap (null deref, OOB, stack exhaustion) is a defined
host-side error that unwinds cleanly, with a precise guest stack trace, and
the unit cannot have touched host memory. Render error page, drop
workspace, keep serving — actually correct, not hopeful.
- **Capability security:** page code gets exactly the imported API surface
and nothing else. Multi-tenant hosting becomes possible.
- **Secondary wins:** architecture-independent unit artifacts (no clang
required on prod), safe module unload/replace (vs. never-safe `dlclose`),
first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU
timeout), per-request memory stats for free.
**Accepted costs:** ~1.2–2× compute slowdown vs. native (still far ahead of
interpreted runtimes); a real ABI/membrane design; ownership of a custom
loader; toolchain rough edges (§10).
---
## 2. Rejected alternatives (recorded so they stay rejected)
- **In-process PMR arena.** Requires re-typedefing `String`/`DTree` and
threading allocators through the entire codebase; the failed global-swap
shortcut is the only cheap version and it is unsound (§1.1).
- **One instance per component.** UCE components are function calls, not
RPCs: callees receive the context **by reference**, mutate `context.call`,
share the `ob_*` capture stack and `ONCE` dedup state. Per-component
instances force serialize/copy/deserialize of the context on every
`component()` call (a dozen+ per page in the starter), require
host-mediation of the ob stack, and silently change reference semantics to
copy semantics. Rejected.
- **One linked module per app ("the blob").** Introduces an "app" concept
UCE does not have, makes every edit a global relink, turns the artifact
cache into a build graph — webpack's worst traits without its benefits.
Rejected. The file stays the unit.
- **Eager pre-loading of known units at worker warm-up.** Rejected; loading
is strictly lazy, on first explicit call, preserving current semantics.
---
## 3. Architecture overview
### 3.1 Execution model
```
host process (linux_fastcgi, per worker)
│
├─ vendored wasm runtime (§10.1)
├─ unit artifact cache: one PIC .wasm per unit (replaces per-unit .so)
├─ loader (§6): dylink parsing, base allocation, GOT resolution,
│ symbol registry, ABI stamp check
├─ core snapshot: "core module, initialized" memory+table image
│
└─ per request: WORKSPACE
├─ linear memory (CoW-born from core snapshot)
├─ shared funcref table
├─ core module instance (uce_lib + libc compiled to wasm)
├─ unit module instances (loaded lazily, on first call, incl. mid-request)
└─ host handle table (sqlite/mysql/file/socket handles + closers)
```
- **Workspace = request.** Born from the core snapshot, dropped at request
end. Memory drop is the arena; handle-table drop is resource cleanup
(this generalizes and replaces the per-connector
`cleanup_*_connections()` pattern — RECOMMENDATIONS.md 1.7 / 5.1 become
structurally unrepresentable).
- **One unit = one PIC wasm module.** Compile, cache, and invalidation
granularity stay per-file. The `.uce → C++` translation pipeline is
unchanged; only the compile target changes
(`clang --target=wasm32-wasi -fPIC` + `wasm-ld -shared`).
- **Strictly lazy loading.** A unit's module is instantiated into a
workspace the first time that workspace calls it — including mid-request.
This is the wasm equivalent of today's compile-and-`dlopen`-on-first-hit
and requires no restart, no fallback path. Placement memoization
(deterministic bases so repeat instantiation is cheaper) is a permitted
optimization; it must not change the loading policy.
- **In-flight isolation.** Module versions are immutable; a recompiled unit
becomes a new module. Running workspaces keep what they loaded; new
workspaces get the new version. Old modules are dropped when unreferenced
(safe unload — impossible with `dlclose`).
### 3.2 Memory model
- **One heap, one allocator, one DTree implementation** — all owned by the
core module. Unit modules *import* `malloc`/`free`/runtime symbols via GOT;
the loader **rejects any unit module that defines rather than imports
them** (two allocators on one heap is the one fatal misconfiguration).
- **Bump allocator option.** Because the workspace heap is dropped wholesale,
the core's allocator may be a bump allocator with no-op free — the
`_scratchpad.cpp` design, now correct by construction. Per-deployment
flag; fallback is wasi-libc dlmalloc. Memory stats = heap pointer − base
(replaces the tracking `operator new` in `types.h`).
- **Unit statics reset per request** (workspace is born from the core
snapshot, which does not include unit data; unit data segments initialize
at unit load within the workspace). This is *more* shared-nothing than
today, where `.so` statics persist across requests within a worker.
Cross-request state must use explicit host facilities (sessions, caches).
**Breaking change — must be called out in docs and checked against the
site/ tree during Phase 5.**
### 3.3 The host membrane
Exactly three currencies cross between host and workspace:
1. **scalars** (i32/i64/f64),
2. **byte buffers** (`ptr+len` into linear memory; inbound buffers are
placed via the core's exported allocator),
3. **handles** (opaque `u32` indices into the per-workspace host handle
table; each entry carries a closer callback).
Everything pointer-shaped stays on its own side. Hostcall surface budget:
30–60 functions (§5.1). Host errors return as error values; traps are
reserved for unit faults. Nothing throws across the membrane.
**DTrees cross the membrane only as the versioned wire encoding** (§5.3),
at exactly three sites: request context in (once), response out (once),
bulk I/O results in (per query, optional vs. cursor-style hostcalls).
### 3.4 DTree inside the workspace: no serialization, ever
Within a workspace, all modules share one address space, one toolchain, one
set of headers — the C/C++ ABI is intact across module boundaries. Therefore:
- A `DTree` is a pointer (an i32 offset into linear memory).
- `component(path, context)` resolves path → table index (host registry or
guest-resident map) and `call_indirect`s, passing the context pointer.
Reference semantics, mutation visibility, shared ob stack, working `ONCE`
— identical to today.
- Function pointers are shared-table indices, valid across modules: virtual
calls, `std::function` callbacks (`dtree_map` lambdas) work across units.
- Cost: one `call_indirect` (single-digit ns) + GOT loads for cross-module
symbols — the same shape of overhead native PIC pays through the PLT/GOT,
i.e. what dlopened `.so` units pay today.
Encode/decode is **not** part of internal component calls. It exists only at
the membrane (§3.3) and on the cross-instance plane (§4).
### 3.5 The DTree C ABI (load-bearing, build it first)
The stable contract of the workspace is a **C ABI**, not the C++ class:
the core exports `extern "C"` accessors over an opaque `uce_dtree*` (§5.2),
plus the string/ob/print helpers. C++ units may bypass it and use the class
directly (same headers, zero cost — a private fast path). Every other
workspace language uses the C surface.
This ABI is versioned: every unit artifact carries a custom section
(`uce.abi`: core ABI version + toolchain fingerprint); the loader refuses
stale units and triggers lazy recompilation (units are lazily compiled
anyway, so this costs nothing structurally).
**Phase 1 of the implementation plan is to introduce this C ABI in the
current native runtime** — it is useful immediately (plugin surface,
testability) and de-risks the rest.
---
## 4. Two component-call planes / multi-language support
The design stratifies languages by one question: *can the toolchain produce a
PIC linear-memory module that adopts a foreign allocator?*
**Plane A — workspace peers** (C++, C, Rust, Zig, …):
- Join the workspace as PIC modules importing core symbols.
- Must adopt the core allocator (Rust: `#[global_allocator]`; Zig: allocator
parameter) and must not unwind across boundaries (`panic=abort` /
catch-at-edge).
- Access DTrees through the C ABI: pointer semantics, no copies, ns-scale
calls. Idiomatic wrappers per language (e.g. Rust `DTree<'request>` —
the borrow checker enforces the arena invariant).
**Plane B — runtime-carrying languages** (JS, Python, Go, C#, …):
- Their GC/runtime owns its memory; they run as **separate instances**
within the request and communicate through the host using the wire
encoding and handles. Bindings choose per-access hostcalls or bulk
subtree hydration into native dicts/objects — a tuning decision, not an
architectural fork.
**The semantic rule (enforced by the loader, not by convention):**
cross-plane components do **not** receive the mutable context. They get an
explicit interface — props in (copied by definition), rendered output and
declared results back. Plane A keeps the full "here's the world, mutate it"
contract. A `component()` call must never silently change mutation semantics
based on the callee's implementation language.
The cross-instance call mechanism is shared by: Plane B units, and future
cross-trust-boundary components (multi-tenant). Serialization boundaries and
isolation boundaries are the same lines, by design.
---
## 5. ABI sketches (to be finalized in Phase 0/1)
### 5.1 Hostcall surface (grouped; target ≤ 60 functions)
```
request: uce_host_ctx_read(buf) → len // wire-encoded context, once
response: uce_host_respond(status, hdrs_buf, body_buf)
uce_host_stream_write(buf) // chunked/streaming path
log: uce_host_log(level, buf)
sqlite: uce_host_sqlite_connect(path_buf) → handle | err
uce_host_sqlite_query(handle, sql_buf, params_buf) → result_buf | err
uce_host_sqlite_cursor_*(...) // optional row-cursor variant
uce_host_sqlite_insert_id/affected/error/disconnect(handle)
mysql: (same shape; existing connector APIs are already handle-shaped)
files: uce_host_file_read/write/stat/list(path_buf, ...) // policy-gated
session: uce_host_session_get/set(key_buf, val_buf)
http: uce_host_http_request(req_buf) → handle/result_buf // outbound
misc: uce_host_time(), uce_host_random(buf), uce_host_env(key_buf)
loader: uce_host_component_resolve(path_buf) → table_index // may load (§6)
ws: uce_host_ws_send(buf), event delivery via render entry re-invocation
```
Conventions: all errors as result codes + `uce_host_last_error(buf)`;
inbound buffers placed via the core's exported `uce_alloc`; no hostcall
traps on bad input (clamp/error instead).
### 5.2 DTree C ABI (core exports; sketch)
```c
typedef struct uce_dtree uce_dtree; // opaque; workspace-owned
uce_dtree* uce_dtree_root(void); // request context
uce_dtree* uce_dtree_get(uce_dtree*, const char* key, size_t klen); // create-on-write
uce_dtree* uce_dtree_find(uce_dtree*, const char* key, size_t klen); // NULL if absent
const char* uce_dtree_value(uce_dtree*, size_t* len);
void uce_dtree_set_value(uce_dtree*, const char* v, size_t vlen);
size_t uce_dtree_count(uce_dtree*);
int uce_dtree_is_list(uce_dtree*);
/* iteration */
uce_dtree_iter uce_dtree_iter_begin(uce_dtree*);
int uce_dtree_iter_next(uce_dtree*, uce_dtree_iter*,
const char** key, size_t* klen, uce_dtree** child);
/* encode/decode at the membrane */
size_t uce_dtree_encode(uce_dtree*, char* buf, size_t cap); // → UCEB1
uce_dtree* uce_dtree_decode(const char* buf, size_t len);
/* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close,
uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */
```
No unwinding across this surface; C++ exceptions are caught at the edge and
surfaced as error returns where fallible.
### 5.3 Wire encoding "UCEB1" (membrane + cross-instance only)
Length-prefixed binary tree; **not** JSON. Sketch (finalize against DTree's
actual fields — value + ordered children):
```
node := value children
value := varint len, bytes (utf-8)
children := varint count, count × ( key: varint len + bytes, node )
flags := one leading byte per node reserved (bit0: is_list hint)
header := "UCEB" u8 version
```
This encoding is a **versioned protocol** from day one (header byte). It is
the Plane B contract and the membrane format; internal calls never see it.
### 5.4 Unit module contract
```
custom sections: dylink.0 (standard), uce.abi { abi_version, toolchain_id }
imports: env.memory, env.__indirect_function_table,
env.__memory_base, env.__table_base,
GOT.mem.* / GOT.func.* (resolved by loader),
core symbols (malloc, uce_dtree_*, uce_print, ...)
exports: uce_unit_setup, uce_unit_render,
uce_unit_component, uce_unit_websocket
(same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET
dlsym symbols in compiler.cpp)
forbidden: defining malloc/free/operator new, own memory, start fn
with side effects beyond data init
```
---
## 6. The loader (host-side, custom, load-bearing)
Owned code, ~1–2k lines, vendored-runtime-adjacent. Reference logic:
Emscripten's dylink loader (the ABI is the stable, battle-tested part; the
server-side loader is what doesn't exist off the shelf).
Per `load(unit)` into a workspace:
1. Fetch compiled module from artifact cache (compile on miss — today's
lazy-compile path, retargeted).
2. Verify `uce.abi` stamp against the core; on mismatch, recompile unit.
3. Verify import discipline (no allocator/runtime definitions; §3.2).
4. Parse `dylink.0`: data size/alignment, table slots needed.
5. Allocate `__memory_base` (bump within workspace data region) and
`__table_base` (append to shared table).
6. Instantiate with bases; resolve `GOT.*` imports against the workspace
symbol registry (core symbols + previously loaded units); register the
unit's exports.
7. Register entry points in the path → table-index dispatch map.
`uce_host_component_resolve(path)` consults the dispatch map and calls
`load()` on miss — this is how lazy, programmatic, mid-request loading works
with no special cases.
Placement memoization (optional, later): record each unit's first-assigned
bases; reuse across workspaces so instantiation is cheaper and snapshot
growth (below) stays consistent. Does not change the lazy policy.
**Core snapshot:** the only pre-built state is "core module, initialized" —
memory bytes + table state captured once per core build. Workspaces are born
from it via CoW (`mmap(MAP_PRIVATE)` of the snapshot image; the host owns
the Memory object, so OS-level CoW is available). No units are pre-fed.
---
## 7. Request lifecycle (replaces the native flow in linux_fastcgi.cpp)
```
1. accept request
2. workspace = birth_from_core_snapshot() (CoW, ~µs)
3. write wire-encoded context into workspace; core decodes → context DTree
4. resolve entry unit (load on first call); call uce_unit_render(ctx_ptr)
5. component(path) inside guest → resolve hostcall → (lazy load) →
call_indirect — reference semantics throughout
6. I/O via hostcalls; resources land in the workspace handle table
7. on return: encode response/headers out; write FastCGI response
on trap: defined error → render error page with guest stack trace;
workspace state is irrelevant because…
8. drop workspace: linear memory gone (arena), handle table closed
(generalized resource cleanup), instances released
```
CPU limit: epoch/fuel interruption → same path as trap.
Memory limit: linear memory max → allocation failure / trap → same path.
---
## 8. What carries over unchanged
- The `.uce → C++` translation, parser, and page semantics.
- The lazy compile-on-first-request model and per-unit artifact caching
(different artifact format).
- The host-side connectors (sqlite/mysql) — already handle-shaped APIs; they
move behind hostcalls with the same `.uce`-visible signatures.
- The site tree, docs, demo, and the network test suite
(`tests/run_network_tests.py`) — which becomes the parity harness (§9).
- nginx/FastCGI front-end integration, worker model, websocket event flow
(events re-enter via the websocket entry point; note: a workspace-per-event
or workspace-per-connection decision is an open question, §11).
---
## 9. Implementation plan
Phases are sequential; each has an exit criterion. No phase except 0 and 2's
scaffolding produces throwaway work.
**Phase 0 — toolchain & runtime spike (timeboxed).**
Validate: wasi-sdk `-fPIC` + `wasm-ld -shared` on a representative generated
unit; cross-module C++ calls with shared memory/table; exceptions decision
(wasm EH vs. error-code discipline at unit boundaries — pick one, record it);
vendored runtime selection. Candidates: **WAMR** (C, small, designed for
embedding, easiest to vendor and patch — fits the project's vendoring
practice) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier
to vendor/patch). Selection criteria: imported-memory + shared-table support,
AOT artifact quality, patchability. Exit: a two-module (core stub + unit
stub) hello-world linked at runtime by a minimal loader, in the chosen
vendored runtime.
**Phase 1 — DTree C ABI in the native runtime.**
Introduce `uce_dtree_*` (§5.2) and the UCEB1 codec in `src/lib/`, used
natively. Zero wasm dependency; immediately testable; freezes the contract
everything else builds on. Exit: codec round-trip + accessor tests in the
existing suite; ABI doc checked in.
**Phase 2 — core module + membrane.**
Compile `uce_lib` (+ wasi-libc) to wasm as the core module; implement the
hostcall surface (§5.1) in the host; temporary scaffolding allowed: one
statically-linked unit + core to validate codegen and membrane without the
loader. Exit: one real `.uce` page (e.g. `site/tests/core.uce`) renders
correctly through the membrane. Scaffolding is marked throwaway.
**Phase 3 — the loader + workspace.**
Implement §6 in full: dylink parsing, base allocation, GOT resolution,
ABI/import verification, lazy mid-request loading, path dispatch. Per-request
workspace birth/drop (plain memcpy birth is fine here; CoW is Phase 4).
Exit: the uce-starter renders end-to-end with components loading lazily;
`tests/run_network_tests.py --match starter` passes against the wasm worker.
**Phase 4 — production mechanics.**
Core snapshot + CoW birth; bump-allocator flag; epoch/memory limits; trap →
error-page path with guest stack traces (this supersedes the
signal/longjmp machinery and closes RECOMMENDATIONS.md 1.5 structurally);
handle-table cleanup (closes 1.7/5.1); artifact/ABI versioning end-to-end.
Exit: kill-tests (deliberate null-deref page, infinite-loop page, OOM page)
produce clean error pages and an unharmed worker.
**Phase 5 — parity & performance.**
Full network suite green on the wasm worker; differential native-vs-wasm runs
on the site tree; audit `site/` for cross-request-static reliance (§3.2
breaking change); benchmark suite (template-heavy page, sqlite page,
component-heavy starter page) with budgets: ≤2× native page latency,
workspace birth ≤100µs, internal component call overhead within 10× native
call cost. Exit: numbers published in this document; go/no-go for default
backend.
**Phase 6 — second plane (deferred until wanted).**
Cross-instance call mechanism (props-in/output-out, UCEB1), first Plane B
language binding, loader enforcement of the cross-plane context rule (§4).
Plane A second language (Rust) as the cheaper first polyglot proof.
The native `.so` backend remains in-tree and selectable until Phase 5's
go/no-go; both backends share the Phase 1 C ABI.
---
## 10. Risks & mitigations
| Risk | Mitigation |
|---|---|
| wasi-sdk PIC / shared-library maturity (least-trodden toolchain path) | Phase 0 spike before any commitment; pin toolchain versions; statically link libc into the core and export from there (avoid shared wasi-libc entirely) |
| C++ exceptions × PIC × wasm EH | Phase 0 decision point; fallback is error-code discipline at unit entry points (units already have a uniform entry shape) |
| Custom loader correctness (GOT, bases, relocation) | Small, contained (~1–2k lines); crib logic from Emscripten's reference loader; fuzz with adversarial modules; loader rejects > loader guesses |
| Vendored runtime patches drift from upstream | Same practice as vendored SQLite: provenance + patch files under `docs/patches/`; pin upstream tag; tests gate upgrades |
| Performance regression beyond budget | Phase 5 gates; bump allocator and placement memoization in reserve; native backend retained |
| Multi-module DWARF / debugging story | Trap stack traces cover the production case (better than today); accept weaker interactive debugging; keep native backend for local deep-debugging |
| Unit-statics semantic change breaks existing pages | Phase 5 audit of `site/`; documented migration note; host-side cache facility if a real need surfaces |
## 11. Open questions
1. **Exceptions:** wasm EH or error codes at unit boundaries? (Phase 0.)
2. **WebSocket granularity:** workspace per event (pure arena, statics reset
per event) or per connection (state across events, bounded lifetime)?
Leaning per-event for consistency; needs a look at current WS page usage.
3. **UCEB1 final layout** vs. DTree's actual field set (value/children/list
flag) — finalize in Phase 1.
4. **Cursor vs. bulk** as the default for `sqlite_query` results at the
membrane (offer both; pick the default after Phase 5 benchmarks).
5. **Streaming output:** today's ob model buffers; does the membrane expose
`uce_host_stream_write` from day one or post-MVP?
6. **Core snapshot rebuild cadence** once placement memoization lands
(dead-base reclamation policy).
## 12. Summary
The module is the **unit** (file-grained, lazily compiled, lazily loaded —
including mid-request). The instance set is the **workspace** (one per
request; shared memory + table; born from a core-only CoW snapshot; dropped
wholesale — the arena, done right). The contract is the **DTree C ABI**
inside the workspace (pointer semantics, no serialization) and the **UCEB1
wire encoding + handles** at every true address-space boundary (host
membrane, Plane B languages, future trust boundaries). Serialization
boundaries and isolation boundaries are the same lines; component calls stay
function calls; and the file stays the unit.
+15
View File
@@ -0,0 +1,15 @@
commit ad3d65cdacdcc5aed72fe12354f66190d7be454d
Initial commit
commit c047927b189860220428b42400ed18942150882c
initial import
commit 3e8f0f1fa780da7836128fdbc39bbe8c2ba97de8
initial import
commit 939009f9a11d0934226c830375057491f088d362
shell stuff, preprocessor directive
+4
View File
@@ -0,0 +1,4 @@
commit 18ca2368bc1d4a5ef7e4c8470a9b3e32f496fc86
VERY basic UTF8 parser
+12
View File
@@ -0,0 +1,12 @@
commit 17336fe649c52c162a4541968c8d24c0c2ab3eb9
Unicode stuff
commit 5df52146be3f5a45759459d5716aec4ef8d3c33a
split_space
commit 3a9dfba86cb7dadb1268fcfd689b14066779e6aa
fixed bug in #load
+4
View File
@@ -0,0 +1,4 @@
commit 0a6ebc60f0b8aacf8b01e14bd9b6447c750edeb5
precompile on startup
+14
View File
@@ -0,0 +1,14 @@
commit 6f6919f3f01686175001251e2fca215bccc4806f
backage builder
commit defc628204f95d0cdd16c95c89ea30efe922b31a
packaging
commit 9e5b7d39e3b8be9f4ad537a134b1081564ae4abc
Basic readme
commit 21fdeafb6c5b8bc80a9d93ed61c36e05fbe7b267
Basic documentation
+8
View File
@@ -0,0 +1,8 @@
commit 364d83b199afa6a341173a80791060a42c169f0c
doc update
commit 392b7224b16e41198b0425c5ae04f46e014e78c7
Merge branch 'main' of github.com:ThingamaNet/uce
+4
View File
@@ -0,0 +1,4 @@
commit 984453f336a9b2ad47689d161c89d7020006f75a
update sigh
+26
View File
@@ -0,0 +1,26 @@
commit cd8f07aaa78fc488eba3b0e435350e5c2823e53e
Refactor FastCGI server and compiler, enhance HTTP header parsing, and add WebSocket support
- Refactored FastCGIServer class to improve socket handling and added shutdown functionality.
- Updated compiler functions to streamline HTML and text literal processing.
- Enhanced split_kv function to support uppercase keys and added split_http_headers for better HTTP header parsing.
- Introduced new session management functions to validate and handle session IDs.
- Added WebSocket frame parsing and handling in the URI module.
- Created systemd service scripts for easier deployment and management of the UCE FastCGI runtime.
- Added new test cases for header handling, time parsing, and WebSocket functionality.
commit 86dc93864e8eab9227ac50b16f1fc7f561383f5b
Add WebSocket support and enhance chat functionality
- Implement WebSocket handling in the FastCGI server, allowing for real-time communication.
- Introduce functions for managing WebSocket connections, broadcasting messages, and sending to specific connections.
- Create a chat interface in the `websockets.ws.uce` file, including message handling and user notifications.
- Refactor existing code to accommodate WebSocket integration, ensuring compatibility with HTTP requests.
- Update documentation to reflect new features and usage instructions for WebSocket functionality.
commit 46d98a092fb606c87ab4b9eef32fe68eeca2b2cb
Enhance WebSocket support: add opcode handling, binary message support, and improve connection validation
+20
View File
@@ -0,0 +1,20 @@
commit be514d63d6ed709a40441db9dc809837081d54f6
trying to port web app starter from PHP
commit 2b5586d7dfffc5c1c40cac97e6d4d0e1f86f151a
decided in favor of dedicated COMPONENT() macro, updates to documentation
commit d1167aec3b4d7870db0b289ee0ea690f8e7e5cc9
getting closer to full port of web app starter
commit af642c8167383f9acf99274d3b3c94017d860dcf
getting closer to full port of web app starter
commit dc05f9faa5eab980f1f1fd44cee06f811cc748ff
PHP familiarity shortcuts
+9
View File
@@ -0,0 +1,9 @@
commit b53eb6e4f1b5fa4e2a4907022f591eb9a1860c33
Enhance template parser to handle C++ comments and refactor preprocessing logic
- Updated parser to correctly interpret C++ `//` and `/* ... */` comments within template code.
- Split preprocessing implementation into separate files for better organization.
- Added regression test for comment parsing in templates.
- Adjusted CSS styles for improved layout and readability in documentation.
+16
View File
@@ -0,0 +1,16 @@
commit 8223dcc6b3cb7f4d26c69097fac973c2671f0938
I think I need to change the documentation format
commit f7b066b374f1031d38e3d2b344f6680534c3a2b2
changing doc format and HTML literals
commit 14ebf10a229021bb5d0fb1eae42a69c042699318
changing doc format and HTML literals
commit 223cf4c6e1c7a205bf1a27bebdc6d04fd7557df0
website with slop placeholders
+4
View File
@@ -0,0 +1,4 @@
commit cd445f3c9b6aa079a969351d6eb742b8a4d24f6b
some cleanup
+4
View File
@@ -0,0 +1,4 @@
commit 9f7625c7fdd6a9d2a184dc2cdf9f8a719d219ec2
working on documentation and more API functions
+24
View File
@@ -0,0 +1,24 @@
commit 02e153a6a78bf8ded1c7380713023b4f9c3b656f
Add archive helpers and harden task runtime
commit d37517041de0c5ebe4dfd232093a78ff49cff383
Add custom server API and runtime limits
commit 0d8b74930c74cc8018cb1c0cfc7cfe6740b49cf6
Harden HTTP path headers sessions and archives
commit 41e9ca219fd684c75e78a5493e20c754c7899105
Streamline hardening helpers and expand coverage
commit 8b37e7ea1e58e36c97852f79ec121947a1ffea08
Fix direct HTTP status sanitizer fallback
commit 71ddcaf7d48ffd1343e597e4715b570c922a306c
Consolidate config and base64 helpers
+12
View File
@@ -0,0 +1,12 @@
commit 7f757654b658d049564f1725f54fde781575e304
fix: harden UCE runtime and starter
commit 20db6695890a3ac274217fd0ed9e0edf7c5eb093
fix: stabilize runtime follow-up regressions
commit b957a2373b8464ffcb7791cc6b0031c441d5b09c
chore: ignore Python cache artifacts
+28
View File
@@ -0,0 +1,28 @@
commit 941f5aea08941327eecbc43128d8f171a4064616
feat: add configurable UCE error pages
commit 7066da3cdef08b01dda64bca34caadc7000587b2
refactor: rename DTree to DValue
commit 7e2faf1472c84f302ce5b7f9674091a1a2b3930f
spike: validate WASM phase 0
commit 80285b7fb4acd15850218ee87c72ec8272332c05
feat: implement WASM phase 1 DValue ABI
commit 961df2d542ff6554e32da9ee28e73f5fc59b5975
phase 5
commit 89b5499c8f4e1a8acc8748e66c9dba9eb9679369
fix: harden WASM phase 5 harness
commit eb8f303f94f0f43699eca7cd6474eedaef1ed711
docs: remove unsupported second-plane language roadmap
+36
View File
@@ -0,0 +1,36 @@
commit 577aae076e3603137265332adfdded3c15cfa8f4
W3
commit afaa4dd7c0203f322d625593a0d1c96c37130005
feat: cut over to WASM backend
commit 5a56d4f39e4b30a9a4e43985d64906aa16a642e6
W5
commit c84fc86e6cdfe254a59d57b909f647e6c254ef76
W6
commit fe83c524118722c062295bc9575cb71eff64dacb
feat: membrane remaining wasm host surfaces
commit d6421cb8f3721e69e999b84335e9094ad96356e9
feat: extend wasm backend entrypoints
commit af6500d1343581abdac475dcb6855821afd8c8d0
fix: harden wasm w7 entrypoint paths
commit b1856c1725970b31027383a7ae79c2140a62a896
chore: narrow wasm backend entrypoint API
commit 6bb4f7f0ad801a0127956f49e54d2be722c6537f
test: port network suite to uce cli runner
+111
View File
@@ -0,0 +1,111 @@
commit 15e8d092bc13c425b5c7f7a19dfaf3c9728104f5
W7+
commit 8587fbc5aa0f38739182fa50383bb94c8d821ed7
wasm runtime: central WS broker, unified handlers, W7d holdouts, membrane completeness
- WS: a dedicated broker process owns HTTP_PORT + every connection; it forwards
renders to the worker pool over uce.sock (non-blocking) and applies ws_*
command batches flushed back at workspace teardown. Removes the now-dead
per-worker websocket executor (-509 lines).
- Dispatch: unify CLI / WebSocket / serve_http / page render through one
serve_via_wasm(entry_unit, handler) path; handler string -> __uce_<handler>
export symbol.
- W7d: rewrite zip.uce to the membrane return-value error contract (no C++
try/catch), error-reporting.uce to genuine wasm traps instead of throw, and
sharedunit.uce to unit_info(); empty the native-only token gate.
- Membrane: wire ls / mkdir / file_mtime through new uce_host_file_list /
uce_host_file_mkdir / uce_host_file_mtime hostcalls (resolve_guest_file gains
directory support). Fixes /doc/index.uce listing nothing; adds a regression
assertion that the index enumerates items.
- Docs: add docs/wasm-runtime-architecture.md; record the W7e staged native-
deletion plan in WASM-PROPOSAL.md.
commit 2debd338040cf97e82bb8ccbb5ec09caee089c17
W7e: wasm-preferred dispatch + compile-on-demand; harden unit ABI validator
- Dispatch (linux_fastcgi.cpp): route every request through wasm. On a
cold/stale artifact, compile the unit on demand (get_shared_unit, forced) and
serve wasm; native compiler_invoke* remains only as a fallback when wasm
cannot be made ready (compile failure / backend disabled). Applies to the 4
handle_complete branches and the CLI socket path.
- backend.cpp: delete the now-vestigial native-only fallback token gate
(wasm_backend_native_fallback_*), empty since W7d; should_handle now gates on
config + current artifact + healthy worker only.
- check_unit_wasm.py: skip the defense-in-depth llvm-nm allocator scan when
llvm-nm SIGSEGVs on a degenerate-but-valid module (e.g. a unit with no
exported handlers). Fixes site/demo/empty.uce, the last unit that could not
produce a .wasm; forbidden allocator *exports* are still rejected.
A pi-assisted review caught that the compile-freshness check keys off the .so
mtime only, so force_recompile is required to rebuild a missing/stale .wasm; a
unit already cached in-process whose .wasm later vanished still uses native
fallback (closed in W7e stage B). Native execution is otherwise bypassed for
all real traffic.
commit fb728d63bcd0d80bd5161537e81c30d27eb4df7b
W7e stage B: factor the .wasm artifact into unit compile-freshness
inspect_shared_unit_filesystem() tracked only the .so mtime, so a missing or
stale .wasm with a current .so never triggered a rebuild and the unit fell to
native indefinitely (the in-process cache also never invalidated). Account for
su->wasm_name when wasm unit compilation is enabled: a unit counts as compiled
only as of the OLDER of the two artifacts; a missing .wasm forces a recompile.
Closes the cached-vanished-.wasm gap noted in the stage A review.
commit cf513368730e5f337e64a9c9f1456e6a64f49d9c
W7e: delete the native unit pipeline (.so compile + dlopen execution)
Units now run exclusively on wasm; the native generated-C++ -> clang -> .so ->
dlopen path and its request-time fallback are removed.
- Dispatch (linux_fastcgi.cpp): the 4 handle_complete branches + the CLI-socket
path route every request through wasm (wasm_ready compiles cold/stale on
demand); a wasm-unavailable unit now yields a clean error page
(fail_wasm_unavailable / render_request_failure) instead of native execution.
compiler_invoke / _cli / _websocket / _serve_http deleted.
- compiler.cpp (-1274): removed the native .so compile (COMPILE_SCRIPT),
load_shared_unit, dlopen/dlsym/dlclose, compiler_load_shared_unit, and the
SharedUnit .so function-pointer fields (on_setup/on_render/on_component/
on_websocket/on_cli/on_once/on_init) in types.h/types.cpp. compile_shared_unit
now builds only the .wasm side-module; the .uce preprocessor/parser front-end
is kept (it emits the C++ the wasm compile consumes).
- unit_call()/component()/once/init now resolve across units through the wasm
host component resolver (uce_host_component_resolve) instead of native dlsym;
configured runtime error pages render through the wasm backend.
- Dropped the WASM_BACKEND_ENABLED feature flag and dead COMPILE_SCRIPT /
COMPILE_WASM_UNITS config; unit ABI freshness tied to UCE_UNIT_ABI_VERSION;
guard against serving a stale .wasm for a deleted source; retired the obsolete
W5 native-vs-wasm toggle script. Docs updated.
commit bfd6d338299dd9001fe2d4a66f82bbc2b3b07ed9
W7f: sweep dead/legacy/fallback leftovers after native-pipeline removal
Post-deletion cleanup (units run only on wasm):
- types.h/compiler.cpp: drop the native-era SharedUnit fields so_name,
bin_file_name, and the opt_so_optional cache-mode plumbing (no native
optional .so path remains). The per-unit compile lock is re-keyed from
so_name+.lock to wasm_name+.lock (still per-unit).
- unit_info() and to_string(SharedUnit*) no longer expose .so artifact fields.
- backend.h: drop the stale "+ fallback-token gate" comment.
- Docs/comments corrected to wasm-only reality: README, tests/README,
site/doc C++ preprocessor + error_pages + unit_info pages, site/info intro,
site/demo/unit-browser artifact card; the Phase-5 native-vs-wasm benchmark
harness (tests/wasm_benchmark.py) reframed for the wasm-only backend.
Audit confirmed no live references remain to so_handle, load_shared_unit,
compiler_load_shared_unit, compiler_invoke*/_cli/_websocket/_serve_http,
COMPILE_SCRIPT/COMPILE_WASM_UNITS, or the native export-symbol constants;
request_ref_handler/dv_call_handler are kept (live wasm funcref casts).
Swept via the pi agent (delegated to a gpt-5.3-codex-spark sub-model);
independently re-verified on the host: run_cli_tests --include-wasm-kill =>
87 passed, 0 failed, 0 skipped.
+58
View File
@@ -0,0 +1,58 @@
commit 560290ca1d18d4d5a0d204bb187d1aa68c3b5ac7
perf: cache the compiled core module so fresh workers deserialize, not recompile
Workers recycle every 8 requests (calls_until_termination=8). Each fresh worker
ran wasmtime::Module::compile() on the 6.8MB bin/wasm/core.wasm — a ~1.3s
Cranelift JIT — on its first request, so every ~8th request spiked to ~1.3s and
dominated suite wall-clock.
Cache the compiled artifact: on worker core-module load, if bin/wasm/core.cwasm
exists and is newer than core.wasm, load it via Module::deserialize_file() (mmap,
~ms); otherwise Module::compile() as before and atomically (temp+rename) write
the serialized artifact for the next worker. Deserialize failure / stale cache
falls back to a normal compile, so it is self-healing; rebuilding core.wasm
(newer mtime) invalidates the cache. Engine config (epoch_interruption,
signals_based_traps(false)) is unchanged, which the serialized format requires.
commit 83ab9e10f7c0f17805e40a9a3d2b4a3357e5280f
perf: extend the compiled-module disk cache to per-unit modules
Follow-up to 560290c. Per-unit .wasm modules were still Cranelift-compiled per
worker on first use (~40-70ms each), so with 8-call worker recycling fresh
workers re-JIT every unit they touch.
Refactor the core cache logic into a shared WasmWorker helper:
- cached_wasm_path(p): maps <...>.wasm -> <...>.cwasm.
- load_or_compile_cached_module(engine, cached, wasm, bytes, err): deserialize_file
the .cwasm when it is newer than the .wasm; otherwise Module::compile + serialize,
written atomically (temp+rename); deserialize failure falls back to compile.
Both the core module load and unit_module() now go through this helper, so unit
artifacts get the same <unit>.uce.cwasm cache the core got.
commit 4f84ac544d9c91a66d068d372055eec8e1723def
feat: request_perf() worker-side timing hostcall; restore demo System Info
Units run in the wasm sandbox, so my_pid/parent_pid/context.server->request_count
read as sandbox stubs — the demo System Info counters were broken, and there was
no authoritative server-side request timing available to unit code (client-side
measurement cannot see queue/dispatch latency).
Add a request_perf() unit API backed by a new uce_host_request_perf hostcall.
The native worker answers it live, returning a DValue:
worker_pid, parent_pid, request_count,
accept_us = (time_start - time_init)*1e6 (entry -> dispatch wait),
running_us = (now - time_start)*1e6 (since dispatch, live),
total_us = (now - time_init)*1e6 (since the request entered UCE),
workspace_birth_us.
time_init is captured at request entry (handle_request, with a handle_complete
fallback); a RequestPerfSnapshot {pids, request_count, time_init, time_start} is
threaded from wasm_backend_serve through wasm_worker_serve onto the workspace,
and the hostcall computes the live deltas at call time. Wired like uce_host_units
(sized DValue hostcall): core_hostcalls.syms + sys.cpp/sys.h request_perf().
site/demo/index.uce System Info now uses request_perf() and shows the real worker
PID, an incrementing per-worker request count, and the timing counters.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -1,6 +1,6 @@
diff --git a/vendor/miniz/miniz_tdef.c b/vendor/miniz/miniz_tdef.c
--- a/vendor/miniz/miniz_tdef.c
+++ b/vendor/miniz/miniz_tdef.c
diff --git a/src/3rdparty/miniz/miniz_tdef.c b/src/3rdparty/miniz/miniz_tdef.c
--- a/src/3rdparty/miniz/miniz_tdef.c
+++ b/src/3rdparty/miniz/miniz_tdef.c
@@
-static const mz_uint s_tdefl_num_probes[11];
+static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
-147
View File
@@ -1,147 +0,0 @@
# React Developer Affordances Todo
## Objective
Add practical value for developers coming from React frameworks while preserving UCE's server-first C++ model. Defer component syntax/children work, avoid global head/assets/islands in the runtime, and focus on function-library data helpers, diagnostics, docs, examples, demos, and a starter-local router with starter-local asset/island components.
## Success Criteria
- [x] Function library has useful collection/data-shaping helpers with docs and tests.
- [x] Compile/runtime diagnostics are more helpful, especially for generated-code and common preprocessor mistakes.
- [x] Docs include a concise React/Next/Remix orientation guide.
- [x] Starter example uses a centralized hierarchical/file-based router in `index.uce` efficiently.
- [x] Starter-local asset/island affordances live as component handlers in the starter, not global runtime APIs.
- [x] Network tests and relevant build checks pass on `k-uce`.
## Current State
- Status: complete
- Last updated: 2026-05-28
- Source of truth: `/root/mount_ssh/k-uce-root-htdocs-uce`
- Runtime/live target: `k-uce:/Code/uce.openfu.com/uce`; rebuilt and restarted `uce.service` on `k-uce`.
## Goal Tree
Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded
- [x] G1: Add collection/data helpers to function library
- Why: React-framework developers routinely shape arrays/objects near render code.
- Done when: helpers are declared, implemented, documented, and covered by tests.
- Verify: build plus focused site/network tests.
- [x] G1.1: Identify current `StringList`/`DTree` idioms and choose helper surface.
- [x] G1.2: Implement minimal high-value helpers without broad template complexity.
- [x] G1.3: Add docs and examples for helpers.
- [x] G1.4: Add/extend tests.
- [x] G2: Improve developer diagnostics
- Why: React frameworks win by making failures easy to act on.
- Done when: compile/runtime error output includes actionable context and docs mention debugging flow.
- Verify: intentional broken page surfaces improved message.
- [x] G2.1: Inspect current compiler/runtime error rendering path.
- [x] G2.2: Add source excerpt / generated path / common-hint text where appropriate.
- [x] G2.3: Document diagnostics.
- [x] G3: Add React/Next/Remix orientation docs
- Why: mapping familiar concepts reduces onboarding cost without adding syntax.
- Done when: docs page exists and is linked from docs/README/demo surfaces.
- Verify: docs page renders live.
- [x] G4: Starter-local router and starter affordances
- Why: User specifically wants hierarchical/file-based routing beautifully in starter `index.uce`.
- Done when: starter routes go through a central router in `index.uce`, and starter-local asset/island component handlers exist and are used where sensible.
- Verify: key starter routes render 200.
- [x] G4.1: Inspect current starter routing.
- [x] G4.2: Refactor to clear route table / hierarchical file resolution in `index.uce`.
- [x] G4.3: Add starter-local `COMPONENT:asset` / `COMPONENT:island` style handlers in one unit.
- [x] G4.4: Use them efficiently in starter pages/layout.
- [x] G5: Demos and examples
- Why: Affordances must be visible to developers, not hidden in APIs.
- Done when: docs/demo/tests expose examples.
- Verify: demo URLs return 200 and tests pass.
- [x] G6: Verification and project docs
- Done when: build/test commands are run on `k-uce`, project notes updated, and adversarial review completed.
## Execution Queue
Complete.
## Decisions
- 2026-05-28: Defer component children/slots and JSX-like preprocessor syntax.
- 2026-05-28: Do not add global runtime head/assets/islands APIs; implement asset/island as starter-local components.
- 2026-05-28: Do not add a generic runtime file-based-routing system; demonstrate hierarchical/file routing inside starter `index.uce`.
- 2026-05-28: Keep collection helpers explicit (`list_*`, `dtree_*`) instead of overloading generic names such as `map`/`sort`.
## Assumptions
- Current source-of-truth mount is live-editable; runtime validation requires SSH to `k-uce`.
- Existing site tests are the right place for function-library coverage.
## Blockers and Risks
- No current blockers.
- Future risk: if UCE grows a full parser or component-tag syntax, keep this pass's explicit component/router APIs as a stable lower-level fallback.
## Evidence and Verification Log
- 2026-05-28: Created plan after reviewing README, preprocessor docs, and function library headers.
- 2026-05-28: `ssh k-uce 'cd /Code/uce.openfu.com/uce && bash scripts/build_linux.sh'` succeeded.
- 2026-05-28: Restarted `uce.service` on `k-uce`.
- 2026-05-28: `tests/run_network_tests.py --match core` passed.
- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dtree_group_by`.
- 2026-05-28: Full internal network suite passed, 25/25.
- 2026-05-28: Temporary broken `/tests/diagnostic-probe.uce` returned `500` with formatted `UCE compile error` diagnostics; source and cache artifacts were removed afterward.
## Change Log
- 2026-05-28: Created initial goal tree.
- 2026-05-28: Implemented helpers, diagnostics, docs, demo, starter router, starter-local web affordance components, tests, and validation.
## Follow-up Cleanup 2026-05-29
- Removed duplicate route cleanup from `starter_router_candidates()` because `app_make_route()` is the single normalization point for `l_path`.
- Weeded out nearby duplicate/obsolete starter code:
- `starter_router_add_candidate(...)` now owns repeated candidate tree construction.
- Removed unused `app_resolve_view()` / `starter_resolve_view()` after moving routing into starter `index.uce`.
- Removed unused legacy registered asset rendering functions from `lib/app.uce`; registered assets now render through `components/theme/web_affordances.uce`.
- `app_init()` now reuses `app_base_url(context)` instead of repeating base URL derivation.
- `web_affordances.uce` now uses one `starter_render_asset_group(...)` loop for CSS and JS.
- Verification: rebuilt on `k-uce`, restarted `uce.service`, checked key starter routes, and ran `tests/run_network_tests.py --match core` successfully.
## Follow-up Routed Views 2026-05-29
- Changed starter route dispatch from `unit_render(...)` to `component(...)`.
- Converted all routed `site/examples/uce-starter/views/*.uce` files to `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are central-router-only views.
- Updated the starter README and verified key starter routes. No service restart was required because only `.uce`/docs changed.
## Follow-up Canonical Starter URLs 2026-05-29
- Canonicalized starter self-links from `/examples/uce-starter/index.uce?...` to `/examples/uce-starter/?...` with `app_canonical_script_url(...)`.
- Updated the starter README to show canonical directory URLs.
- Touched the starter front controller to force the `#load`ed helper change into the cached generated unit.
- Verified canonical/direct starter routes and checked generated self-links. No service restart was required.
## Follow-up Not Found Component 2026-05-29
- Moved `starter_router_render_not_found` markup into `components/basic/notfound.uce`.
- Router now delegates 404 body rendering through `component("components/basic/notfound", props, context)`.
- Verified missing routes return `404` and normal dashboard route returns `200`. No service restart required.
## Follow-up Page Shell Component 2026-05-29
- Moved app page rendering into `themes/page.uce` as a component.
- Removed `app_render_page`, `app_theme_page_component`, and `starter_render_page` from `lib/app.uce`.
- Page template resolution now follows `context.call["app"]["page_type"]`: current theme first, common fallback second.
- Verified representative HTML routes and the JSON page-type fallback. No service restart required.
## Follow-up Deep Starter Context Cleanup 2026-05-29
- Removed repeated `starter_boot(context)` calls; root `index.uce` is the boot point.
- Removed `context.call["starter"]` duplicated state and JSON side-channel state.
- JSON routes now use only `context.call["app"]["page_type"]` plus normal captured output.
- Simplified `themes/page.uce` and `themes/common/page.json.uce` accordingly.
- Replaced `starter_*` alias helper usage with direct `app_*` helpers and removed alias wrappers except `StarterUser`.
- Verified key routes and core tests. No service restart required.
## Follow-up Route Context Flattening 2026-05-29
- Flattened `context.call["app"]["route"]` to `context.call["route"]`.
- Moved former `context.call["app"]["router"]` metadata into `context.call["route"]`.
- Verified representative starter HTML, 404, and JSON routes. No service restart required.
+674
View File
@@ -0,0 +1,674 @@
# UCE Runtime Setup
This guide describes how to run UCE behind nginx or Apache. UCE is a FastCGI application server for `.uce` units; the web server should serve static files directly and forward dynamic `.uce` requests to the UCE runtime.
## Deployment shape
A typical deployment has four pieces:
1. A checked-out or packaged UCE runtime tree.
2. `/etc/uce/settings.cfg`, read by the UCE runtime at startup.
3. `uce.service`, a systemd service that builds/starts/restarts the runtime.
4. nginx or Apache as the public HTTP server.
Recommended filesystem layout for a source checkout:
```text
/opt/uce/ UCE repository/runtime root
/var/www/html/ public web root served by nginx/Apache
/etc/uce/settings.cfg runtime configuration
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
/run/uce/cli.sock local CLI/admin/test socket
/var/cache/uce/work generated source, wasm modules, caches
/var/lib/uce/uploads multipart upload scratch space
/var/lib/uce/sessions session files
```
For packaged installs, the runtime may live under `/usr/lib/uce` instead of `/opt/uce`. Keep the public web root at `/var/www/html` or another normal web-root path, not under the runtime source tree.
## Build requirements
On Debian/Ubuntu-like systems, install the distro packages first:
```bash
apt update
apt install -y clang build-essential libpcre2-dev mariadb-client libmariadb-dev curl rsync ca-certificates
```
UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans.
- **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects:
- `/opt/wasmtime/include/wasmtime.hh`
- `/opt/wasmtime/include/wasmtime/*.h`
- `/opt/wasmtime/lib/libwasmtime.so`
- **Pinned WASI SDK** at `/opt/wasi-sdk` by default. `scripts/build_core_wasm.sh` and request-time `scripts/compile_wasm_unit` expect:
- `/opt/wasi-sdk/bin/clang++`
- `/opt/wasi-sdk/bin/wasm-ld`
- `/opt/wasi-sdk/bin/llvm-objcopy`
You can use different install locations by setting environment variables before building and in the systemd service environment:
```bash
export WASMTIME_HOME=/path/to/wasmtime
export WASI_SDK=/path/to/wasi-sdk
```
Install one web server:
```bash
apt install -y nginx
# or
apt install -y apache2
```
Build UCE from the repository root:
```bash
cd /opt/uce
bash scripts/build_core_wasm.sh
bash scripts/build_linux.sh
```
Publish the starter site or your application files into the web root:
```bash
mkdir -p /var/www/html
rsync -a site/ /var/www/html/
```
The main binary is written to:
```text
bin/uce_fastcgi.linux.bin
```
### Installing Wasmtime and WASI SDK
The UCE build does not download these dependencies for you. Install Wasmtime through a compatible distro package or a pinned upstream C API archive. Install WASI SDK with UCE's pinned installer, or unpack the same pinned archive under `/opt/wasi-sdk`.
Do not use `curl | sh` installers in production setup scripts. Download archives from the upstream release pages, verify checksums/signatures when available, and record the exact versions in your deployment notes. Avoid installing a release published in the last few days unless you have reviewed it separately.
The expected directories are:
```text
/opt/wasmtime/include/wasmtime.hh
/opt/wasmtime/include/wasmtime/store.h
/opt/wasmtime/lib/libwasmtime.so
/opt/wasi-sdk/bin/clang++
/opt/wasi-sdk/bin/wasm-ld
/opt/wasi-sdk/bin/llvm-objcopy
```
Install the WASI SDK:
```bash
cd /opt/uce
scripts/install_wasi_sdk.sh
scripts/install_wasi_sdk.sh --check-only
```
The current pin is documented in `docs/wasi-sdk-toolchain.md`. The script verifies the archive SHA256 before installing and updates `/opt/wasi-sdk` to point at the pinned versioned directory.
For Wasmtime, use a compatible distro package or an upstream C API archive. Example flow using an archive you have already chosen and verified:
```bash
mkdir -p /opt /tmp/uce-deps
cd /tmp/uce-deps
# Download the Wasmtime C API archive for your architecture from the upstream
# release page, then verify its checksum before unpacking. The archive name
# normally contains "c-api".
sha256sum -c wasmtime-c-api.sha256
mkdir -p /opt/wasmtime
tar -xf wasmtime-*-c-api*.tar.* -C /opt/wasmtime --strip-components=1
```
After unpacking, verify the tools UCE needs. Also record the exact Wasmtime and WASI SDK versions used. The native build embeds an rpath for `$WASMTIME_HOME/lib`, so the service environment should use the same `WASMTIME_HOME` value used during build.
```bash
test -f /opt/wasmtime/include/wasmtime.hh
test -f /opt/wasmtime/lib/libwasmtime.so
/opt/wasi-sdk/bin/clang++ --version
/opt/wasi-sdk/bin/wasm-ld --version
/opt/wasi-sdk/bin/llvm-objcopy --version
```
If your paths differ, export the variables for manual builds:
```bash
WASMTIME_HOME=/usr/local/wasmtime WASI_SDK=/usr/local/wasi-sdk bash scripts/build_core_wasm.sh
WASMTIME_HOME=/usr/local/wasmtime WASI_SDK=/usr/local/wasi-sdk bash scripts/build_linux.sh
```
For systemd, add an override:
```bash
systemctl edit uce.service
```
```ini
[Service]
Environment=WASMTIME_HOME=/usr/local/wasmtime
Environment=WASI_SDK=/usr/local/wasi-sdk
```
Then reload and restart:
```bash
systemctl daemon-reload
systemctl restart uce.service
```
## Runtime configuration
Create `/etc/uce/settings.cfg` from `etc/uce/settings.cfg` and adjust paths if your runtime is not under `/opt/uce`. Replace any checkout-specific paths such as `WASM_CORE_PATH` with `/opt/uce/bin/wasm/core.wasm` or your actual runtime path.
Minimum useful settings:
```ini
BIN_DIRECTORY=/var/cache/uce/work
TMP_UPLOAD_PATH=/var/lib/uce/uploads
SESSION_PATH=/var/lib/uce/sessions
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
CLI_SOCKET_PATH=/run/uce/cli.sock
SITE_DIRECTORY=/var/www/html
HTTP_DOCUMENT_ROOT=/var/www/html
JIT_COMPILE_ON_REQUEST=1
PROACTIVE_COMPILE_ENABLED=1
PROACTIVE_COMPILE_CHECK_INTERVAL=60
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
WASM_BACKEND_VERBOSE=0
WASM_CORE_PATH=/opt/uce/bin/wasm/core.wasm
WASM_MEMORY_LIMIT_BYTES=536870912
WASM_EPOCH_DEADLINE_TICKS=200
WASM_EPOCH_PERIOD_MS=50
WORKER_COUNT=4
MAX_MEMORY=16777216
SESSION_TIME=2592000
HTTP_PORT=8080
```
Important settings:
- `FCGI_SOCKET_PATH` is the Unix socket used for normal `.uce` requests. Set it explicitly and keep this value and the web-server `fastcgi_pass` path identical. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units.
- `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory.
- `HTTP_DOCUMENT_ROOT` is the root used by the built-in HTTP/WebSocket listener when it resolves upgrade requests. Set it to the same web root as nginx/Apache.
- `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches.
- `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime.
- `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point.
- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host.
- `WASM_CORE_PATH` must point at the built `core.wasm` file.
After editing settings, restart UCE:
```bash
systemctl restart uce.service
```
## systemd service
For source-checkout deployments, install the provided service helper:
```bash
cd /opt/uce
scripts/systemd/manage-uce-service.sh setup
```
That helper installs `scripts/systemd/uce.service`, creates runtime directories, enables the service, and starts it.
Useful commands:
```bash
scripts/systemd/manage-uce-service.sh status
scripts/systemd/manage-uce-service.sh restart
scripts/systemd/manage-uce-service.sh logs 200
```
Equivalent manual systemd service for a source checkout:
```ini
[Unit]
Description=UCE FastCGI Runtime
After=network-online.target mariadb.service memcached.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/uce
RuntimeDirectory=uce
StateDirectory=uce
CacheDirectory=uce
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
ExecStartPre=/usr/bin/bash /opt/uce/scripts/build_linux.sh
ExecStart=/opt/uce/bin/uce_fastcgi.linux.bin
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
Restart=always
RestartSec=2
TimeoutStopSec=15
KillMode=mixed
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
```
Install it as `/etc/systemd/system/uce.service` and run:
```bash
systemctl daemon-reload
systemctl enable --now uce.service
```
### Debian package build
To build a Debian package from the repository root:
```bash
bash scripts/make_deb.sh 0.1.2
```
The Debian package creator bundles WASI SDK and Wasmtime by default when `/opt/wasi-sdk` and `/opt/wasmtime` are present. Verify the pinned SDK before building:
```bash
scripts/install_wasi_sdk.sh --check-only
bash scripts/make_deb.sh 0.1.2
```
This includes the resolved `/opt/wasi-sdk-...` tree, `/opt/wasi-sdk` symlink, resolved `/opt/wasmtime-...` tree, and `/opt/wasmtime` symlink in the package. It makes the package large, but keeps request-time unit compilation and runtime linking tied to the toolchain versions that passed the test suite. Set `UCE_DEB_BUNDLE_WASI_SDK=0` or `UCE_DEB_BUNDLE_WASMTIME=0` only if your deployment provides those exact dependencies separately.
### RPM package build
To build an RPM package from the repository root, install `rpmbuild` on the packaging host, verify the pinned SDK, then run:
```bash
scripts/install_wasi_sdk.sh --check-only
bash scripts/make_rpm.sh 0.1.2
```
The RPM creator mirrors the Debian package layout: runtime files under `/usr/lib/uce`, public files under `/var/www/html`, config under `/etc/uce/settings.cfg`, systemd unit under `/usr/lib/systemd/system/uce.service`, and bundled `/opt/wasi-sdk` plus `/opt/wasmtime` trees by default. Set `UCE_RPM_BUNDLE_WASI_SDK=0` or `UCE_RPM_BUNDLE_WASMTIME=0` only if your deployment provides those exact dependencies separately.
## How request routing works
### Static files
The web server should serve ordinary static files directly from the public web root, for example `/var/www/html`.
Examples:
```text
/style.css
/images/logo.png
/examples/uce-starter/js/site.js
```
These should not touch the UCE runtime.
### Normal `.uce` page requests
For a request such as:
```text
GET /doc/index.uce?p=component
```
nginx/Apache forwards the request to `FCGI_SOCKET_PATH` as FastCGI. The web server must provide CGI/FastCGI variables including:
- `SCRIPT_FILENAME` — full filesystem path to the `.uce` file.
- `DOCUMENT_ROOT` — public web root, normally `/var/www/html` or whatever nginx/Apache uses as `root`/`DocumentRoot`.
- `SCRIPT_NAME` — URL path to the script, such as `/doc/index.uce`.
- `DOCUMENT_URI` — normalized URI path without query string.
- `REQUEST_URI` — original request URI including query string.
- standard request variables such as method, query string, content type, body length, cookies, and headers.
UCE resolves the unit, compiles it to wasm if needed, creates a request workspace, and calls:
```cpp
RENDER(Request& context)
```
The unit writes output with template literals or `print()`. Response headers and status are set through `context.header` and `context.set_status()`.
### Component and sub-render calls
Inside a request, UCE code can call other units:
```cpp
component("components/card", props, context);
unit_render("other-page.uce", context);
```
These calls stay inside the UCE runtime. They are not new HTTP requests and do not go back through nginx or Apache.
### WebSocket pages
Any `.uce` unit can provide both an ordinary page render and WebSocket message handling:
```cpp
RENDER(Request& context) { ... } // normal page load
WS(Request& context) { ... } // later WebSocket messages
```
The nginx and Apache examples below split traffic by checking for a WebSocket upgrade request on `.uce` paths. A file such as `chat.uce` or `events.uce` can expose `WS(Request& context)`.
Routing split:
- Plain `GET /demo/chat.uce` should use FastCGI, just like any other page render.
- WebSocket upgrade requests for `/demo/chat.uce` should proxy to the UCE built-in HTTP/WebSocket listener at `HTTP_PORT`.
The built-in listener owns the socket lifecycle. When a message arrives, the broker forwards a render-style invocation back to the worker pool so `WS(Request& context)` runs inside the same wasm runtime model as normal pages.
### CLI requests
`CLI(Request& context)` handlers are not public web endpoints. They are invoked over `CLI_SOCKET_PATH`:
```bash
scripts/uce-cli /tests/cli.uce action=echo message=hello
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
```
Use CLI units for local tests, admin commands, and maintenance tools. Do not expose the CLI socket through nginx or Apache.
### Custom runtime HTTP servers
UCE code can start local custom HTTP listeners with `server_start_http()`. Those are runtime-managed listeners for app-specific local services. They are separate from the public nginx/Apache entry point and should be firewalled or bound locally unless you explicitly want them reachable.
## nginx configuration
### Required modules
A normal nginx build includes the needed FastCGI and proxy modules. Confirm nginx is installed and can load your config:
```bash
nginx -t
```
### WebSocket upgrade map
Put this in the nginx `http` block if using WebSockets:
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
```
### Server block
Example site config:
```nginx
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.uce index.html;
# Serve static files directly. Directory requests use index.uce when present.
location / {
try_files $uri $uri/ =404;
}
# UCE page requests use FastCGI. If the client asks to upgrade a .uce
# request to WebSocket, send that connection to the built-in listener.
location ~ \.uce$ {
error_page 418 = @uce_websocket;
if ($http_upgrade = "websocket") {
return 418;
}
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param DOCUMENT_URI $uri;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_pass unix:/run/uce/fastcgi.sock;
}
location @uce_websocket {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:8080;
}
# Defense in depth if the root is changed later.
location ~ ^/(src|scripts|etc|bin|work|dist|pkg|docs|changelog)/ {
return 404;
}
}
```
Notes:
- `fastcgi_pass` must match `FCGI_SOCKET_PATH`.
- `proxy_pass` must match `HTTP_PORT`.
- The example routes WebSocket upgrades for `.uce` paths to the HTTP/WebSocket listener.
- The built-in HTTP/WebSocket listener resolves scripts from `HTTP_DOCUMENT_ROOT`; do not depend on client-supplied or proxied `Script-Filename` headers for routing.
- Ordinary `.uce` page loads continue to use FastCGI.
- Keep `root` pointed at `/var/www/html`, not the runtime repository root.
- If your app uses a front controller, replace `location /` with a `try_files` rule that ends at `/index.uce`.
Front-controller variant:
```nginx
location / {
try_files $uri $uri/ /index.uce?$query_string;
}
```
Reload nginx:
```bash
nginx -t
systemctl reload nginx
```
## Apache configuration
Apache can run UCE through `mod_proxy_fcgi` for FastCGI and `mod_proxy_wstunnel` or `mod_proxy_http` for WebSocket upgrades.
### Enable modules
On Debian/Ubuntu:
```bash
a2enmod proxy proxy_fcgi proxy_http proxy_wstunnel rewrite headers setenvif
systemctl restart apache2
```
### VirtualHost example
```apache
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
<Directory /var/www/html>
Require all granted
Options FollowSymLinks
AllowOverride None
DirectoryIndex index.uce index.html
</Directory>
# Do not expose repository internals if DocumentRoot changes later.
<LocationMatch "^/(src|scripts|etc|bin|work|dist|pkg|docs|changelog)/">
Require all denied
</LocationMatch>
RewriteEngine On
# WebSocket upgrade traffic for any .uce unit goes to UCE's built-in HTTP listener.
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteCond %{REQUEST_URI} \.uce(?:\?|$) [NC]
RewriteRule ^/(.*)$ ws://127.0.0.1:8080/$1 [P,L]
# Normal .uce page loads go to FastCGI.
<FilesMatch "\.uce$">
SetHandler "proxy:unix:/run/uce/fastcgi.sock|fcgi://localhost/"
</FilesMatch>
# Optional: make the key CGI variables explicit for UCE.
ProxyFCGISetEnvIf "true" DOCUMENT_ROOT "/var/www/html"
ProxyFCGISetEnvIf "true" DOCUMENT_URI "%{REQUEST_URI}"
</VirtualHost>
```
Apache notes:
- `SetHandler "proxy:unix:/run/uce/fastcgi.sock|fcgi://localhost/"` must use the same socket path as `FCGI_SOCKET_PATH`.
- The WebSocket rewrite rule must run before the FastCGI handler.
- Plain `.uce` page loads should not be proxied as WebSockets unless the client sends `Upgrade: websocket`.
- Apache's FastCGI environment differs by version and module configuration. If UCE cannot resolve a page, inspect the request environment and make sure `SCRIPT_FILENAME` points to the target file under the web root.
If your Apache version does not populate `SCRIPT_FILENAME` correctly through `SetHandler`, use `ProxyPassMatch` for `.uce` files instead:
```apache
ProxyPassMatch ^/(.*\.uce)$ unix:/run/uce/fastcgi.sock|fcgi://localhost/var/www/html/$1
```
Use only one FastCGI mapping style at a time (`SetHandler` or `ProxyPassMatch`) to avoid duplicate routing.
## Permissions
The web server needs permission to connect to `/run/uce/fastcgi.sock`. Common approaches:
- run UCE and the web server under compatible groups;
- add the web server user (`www-data` on Debian/Ubuntu) to the socket's group;
- adjust the service or runtime socket mode if needed.
The runtime creates the FastCGI socket and CLI socket under `/run/uce`. The CLI socket should remain local-only and should not be reachable from the public web server.
Writable paths for the runtime:
```text
/var/cache/uce/work
/var/lib/uce/uploads
/var/lib/uce/sessions
/run/uce
```
## Verification
Check service state:
```bash
systemctl status uce.service
journalctl -u uce.service -n 100 --no-pager
```
Check the FastCGI/web-server path:
```bash
curl -i http://127.0.0.1/doc/index.uce -H 'Host: example.com'
curl -i http://127.0.0.1/examples/uce-starter/ -H 'Host: example.com'
```
Check the local CLI path:
```bash
cd /opt/uce
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/run_cli_tests.sh
```
Check WebSocket routing with a WebSocket client against a `.uce` endpoint that defines `WS(Request& context)` through nginx/Apache, not directly against `HTTP_PORT`:
```bash
python3 - <<'PY'
import base64, os, socket
host = "example.com"
path = "/chat.uce"
key = base64.b64encode(os.urandom(16)).decode()
request = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n"
).encode()
sock = socket.create_connection(("127.0.0.1", 80), timeout=5)
sock.sendall(request)
print(sock.recv(4096).decode("latin1", "replace").split("\r\n", 1)[0])
sock.close()
PY
```
## Operational footguns
- Keep the FastCGI socket path consistent: `FCGI_SOCKET_PATH` and the web-server `fastcgi_pass` must match exactly. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
- Keep the public web root separate from the runtime source tree. The examples use `/opt/uce` for runtime files and `/var/www/html` for public files.
- Set `HTTP_DOCUMENT_ROOT` when the web root is outside the runtime working directory. The built-in HTTP/WebSocket listener resolves upgrade paths from this setting.
- Do not expose `CLI_SOCKET_PATH` or `HTTP_PORT` as public entry points. The public path should be nginx/Apache.
- Do not trust `Script-Filename` request headers from direct HTTP clients. The built-in HTTP listener resolves from `HTTP_DOCUMENT_ROOT` and rejects `..` path segments.
- WASI SDK is a deployment/runtime dependency, not just a developer build tool. UCE compiles units to wasm on demand during requests and during proactive startup scans, so each host must use the pinned SDK version documented in `docs/wasi-sdk-toolchain.md`.
- After toolchain or compile-script fixes, clear stale failed artifacts under `BIN_DIRECTORY`; otherwise a later request may report an old compile failure.
## Troubleshooting
### 502 Bad Gateway
Check:
- `systemctl status uce.service`
- `journalctl -u uce.service -n 200 --no-pager`
- socket path in web server config equals `FCGI_SOCKET_PATH`
- web server user can connect to the Unix socket
- `SCRIPT_FILENAME` resolves to an existing `.uce` file
### Raw `.uce` source is downloaded or displayed
The `.uce` request did not match the FastCGI rule. Check location/order rules and confirm the public root is `/var/www/html` or your chosen web-root path.
### Static files 404
Confirm the web server `root`/`DocumentRoot` is `/var/www/html` or your chosen web-root path and that `location /` or Apache directory rules allow static file reads.
### WebSocket page renders but upgrade fails
Check:
- the client sends `Upgrade: websocket`
- `.uce` upgrade traffic reaches `HTTP_PORT`
- nginx/Apache preserves `Upgrade` and `Connection` headers
- `HTTP_DOCUMENT_ROOT` matches the web-server root; if it points at the runtime tree while files live in `/var/www/html`, the built-in listener will return `404 script not found`
- firewall/network policy allows localhost access to `HTTP_PORT`
### Page compiles fail
Check the compile artifact paths shown in the UCE error response and service logs. Generated files and compile output live under `BIN_DIRECTORY`.
Common compile footguns:
- `WASM_COMPILE_SCRIPT` is unset or points at a removed script such as `scripts/compile`; set it to `scripts/compile_wasm_unit`.
- `scripts/check_unit_wasm.py` is missing or not executable; `scripts/compile_wasm_unit` calls it after linking each unit.
- `WASI_SDK` does not point at the pinned tree with `clang++`, `wasm-ld`, `llvm-objcopy`, and `llvm-nm`; run `scripts/install_wasi_sdk.sh --check-only`.
- `WASMTIME_HOME` does not point at a tree with Wasmtime headers and `libwasmtime.so`.
- A previous failed compile left stale `.compile.txt`, `.wasm-check.txt`, or partial `.wasm` files under `BIN_DIRECTORY`.
Failed compile output is persisted under the unit's generated path in `BIN_DIRECTORY` and may be reused until the source or compiler inputs change. First fix the source/toolchain issue and reload the page. If the cache itself is suspect, stop UCE, move only the affected unit artifact files or directory aside, and restart so the runtime recompiles from source. Avoid deleting the whole `BIN_DIRECTORY` unless you intentionally want a full rebuild.
### CLI commands fail
Check:
- `CLI_SOCKET_PATH` in `/etc/uce/settings.cfg`
- `/run/uce/cli.sock` exists
- `scripts/uce-cli --socket /run/uce/cli.sock /ping` works
- the target unit defines `CLI(Request& context)`
+55
View File
@@ -0,0 +1,55 @@
# WASI SDK Toolchain Pin
UCE treats WASI SDK as a deployment/runtime dependency, not just a developer build tool.
The runtime compiles `.uce` units to wasm on demand during requests and during the proactive compiler scan. That means every deployment host must have the same compiler/linker toolchain available, and the generated `.wasm`/`.cwasm` artifacts are tied to that toolchain version and UCE unit ABI version.
## Current pin
- Upstream: <https://github.com/WebAssembly/wasi-sdk>
- Release tag: `wasi-sdk-33`
- Version: `33.0`
- Linux x86_64 archive: `wasi-sdk-33.0-x86_64-linux.tar.gz`
- URL: `https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-linux.tar.gz`
- SHA256: `0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce`
- Expected install symlink: `/opt/wasi-sdk`
- Expected resolved path: `/opt/wasi-sdk-33.0-x86_64-linux`
Install or verify with:
```bash
scripts/install_wasi_sdk.sh
scripts/install_wasi_sdk.sh --check-only
```
## Required tools
UCE expects these executables on each deployment host:
```text
/opt/wasi-sdk/bin/clang++
/opt/wasi-sdk/bin/wasm-ld
/opt/wasi-sdk/bin/llvm-objcopy
/opt/wasi-sdk/bin/llvm-nm
```
`llvm-nm` is used by `scripts/check_unit_wasm.py`, which is called by `scripts/compile_wasm_unit` after linking each unit.
## Upgrade policy
Treat WASI SDK upgrades like runtime dependency upgrades:
1. Update `scripts/install_wasi_sdk.sh` version, URL, and SHA256.
2. Record the new release and checksum here.
3. Rebuild `bin/wasm/core.wasm` with `scripts/build_core_wasm.sh`.
4. Rebuild the native runtime with `scripts/build_linux.sh`.
5. Clear or invalidate stale unit wasm artifacts by bumping `UCE_UNIT_ABI_VERSION` when required, or by removing affected generated artifacts under `BIN_DIRECTORY`.
6. Run the full CLI suite including wasm kill tests:
```bash
scripts/run_cli_tests.sh --include-wasm-kill
```
## Known footgun
WASI SDK 33's `llvm-nm` was observed to crash on a degenerate but valid unit module with no exported handlers. `scripts/check_unit_wasm.py` treats that specific validator-tool crash as a skipped allocator-definition scan while still rejecting forbidden allocator exports and other ABI violations. This is one reason the toolchain is pinned instead of relying on whatever `/opt/wasi-sdk` happens to contain.
+305
View File
@@ -0,0 +1,305 @@
# UCE WASM Runtime Architecture
Status: current as of the W7e native-pipeline removal (June 2026). This document
describes the **runtime architecture as built** — the process topology, the
wasm membrane, the unified request dispatch, and the central WebSocket broker.
Native `.so` unit execution/dlopen fallback has been removed; the parser and
preprocessor remain only as the front-end that emits C++ for wasm side-module
compilation.
The guiding principle: **request code never shares an address space or an
allocator with the runtime.** Every unit runs as a WebAssembly module inside a
per-request workspace behind a narrow host membrane. Long-lived connection
state (WebSocket connections, listening HTTP sockets) lives in dedicated native
broker processes that own no unit code — they hold the connection and forward
the actual unit invocation to a clean-engine worker, exactly like a normal
page render.
---
## 1. Process topology
A single native binary (`uce_fastcgi`) forks into a small set of long-lived
roles. None of them is special-cased per request mode; the differences are
purely *which socket a process listens on* and *which function inside a unit
gets invoked*.
```
┌────────────────────────────┐
nginx ──FastCGI──► worker pool (N processes) │ /run/uce.sock
(port 80 etc.) │ uniform unit renderers │ (FastCGI + CLI)
└─────────────▲──────────────┘
│ forward render (FastCGI, uce.sock)
│
browser ──raw HTTP / WS──► ┌────────┴─────────┐
(HTTP_PORT 8080) │ WS broker │ owns HTTP_PORT + every
│ (1 process) │ WS connection; renders
└────────▲─────────┘ nothing itself
│ ws_* command flush
│ (FastCGI, ws-broker.sock)
on-demand serve_http ──► ┌────────┴─────────┐
(per bind addr) │ custom-server │ owns one serve_http
│ dispatcher(s) │ bind addr; forwards to pool
└──────────────────┘
parent process: spawns/respawns all of the above + the proactive compiler.
```
| Process | Owns | Renders units? | Source |
|---|---|---|---|
| **Parent** | nothing; supervises children | no | `main()`, `init_base_process()` |
| **Worker** (×`WORKER_COUNT`) | `FCGI_SOCKET_PATH` (`/run/uce.sock`) + `CLI_SOCKET_PATH` | **yes** — the only processes that run wasm | `listen_for_connections()` |
| **WS broker** (×1) | `HTTP_PORT` + every live WS connection + `WS_BROKER_SOCKET_PATH` | no — forwards to the pool | `run_ws_broker()` |
| **serve_http dispatcher** (×bind) | one custom-server bind address | no — forwards to the pool | `custom_server_http_dispatcher_loop()` |
| **Proactive compiler** | nothing; pre-compiles units | no | `run_proactive_compiler()` |
**only workers instantiate Wasmtime and run unit code.**
Every connection-owning process (broker, serve_http dispatcher) forwards the
request invocation back to a worker over `/run/uce.sock` using the minimal
FastCGI client in [`src/lib/fcgi_forward.h`](../src/lib/fcgi_forward.h). This is
forced by Wasmtime: an `Engine`/`Store` cannot be safely re-created across
`fork()`, and the brokers fork from the parent that already touched the
runtime. So the brokers hold the long-lived connection and units respond to
events the same way they respond to a page request — through a clean worker.
---
## 2. The membrane and the DValue ABI
Units are compiled to WebAssembly and linked against a host import surface (the
"membrane"). Host and guest exchange structured values as **UCEB** (a compact
binary `DValue` encoding — `ucb_encode`/`ucb_decode`); strings cross as
`std::string` on the native side throughout (no raw `char*` ownership across
the boundary).
- `DValue` is the universal value type: scalars plus an ordered child map
(`_map`, a `std::map<String,DValue>`). `operator[]` is non-const (creates);
`.key(k)` is the const probe returning `const DValue*`; `.each(fn)` iterates
children yielding `const DValue&`.
- The request context (`params`/`get`/`post`/`cookies`/`session`, the raw body
`in`, and—for WS—the connection context) is marshalled into a single `ctx`
DValue, UCEB-encoded, and handed to the workspace. The response (body,
headers, status, and any `meta` such as `ws_commands`) comes back the same
way.
See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire
format details.
---
## 3. Units, handlers, and export naming
A `.uce` unit compiles to a wasm module that exports one symbol per **handler**.
There is no per-mode machinery — a "CLI unit", "WebSocket unit", and "page" are
the same compiled artifact invoked at different exports. The dispatcher passes a
**handler string**; the host maps it to an export symbol:
```
__uce_<base>[_<sanitize(suffix)>]
```
`handler_export_symbol()` (`src/wasm/worker.cpp`) splits on the first `:`:
| Handler string | Export symbol |
|---|---|
| `render` | `__uce_render` |
| `cli` | `__uce_cli` |
| `websocket` | `__uce_websocket` |
| `once` | `__uce_once` (optional; absence is not an error) |
| `serve_http` | `__uce_serve_http` |
| `serve_http:named` | `__uce_serve_http_named` |
| `component:CARD` | `__uce_component_CARD` |
| `exists` | probe only — resolves the unit, loads nothing |
`sanitize_symbol_suffix()` keeps `[A-Za-z0-9_]` (mirrors `ascii_safe_name`).
`wasm_resolve_target(unit, handler)` (`src/wasm/core.cpp`) resolves the source
path and looks up the export's funcref slot; `exists` lets callers probe a unit
without instantiating it.
`wasm_backend_should_handle(request, entry_unit)` checks whether the wasm
backend is initialized and the requested artifact/handler is currently
available. If an artifact is cold or stale, dispatch compiles it on demand via
`get_shared_unit()` and rechecks. There is no native unit-execution fallback.
---
## 4. The workspace runtime
Each request gets a fresh **workspace** — a per-request wasm instance tree with
the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)`
(`src/wasm/worker.cpp`) is the single entry point for *every* mode:
1. Birth a workspace (CoW-snapshot-based where available).
2. Resolve `entry_unit` + `handler` to an export; components referenced at
runtime are resolved on demand via the `uce_host_component_resolve` hostcall
(`component_resolve()` → `__uce_<...>` slot), loading dependency modules
lazily and recording resolve counts/timings.
3. Invoke the export. Unit code calls host functions (filesystem, sqlite, regex,
markdown, time, tasks, `ws_*`, …) across the membrane.
4. Collect the response (`WasmResponse`: body, headers, status, `meta`).
Because the workspace owns no process-lifetime state, a unit crash or trap is
contained: it fails the one request and the worker stays healthy. Workers
suspend the native SIGSEGV/SIGILL recovery handler around the wasm call so that
Wasmtime's own trap signals are not escalated into a native fatal signal (see
`serve_via_wasm` in `handle_complete`).
---
## 5. Request dispatch (`handle_complete`)
`handle_complete()` (`src/linux_fastcgi.cpp`) is the worker's single dispatch
point. It selects a handler string from request params and calls the same
`serve_via_wasm(entry_unit, handler)` lambda for all of them:
```
UCE_WS == "1" → serve_via_wasm(entry_unit, "websocket")
request.resources.is_cli → serve_via_wasm(entry_unit, "cli")
UCE_SERVE_HTTP == "1" → serve_via_wasm(entry_unit, "serve_http"[:fn])
otherwise (page) → serve_via_wasm(entry_unit, "render")
```
The `UCE_*` params are set by whichever broker forwarded the request:
- **Page render**: FastCGI nginx → `/run/uce.sock` directly; no `UCE_*` flags → `render`.
- **CLI**: the CLI socket sets `is_cli`.
- **serve_http**: the custom-server dispatcher sets `UCE_SERVE_HTTP=1` plus
`UCE_SERVE_HTTP_FUNCTION` and rewrites `SCRIPT_FILENAME` to the configured
unit (`custom_server_http_complete`).
- **WebSocket**: the WS broker sets `UCE_WS=1` and carries the connection
identity as `UCE_WS_*` params (see §6). `handle_complete` rebuilds
`request.resources.websocket_*` and `request.connection` from them before
invoking `__uce_websocket`.
If a unit still cannot be served after the on-demand wasm compile, the worker
returns a clean 500 with the wasm/compile error; it does not execute native unit
code.
---
## 6. The central WebSocket broker
**One process owns the HTTP port
and every WebSocket connection**, so any unit's `ws_*` call can reach any or all
connections, and a unit-code crash (which happens in a worker) never drops live
connections.
### 6.1 Inbound: a WS frame → a worker render (non-blocking)
`ws_broker_ws_message(request, message, opcode)` fires when a complete
(reassembled) WS message arrives on a connection. It does **not** block the
broker loop:
1. Build FastCGI params: `SCRIPT_FILENAME`, `REQUEST_METHOD=GET`, a
`REQUEST_URI` (required — `handle_request()` rejects requests without one
*before* `on_complete` runs), `UCE_WS=1`, and the connection context as
`UCE_WS_CONNECTION_ID / SCOPE / OPCODE / BINARY / CONNECTIONS / STATE`.
2. The message rides as `UCE_WS_MESSAGE` (base64) with an **empty STDIN body** —
a non-empty STDIN makes the FastCGI transport flush a premature response
before `on_complete` ever runs.
3. Connect to `/run/uce.sock` (non-blocking) and queue the encoded request in
`ws_broker_outbound[fd]`.
`ws_broker_drain_outbound()` runs after every `process(50)` tick: it finishes
writing each queued request, then drains and discards the reply (the unit's
output comes back via the command socket, not this reply), closing the fd when
the worker closes its end.
### 6.2 Outbound: `ws_*` commands flushed back to the broker
Any unit code — not just WebSocket handlers — may call `ws_send` / `ws_send_to`
/ `ws_close`. In the workspace these **record dispatch commands** rather than
touching a socket (the workspace owns no connections); `wasm-core`'s `ws_*`
(`src/lib/sys.cpp`) append to `websocket_dispatch_commands`, and
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands` (plus
`ws_connection_state` if the handler mutated per-connection state).
`wasm_backend_serve` (`src/wasm/backend.cpp`) flushes that batch at workspace
teardown — in **any** scenario, not just WS handlers — to the broker's command
socket (`WS_BROKER_SOCKET_PATH`, `/run/uce/ws-broker.sock`) via
`fcgi_forward_request` with `UCE_WS_DISPATCH=1`. This is the only path WS data
takes out of a workspace.
`ws_broker_apply_commands()` decodes the batch and applies each command against
the full registry it owns: `broadcast` (by scope), `send_to` (by connection id),
`close`. If the batch carries `connection_state`, it persists that onto the
matching live connection's `websocket_state`.
### 6.3 Un-upgraded HTTP on the WS port
The WS port can also receive ordinary (non-Upgrade) HTTP requests.
`ws_broker_complete()` routes by param: `UCE_WS_DISPATCH=1` → apply commands;
otherwise → `forward_request_to_worker()` — the *same* shared facility the
serve_http dispatcher uses, so there is no duplicated request-forwarding code.
### 6.4 The broker loop
`run_ws_broker()` drops the worker listeners it inherited
(`close_inherited_server_sockets`), installs permissive `on_request`/`on_data`
handlers (it renders nothing, so it accepts every request straight through to
`on_complete`), wires `on_complete=ws_broker_complete` and
`on_websocket_message=ws_broker_ws_message`, listens on `HTTP_PORT` and the
command socket, and loops `process(50)` + `drain_outbound()`. The design is
**non-blocking outbound dispatch + async command-socket flush, all in the
broker's single epoll loop** — the broker never blocks on a worker.
The parent respawns the broker if it dies (`ws_broker_alive` / `ensure_ws_broker`
in `main()`).
---
## 7. The serve_http facility
`serve_http` units are reachable on their own bind address via a custom-server
dispatcher (`custom_server_http_dispatcher_loop`). The dispatcher owns the bind
socket and, on each request, sets `UCE_SERVE_HTTP=1` + the configured unit/
function and calls `forward_request_to_worker()`. The worker then runs
`serve_via_wasm(entry_unit, "serve_http"[:fn])`. This is the same
hold-connection-forward-render model as the WS broker, sharing
`forward_request_to_worker` and `fcgi_forward.h`.
---
## 8. Build / object layout
The native side is split into separately-compiled objects so editing the wasm
runtime does not recompile the whole TU (`scripts/build_linux.sh`):
| Object | Contents |
|---|---|
| `bin/sqlite3.o` | sqlite amalgamation (cached) |
| `bin/wasm.o` | `src/wasm/wasm_module.cpp` (backend.cpp + worker.cpp + wasmtime) |
| `bin/main.o` | `src/linux_fastcgi.cpp` (includes `lib/uce_lib.cpp`) |
Linked into one `-rdynamic` binary. ODR hazards across the objects are handled
deliberately: `context` and `my_pid`/`parent_pid` are `extern` (guarded for the
wasm core vs. unit builds), `operator new`/`delete` live in `types.cpp`, and
header free-functions are `inline`. The wasm backend exposes only declarations
(`src/wasm/backend.h`) to `main.o`.
---
## 9. Configuration keys
| Key | Default | Meaning |
|---|---|---|
| `WASM_BACKEND_VERBOSE` | `0` | Emit `X-UCE-Wasm-*` workspace timing headers (benchmark only). |
| `FCGI_SOCKET_PATH` | `/run/uce.sock` | Worker pool FastCGI socket (brokers forward here). |
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI socket. |
| `HTTP_PORT` | `8080` | Raw HTTP + WebSocket port — owned by the WS broker. |
| `WS_BROKER_SOCKET_PATH` | `/run/uce/ws-broker.sock` | Broker command socket for `ws_*` flushes. |
| `WORKER_COUNT` | `4` | Number of uniform worker processes. |
---
## 10. Testing
- **Regression gate**: `scripts/run_cli_tests.sh --include-wasm-kill` runs the
in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages.
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
broker → worker → broker → client chain across process boundaries.
+14
View File
@@ -11,6 +11,10 @@ FCGI_PORT=9993
# Example: curl --unix-socket /run/uce/cli.sock http://localhost/ping
CLI_SOCKET_PATH=/run/uce/cli.sock
# Built-in HTTP/WebSocket listener used for WebSocket Upgrade requests.
# Keep this behind nginx/Apache on localhost or firewall it from public access.
HTTP_PORT=8080
# OPTIONAL PROACTIVE COMPILE ROOT
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
PRECOMPILE_FILES_IN=
@@ -21,6 +25,16 @@ SITE_DIRECTORY=site
# ENABLE JIT COMPILATION WHEN A PAGE REQUEST HITS A STALE OR MISSING UNIT
JIT_COMPILE_ON_REQUEST=1
# WASM SIDE-MODULE COMPILER USED FOR .uce UNITS
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
# WASM RUNTIME SETTINGS. Unit execution is always routed through wasm.
WASM_BACKEND_VERBOSE=0
WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm
WASM_MEMORY_LIMIT_BYTES=536870912
WASM_EPOCH_DEADLINE_TICKS=200
WASM_EPOCH_PERIOD_MS=50
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
PROACTIVE_COMPILE_ENABLED=1
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Guard the hand-maintained unit-facing API coverage manifest.
This intentionally avoids network/external services. It checks that public API
names we expose to wasm units are either mentioned by a site test or explicitly
marked internal/integration-only, and that active docs exist for doc-required
APIs. The manifest is deliberately source-controlled so a new public function
requires an explicit coverage decision.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEST_DIR = ROOT / "site" / "tests"
DOC_DIR = ROOT / "site" / "doc" / "pages"
# name, needs_doc, status. status: public | internal | integration
PUBLIC_APIS = [
("shell_exec", True, "public"), ("shell_escape", True, "public"),
("basename", True, "public"), ("dirname", True, "public"), ("path_join", True, "public"),
("path_real", True, "public"), ("path_is_within", True, "public"),
("file_open_locked", True, "public"), ("file_close_locked", True, "public"),
("file_release_process_locks", True, "public"), ("file_get_contents_locked_fd", True, "public"),
("file_put_contents_locked_fd", True, "public"), ("file_get_contents", True, "public"),
("file_put_contents", True, "public"), ("file_append_contents", False, "public"),
("cwd_get", True, "public"), ("cwd_set", True, "public"), ("process_start_directory", True, "public"),
("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"),
("ls", True, "public"), ("config_map_u64", True, "public"), ("config_map_f64", True, "public"),
("config_bool_value", True, "public"), ("config_map_bool", True, "public"),
("config_u64", True, "public"), ("config_f64", True, "public"), ("config_bool", True, "public"),
("request_perf", True, "public"), ("time_format_local", True, "public"),
("time_format_relative", True, "public"), ("time_parse", True, "public"),
("backtrace_frames_string", False, "public"), ("capture_backtrace_string", False, "public"),
("signal_name", False, "public"), ("memcache_escape_key", True, "public"),
("memcache_escape_keys", True, "public"), ("memcache_command", True, "public"),
("memcache_get_multiple", True, "public"), ("runtime_safe_key", True, "public"),
("float_val", True, "public"), ("nibble", True, "public"), ("json_consume_space", False, "public"),
("array_merge", True, "public"), ("safe_name", True, "public"), ("ascii_safe_name", True, "public"),
("to_json", False, "public"), ("remove", False, "public"), ("clear", False, "public"),
("gen_sha1", True, "public"), ("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"),
("draw_int", True, "public"), ("draw_float", True, "public"),
("encode_query", True, "public"), ("request_script_url", True, "public"),
("request_base_url", True, "public"), ("request_route_from_raw_path", True, "public"),
("cli_arg", True, "public"), ("unit_compile", True, "public"),
("cleanup_sqlite_connections", False, "internal"), ("cleanup_mysql_connections", False, "internal"),
("mysql_connect", True, "integration"), ("mysql_query", True, "integration"),
]
REMOVED_APIS = ["unit_load", "concat"]
def all_test_text() -> str:
parts = []
for path in TEST_DIR.glob("*.uce"):
parts.append(path.read_text(errors="ignore"))
return "\n".join(parts)
def doc_exists(name: str) -> bool:
path = DOC_DIR / f"{name}.txt"
return path.exists() and "Removed" not in path.read_text(errors="ignore")[:200]
def has_call(text: str, name: str) -> bool:
return f"{name}(" in text or f".{name}(" in text or f'"{name}"' in text
def main() -> int:
tests = all_test_text()
errors = []
for name, needs_doc, status in PUBLIC_APIS:
if status == "public" and not has_call(tests, name):
errors.append(f"missing test coverage: {name}")
if needs_doc and status in {"public", "integration"} and not doc_exists(name):
errors.append(f"missing active doc page: {name}")
compiler_h = (ROOT / "src" / "lib" / "compiler.h").read_text(errors="ignore")
if "#ifndef __UCE_WASM_UNIT__\nSharedUnit* unit_load" not in compiler_h:
errors.append("unit_load is not guarded out of wasm-unit exposure")
for name in REMOVED_APIS:
page = DOC_DIR / f"{name}.txt"
if name == "concat" and page.exists() and "Removed" not in page.read_text(errors="ignore")[:300]:
errors.append("concat doc is not tombstoned")
if name == "unit_load" and page.exists() and "native-only" not in page.read_text(errors="ignore"):
errors.append("unit_load doc is not native-only/tombstoned")
if errors:
print("API coverage manifest FAILED")
for error in errors:
print("- " + error)
return 1
print(f"API coverage manifest ok: {len(PUBLIC_APIS)} entries checked")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# Build the production W1 UCE WASM core from the production 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,-z,stack-size=8388608 \
-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
+67 -13
View File
@@ -9,27 +9,81 @@ GF="uce_fastcgi"
mkdir bin > /dev/null 2>&1
mkdir bin/tmp > /dev/null 2>&1
mkdir bin/assets > /dev/null 2>&1
mkdir bin/wasm > /dev/null 2>&1
mkdir work > /dev/null 2>&1
COMPILER="clang++"
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
# -rdynamic is a link-time flag; the -c compiles below do not need it.
FLAGS="-g -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`"
# Wasmtime C++ API — needed only by the wasm backend object (src/wasm).
WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
WASM_FLAGS="-I$WASMTIME_HOME/include"
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs` $WASM_LIBS"
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
echo "Compiling SQLite..."
clang -g -O2 -fPIC \
-DSQLITE_THREADSAFE=1 \
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
-DSQLITE_DQS=0 \
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
-c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1
if [ $? -ne 0 ]; then exit 1; fi
# The runtime is split into separately-compiled objects so an edit to one
# module no longer recompiles the others (notably: editing the wasm backend
# does not recompile the rest, and vendored SQLite is built once):
# bin/sqlite3.o vendored SQLite amalgamation (depends only on its own source)
# bin/wasm.o wasm backend + worker + wasmtime.hh (src/wasm)
# bin/main.o linux_fastcgi.cpp + the uce_lib core amalgamation
# All link into the single -rdynamic binary. Delete bin/*.o to force a clean
# rebuild.
# Rebuild $1 if it is missing or anything under the remaining find-args is newer.
needs_rebuild() {
local obj="$1"; shift
[ ! -f "$obj" ] && return 0
[ -n "$(find "$@" -newer "$obj" -print -quit 2>/dev/null)" ] && return 0
return 1
}
echo "Compiling executable..."
time -p $COMPILER src/linux_fastcgi.cpp bin/sqlite3.o $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
# core.wasm: guest runtime loaded by the native wasm backend.
if needs_rebuild bin/wasm/core.wasm src/wasm/core.cpp src/lib src/wasm/core_hostcalls.syms src/wasm/core_libc_exports.syms scripts/build_core_wasm.sh; then
echo "Compiling wasm core..."
bash scripts/build_core_wasm.sh || exit 1
else
echo "Reusing bin/wasm/core.wasm"
fi
# SQLite: vendored C, depends only on its own source (not our headers).
if needs_rebuild bin/sqlite3.o src/3rdparty/sqlite/sqlite3.c src/3rdparty/sqlite/sqlite3.h; then
echo "Compiling SQLite..."
clang -g -O2 -fPIC \
-DSQLITE_THREADSAFE=1 \
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
-DSQLITE_DQS=0 \
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
-c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1 || exit 1
else
echo "Reusing bin/sqlite3.o"
fi
# wasm backend object: the wasm sources plus the lib headers it includes for
# declarations (not the lib .cpp — those are compiled into main.o).
if needs_rebuild bin/wasm.o src/wasm src/lib/*.h; then
echo "Compiling wasm backend..."
time -p $COMPILER -c src/wasm/wasm_module.cpp $SRCFLAGS $FLAGS $WASM_FLAGS -o bin/wasm.o 2>&1 || exit 1
else
echo "Reusing bin/wasm.o"
fi
# main object: the FastCGI entrypoint + the uce_lib core amalgamation. Depends
# on linux_fastcgi.cpp, the whole lib tree, fcgicc, and the wasm backend header
# (its only view of the wasm object) — but not the wasm .cpp sources.
if needs_rebuild bin/main.o src/linux_fastcgi.cpp src/lib src/fastcgi src/wasm/backend.h; then
echo "Compiling main..."
time -p $COMPILER -c src/linux_fastcgi.cpp $SRCFLAGS $FLAGS -o bin/main.o 2>&1 || exit 1
else
echo "Reusing bin/main.o"
fi
echo "Linking..."
$COMPILER -rdynamic bin/main.o bin/wasm.o bin/sqlite3.o $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
if [ $? -eq 0 ]
then
+235
View File
@@ -0,0 +1,235 @@
#!/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:
# llvm-nm (wasi-sdk) can SIGSEGV on degenerate-but-valid modules — e.g. a
# unit with no exported handlers (`empty.uce`). A toolchain crash is not
# evidence of a forbidden allocator, so skip this defense-in-depth scan
# rather than fail the unit; forbidden allocator *exports* are still
# rejected by the export-section check above.
crashed = proc.returncode < 0 or "Stack dump:" in proc.stderr or "PLEASE submit a bug report" in proc.stderr
if crashed:
return []
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())
-47
View File
@@ -1,47 +0,0 @@
#!/bin/bash
cd "$(dirname "$0")"
cd ..
SRC_DIR="$1"
DEST_DIR="$2"
SRC_FN="$3"
PP_FN="$4"
SO_FN="$5"
LINK_OBJECTS="$6"
#echo "Source Dir: $SRC_DIR"
#echo "Dest Dir: $DEST_DIR"
#echo "Source File: $SRC_FN"
#echo "Preprocessed File: $PP_FN"
#echo "Dest File: $SO_FN"
mkdir -p "$DEST_DIR" > /dev/null 2>&1
export CPLUS_INCLUDE_PATH="${CPLUS_INCLUDE_PATH:+${CPLUS_INCLUDE_PATH}:}$SRC_DIR"
BUILDMODE="debug"
OPT_FLAG="O0"
COMPILER="clang++"
#COMPILER="g++"
FLAGS="-shared -g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math -fPIC"
LIBS="-ldl -lm -lpthread"
SRCFLAGS="-D PLATFORM_NAME=\"linux\""
# echo "Compliling executable..."
$COMPILER "$DEST_DIR/$PP_FN" $SRCFLAGS $FLAGS $LIBS -o "$DEST_DIR/$SO_FN"
# separate .o file
#$COMPILER -c "$DEST_DIR/$PP_FN" $SRCFLAGS $FLAGS $LIBS -o "$DEST_DIR/$PP_FN.o"
#$COMPILER "$DEST_DIR/$PP_FN.o" "$LINK_OBJECTS" $SRCFLAGS $FLAGS $LIBS -o "$DEST_DIR/$SO_FN"
if [ $? -eq 0 ]
then
# ls -lh "$DEST_DIR"
exit 0
else
exit 1
fi
+97
View File
@@ -0,0 +1,97 @@
#!/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
# The server captures this script's output and treats any non-empty result as
# a compile failure (then drops the .wasm), so a successful build must be
# silent; warnings are intentionally suppressed.
-w
# 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" ] && [ -z "$(find src/lib -maxdepth 1 -name '*.h' -type f -newer "$PCH_FN" -print -quit)" ]; 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/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
rm -f "$OBJ_FN" "$ABI_TMP"
+1 -1
View File
@@ -3,7 +3,7 @@ Version: @VERSION@
Section: web
Priority: optional
Architecture: @ARCH@
Depends: bash, clang, libmariadb-dev | default-libmysqlclient-dev, systemd
Depends: bash, python3, curl, systemd, libpcre2-8-0, zlib1g, libssl3, libstdc++6, libgcc-s1, libmariadb3 | libmysqlclient21 | libmysqlclient24
Maintainer: Udo Schroeter <udo@openfu.com>
Installed-Size: @INSTALLED_SIZE@
Description: UCE FastCGI runtime and live-compiling C++ web environment
+1
View File
@@ -5,6 +5,7 @@ if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null 2>&1 || true
if [ "$1" = "configure" ]; then
systemctl enable uce.service >/dev/null 2>&1 || true
systemctl restart uce.service >/dev/null 2>&1 || true
fi
fi
-1
View File
@@ -11,7 +11,6 @@ StateDirectory=uce
CacheDirectory=uce
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
ExecStartPre=/usr/bin/bash /usr/lib/uce/scripts/build_linux.sh
ExecStart=/usr/lib/uce/bin/uce_fastcgi.linux.bin
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
Restart=always
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(git rev-parse --show-toplevel)
cd "$repo_root"
out_dir="${1:-changelog}"
mkdir -p "$out_dir"
rm -f "$out_dir"/*.log
while IFS= read -r commit; do
commit_date=$(git show -s --format=%cs "$commit")
out_file="$out_dir/$commit_date.log"
{
printf 'commit %s\n' "$commit"
git show -s --format=%B "$commit"
printf '\n'
} >> "$out_file"
done < <(git rev-list --reverse HEAD)
printf 'Wrote changelog files to %s\n' "$out_dir"
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail
# Install the pinned WASI SDK used by UCE's request-time wasm compiler.
# This is a runtime dependency: UCE compiles .uce units on demand and during
# proactive startup scans, so every deployment host needs the same toolchain.
WASI_SDK_VERSION="${WASI_SDK_VERSION:-33.0}"
WASI_SDK_RELEASE_TAG="${WASI_SDK_RELEASE_TAG:-wasi-sdk-33}"
WASI_SDK_ARCHIVE="${WASI_SDK_ARCHIVE:-wasi-sdk-33.0-x86_64-linux.tar.gz}"
WASI_SDK_SHA256="${WASI_SDK_SHA256:-0ba8b5bfaeb2adf3f29bab5841d76cf5318ab8e1642ea195f88baba1abd47bce}"
WASI_SDK_URL="${WASI_SDK_URL:-https://github.com/WebAssembly/wasi-sdk/releases/download/${WASI_SDK_RELEASE_TAG}/${WASI_SDK_ARCHIVE}}"
INSTALL_BASE="${INSTALL_BASE:-/opt}"
INSTALL_DIR="${WASI_SDK_INSTALL_DIR:-${INSTALL_BASE}/wasi-sdk-${WASI_SDK_VERSION}-x86_64-linux}"
SYMLINK_PATH="${WASI_SDK_SYMLINK:-${INSTALL_BASE}/wasi-sdk}"
CACHE_DIR="${WASI_SDK_CACHE_DIR:-/tmp/uce-deps}"
usage() {
cat <<EOF
Usage: scripts/install_wasi_sdk.sh [--check-only]
Installs pinned WASI SDK ${WASI_SDK_VERSION} for UCE request-time unit compilation.
Environment overrides:
WASI_SDK_VERSION ${WASI_SDK_VERSION}
WASI_SDK_RELEASE_TAG ${WASI_SDK_RELEASE_TAG}
WASI_SDK_ARCHIVE ${WASI_SDK_ARCHIVE}
WASI_SDK_SHA256 ${WASI_SDK_SHA256}
WASI_SDK_URL ${WASI_SDK_URL}
WASI_SDK_INSTALL_DIR ${INSTALL_DIR}
WASI_SDK_SYMLINK ${SYMLINK_PATH}
WASI_SDK_CACHE_DIR ${CACHE_DIR}
EOF
}
check_only=0
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
elif [[ "${1:-}" == "--check-only" ]]; then
check_only=1
elif [[ $# -gt 0 ]]; then
usage >&2
exit 2
fi
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Required command not found: $1" >&2
exit 1
fi
}
verify_tree() {
local root="$1"
for tool in clang++ wasm-ld llvm-objcopy llvm-nm; do
if [[ ! -x "$root/bin/$tool" ]]; then
echo "Missing WASI SDK tool: $root/bin/$tool" >&2
return 1
fi
done
"$root/bin/clang++" --version | head -n 1
}
if [[ $check_only -eq 1 ]]; then
verify_tree "${WASI_SDK:-$SYMLINK_PATH}"
exit 0
fi
require_command curl
require_command sha256sum
require_command tar
require_command mkdir
require_command ln
mkdir -p "$CACHE_DIR" "$INSTALL_BASE"
archive_path="$CACHE_DIR/$WASI_SDK_ARCHIVE"
if [[ ! -f "$archive_path" ]]; then
echo "Downloading $WASI_SDK_URL"
curl -fL --proto '=https' --tlsv1.2 -o "$archive_path.tmp" "$WASI_SDK_URL"
mv "$archive_path.tmp" "$archive_path"
fi
printf '%s %s\n' "$WASI_SDK_SHA256" "$archive_path" | sha256sum -c -
rm -rf -- "$INSTALL_DIR.tmp"
mkdir -p "$INSTALL_DIR.tmp"
tar -xf "$archive_path" -C "$INSTALL_DIR.tmp" --strip-components=1
verify_tree "$INSTALL_DIR.tmp"
rm -rf -- "$INSTALL_DIR"
mv "$INSTALL_DIR.tmp" "$INSTALL_DIR"
ln -sfn "$INSTALL_DIR" "$SYMLINK_PATH"
echo "Installed WASI SDK at $INSTALL_DIR"
echo "Updated symlink $SYMLINK_PATH -> $INSTALL_DIR"
verify_tree "$SYMLINK_PATH"
+97 -13
View File
@@ -5,16 +5,21 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEB_ASSET_DIR="$SCRIPT_DIR/deb"
PACKAGE_NAME="uce"
REVISION="${UCE_DEB_REVISION:-1}"
REVISION="${UCE_DEB_REVISION:-}"
usage() {
cat <<'EOF'
Usage:
scripts/make_deb.sh VERSION
scripts/make_deb.sh [VERSION]
When VERSION is omitted, scripts/make_deb.sh reads VERSION from version.txt.
Environment:
UCE_DEB_REVISION Debian package revision suffix (default: 1)
UCE_DEB_ARCH Override package architecture
UCE_DEB_REVISION Optional Debian package revision suffix
UCE_DEB_ARCH Override package architecture
UCE_DEB_WEBROOT Public web root staged into the package (default: /var/www/html)
UCE_DEB_BUNDLE_WASI_SDK Bundle pinned /opt/wasi-sdk into the package (default: 1)
UCE_DEB_BUNDLE_WASMTIME Bundle /opt/wasmtime into the package (default: 1)
EOF
}
@@ -39,7 +44,7 @@ resolve_arch() {
validate_version() {
local version="$1"
if [[ ! "$version" =~ ^[0-9][A-Za-z0-9.+:~]*$ ]]; then
if [[ ! "$version" =~ ^[0-9][A-Za-z0-9.+:~-]*$ ]]; then
echo "Invalid Debian version string: $version" >&2
exit 1
fi
@@ -47,12 +52,37 @@ validate_version() {
copy_payload() {
local destination="$1"
local webroot="$2"
local stage_dir="$3"
local path
for path in LICENSE README.md codesearch bin scripts site src; do
for path in LICENSE README.md codesearch bin scripts src docs; do
cp -a "$REPO_ROOT/$path" "$destination/"
done
mkdir -p "$destination/etc"
mkdir -p "$destination/etc" "$stage_dir$webroot"
cp -a "$REPO_ROOT/etc/uce" "$destination/etc/"
cp -a "$REPO_ROOT/site/." "$stage_dir$webroot/"
}
write_packaged_settings() {
local output_file="$1"
local webroot="$2"
python3 - "$REPO_ROOT/etc/uce/settings.cfg" "$output_file" "$webroot" <<'PY'
from pathlib import Path
import sys
src, dst, webroot = sys.argv[1:4]
s = Path(src).read_text()
replacements = {
"SITE_DIRECTORY=site": f"SITE_DIRECTORY={webroot}",
"WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm": "WASM_CORE_PATH=/usr/lib/uce/bin/wasm/core.wasm",
"HTTP_DOCUMENT_ROOT=": f"HTTP_DOCUMENT_ROOT={webroot}",
}
for old, new in replacements.items():
if old in s:
s = s.replace(old, new)
if "WASM_COMPILE_SCRIPT=" not in s:
s += "\nWASM_COMPILE_SCRIPT=scripts/compile_wasm_unit\n"
Path(dst).write_text(s)
PY
}
write_control_file() {
@@ -69,20 +99,68 @@ write_control_file() {
"$DEB_ASSET_DIR/control.in" > "$output_file"
}
bundle_wasi_sdk() {
local stage_dir="$1"
local wasi_root="${WASI_SDK:-/opt/wasi-sdk}"
if [[ "${UCE_DEB_BUNDLE_WASI_SDK:-1}" != "1" ]]; then
return
fi
if [[ ! -x "$wasi_root/bin/clang++" || ! -x "$wasi_root/bin/wasm-ld" || ! -x "$wasi_root/bin/llvm-nm" ]]; then
echo "UCE_DEB_BUNDLE_WASI_SDK=1 but WASI_SDK does not point at a complete SDK: $wasi_root" >&2
exit 1
fi
local resolved
resolved="$(readlink -f "$wasi_root")"
local base
base="$(basename "$resolved")"
mkdir -p "$stage_dir/opt"
cp -a "$resolved" "$stage_dir/opt/$base"
ln -sfn "$base" "$stage_dir/opt/wasi-sdk"
}
bundle_wasmtime() {
local stage_dir="$1"
local wasmtime_root="${WASMTIME_HOME:-/opt/wasmtime}"
if [[ "${UCE_DEB_BUNDLE_WASMTIME:-1}" != "1" ]]; then
return
fi
if [[ ! -f "$wasmtime_root/include/wasmtime.hh" || ! -f "$wasmtime_root/lib/libwasmtime.so" ]]; then
echo "UCE_DEB_BUNDLE_WASMTIME=1 but WASMTIME_HOME does not point at a complete C API tree: $wasmtime_root" >&2
exit 1
fi
local resolved
resolved="$(readlink -f "$wasmtime_root")"
local base
base="$(basename "$resolved")"
mkdir -p "$stage_dir/opt"
cp -a "$resolved" "$stage_dir/opt/$base"
ln -sfn "$base" "$stage_dir/opt/wasmtime"
}
write_md5sums() {
local stage_dir="$1"
(
cd "$stage_dir"
find usr etc lib -type f -print0 | sort -z | xargs -0 md5sum > DEBIAN/md5sums
find usr etc lib opt -type f -print0 2>/dev/null | sort -z | xargs -0 --no-run-if-empty md5sum > DEBIAN/md5sums
)
}
if [[ $# -ne 1 ]]; then
if [[ $# -gt 1 ]]; then
usage >&2
exit 1
fi
VERSION="$1"
if [[ $# -eq 1 ]]; then
VERSION="$1"
else
if [[ ! -r "$REPO_ROOT/version.txt" ]]; then
echo "Missing version.txt and no VERSION argument supplied" >&2
exit 1
fi
# shellcheck disable=SC1091
. "$REPO_ROOT/version.txt"
VERSION="${VERSION:-}"
fi
validate_version "$VERSION"
require_command bash
@@ -101,11 +179,15 @@ require_command cp
require_command sort
ARCH="$(resolve_arch)"
PACKAGE_VERSION="${VERSION}-${REVISION}"
PACKAGE_VERSION="$VERSION"
if [[ -n "$REVISION" ]]; then
PACKAGE_VERSION="${PACKAGE_VERSION}-${REVISION}"
fi
PACKAGE_BASENAME="${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCH}"
STAGE_DIR="$REPO_ROOT/pkg/$PACKAGE_BASENAME"
DEBIAN_DIR="$STAGE_DIR/DEBIAN"
INSTALL_ROOT="$STAGE_DIR/usr/lib/uce"
WEBROOT="${UCE_DEB_WEBROOT:-/var/www/html}"
DIST_DIR="$REPO_ROOT/dist"
OUTPUT_DEB="$DIST_DIR/$PACKAGE_BASENAME.deb"
@@ -117,9 +199,11 @@ bash "$REPO_ROOT/scripts/build_linux.sh"
rm -rf -- "$STAGE_DIR"
mkdir -p "$DEBIAN_DIR" "$INSTALL_ROOT" "$STAGE_DIR/etc/uce" "$STAGE_DIR/lib/systemd/system" "$DIST_DIR"
copy_payload "$INSTALL_ROOT"
copy_payload "$INSTALL_ROOT" "$WEBROOT" "$STAGE_DIR"
bundle_wasi_sdk "$STAGE_DIR"
bundle_wasmtime "$STAGE_DIR"
install -m 0644 "$REPO_ROOT/etc/uce/settings.cfg" "$STAGE_DIR/etc/uce/settings.cfg"
write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT"
install -m 0644 "$DEB_ASSET_DIR/uce.service" "$STAGE_DIR/lib/systemd/system/uce.service"
install -m 0644 "$DEB_ASSET_DIR/conffiles" "$DEBIAN_DIR/conffiles"
install -m 0755 "$DEB_ASSET_DIR/postinst" "$DEBIAN_DIR/postinst"
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PACKAGE_NAME="uce"
RELEASE="${UCE_RPM_RELEASE:-1}"
WEBROOT="${UCE_RPM_WEBROOT:-/var/www/html}"
usage() {
cat <<'EOF'
Usage:
scripts/make_rpm.sh [VERSION]
When VERSION is omitted, scripts/make_rpm.sh reads VERSION, MAJOR, and RELEASE from version.txt.
Environment:
UCE_RPM_RELEASE Override RPM release suffix (default: RELEASE from version.txt)
UCE_RPM_ARCH Override RPM architecture
UCE_RPM_WEBROOT Public web root staged into the package (default: /var/www/html)
UCE_RPM_BUNDLE_WASI_SDK Bundle pinned /opt/wasi-sdk into the package (default: 1)
UCE_RPM_BUNDLE_WASMTIME Bundle /opt/wasmtime into the package (default: 1)
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Required command not found: $1" >&2
exit 1
fi
}
validate_version() {
local version="$1"
if [[ ! "$version" =~ ^[0-9][A-Za-z0-9._+~]*$ ]]; then
echo "Invalid RPM version string: $version" >&2
exit 1
fi
}
resolve_arch() {
if [[ -n "${UCE_RPM_ARCH:-}" ]]; then
printf '%s\n' "$UCE_RPM_ARCH"
return
fi
uname -m
}
copy_payload() {
local destination="$1"
local webroot="$2"
local stage_dir="$3"
local path
for path in LICENSE README.md codesearch bin scripts src docs; do
cp -a "$REPO_ROOT/$path" "$destination/"
done
mkdir -p "$destination/etc" "$stage_dir$webroot"
cp -a "$REPO_ROOT/etc/uce" "$destination/etc/"
cp -a "$REPO_ROOT/site/." "$stage_dir$webroot/"
}
write_packaged_settings() {
local output_file="$1"
local webroot="$2"
python3 - "$REPO_ROOT/etc/uce/settings.cfg" "$output_file" "$webroot" <<'PY'
from pathlib import Path
import sys
src, dst, webroot = sys.argv[1:4]
s = Path(src).read_text()
replacements = {
"SITE_DIRECTORY=site": f"SITE_DIRECTORY={webroot}",
"WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm": "WASM_CORE_PATH=/usr/lib/uce/bin/wasm/core.wasm",
"HTTP_DOCUMENT_ROOT=": f"HTTP_DOCUMENT_ROOT={webroot}",
}
for old, new in replacements.items():
if old in s:
s = s.replace(old, new)
if "WASM_COMPILE_SCRIPT=" not in s:
s += "\nWASM_COMPILE_SCRIPT=scripts/compile_wasm_unit\n"
Path(dst).write_text(s)
PY
}
bundle_wasi_sdk() {
local stage_dir="$1"
local wasi_root="${WASI_SDK:-/opt/wasi-sdk}"
if [[ "${UCE_RPM_BUNDLE_WASI_SDK:-1}" != "1" ]]; then
return
fi
if [[ ! -x "$wasi_root/bin/clang++" || ! -x "$wasi_root/bin/wasm-ld" || ! -x "$wasi_root/bin/llvm-nm" ]]; then
echo "UCE_RPM_BUNDLE_WASI_SDK=1 but WASI_SDK does not point at a complete SDK: $wasi_root" >&2
exit 1
fi
local resolved base
resolved="$(readlink -f "$wasi_root")"
base="$(basename "$resolved")"
mkdir -p "$stage_dir/opt"
cp -a "$resolved" "$stage_dir/opt/$base"
ln -sfn "$base" "$stage_dir/opt/wasi-sdk"
}
bundle_wasmtime() {
local stage_dir="$1"
local wasmtime_root="${WASMTIME_HOME:-/opt/wasmtime}"
if [[ "${UCE_RPM_BUNDLE_WASMTIME:-1}" != "1" ]]; then
return
fi
if [[ ! -f "$wasmtime_root/include/wasmtime.hh" || ! -f "$wasmtime_root/lib/libwasmtime.so" ]]; then
echo "UCE_RPM_BUNDLE_WASMTIME=1 but WASMTIME_HOME does not point at a complete C API tree: $wasmtime_root" >&2
exit 1
fi
local resolved base
resolved="$(readlink -f "$wasmtime_root")"
base="$(basename "$resolved")"
mkdir -p "$stage_dir/opt"
cp -a "$resolved" "$stage_dir/opt/$base"
ln -sfn "$base" "$stage_dir/opt/wasmtime"
}
if [[ $# -gt 1 ]]; then
usage >&2
exit 1
fi
FULL_VERSION=""
if [[ $# -eq 1 ]]; then
FULL_VERSION="$1"
RPM_VERSION="${FULL_VERSION%%-*}"
RPM_RELEASE_PART="${FULL_VERSION#*-}"
if [[ "$RPM_RELEASE_PART" == "$FULL_VERSION" ]]; then
RPM_RELEASE_PART="$RELEASE"
fi
else
if [[ ! -r "$REPO_ROOT/version.txt" ]]; then
echo "Missing version.txt and no VERSION argument supplied" >&2
exit 1
fi
# shellcheck disable=SC1091
. "$REPO_ROOT/version.txt"
FULL_VERSION="${VERSION:-}"
RPM_VERSION="${MAJOR:-${FULL_VERSION%%-*}}"
RPM_RELEASE_PART="${RELEASE:-${FULL_VERSION#*-}}"
fi
if [[ -z "$FULL_VERSION" || -z "$RPM_VERSION" || -z "$RPM_RELEASE_PART" ]]; then
echo "Could not derive RPM version from VERSION/MAJOR/RELEASE" >&2
exit 1
fi
RPM_RELEASE_PART="${RPM_RELEASE_PART//-/_}"
VERSION="$RPM_VERSION"
RELEASE="${UCE_RPM_RELEASE:-$RPM_RELEASE_PART}"
validate_version "$VERSION"
require_command bash
require_command rpmbuild
require_command find
require_command cp
require_command tar
require_command sed
require_command python3
require_command readlink
ARCH="$(resolve_arch)"
PACKAGE_VERSION="$FULL_VERSION"
BUILD_ROOT="$REPO_ROOT/pkg/rpm-build"
STAGE_DIR="$BUILD_ROOT/stage"
RPMBUILD_DIR="$BUILD_ROOT/rpmbuild"
INSTALL_ROOT="$STAGE_DIR/usr/lib/uce"
SPEC_FILE="$RPMBUILD_DIR/SPECS/$PACKAGE_NAME.spec"
DIST_DIR="$REPO_ROOT/dist"
bash "$REPO_ROOT/scripts/build_linux.sh"
rm -rf -- "$BUILD_ROOT"
mkdir -p "$INSTALL_ROOT" "$STAGE_DIR/etc/uce" "$STAGE_DIR/usr/lib/systemd/system" "$STAGE_DIR/var/cache/uce" "$STAGE_DIR/var/lib/uce" "$RPMBUILD_DIR"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} "$DIST_DIR"
copy_payload "$INSTALL_ROOT" "$WEBROOT" "$STAGE_DIR"
bundle_wasi_sdk "$STAGE_DIR"
bundle_wasmtime "$STAGE_DIR"
write_packaged_settings "$STAGE_DIR/etc/uce/settings.cfg" "$WEBROOT"
install -m 0644 "$REPO_ROOT/scripts/deb/uce.service" "$STAGE_DIR/usr/lib/systemd/system/uce.service"
(
cd "$STAGE_DIR"
tar --sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner -czf "$RPMBUILD_DIR/SOURCES/$PACKAGE_NAME-$VERSION.tar.gz" .
)
cat > "$SPEC_FILE" <<EOF
%global __os_install_post %{nil}
Name: $PACKAGE_NAME
Version: $VERSION
Release: $RELEASE%{?dist}
Summary: UCE FastCGI runtime and live-compiling C++ web environment
License: GPL-3.0-or-later
URL: https://example.com/uce
BuildArch: $ARCH
Requires: bash
Requires: python3
Requires: curl
Requires: systemd
Requires: pcre2
Requires: zlib
Requires: openssl-libs
Requires: libstdc++
Requires: mariadb-connector-c
%description
UCE is an experimental C/C++ web runtime with FastCGI request handling,
on-demand wasm unit compilation, and a packaged site/doc/test tree.
%prep
rm -rf %{_builddir}/$PACKAGE_NAME-$VERSION
mkdir -p %{_builddir}/$PACKAGE_NAME-$VERSION
cd %{_builddir}/$PACKAGE_NAME-$VERSION
tar -xzf %{_sourcedir}/$PACKAGE_NAME-$VERSION.tar.gz
%build
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}
cp -a %{_builddir}/$PACKAGE_NAME-$VERSION/. %{buildroot}/
%post
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null 2>&1 || true
systemctl enable uce.service >/dev/null 2>&1 || true
systemctl restart uce.service >/dev/null 2>&1 || true
fi
%preun
if [ "\$1" = "0" ] && command -v systemctl >/dev/null 2>&1; then
systemctl disable --now uce.service >/dev/null 2>&1 || true
fi
%postun
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null 2>&1 || true
fi
%files
%license /usr/lib/uce/LICENSE
%doc /usr/lib/uce/README.md
%config(noreplace) /etc/uce/settings.cfg
/usr/lib/uce
/usr/lib/systemd/system/uce.service
$WEBROOT
%dir /var/cache/uce
%dir /var/lib/uce
/opt/*
%changelog
* Mon Jun 15 2026 UCE Packager <root@localhost> - $PACKAGE_VERSION
- Package UCE runtime with pinned deployment toolchains.
EOF
rpmbuild --define "_topdir $RPMBUILD_DIR" -bb "$SPEC_FILE"
find "$RPMBUILD_DIR/RPMS" -type f -name '*.rpm' -print -exec cp -a {} "$DIST_DIR/" \;
find "$DIST_DIR" -maxdepth 1 -type f -name "${PACKAGE_NAME}-${VERSION}-${RELEASE}*.rpm" -print
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
socket_path="${UCE_CLI_SOCKET:-/run/uce/cli.sock}"
if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
if [[ -n "${configured_socket:-}" ]]; then
socket_path="$configured_socket"
fi
fi
include_kill=0
action="run"
while [[ $# -gt 0 ]]; do
case "$1" in
--include-wasm-kill|--include-kill)
include_kill=1
shift
;;
--list)
action="list"
shift
;;
-h|--help)
cat <<'USAGE'
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--list]
Runs the UCE unit-based test suite through the runtime CLI socket.
USAGE
exit 0
;;
*)
echo "unknown option: $1" >&2
exit 2
;;
esac
done
if [[ ! -S "$socket_path" ]]; then
echo "UCE CLI socket not found: $socket_path" >&2
exit 1
fi
url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}"
exec curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
+1 -1
View File
@@ -1,6 +1,6 @@
// Minimal exported helper used by the `unit_call()` demo page.
EXPORT DTree* test_func(DTree* call_param)
EXPORT DValue* test_func(DValue* call_param)
{
print("HELLO FROM TEST FUNCTION");
return(0);
+7 -7
View File
@@ -2,7 +2,7 @@
RENDER(Request& context)
{
DTree p;
DValue p;
p.set(context.params);
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
@@ -10,15 +10,15 @@ RENDER(Request& context)
StringList sorted_routes = list_sort(unique_routes);
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
DTree cards;
DTree card;
DValue cards;
DValue card;
card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear();
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); });
DValue app_cards = dv_filter(cards, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
DValue titles = dv_map(app_cards, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
DValue grouped = dv_group_by(cards, [](DValue item, String key) { return(item["section"].to_string()); });
<><html>
<head>
@@ -30,7 +30,7 @@ RENDER(Request& context)
<h2>Collection Helpers</h2>
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
<div class="system-info"><h3>DTree app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
<div class="system-info"><h3>DValue app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
<details>
<summary>Request Parameters</summary>
+2 -2
View File
@@ -1,11 +1,11 @@
RENDER(Request& context)
{
DTree card_props;
DValue card_props;
card_props["title"] = "Component Example";
card_props["body"] = "This card body comes from context.props and is rendered through component().";
DTree named_props;
DValue named_props;
named_props["title"] = "Named Render Example";
named_props["body"] = "This content is rendered through the COMPONENT:BODY(Request& context) entry point.";
+1 -1
View File
@@ -78,7 +78,7 @@ void test_demo_render_restricted_json(Request& context, String title, String ris
{
context.set_status(403, "Restricted");
context.header["Content-Type"] = "application/json; charset=utf-8";
DTree payload;
DValue payload;
payload["ok"].set_bool(false);
payload["error"] = "restricted";
payload["title"] = title;
+16 -16
View File
@@ -2,13 +2,13 @@
RENDER(Request& context)
{
DTree t;
DValue t;
<>
<link rel="stylesheet" href='style.css'></link>
<h1>
<a href="index.uce">UCE Test</a>:
DTree
DValue
</h1>
<h2>Value Types</h2>
@@ -33,21 +33,21 @@ RENDER(Request& context)
<pre><?
DTree scalar_text;
DValue scalar_text;
scalar_text = "42.75";
print("text -> f64: ", scalar_text.to_f64(), "\n");
print("text -> u64: ", scalar_text.to_u64(), "\n");
print("text -> s64: ", scalar_text.to_s64(), "\n");
DTree bool_text;
DValue bool_text;
bool_text = "yes";
print("yes -> bool: ", bool_text.to_bool() ? "true" : "false", "\n");
DTree single_value_map;
DValue single_value_map;
single_value_map["value"] = "123";
print("{value:123} -> u64: ", single_value_map.to_u64(), "\n");
DTree headers;
DValue headers;
headers["Content-Type"] = "text/plain";
headers["X-Test"] = "123";
print("map -> StringMap:\n", var_dump(headers.to_stringmap()), "\n");
@@ -58,12 +58,12 @@ RENDER(Request& context)
<pre><?
DTree lookup;
DValue lookup;
print("has(user) before create: ", lookup.has("user") ? "true" : "false", "\n");
print("key(user) before create: ", lookup.key("user") ? "present" : "missing", "\n");
lookup.get_or_create("user")->set("Ada");
print("has(user) after create: ", lookup.has("user") ? "true" : "false", "\n");
if(DTree* user = lookup.key("user"))
if(DValue* user = lookup.key("user"))
print("key(user) after create: ", user->to_string(), "\n");
?></pre>
@@ -90,11 +90,11 @@ RENDER(Request& context)
<pre><?
DTree assoc_left;
DValue assoc_left;
assoc_left["name"] = "left";
assoc_left["shared"] = "left-value";
DTree assoc_right;
DValue assoc_right;
assoc_right["shared"] = "right-value";
assoc_right["extra"] = "new-value";
@@ -108,15 +108,15 @@ RENDER(Request& context)
<pre><?
DTree list_left;
DValue list_left;
list_left.set_array();
DTree a; a = "A"; list_left.push(a);
DTree b; b = "B"; list_left.push(b);
DValue a; a = "A"; list_left.push(a);
DValue b; b = "B"; list_left.push(b);
DTree list_right;
DValue list_right;
list_right.set_array();
DTree c; c = "C"; list_right.push(c);
DTree d; d = "D"; list_right.push(d);
DValue c; c = "C"; list_right.push(c);
DValue d; d = "D"; list_right.push(d);
print("left:\n", var_dump(list_left), "\n");
print("right:\n", var_dump(list_right), "\n");
+26 -11
View File
@@ -1,21 +1,36 @@
#include "demo_guard.h"
// Fault-injection demo. Under the wasm runtime every unit fault is a guest
// trap that the workspace turns into a clean 500 without harming the worker;
// these modes exercise the three distinct trap causes (mirrors tests/wasm-kill).
u64 error_reporting_recurse(volatile u64 depth)
{
volatile u64 next = depth + 1;
return(next + error_reporting_recurse(next));
}
RENDER(Request& context)
{
if(!test_demo_request_allowed(context))
{
test_demo_render_restricted_html(context, "Error reporting", "intentionally crash or abort request workers");
test_demo_render_restricted_html(context, "Error reporting", "crash or abort request workers for diagnostics");
return;
}
String mode = context.get["mode"];
if(mode == "exception")
throw std::runtime_error("Intentional test exception from /test/error-reporting.uce");
if(mode == "abort")
raise(SIGABRT);
if(mode == "segfault")
raise(SIGSEGV);
if(mode == "trap")
__builtin_trap();
if(mode == "recurse")
{
volatile u64 sink = error_reporting_recurse(0);
(void)sink;
}
if(mode == "loop")
{
volatile u64 i = 0;
while(i >= 0)
i++;
}
<>
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
@@ -23,11 +38,11 @@ RENDER(Request& context)
<a href="index.uce">UCE Test</a>:
Error reporting
</h1>
<p>These actions intentionally trigger failures so you can verify that UCE returns a usable `500` response instead of dropping the upstream connection.</p>
<p>These actions trigger guest traps so you can verify that UCE returns a usable `500` response from a healthy worker instead of dropping the upstream connection.</p>
<ul>
<li><a href="?mode=exception">Trigger uncaught exception</a></li>
<li><a href="?mode=abort">Trigger SIGABRT</a></li>
<li><a href="?mode=segfault">Trigger SIGSEGV</a></li>
<li><a href="?mode=trap">Trigger an explicit trap (`__builtin_trap`)</a></li>
<li><a href="?mode=recurse">Trigger stack exhaustion (unbounded recursion)</a></li>
<li><a href="?mode=loop">Trigger a runaway loop (epoch interrupt)</a></li>
</ul>
</>
}
+1 -1
View File
@@ -11,7 +11,7 @@ RENDER(Request& context)
test_demo_render_restricted_html(context, "File Append", "write to server-side files");
return;
}
DTree t;
DValue t;
<>
<link rel="stylesheet" href='style.css'></link>
+1 -1
View File
@@ -2,7 +2,7 @@
RENDER(Request& context)
{
DTree t;
DValue t;
<>
<link rel="stylesheet" href='style.css'></link>
+1 -1
View File
@@ -2,7 +2,7 @@
RENDER(Request& context)
{
DTree t;
DValue t;
<>
<link rel="stylesheet" href='style.css'></link>
+14 -8
View File
@@ -12,7 +12,7 @@ void render_card(String url, String title, String desc)
RENDER(Request& context)
{
DTree p;
DValue p;
p.set(context.params);
bool allow_server_demos = test_demo_request_allowed(context);
@@ -35,10 +35,10 @@ RENDER(Request& context)
<? if(allow_server_demos) { render_card("error-reporting.uce", "Error Reporting", "Error handling and output"); } ?>
<div class="grid-heading">Data Types &amp; Parsing</div>
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
<? render_card("dvalue.uce", "DValue", "Dynamic hierarchical data tree"); ?>
<? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?>
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DTree"); ?>
<? render_card("yaml.uce", "YAML", "Concise config files with DTree"); ?>
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DValue"); ?>
<? render_card("yaml.uce", "YAML", "Concise config files with DValue"); ?>
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
<? render_card("string.uce", "String", "String operations"); ?>
@@ -70,7 +70,7 @@ RENDER(Request& context)
<? render_card("once-init.uce", "ONCE / INIT", "Unit lifecycle hooks for worker load and request entry"); ?>
<? render_card("markdown.uce", "Markdown", "Markdown parsing with components"); ?>
<? render_card("script.uce", "Script", "UCE script integration"); ?>
<? render_card("websockets.ws.uce", "WebSockets", "Real-time WebSocket chat"); ?>
<? render_card("websockets.ws.uce", "WebSockets", "Live WebSocket chat"); ?>
<? render_card("unit-browser.uce", "Unit Browser", "units_list(), unit_info(), unit_compile()"); ?>
<? if(allow_server_demos) { render_card("task.uce", "Task", "Background task execution"); } ?>
<? if(allow_server_demos) { render_card("task_repeat.uce", "Task Repeat", "Recurring task scheduling"); } ?>
@@ -84,10 +84,16 @@ RENDER(Request& context)
<div class="system-info">
<h3>System Info</h3>
<pre><?
print("Worker PID: ", my_pid, "\n");
print("Parent PID: ", parent_pid, "\n");
DValue perf = request_perf();
print("Worker PID: ", (u64)perf["worker_pid"].to_u64(), "\n");
print("Parent PID: ", (u64)perf["parent_pid"].to_u64(), "\n");
print("Request #: ", (u64)perf["request_count"].to_u64(), "\n");
print("Accept us: ", (f64)perf["accept_us"].to_f64(), "\n");
print("Running us: ", (f64)perf["running_us"].to_f64(), "\n");
print("Total us: ", (f64)perf["total_us"].to_f64(), "\n");
if(perf["workspace_birth_us"].type != 'S')
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
print("Output buffer size: ", context.ob->str().length(), "\n");
print("Request #", context.server->request_count, "\n");
?></pre>
</div>
<? } ?>
+2 -2
View File
@@ -2,13 +2,13 @@
RENDER(Request& context)
{
DTree t;
DValue t;
<>
<link rel="stylesheet" href='style.css'></link>
<h1>
<a href="index.uce">UCE Test</a>:
DTree/JSON
DValue/JSON
</h1>
JSON:
+1 -1
View File
@@ -1,6 +1,6 @@
# Markdown Demo
This page exercises **strong**, *emphasis*, ~~strikethrough~~, `code spans`, and a bare URL: https://uce.openfu.com/doc/index.uce
This page exercises **strong**, *emphasis*, ~~strikethrough~~, `code spans`, and a bare URL: https://example.com/doc/index.uce
## Task List
+2 -2
View File
@@ -1,6 +1,6 @@
RENDER(Request& context)
{
DTree options;
DValue options;
options["components"][":::warning"] = "components/markdown/warning";
options["components"]["node.code_block"] = "components/markdown/code_block";
@@ -9,7 +9,7 @@ RENDER(Request& context)
file_get_contents("markdown-example.md")
);
DTree ast = markdown_to_ast(markdown_src, options);
DValue ast = markdown_to_ast(markdown_src, options);
String html = markdown_to_html(markdown_src, options);
<>
+1 -1
View File
@@ -9,7 +9,7 @@ RENDER(Request& context)
test_demo_render_restricted_html(context, "MemcacheD", "inspect and mutate local memcached state");
return;
}
DTree t;
DValue t;
<>
<link rel="stylesheet" href='style.css'></link>
+3 -3
View File
@@ -28,13 +28,13 @@ COMPONENT:PROBE(Request& context)
RENDER(Request& context)
{
DTree first;
DValue first;
first["label"] = "First component call";
DTree second;
DValue second;
second["label"] = "Second component call in the same request";
DTree third;
DValue third;
third["label"] = "Third component call through unit_call(\"COMPONENT:PROBE\")";
<><html>
+3 -3
View File
@@ -19,8 +19,8 @@ void regex_bool_row(String label, bool result, bool expect)
RENDER(Request& context)
{
String text = "Contact ops@example.test or tag #uce, #docs, and café.";
DTree first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
DTree tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
DValue first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
DValue tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown");
<>
@@ -38,7 +38,7 @@ RENDER(Request& context)
Regular Expressions
</h1>
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DTree values.</p>
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DValue values.</p>
<p>sample = <code><?= text ?></code></p>
<h2>Validation And Search</h2>
+5 -3
View File
@@ -1,7 +1,9 @@
RENDER(Request& context)
{
auto p = compiler_load_shared_unit(&context, "post.uce");
if(p)
print(to_string(p));
// The native SharedUnit loader has been retired. Unit metadata is now
// exposed through the wasm membrane via unit_info() (uce_host_units), which
// resolves and introspects another unit without dlopen-ing a native .so.
DValue info = unit_info("post.uce");
print(json_encode(info));
}
+2 -2
View File
@@ -23,7 +23,7 @@ RENDER(Request& context)
return;
}
DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
DValue notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
String error = sqlite_error(db);
?><html>
<head>
@@ -41,7 +41,7 @@ RENDER(Request& context)
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
</div>
<div class="test-grid">
<? notes.each([&](DTree note, String key) { ?>
<? notes.each([&](DValue note, String key) { ?>
<div class="test-card">
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
<span><?= note["body"].to_string() ?></span>
+1 -1
View File
@@ -8,7 +8,7 @@ RENDER(Request& context)
test_demo_render_restricted_html(context, "Tasks", "spawn and inspect background tasks");
return;
}
DTree t;
DValue t;
String task_name = first(context.get["task-name"], "example-task");
+1 -1
View File
@@ -8,7 +8,7 @@ RENDER(Request& context)
test_demo_render_restricted_html(context, "Task repeat", "schedule recurring background tasks");
return;
}
DTree t;
DValue t;
String task_name = first(context.get["task-name"], "example-task");
+5 -5
View File
@@ -27,7 +27,7 @@ String unit_status_class(String compile_status, String error_status)
return("status-muted");
}
String unit_flag_label(DTree value)
String unit_flag_label(DValue value)
{
return(value.to_string() == "(true)" ? "✅" : "❌");
}
@@ -61,7 +61,7 @@ RENDER(Request& context)
if(selected_path == "" && unit_paths.size() > 0)
selected_path = unit_paths.front();
DTree selected_info = unit_info(selected_path);
DValue selected_info = unit_info(selected_path);
String selected_compile_status = selected_info["compile_status"].to_string();
String selected_error_status = selected_info["error_status"].to_string();
String selected_status_class = unit_status_class(selected_compile_status, selected_error_status);
@@ -157,16 +157,16 @@ RENDER(Request& context)
</div>
<div class="detail-card">
<strong>Runtime Flags</strong>
<span>loaded <?= unit_flag_label(selected_info["loaded"]) ?>, stale <?= unit_flag_label(selected_info["stale"]) ?>, current <?= unit_flag_label(selected_info["current_unit"]) ?></span>
<span>wasm <?= unit_flag_label(selected_info["wasm_available"]) ?>, stale <?= unit_flag_label(selected_info["stale"]) ?>, current <?= unit_flag_label(selected_info["current_unit"]) ?></span>
</div>
<div class="detail-card">
<strong>Artifacts</strong>
<code><?= selected_info["so_name"].to_string() ?></code>
<code><?= selected_info["wasm_name"].to_string() ?></code>
</div>
<div class="detail-card">
<strong>Timestamps</strong>
<span>Source file: <?= time_format_relative(selected_info["source_mtime"].to_u64()) ?><br>
Binary: <?= time_format_relative(selected_info["compiled_mtime"].to_u64()) ?><br>
Wasm: <?= time_format_relative(selected_info["compiled_mtime"].to_u64()) ?><br>
Meta info: <?= time_format_relative(selected_info["metadata_mtime"].to_u64()) ?></span>
</div>
</div>
+3 -3
View File
@@ -3,7 +3,7 @@
RENDER(Request& context)
{
bool allow_mutation = test_demo_request_allowed(context);
DTree payload;
DValue payload;
if(context.get["compile"] != "")
{
@@ -19,11 +19,11 @@ RENDER(Request& context)
payload["current"] = unit_info();
DTree units;
DValue units;
units.set_array();
for(auto& unit_path : units_list())
{
DTree item;
DValue item;
item = unit_path;
units.push(item);
}
+4 -4
View File
@@ -8,9 +8,9 @@ String clean_chat_field(String raw, u32 max_length)
return(value);
}
DTree chat_event(Request& context, String type, String body)
DValue chat_event(Request& context, String type, String body)
{
DTree event;
DValue event;
event["type"] = type;
event["body"] = body;
event["connection_id"] = context.params["WS_CONNECTION_ID"];
@@ -159,7 +159,7 @@ WS(Request& context)
return;
}
DTree payload = json_decode(context.in);
DValue payload = json_decode(context.in);
String type = clean_chat_field(payload["type"].to_string(), 24);
String name = clean_chat_field(payload["name"].to_string(), 32);
String body = clean_chat_field(payload["body"].to_string(), 500);
@@ -183,7 +183,7 @@ WS(Request& context)
context.connection["last_type"] = type;
context.connection["last_body"] = body;
context.connection["message_count"] = (f64)(message_count + 1);
DTree event = chat_event(context, "message", body);
DValue event = chat_event(context, "message", body);
ws_send(json_encode(event));
return;
}
+9 -9
View File
@@ -2,31 +2,31 @@
RENDER(Request& context)
{
DTree book;
DValue book;
book["name"] = "book";
book["attrs"]["id"] = "b1";
book["attrs"]["type"] = "reference";
DTree title;
DValue title;
title["name"] = "title";
title["text"] = "UCE & XML";
book["children"].push(title);
DTree chapter;
DValue chapter;
chapter["name"] = "chapter";
chapter["attrs"]["number"] = "1";
chapter["text"] = "Structural conversion without schema validation.";
book["children"].push(chapter);
String encoded = xml_encode(book);
DTree decoded = xml_decode(encoded);
DValue decoded = xml_decode(encoded);
DTree simple;
DValue simple;
simple["title"] = "Hello";
simple["count"] = "3";
String incoming = "<note priority=\"high\"><to>UCE</to><body><![CDATA[5 < 6]]></body><symbol>&#x41;&#66;</symbol></note>";
DTree incoming_tree = xml_decode(incoming);
DValue incoming_tree = xml_decode(incoming);
<>
<link rel="stylesheet" href='style.css'></link>
@@ -35,12 +35,12 @@ RENDER(Request& context)
XML
</h1>
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DTree</code> values without schema validation.</p>
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DValue</code> values without schema validation.</p>
<h2>Encoded Element Tree</h2>
<pre><?= encoded ?></pre>
<h2>Decoded DTree</h2>
<h2>Decoded DValue</h2>
<pre><?= json_encode(decoded) ?></pre>
<h2>Simple Map Encoding</h2>
@@ -49,7 +49,7 @@ RENDER(Request& context)
<h2>Decode Existing XML</h2>
<p>Input XML:</p>
<pre><?= incoming ?></pre>
<p>Decoded DTree:</p>
<p>Decoded DValue:</p>
<pre><?= json_encode(incoming_tree) ?></pre>
<p>
+7 -7
View File
@@ -12,19 +12,19 @@ RENDER(Request& context)
" - cache\n"
"message: |\n"
" Keep config files readable.\n"
" Load them as DTree values.\n";
" Load them as DValue values.\n";
DTree cfg = yaml_decode(source);
DValue cfg = yaml_decode(source);
String encoded = yaml_encode(cfg);
DTree roundtrip = yaml_decode(encoded);
DValue roundtrip = yaml_decode(encoded);
DTree generated;
DValue generated;
generated["database"]["host"] = "localhost";
generated["database"]["port"] = (f64)3306;
generated["features"]["components"].set_bool(true);
generated["features"]["markdown"].set_bool(true);
DTree theme;
DValue theme;
theme = "clean";
generated["themes"].push(theme);
theme = "compact";
@@ -37,12 +37,12 @@ RENDER(Request& context)
YAML
</h1>
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DTree</code> values.</p>
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DValue</code> values.</p>
<h2>Config Source</h2>
<pre><?= source ?></pre>
<h2>Decoded DTree</h2>
<h2>Decoded DValue</h2>
<pre><?= json_encode(cfg) ?></pre>
<h2>Encoded Again</h2>
+2 -2
View File
@@ -14,12 +14,12 @@ RENDER(Request& context)
mkdir(base);
mkdir(extract_dir);
DTree entries;
DValue entries;
entries["hello.txt"] = "Hello from a generated ZIP archive.\n";
entries["notes/readme.txt"] = "zip_create(), zip_list(), zip_read(), and zip_extract() are available to UCE pages.\n";
zip_create(archive, entries);
DTree listing = zip_list(archive);
DValue listing = zip_list(archive);
String hello = zip_read(archive, "hello.txt");
zip_extract(archive, extract_dir);
String extracted = file_get_contents(path_join(extract_dir, "notes/readme.txt"));
+4
View File
@@ -1,4 +1,8 @@
Runtime
backtrace_frames_string
capture_backtrace_string
error_pages
signal_name
unit_info
units_list
unit_compile
+5 -1
View File
@@ -1,11 +1,13 @@
String Functions
ascii_safe_name
base64_decode
base64_encode
contains
concat
filter
first
join
json_consume_space
json_encode
list_every
list_find
@@ -20,6 +22,8 @@ str_ends_with
str_starts_with
substr
split
split_http_headers
split_kv
split_space
split_utf8
replace
+1
View File
@@ -4,6 +4,7 @@ basename
dirname
expand_path
file_append
file_append_contents
file_exists
file_get_contents
file_mtime
+1
View File
@@ -1,5 +1,6 @@
Time and Date Functions
usleep
time_precise
time
time_format_local
+16 -8
View File
@@ -2,19 +2,27 @@ Types
0_Request
array_merge
0_DTree
dtree_filter
dtree_group_by
dtree_keys
dtree_map
dtree_omit
dtree_pick
dtree_values
0_DValue
dv_filter
dv_group_by
dv_keys
dv_map
dv_omit
dv_pick
dv_values
each
get_by_path
has
is_array
is_list
set_status
String
StringList
StringMap
to_bool
to_f64
to_json
to_s64
to_string
to_stringmap
to_u64
+1
View File
@@ -12,5 +12,6 @@ route_path_normalize
route_path_sanitize
session_id_create
parse_query
parse_uri
uri_decode
uri_encode
@@ -1,8 +1,8 @@
:title
DTree
DValue
:sig
DTree
DValue
:see
>types
@@ -10,23 +10,23 @@ get_by_path
json_decode
:content
`DTree` is UCE's general-purpose structured value type. It is the runtime's default container for nested data such as configuration trees, call payloads, decoded JSON, connection state, and metadata returned by runtime helpers.
`DValue` is UCE's general-purpose structured value type. It is the runtime's default container for nested data such as configuration trees, call payloads, decoded JSON, connection state, and metadata returned by runtime helpers.
## Value Kinds
`DTree` can hold:
`DValue` can hold:
- `String`
- `f64`
- `bool`
- pointer values
- nested child `DTree` values in a map-shaped container
- nested child `DValue` values in a map-shaped container
Map-shaped `DTree` values can also represent list-like data when their keys are numeric strings in sequence.
Map-shaped `DValue` values can also represent list-like data when their keys are numeric strings in sequence.
## Where It Appears
You will encounter `DTree` throughout the runtime, especially in:
You will encounter `DValue` throughout the runtime, especially in:
- `context.cfg`
- `context.props`
@@ -42,14 +42,21 @@ You will encounter `DTree` throughout the runtime, especially in:
- `.key("key")` returns a child pointer when it already exists.
- `.get_or_create("key")` returns a child pointer and creates it when missing.
- `.get_by_path("a/b/c")` traverses nested children without creating missing keys.
- `.to_string()` reads scalar content as text.
- `.to_s64()`, `.to_u64()`, and `.to_f64()` perform best-effort numeric conversion.
- `.to_bool()` performs best-effort boolean conversion.
- `.to_string(default)` reads scalar content as text.
- `.to_s64(default)`, `.to_u64(default)`, and `.to_f64(default)` perform best-effort numeric conversion.
- `.to_bool(default)` performs best-effort boolean conversion.
- `.to_stringmap()` converts a map-shaped tree into `StringMap`.
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules:
```cpp
String title = context.props["title"].to_string("Untitled");
s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25);
```
`operator[]` creates missing entries, just like `std::map`. `.has()` and `.key()` are the non-mutating lookup helpers, and `.get_by_path()` is the non-creating traversal helper.
`json_decode()` currently stores JSON numbers as string-valued `DTree` nodes, so typed numeric conversion is the normal way to consume those values.
`json_decode()` currently stores JSON numbers as string-valued `DValue` nodes, so typed numeric conversion is the normal way to consume those values.
References are dereferenced automatically in most normal reads.
@@ -60,7 +67,7 @@ References are dereferenced automatically in most normal reads.
- `bool` values convert numerically to `1` and `0`.
- Pointer values convert numerically when read as numbers.
- Single-value maps can act as scalar wrappers for numeric and boolean conversion.
- Invalid numeric input falls back to `0`.
- Missing values and invalid numeric input fall back to the accessor's `default_value` argument (`0`, `""`, or `false` when not given).
- `.to_stringmap()` converts map children key-by-key using each child's `to_string()`.
## Writing Values
@@ -93,11 +100,11 @@ Useful inspection helpers include:
## each()
`each(std::function<void (DTree t, String key)> f)` iterates over the current tree value.
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see the `each` page for details).
For map-shaped `DTree` values, the callback runs once per child entry and receives:
For map-shaped `DValue` values, the callback runs once per child entry and receives:
- `t` as the child value
- `t` as the child value, by const reference (no copy)
- `key` as the child key
For non-map values, `each()` still invokes the callback once:
@@ -106,7 +113,7 @@ For non-map values, `each()` still invokes the callback once:
- `key` is an empty string
```cpp
context.connection["items"].each([&](DTree item, String key) {
context.connection["items"].each([&](const DValue& item, String key) {
print(key, ": ", item.to_string(), "\n");
});
```
@@ -120,7 +127,7 @@ u64 compiled_mtime = unit_info("test/hello.uce")["compiled_mtime"].to_u64();
bool dark_mode = context.props["dark_mode"].to_bool();
if(DTree* user = payload.key("user")) {
if(DValue* user = payload.key("user")) {
print(user->to_json());
}
+9 -9
View File
@@ -24,7 +24,7 @@ ws_message
ws_connection_id
ws_connections
ws_send
0_DTree
0_DValue
StringMap
UploadedFile
@@ -120,7 +120,7 @@ Raw request body. For WebSocket handlers, this is the current message payload.
Use this for JSON APIs:
```cpp
DTree body = json_decode(context.in);
DValue body = json_decode(context.in);
```
### `context.uploaded_files`
@@ -165,7 +165,7 @@ Related helpers:
### `context.call`
Type: `DTree`
Type: `DValue`
General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components.
@@ -181,7 +181,7 @@ Prefer clear top-level names when state is app-wide (`route`, `fragments`) and n
### `context.cfg`
Type: `DTree`
Type: `DValue`
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
@@ -199,19 +199,19 @@ String site_name = context.cfg.get_by_path("site/name").to_string();
### `context.props`
Type: `DTree`
Type: `DValue`
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
```cpp
DTree props;
DValue props;
props["title"] = "Dashboard";
print(component("components/card", props, context));
```
### `context.connection`
Type: `DTree`
Type: `DValue`
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
@@ -394,7 +394,7 @@ RENDER(Request& context)
RENDER(Request& context)
{
context.header["Content-Type"] = "application/json";
DTree response;
DValue response;
response["ok"].set_bool(true);
print(json_encode(response));
}
@@ -413,7 +413,7 @@ RENDER(Request& context)
### Component props
```cpp
DTree props;
DValue props;
props["title"] = "Welcome";
print(component("components/card", props, context));
```
+2 -2
View File
@@ -46,12 +46,12 @@ Inside the handler, the usual `Request& context` fields are available:
CLI responses default to `text/plain; charset=utf-8`, but the handler may set headers and status explicitly.
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DTree`.
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DValue`.
```cpp
CLI(Request& context)
{
DTree input = cli_input(context);
DValue input = cli_input(context);
String action = first(input["action"].to_string(), "ping");
if(action == "ping")
{
+1 -1
View File
@@ -15,7 +15,7 @@ INIT(Request& context)
:content
Defines a worker-load hook for the current `.uce` unit.
When a worker loads the unit's compiled shared object into memory, the runtime checks whether the unit exposes `INIT(Request& context)`. If it does, the hook runs once for that load before the unit begins serving later requests from that in-memory copy.
When a worker instantiates the unit's compiled wasm module, the runtime checks whether the unit exposes `INIT(Request& context)`. If it does, the hook runs once for that instance before the unit begins serving requests from that worker-local copy.
Because UCE usually loads units on demand during a request, `INIT()` still receives a valid `Request& context`. Use it for worker-local initialization, not for request-local state that should reset each request.
+1 -1
View File
@@ -32,7 +32,7 @@ For a normal direct page request, `context.props` starts empty.
If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`.
Pages intended to serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. Files may also define `COMPONENT()` handlers when they intentionally need both page and component behavior in one unit.
Pages that serve WebSocket traffic may expose both `RENDER(Request& context)` and `WS(Request& context)`. Files may also define `COMPONENT()` handlers when one unit needs both page and component behavior.
In that case:
+3 -3
View File
@@ -12,15 +12,15 @@ WS(Request& context)
>1_RENDER
:content
Defines the WebSocket message handler for the current `.ws.uce` page.
Defines the WebSocket message handler for the current `.uce` page.
The same page may expose both `RENDER(Request& context)` and `WS(Request& context)`. `RENDER(Request& context)` serves the initial HTTP response, while `WS(Request& context)` is called whenever a complete WebSocket message arrives for that page.
Any `.uce` unit may expose both `RENDER(Request& context)` and `WS(Request& context)`. `RENDER(Request& context)` serves the initial HTTP response, while `WS(Request& context)` is called whenever a complete WebSocket message arrives for that page. Configure nginx/Apache to route WebSocket upgrade requests for `.uce` paths to the runtime's HTTP/WebSocket listener.
UCE reassembles fragmented messages before calling `WS(Request& context)`. Text and binary frames are both delivered. The current payload is available directly on `context.in`, message metadata is mirrored into `context.params["WS_..."]`, and connection-local state lives on `context.connection`.
## Connection State
`context.connection` is a broker-owned `DTree` for the current socket. It starts empty for a new client and persists across later `WS(Request& context)` calls on that same connection.
`context.connection` is a broker-owned `DValue` for the current socket. It starts empty for a new client and persists across later `WS(Request& context)` calls on that same connection.
## Message Data
+6 -6
View File
@@ -14,7 +14,7 @@ unit_call
:content
UCE runs a small custom source-to-source preprocessor before Clang sees a `.uce` or `.ws.uce` file.
The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, with orchestration in `src/lib/compiler.cpp`. It does not try to parse all of C++. Instead, it performs a narrow character-wise rewrite that understands literal output, inline code islands, `#load`, and `EXPORT` harvesting, then writes a generated `.cpp` file and compiles that file into a shared object.
The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, with orchestration in `src/lib/compiler.cpp`. It does not try to parse all of C++. Instead, it performs a narrow character-wise rewrite that understands literal output, inline code islands, `#load`, and `EXPORT` harvesting, then writes a generated `.cpp` file and compiles that file into a WebAssembly side module.
## Syntax
@@ -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`; the wasm unit compile script provides 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.
@@ -47,8 +47,8 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- Lines beginning with `RENDER:NAME(...)` are rewritten into exported `__uce_render_NAME(...)` functions.
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers.
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
- `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`.
- When a worker loads the compiled unit into memory, the runtime checks for `INIT(Request& context)` and calls it once for that worker-side load.
- `scripts/compile_wasm_unit` then compiles that generated `.cpp` into `source_file + ".wasm"` as a PIC WebAssembly side module.
- When a worker instantiates the compiled unit, the runtime checks for `INIT(Request& context)` and calls it once for that worker-side instance.
- On each request, the first time a given unit is entered through `RENDER()`, `CLI()`, or any `COMPONENT...` handler, the runtime checks for `ONCE(Request& context)` and calls it before the selected handler.
## Generated Files
@@ -56,7 +56,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
For a source file like `/some/path/page.uce`, the preprocessor produces:
- generated C++: `BIN_DIRECTORY/some/path/page.uce.cpp`
- shared object: `BIN_DIRECTORY/some/path/page.uce.so`
- wasm side module: `BIN_DIRECTORY/some/path/page.uce.wasm`
- export list: `BIN_DIRECTORY/some/path/page.uce.exports.txt`
## Examples
@@ -153,7 +153,7 @@ The page template can then render `context.call["fragments"]["head"]` inside `<h
- `EXPORT` harvesting only triggers when the current line starts with `EXPORT` at column 1 and is followed by whitespace.
- Relative `#load` paths are expanded against the including unit's source directory.
- `unit_render()` and `unit_call()` are runtime APIs. `#load` is a compile-time composition feature.
- `INIT()` runs when the shared object is loaded into a worker during a request-triggered load, so it still receives a valid `Request& context`.
- `INIT()` runs when the wasm unit is instantiated by a worker during a request-triggered load, so it still receives a valid `Request& context`.
- `ONCE()` is tracked per request and per resolved unit file. A file entered multiple times in one request only runs `ONCE()` once.
## Limitations
+3 -3
View File
@@ -1,6 +1,6 @@
:sig
StringMap array_merge(StringMap a, StringMap b)
DTree array_merge(DTree a, DTree b)
DValue array_merge(DValue a, DValue b)
:params
a : left-hand source map or tree
@@ -9,7 +9,7 @@ return value : merged result
:see
>types
0_DTree
0_DValue
StringMap
json_decode
@@ -18,7 +18,7 @@ Merges two maps or trees using PHP-like merge behavior.
For `StringMap`, keys from `b` overwrite keys from `a`.
For `DTree`, string keys from `b` overwrite keys from `a`. Numeric keys are appended and reindexed when either side behaves like a list.
For `DValue`, string keys from `b` overwrite keys from `a`. Numeric keys are appended and reindexed when either side behaves like a list.
This helper is the closest UCE equivalent to PHP `array_merge()` for common request, config, and JSON-shaped data.
@@ -0,0 +1,18 @@
:sig
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames = 0)
:params
frames : frame pointer array returned by native backtrace collection
size : number of frames in the array
skip_frames : number of newest frames to omit
return value : formatted backtrace string
:see
>runtime
capture_backtrace_string
signal_name
:content
Formats a captured native backtrace frame array.
Most page code should use `capture_backtrace_string()` instead. Use this helper when you already have raw frame pointers from lower-level diagnostic code.
+30
View File
@@ -0,0 +1,30 @@
:sig
String base64_decode(String raw, bool& ok)
:params
raw : Base64 encoded string
ok : set to `true` when decoding succeeds; set to `false` for invalid input
return value : decoded binary-safe string, or an empty string when decoding fails
:see
>string
base64_encode
:content
Decodes a Base64 string.
Pass a `bool` variable for `ok` so callers can distinguish invalid input from a valid empty decoded value.
Example:
```uce
bool ok = false;
String decoded = base64_decode("aGVsbG8=", ok);
// ok == true
// decoded == "hello"
```
Related:
- PHP: `base64_decode($value, true)`
- JavaScript / Node.js: `Buffer.from(value, "base64")`
+27
View File
@@ -0,0 +1,27 @@
:sig
String base64_encode(String raw)
:params
raw : binary-safe source string
return value : Base64 encoded string
:see
>string
base64_decode
:content
Encodes a string with Base64.
UCE strings can contain binary data, so `raw` may include NUL bytes and non-text bytes.
Example:
```uce
String encoded = base64_encode("hello");
// encoded == "aGVsbG8="
```
Related:
- PHP: `base64_encode()`
- JavaScript / Node.js: `Buffer.from(value).toString("base64")`
@@ -0,0 +1,24 @@
:sig
String capture_backtrace_string(u32 max_frames = 32, u32 skip_frames = 0)
:params
max_frames : maximum number of frames to capture
skip_frames : number of newest frames to omit
return value : formatted backtrace string
:see
>runtime
backtrace_frames_string
signal_name
:content
Captures and formats a native backtrace for the current call stack.
This helper is for diagnostics. It is used by runtime error reporting paths and can also be useful in local debugging pages.
Example:
```uce
String trace = capture_backtrace_string(16, 0);
print("<pre>", html_escape(trace), "</pre>");
```
+1 -1
View File
@@ -21,4 +21,4 @@ CLI(Request& context)
}
```
For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DTree` directly.
For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DValue` directly.
+4 -4
View File
@@ -2,17 +2,17 @@
cli_input
:sig
DTree cli_input(Request& context)
DValue cli_input(Request& context)
:see
>1_CLI
>json_decode
>DTree
>DValue
:content
Returns a structured parameter tree for a `CLI(Request& context)` invocation.
`cli_input()` merges simple command inputs into one `DTree`:
`cli_input()` merges simple command inputs into one `DValue`:
1. query parameters from `context.get`
2. form parameters from `context.post`
@@ -25,7 +25,7 @@ If the JSON body is a scalar or array instead of an object, the decoded value is
```cpp
CLI(Request& context)
{
DTree input = cli_input(context);
DValue input = cli_input(context);
String action = first(input["action"].to_string(), "help");
if(action == "echo")
+7 -7
View File
@@ -12,7 +12,7 @@ unit_render
3_C++ Preprocessor
map
filter
dtree_filter
dv_filter
:content
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
@@ -27,7 +27,7 @@ UCE is server-first C++ with a small template preprocessor. It does not try to b
- `ONCE(Request& context)` is per-request setup for a unit before its first render/component entry.
- `INIT(Request& context)` is worker-local setup when a unit is loaded.
- `<?= expression ?>` is escaped interpolation; prefer it for user-visible text.
- `<?: expression ?>` is trusted raw markup output, closer to a deliberate `dangerouslySetInnerHTML` decision.
- `<?: expression ?>` writes trusted raw markup, similar to `dangerouslySetInnerHTML` in React.
- `unit_render()` renders another page unit; `component()` returns component HTML as a string.
## Routes and Layouts
@@ -47,15 +47,15 @@ The function library includes small collection helpers for common route/menu/car
```cpp
auto visible = filter(routes, [](String route) { return(route != "admin"); });
auto labels = map(visible, [](String route) { return(to_upper(route)); });
DTree app_items = dtree_filter(menu, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
DValue app_items = dv_filter(menu, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); });
```
Use these when the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern.
Use these when a short transformation is clearer than a loop. Prefer explicit loops for side effects or multi-step validation.
## Assets and Islands
Global runtime APIs for assets and islands are intentionally not part of UCE core. The starter emits CSS and JavaScript from the owning unit's `ONCE(Request& context)` hook, with a few shared sibling asset components when multiple components need the same files. The only starter web-affordance helper left is `COMPONENT:island` in `components/theme/web_affordances.uce` for small progressive-enhancement modules. This keeps app policy in the app without an asset registry layer.
UCE core does not provide a global asset or island registry. The starter emits CSS and JavaScript from the owning unit's `ONCE(Request& context)` hook, with a few shared sibling asset components when multiple components need the same files. The starter's `COMPONENT:island` helper in `components/theme/web_affordances.uce` covers small progressive-enhancement modules while keeping app policy in the app.
## Debugging
@@ -66,4 +66,4 @@ When a unit fails to compile, UCE reports the source path, generated C++ path, c
- No client-side virtual DOM is built into UCE.
- No global file-router is imposed by the runtime.
- No JSX-like component tags are required for this workflow.
- Component children/slot syntax is intentionally deferred; use explicit props and component calls for now.
- Component children/slot syntax is not part of UCE yet; use explicit props and component calls for now.
+4 -4
View File
@@ -1,5 +1,5 @@
:sig
String component(String name, [DTree props], [Request& context])
String component(String name, [DValue props], [Request& context])
:see
>ob
@@ -37,7 +37,7 @@ When a component unit defines `ONCE(Request& context)`, the runtime calls that h
Default component handler:
```cpp
DTree props;
DValue props;
props["title"] = "Status";
<><?: component("workspace/panel", props, context) ?></>
@@ -46,7 +46,7 @@ props["title"] = "Status";
Named component handler:
```cpp
DTree props;
DValue props;
props["title"] = "System";
props["body"] = "Healthy";
@@ -76,7 +76,7 @@ COMPONENT:BODY(Request& context)
Preparing props in C++ before rendering:
```cpp
DTree props;
DValue props;
props["items"][0] = "alpha";
props["items"][1] = "beta";
props["items"][2] = "gamma";
+1 -1
View File
@@ -11,7 +11,7 @@ component_resolve
:content
Checks whether a component file can be resolved from the current page context.
Resolution tries the exact name first and then the `components/` shorthand form.
Resolution uses the same host resolver as `component_resolve()`: candidate bases include absolute targets, the entry unit directory, the current unit directory, and the site root; each base tries exact, `.uce`, `components/name`, and `components/name.uce` forms.
If `name` contains a colon, only the file portion is used for existence checks.
+2 -2
View File
@@ -1,5 +1,5 @@
:sig
void component_render(String name, [DTree props], [Request& context])
void component_render(String name, [DValue props], [Request& context])
:see
>ob
@@ -24,7 +24,7 @@ Use `component_render()` when you want to write component output directly from C
## Example
```cpp
DTree props;
DValue props;
props["body"] = "Hello";
component_render("components/card:BODY", props, context);
+1 -1
View File
@@ -11,7 +11,7 @@ component_render
:content
Resolves a component name to the concrete `.uce` file path that will be loaded.
Resolution tries the exact file name first, then the same name with `.uce` appended, and then the same two forms under the `components/` prefix.
Resolution searches host-side candidate bases in order: absolute target when supplied, entry unit directory, current unit directory, and site root. Within each base it tries the exact file name, the same name with `.uce` appended, and the same two forms under the `components/` prefix.
If `name` contains a colon, only the file portion is used for resolution.
+2 -13
View File
@@ -1,16 +1,5 @@
:sig
String concat(...vals)
:params
...val : one or more values that should be concatenated
:see
>string
concat(...vals)
:content
Returns a string with all parameters concatenated into one result.
## Related Concepts
- PHP: string concatenation with `.` or helpers like `implode()`
- JavaScript / Node.js: string concatenation with `+`, template literals, or `Array.prototype.join()`
Removed. `concat()` is not a current UCE API. Use string `+`, output streams/`print()`, or `join()` for lists.
+13
View File
@@ -0,0 +1,13 @@
:sig
bool config_bool(String key, bool fallback = true)
:params
key : server configuration key
fallback : value returned when missing
:see
>sys
>config_bool_value
:content
Reads a boolean value from the active server configuration.

Some files were not shown because too many files have changed in this diff Show More