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>
This commit is contained in:
root
2026-06-14 17:51:51 +00:00
co-authored by Claude Opus 4.8
parent 15e8d092bc
commit 8587fbc5aa
14 changed files with 1219 additions and 769 deletions
+59 -24
View File
@@ -19,6 +19,8 @@
#include <atomic>
#include <sys/stat.h>
#include <thread>
// Worker flushes ws_* dispatch batches to the broker via the FastCGI client.
#include "../lib/fcgi_forward.h"
// per forked worker process: one engine + compiled-core cache, one epoch ticker
static WasmWorker* g_wasm_worker = 0;
@@ -116,18 +118,13 @@ static bool wasm_backend_native_fallback_uncached(Request* context, const String
// Supported by the wasm core, intentionally NOT in this list:
// - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in),
// - unit_render()/component() (host resolver).
// What remains is genuinely host-owned / native-only for now:
// - zip pages that use C++ try/catch around zip_* error cases;
// - direct compiler_load_shared_unit() access to native SharedUnit internals.
// Background tasks, sleep/usleep, sockets/custom servers, memcache, mysql,
// and unit_call/unit_compile/unit_info/units_list are host-owned but now have
// membrane hostcalls, so they intentionally do not fallback here.
StringList native_only_tokens = {
"zip_", "compiler_load_shared_unit("
};
for(auto& token : native_only_tokens)
if(source.find(token) != String::npos)
return(true);
// W7d: every former native-only surface now goes through the membrane —
// zip_* (uce_host_zip), unit introspection (uce_host_units: unit_info/
// unit_call/units_list), regex/xml/yaml/markdown, filesystem, sqlite,
// background tasks, sleep, sockets/custom servers, memcache, mysql. No unit
// source token forces native anymore, so there is nothing left to scan for.
// (The native fallback now only covers cold/stale artifacts; W7e removes it.)
(void)source;
return(false);
}
@@ -168,11 +165,12 @@ bool wasm_backend_should_handle(Request& request, const String& entry_unit)
}
// Serve a request through a wasm workspace using the unit handler selected by
// `kind` (page render or cli). Populates the native Request
// (status/headers/cookies/session/body) so the existing transport writes the
// response unchanged. Returns "" on success, or a collapsed error/trace string
// for the caller to route into the configured error page.
String wasm_backend_serve(Request& request, const String& entry_unit, int32_t kind = WasmWorkspace::RESOLVE_RENDER)
// `kind` (page render / cli / serve_http, with an optional named serve_http
// handler). Populates the native Request (status/headers/cookies/session/body)
// so the existing transport writes the response unchanged. Returns "" on
// success, or a collapsed error/trace string for the caller to route into the
// configured error page.
String wasm_backend_serve(Request& request, const String& entry_unit, const String& handler = "render")
{
DValue ctx;
auto copy_map = [&](const StringMap& source, const char* key) {
@@ -186,19 +184,56 @@ String wasm_backend_serve(Request& request, const String& entry_unit, int32_t ki
copy_map(request.session, "session");
ctx["entry_unit"] = entry_unit;
// Raw request body: cli_input() parses a JSON CLI payload from context.in,
// carried into the workspace, not just the form-decoded post map.
// and serve_http handlers read it as req->in; carried into the workspace.
ctx["in"] = request.in;
// WebSocket event context: the workspace owns no connections, so the frame's
// connection identity goes in and the handler's ws_send/ws_close dispatch
// commands come back out (below) for the native broker to apply.
if(handler == "websocket")
{
ctx["ws"]["connection_id"] = request.resources.websocket_connection_id;
ctx["ws"]["scope"] = request.resources.websocket_scope;
ctx["ws"]["opcode"] = (f64)request.resources.websocket_opcode;
ctx["ws"]["binary"].set_bool(request.resources.websocket_is_binary);
for(auto& id : request.resources.websocket_scope_connection_ids)
{
DValue v; v = id;
ctx["ws"]["connections"].push(v);
}
ctx["ws"]["connection_state"] = request.connection;
}
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit, kind);
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit, handler);
if(!response.ok)
return(response.error == "" ? String("wasm workspace failed") : response.error);
// A cli unit that does not export __uce_cli is a 404, matching native
// compiler_invoke_cli ("CLI Entry Point Not Found"). For page render a
// missing handler is simply an empty body (native parity).
if(!response.handler_present && kind == WasmWorkspace::RESOLVE_CLI)
// Any handler may have called ws_send/ws_close (not just WS handlers). If it
// left dispatch commands, flush the batch to the central WS broker, which owns
// all connections and resolves each command's target (id/scope/broadcast)
// against the full registry. This is the only path WS data takes out — the
// workspace owns no connections.
if(DValue* cmds = response.meta.key("ws_commands"))
{
request.set_status(404, "CLI Entry Point Not Found");
DValue batch;
batch["commands"] = *cmds;
if(DValue* cstate = response.meta.key("ws_connection_state"))
{
batch["connection_id"] = request.resources.websocket_connection_id;
batch["connection_state"] = *cstate;
}
StringMap dispatch_params;
dispatch_params["UCE_WS_DISPATCH"] = "1";
String broker_socket = first(request.server ? request.server->config["WS_BROKER_SOCKET_PATH"] : String(),
"/run/uce/ws-broker.sock");
fcgi_forward_request(broker_socket, dispatch_params, ucb_encode(batch), 5);
}
// A cli/serve_http unit that does not export the requested handler is a 404,
// matching native compiler_invoke_cli. For page render a missing handler is
// simply an empty body (native parity).
if(!response.handler_present && handler != "render")
{
request.set_status(404, handler == "cli" ? "CLI Entry Point Not Found" : "Handler Not Found");
return("");
}
+6 -18
View File
@@ -6,27 +6,15 @@
// backend.cpp — so it does not have to compile worker.cpp + wasmtime.hh on
// every build. Include only after uce_lib.h (needs Request / String).
#include <cstdint>
// Resolve-kind selector for wasm_backend_serve, mirrored from
// WasmWorkspace::ResolveKind in worker.cpp. Only the entry kinds actually wired
// to the wasm backend live here; WebSocket/serve_http re-enter when W7b/W7c land
// (via the broker→worker-pool forwarding model, not in-fork rendering).
namespace wasm_kind {
enum {
RENDER = 1,
CLI = 4,
};
}
// True if this request should be served by the wasm backend (config + artifact
// + fallback-token gate); mode-agnostic, the caller picks the kind.
// + fallback-token gate); handler-agnostic, the caller names the handler.
bool wasm_backend_should_handle(Request& request, const String& entry_unit);
// Serve a request through a wasm workspace using the unit handler for `kind`,
// populating the native Request. Returns "" on success or a collapsed error.
String wasm_backend_serve(Request& request, const String& entry_unit,
int32_t kind = wasm_kind::RENDER);
// Serve a request through a wasm workspace by invoking a named unit handler
// "render", "cli", "websocket", "serve_http", "serve_http:named" — and populate
// the native Request. Returns "" on success or a collapsed error. The handler is
// just an export name; there is no per-mode machinery.
String wasm_backend_serve(Request& request, const String& entry_unit, const String& handler = "render");
// Join the per-process epoch ticker before the worker process exits.
void wasm_backend_shutdown();
+68 -33
View File
@@ -475,7 +475,8 @@ extern "C" void uce_wasm_link_anchors()
// slots (loading units lazily) and writes the resolved unit path back so
// nested relative component resolution keeps working.
extern "C" int32_t uce_host_component_resolve(
const char* target, size_t target_len, int32_t kind,
const char* target, size_t target_len,
const char* handler, size_t handler_len,
const char* current_unit, size_t current_unit_len,
char* resolved_buf, size_t resolved_cap);
@@ -534,38 +535,37 @@ struct RequestPropsScope
}
};
// kind values shared with the host loader (src/wasm/worker.cpp)
enum WasmResolveKind {
WASM_RESOLVE_COMPONENT = 0,
WASM_RESOLVE_RENDER = 1,
WASM_RESOLVE_EXISTS = 2,
WASM_RESOLVE_ONCE = 3,
WASM_RESOLVE_CLI = 4,
};
static s32 wasm_resolve_target(String target, s32 kind, String* resolved_out = 0)
// A unit is a bag of exported handlers; invoking any of them is one operation —
// the host resolves __uce_<handler> in the loaded module to a funcref slot. The
// handler is just a string: "render", "component:CARD", "render:VARIANT",
// "once", "cli", "websocket", "serve_http:named" — or "exists" (an existence
// probe that loads nothing). No per-mode kinds.
static s32 wasm_resolve_target(String unit_target, String handler, String* resolved_out = 0)
{
String cache_key = std::to_string(kind) + ":" + target;
String cache_key = handler + "\t" + unit_target;
bool is_exists = (handler == "exists");
auto cached = wasm_component_slots.find(cache_key);
if(cached != wasm_component_slots.end() && kind != WASM_RESOLVE_EXISTS)
if(cached != wasm_component_slots.end() && !is_exists)
return(cached->second);
char resolved[512];
String current = context ? context->resources.current_unit_file : "";
s32 slot = uce_host_component_resolve(
target.data(), target.size(), kind,
unit_target.data(), unit_target.size(), handler.data(), handler.size(),
current.data(), current.size(),
resolved, sizeof(resolved));
if(resolved_out && slot)
*resolved_out = String(resolved, strnlen(resolved, sizeof(resolved)));
if(kind != WASM_RESOLVE_EXISTS)
if(!is_exists)
wasm_component_slots[cache_key] = slot;
return(slot);
}
String component_resolve(String name)
{
String file_name, render_name;
component_parse_target(trim(name), file_name, render_name);
String resolved;
if(wasm_resolve_target(trim(name), WASM_RESOLVE_EXISTS, &resolved))
if(wasm_resolve_target(file_name, "exists", &resolved))
return(resolved);
return("");
}
@@ -585,7 +585,7 @@ static void wasm_run_once(const String& resolved, Request& request)
if(request.once_units.find(resolved) != request.once_units.end())
return;
request.once_units.insert(resolved);
s32 once_slot = wasm_resolve_target(resolved, WASM_RESOLVE_ONCE);
s32 once_slot = wasm_resolve_target(resolved, "once");
if(once_slot == 0)
return;
String previous_unit = request.resources.current_unit_file;
@@ -597,8 +597,11 @@ static void wasm_run_once(const String& resolved, Request& request)
void component_render(String name, DValue props, Request& request)
{
String file_name, render_name;
component_parse_target(trim(name), file_name, render_name);
String handler = render_name == "" ? String("component") : "component:" + render_name;
String resolved;
s32 slot = wasm_resolve_target(trim(name), WASM_RESOLVE_COMPONENT, &resolved);
s32 slot = wasm_resolve_target(file_name, handler, &resolved);
if(!slot)
{
print(component_error_banner("component not found: " + trim(name)));
@@ -611,8 +614,8 @@ void component_render(String name, DValue props, Request& request)
request.resources.current_unit_file = resolved;
// a wasm function pointer is its index in the shared funcref table; the
// host returned the handler's slot, so this is a plain call_indirect
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
handler(request);
request_ref_handler handler_fn = (request_ref_handler)(uintptr_t)slot;
handler_fn(request);
request.resources.current_unit_file = previous_unit;
}
@@ -633,8 +636,11 @@ String component(String name, DValue props) { return(component(name, props, *con
void unit_render(String file_name, Request& request)
{
String unit_name, render_name;
component_parse_target(trim(file_name), unit_name, render_name);
String handler = render_name == "" ? String("render") : "render:" + render_name;
String resolved;
s32 slot = wasm_resolve_target(trim(file_name), WASM_RESOLVE_RENDER, &resolved);
s32 slot = wasm_resolve_target(unit_name, handler, &resolved);
if(!slot)
{
print(component_error_banner("unit not found: " + trim(file_name)));
@@ -644,8 +650,8 @@ void unit_render(String file_name, Request& request)
String previous_unit = request.resources.current_unit_file;
if(resolved != "")
request.resources.current_unit_file = resolved;
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
handler(request);
request_ref_handler handler_fn = (request_ref_handler)(uintptr_t)slot;
handler_fn(request);
request.resources.current_unit_file = previous_unit;
}
@@ -653,25 +659,25 @@ void unit_render(String file_name) { unit_render(file_name, *context); }
extern "C" {
// Host calls this to invoke the request's entry unit through one of its
// directive handlers (render/cli/websocket/serve_http). Routing through the
// resolver gives the entry the same ONCE() + dispatch semantics as components
// (the host pre-loaded it, so resolution is a cache hit). The host pre-checks
// that the handler export exists, so a missing slot here is a silent no-op.
void uce_wasm_invoke_entry(const char* path, size_t len, int32_t kind)
// Host calls this to invoke the request's entry unit through a named handler
// ("render"/"cli"/"websocket"/"serve_http:named"). It is the same resolve +
// ONCE() + dispatch path as components — the entry handler is just another
// export. The host pre-checks the export exists, so a missing slot is a no-op.
void uce_wasm_invoke_entry(const char* path, size_t path_len, const char* handler, size_t handler_len)
{
// the host always runs uce_wasm_core_init + apply_context before this
String file_name(path, len);
String file_name(path, path_len);
String handler_name(handler, handler_len);
String resolved;
s32 slot = wasm_resolve_target(trim(file_name), kind, &resolved);
s32 slot = wasm_resolve_target(trim(file_name), handler_name, &resolved);
if(!slot)
return;
wasm_run_once(resolved, *context);
String previous_unit = context->resources.current_unit_file;
if(resolved != "")
context->resources.current_unit_file = resolved;
request_ref_handler handler = (request_ref_handler)(uintptr_t)slot;
handler(*context);
request_ref_handler handler_fn = (request_ref_handler)(uintptr_t)slot;
handler_fn(*context);
context->resources.current_unit_file = previous_unit;
}
@@ -761,6 +767,27 @@ int uce_wasm_apply_context(const char* buf, size_t len)
wasm_request.resources.current_unit_file = entry->to_string();
DValue* raw_in = decoded.key("in");
wasm_request.in = raw_in ? raw_in->to_string() : "";
// websocket event context: ws_send()/ws_close() capture into the dispatch
// list (the workspace owns no connections), which collect() carries back to
// the broker. Reset per invocation.
wasm_request.resources.websocket_dispatch_commands = DValue();
wasm_request.resources.websocket_dispatch_capture = false;
DValue* ws = decoded.key("ws");
if(ws)
{
wasm_request.resources.websocket_connection_id = (*ws)["connection_id"].to_string();
wasm_request.resources.websocket_scope = (*ws)["scope"].to_string();
wasm_request.resources.websocket_opcode = (u8)(*ws)["opcode"].to_u64();
wasm_request.resources.websocket_is_binary = (*ws)["binary"].to_bool();
wasm_request.resources.websocket_scope_connection_ids.clear();
if(DValue* conns = ws->key("connections"))
conns->each([&](const DValue& v, String) {
wasm_request.resources.websocket_scope_connection_ids.push_back(v.to_string());
});
wasm_request.resources.websocket_dispatch_capture = true;
if(DValue* cstate = ws->key("connection_state"))
wasm_request.connection = *cstate;
}
return(0);
}
@@ -787,6 +814,14 @@ void uce_wasm_finish_response_meta()
}
for(auto& entry : wasm_request.session)
meta["session"][entry.first] = entry.second;
// Any unit code (not just WS handlers) may call ws_send/ws_close; whenever the
// dispatch list is non-empty, carry it back so the worker can flush it to the
// broker. ws_connection_state rides along for stateful WS handlers.
if(!wasm_request.resources.websocket_dispatch_commands._map.empty())
{
meta["ws_commands"] = wasm_request.resources.websocket_dispatch_commands;
meta["ws_connection_state"] = wasm_request.connection;
}
wasm_response_meta = ucb_encode(meta);
}
+3
View File
@@ -22,5 +22,8 @@ uce_host_file_exists
uce_host_file_read
uce_host_file_write
uce_host_file_unlink
uce_host_file_list
uce_host_file_mkdir
uce_host_file_mtime
uce_host_regex
uce_host_sqlite
+127 -48
View File
@@ -42,6 +42,9 @@
#include <unistd.h>
#include <sys/time.h>
#include <vector>
#include <algorithm>
#include <dirent.h>
#include <cerrno>
struct WasmDylinkInfo
{
@@ -388,8 +391,6 @@ public:
// resolve-kind values shared with the guest core (src/wasm/core.cpp)
// must match WasmResolveKind in src/wasm/core.cpp
enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2, RESOLVE_ONCE = 3,
RESOLVE_CLI = 4 };
String birth()
{
@@ -482,38 +483,52 @@ public:
return("");
}
// Invoke the entry unit through one of its directive handlers
// (render/cli/websocket/serve_http, selected by kind). handler_present, when
// provided, reports whether the unit actually exports that handler — the
// caller maps a missing render to an empty body (native parity) and a
// missing cli/serve handler to a 404.
String invoke_entry(const String& entry_source_path, int32_t kind, bool* handler_present = 0)
// Invoke the entry unit through a named handler ("render"/"cli"/"websocket"/
// "serve_http:named"). The handler is just an export name — same resolve +
// ONCE + dispatch path as a component. handler_present, when provided,
// reports whether the unit exports it (caller maps a missing render to an
// empty body, a missing cli/serve handler to a 404).
String invoke_entry(const String& entry_source_path, const String& handler, bool* handler_present = 0)
{
entry_dir = dir_of(entry_source_path);
size_t unit_index = 0;
String error = load_unit(entry_source_path, unit_index);
if(error != "")
return(error);
String symbol = kind == RESOLVE_CLI ? "__uce_cli" : "__uce_render";
bool present = (bool)unit_func(unit_index, symbol);
bool present = (bool)unit_func(unit_index, handler_export_symbol(handler));
if(handler_present)
*handler_present = present;
if(!present)
return("");
// Invoke through the core so the entry gets the same ONCE() + dispatch
// path as components (unit is pre-loaded above; resolution is cached).
// Invoke through the core: it resolves the same handler and runs ONCE +
// dispatch (unit is pre-loaded above; resolution is cached). The core
// entry takes two guest buffers — the unit path and the handler name.
auto entry = core_func("uce_wasm_invoke_entry");
if(!entry)
return("core does not export uce_wasm_invoke_entry");
int32_t guest_ptr = 0;
error = call_core("uce_alloc", { (int32_t)entry_source_path.size() }, &guest_ptr);
if(error != "" || guest_ptr == 0)
int32_t path_ptr = 0, handler_ptr = 0;
error = call_core("uce_alloc", { (int32_t)entry_source_path.size() }, &path_ptr);
if(error != "" || path_ptr == 0)
return(error == "" ? String("guest uce_alloc failed for entry path") : error);
error = guest_write((u32)guest_ptr, entry_source_path);
error = guest_write((u32)path_ptr, entry_source_path);
if(error == "")
{
error = call_core("uce_alloc", { (int32_t)handler.size() }, &handler_ptr);
if(error == "" && handler_ptr == 0)
error = "guest uce_alloc failed for handler";
}
if(error == "")
error = guest_write((u32)handler_ptr, handler);
if(error != "")
{
if(path_ptr) call_core("uce_free", { path_ptr }, 0);
if(handler_ptr) call_core("uce_free", { handler_ptr }, 0);
return(error);
auto result = entry->call(ctx(), { wasmtime::Val(guest_ptr), wasmtime::Val((int32_t)entry_source_path.size()), wasmtime::Val(kind) });
call_core("uce_free", { guest_ptr }, 0);
}
auto result = entry->call(ctx(), { wasmtime::Val(path_ptr), wasmtime::Val((int32_t)entry_source_path.size()),
wasmtime::Val(handler_ptr), wasmtime::Val((int32_t)handler.size()) });
call_core("uce_free", { path_ptr }, 0);
call_core("uce_free", { handler_ptr }, 0);
if(!result)
return(trap_text(result.err()));
return("");
@@ -521,7 +536,7 @@ public:
String render_entry(const String& entry_source_path)
{
return(invoke_entry(entry_source_path, RESOLVE_RENDER));
return(invoke_entry(entry_source_path, "render"));
}
String collect(WasmResponse& response)
@@ -891,6 +906,12 @@ private:
return(stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
}
static bool dir_exists_host(const String& path)
{
struct stat st;
return(stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
}
String resolve_source_path(const String& file_name, const String& current_unit)
{
std::vector<String> bases;
@@ -926,7 +947,7 @@ private:
// guest file access policy: only inside the site tree, resolved against
// the entry unit's directory first (the native cwd convention), then the
// site root; containment checked on the canonicalized path
String resolve_guest_file(const String& raw, const String& current_unit = "")
String resolve_guest_file(const String& raw, const String& current_unit = "", bool allow_dir = false)
{
if(raw == "" || raw.find('\0') != String::npos)
return("");
@@ -970,7 +991,7 @@ private:
}
if(!allowed)
continue;
if(file_exists_host(path))
if(file_exists_host(path) || (allow_dir && dir_exists_host(path)))
return(path);
}
return("");
@@ -1022,8 +1043,20 @@ private:
return(result);
}
// hostcall body: uce_host_component_resolve(target, kind, current) → slot
int32_t component_resolve(const String& target, int32_t kind, const String& current_unit, String& resolved_out)
// __uce_<base>[_<suffix>] for a handler spec like "component:CARD" / "render".
static String handler_export_symbol(const String& handler)
{
auto colon = handler.find(":");
String symbol = "__uce_" + (colon == String::npos ? handler : handler.substr(0, colon));
if(colon != String::npos)
symbol += "_" + sanitize_symbol_suffix(handler.substr(colon + 1));
return(symbol);
}
// hostcall body: uce_host_component_resolve(unit, handler, current) → slot.
// `handler` names the export ("render", "component:CARD", "cli",
// "serve_http:named", "once") or is "exists" (probe only, loads nothing).
int32_t component_resolve(const String& target, const String& handler, const String& current_unit, String& resolved_out)
{
auto probe_start = std::chrono::steady_clock::now();
auto record_probe = [&]() {
@@ -1032,13 +1065,6 @@ private:
std::chrono::steady_clock::now() - probe_start).count();
};
String file_name = target;
String render_name;
auto split = target.find(":");
if(split != String::npos)
{
render_name = target.substr(split + 1);
file_name = target.substr(0, split);
}
if(file_name == "" && current_unit != "")
file_name = current_unit;
if(file_name == "")
@@ -1054,7 +1080,7 @@ private:
return(0);
}
resolved_out = resolved;
if(kind == RESOLVE_EXISTS)
if(handler == "exists")
{
record_probe();
return(1);
@@ -1068,12 +1094,7 @@ private:
record_probe();
return(0);
}
String symbol = kind == RESOLVE_RENDER ? "__uce_render"
: kind == RESOLVE_ONCE ? "__uce_once"
: kind == RESOLVE_CLI ? "__uce_cli"
: "__uce_component";
if(render_name != "")
symbol += "_" + sanitize_symbol_suffix(render_name);
String symbol = handler_export_symbol(handler);
String slot_key = resolved + ":" + symbol;
auto cached = handler_slots.find(slot_key);
if(cached != handler_slots.end())
@@ -1081,17 +1102,17 @@ private:
record_probe();
return((int32_t)cached->second);
}
auto handler = unit_func(unit_index, symbol);
if(!handler)
auto handler_fn = unit_func(unit_index, symbol);
if(!handler_fn)
{
// ONCE is optional per unit; a missing __uce_once is not an error
if(kind != RESOLVE_ONCE)
if(handler != "once")
fprintf(stderr, "[wasm] %s does not export %s\n", resolved.c_str(), symbol.c_str());
record_probe();
return(0);
}
u32 slot = 0;
error = place_funcref(*handler, slot);
error = place_funcref(*handler_fn, slot);
if(error != "")
{
fprintf(stderr, "[wasm] %s\n", error.c_str());
@@ -1184,6 +1205,31 @@ private:
results[0] = Val(resolved != "" ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_mkdir")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_write(path, current);
int ok = 0;
if(resolved != "")
ok = (::mkdir(resolved.c_str(), 0777) == 0 || errno == EEXIST) ? 1 : 0;
results[0] = Val((int32_t)ok);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_mtime")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_file(path, current);
int64_t mtime = 0;
struct stat st;
if(resolved != "" && stat(resolved.c_str(), &st) == 0)
mtime = (int64_t)st.st_mtime;
results[0] = Val((int64_t)mtime);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_read")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
@@ -1204,6 +1250,38 @@ private:
results[0] = Val((int32_t)bytes.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_list")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_file(path, current, true /*allow_dir*/);
String listing;
if(resolved != "")
{
std::vector<String> names;
if(DIR* d = opendir(resolved.c_str()))
{
while(struct dirent* e = readdir(d))
{
String n = e->d_name;
if(n != "." && n != "..")
names.push_back(n);
}
closedir(d);
}
// match the native ls -1 convention: bare names, sorted
std::sort(names.begin(), names.end());
listing = join(names, "\n");
}
u32 cap = (u32)args[5].i32();
int32_t buf = args[4].i32();
// length-query convention: no copy unless the buffer fits
if(buf != 0 && cap >= listing.size())
self->hostcall_write(buf, listing);
results[0] = Val((int32_t)listing.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_write")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current, content;
@@ -1714,17 +1792,18 @@ private:
}));
if(mod == "env" && name == "uce_host_component_resolve")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String target, current, resolved;
String target, handler, current, resolved;
self->hostcall_read(args[0].i32(), args[1].i32(), target);
self->hostcall_read(args[3].i32(), args[4].i32(), current);
int32_t slot = self->component_resolve(target, args[2].i32(), current, resolved);
u32 cap = (u32)args[6].i32();
self->hostcall_read(args[2].i32(), args[3].i32(), handler);
self->hostcall_read(args[4].i32(), args[5].i32(), current);
int32_t slot = self->component_resolve(target, handler, current, resolved);
u32 cap = (u32)args[7].i32();
if(cap > 0)
{
if(resolved.size() >= cap)
resolved = resolved.substr(0, cap - 1);
resolved.push_back('\0');
self->hostcall_write(args[5].i32(), resolved);
self->hostcall_write(args[6].i32(), resolved);
}
results[0] = Val(slot);
return(std::monostate());
@@ -1752,7 +1831,7 @@ private:
// ---- public entry: one request through one workspace -----------------------
inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_tree, const String& entry_source_path,
int32_t kind = WasmWorkspace::RESOLVE_RENDER)
const String& handler = "render")
{
WasmResponse response;
WasmWorkspace workspace(worker);
@@ -1763,7 +1842,7 @@ inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_
if(error == "")
error = workspace.apply_context(context_tree);
if(error == "")
error = workspace.invoke_entry(entry_source_path, kind, &response.handler_present);
error = workspace.invoke_entry(entry_source_path, handler, &response.handler_present);
if(error == "")
error = workspace.collect(response);
response.workspace_birth_us = workspace.workspace_birth_us;