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:
+68
-33
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user