feat: extend wasm backend entrypoints

This commit is contained in:
root
2026-06-13 21:50:19 +00:00
parent fe83c52411
commit d6421cb8f3
14 changed files with 315 additions and 83 deletions
+40 -6
View File
@@ -151,13 +151,13 @@ static bool wasm_backend_native_fallback_needed(Request* context, const String&
// True if this request should be served by the wasm backend. Falls through to
// native when disabled, when init failed, or when the unit has no wasm artifact
// (e.g. units skip-listed for try/catch — automatic, graceful fallback).
// (e.g. units skip-listed for try/catch — automatic, graceful fallback). Mode-
// agnostic: the caller (page render / cli / serve_http) decides which entry to
// route here; this only gates on config, fallback tokens, artifact, and worker.
bool wasm_backend_should_handle(Request& request, const String& entry_unit)
{
if(!wasm_backend_configured(&request))
return(false);
if(request.resources.is_cli)
return(false);
if(wasm_backend_native_fallback_needed(&request, entry_unit))
return(false);
if(!wasm_artifact_exists(&request, entry_unit))
@@ -167,11 +167,13 @@ bool wasm_backend_should_handle(Request& request, const String& entry_unit)
return(true);
}
// Serve the page render through a wasm workspace. Populates the native Request
// Serve a request through a wasm workspace using the unit handler selected by
// `kind` (page render / cli / serve_http). 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)
String wasm_backend_serve(Request& request, const String& entry_unit, int32_t kind = WasmWorkspace::RESOLVE_RENDER,
const String& handler_name = "")
{
DValue ctx;
auto copy_map = [&](const StringMap& source, const char* key) {
@@ -184,11 +186,24 @@ String wasm_backend_serve(Request& request, const String& entry_unit)
copy_map(request.cookies, "cookies");
copy_map(request.session, "session");
ctx["entry_unit"] = entry_unit;
// Raw request body: cli_input() parses a JSON CLI payload from context.in,
// and ws_message() exposes the websocket frame from it — both need it carried
// into the workspace, not just the form-decoded post map.
ctx["in"] = request.in;
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit);
WasmResponse response = wasm_worker_serve(*g_wasm_worker, ctx, entry_unit, kind, handler_name);
if(!response.ok)
return(response.error == "" ? String("wasm workspace failed") : response.error);
// A cli/serve_http unit that does not export the requested handler 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_RENDER)
{
request.set_status(404, kind == WasmWorkspace::RESOLVE_CLI ? "CLI Entry Point Not Found" : "Handler Not Found");
return("");
}
// Diagnostic timing headers are opt-in: they leak workspace internals and
// belong to the W5 benchmark harness, not public responses.
if(config_bool("WASM_BACKEND_VERBOSE", false))
@@ -237,3 +252,22 @@ void wasm_backend_shutdown()
if(g_wasm_epoch_running.exchange(false) && g_wasm_epoch_ticker.joinable())
g_wasm_epoch_ticker.join();
}
// A connection-broker child (the custom-server HTTP dispatcher, the websocket
// exec child) is forked from a worker that may already have brought up its wasm
// engine. The Wasmtime engine and the epoch-ticker thread must not cross fork:
// the child inherits the pointer/flags but only the forking thread survives, so
// the ticker is a phantom and the engine state is unsafe. Reset the per-process
// statics so the child lazily initializes its own engine + ticker on first use.
// The inherited std::thread refers to a thread that does not exist in the child;
// placement-new it back to a default (non-joinable) state so the eventual
// re-assignment in ensure_started does not std::terminate on a "joinable" object,
// and so no join/detach touches the dead handle.
void wasm_backend_reset_after_fork()
{
g_wasm_worker = 0;
g_wasm_init_attempted = false;
g_wasm_init_error = "";
g_wasm_epoch_running.store(false);
new (&g_wasm_epoch_ticker) std::thread();
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
// Public surface of the wasm backend object (src/wasm/backend.cpp → wasm.o).
//
// The native main object (linux_fastcgi.cpp) includes this header — not
// 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. Kept in sync there; the values are
// the wasm unit-handler dispatch kinds the host loader understands.
namespace wasm_kind {
enum {
RENDER = 1,
CLI = 4,
WEBSOCKET = 5,
SERVE_HTTP = 6,
};
}
// True if this request should be served by the wasm backend (config + artifact
// + fallback-token gate); mode-agnostic, the caller picks the kind.
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, const String& handler_name = "");
// Join the per-process epoch ticker before the worker process exits.
void wasm_backend_shutdown();
// Drop the inherited (post-fork unusable) engine so a broker child re-inits its
// own; call right after fork in a long-lived child.
void wasm_backend_reset_after_fork();
+23 -5
View File
@@ -540,6 +540,9 @@ enum WasmResolveKind {
WASM_RESOLVE_RENDER = 1,
WASM_RESOLVE_EXISTS = 2,
WASM_RESOLVE_ONCE = 3,
WASM_RESOLVE_CLI = 4,
WASM_RESOLVE_WEBSOCKET = 5,
WASM_RESOLVE_SERVE_HTTP = 6,
};
static s32 wasm_resolve_target(String target, s32 kind, String* resolved_out = 0)
@@ -652,13 +655,26 @@ void unit_render(String file_name) { unit_render(file_name, *context); }
extern "C" {
// Host calls this to render the request's entry unit. Routing through
// unit_render gives the entry the same ONCE() + dispatch semantics as
// components (the host pre-loaded it, so resolution is a cache hit).
void uce_wasm_render_entry(const char* path, size_t len)
// 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)
{
// the host always runs uce_wasm_core_init + apply_context before this
unit_render(String(path, len), *context);
String file_name(path, len);
String resolved;
s32 slot = wasm_resolve_target(trim(file_name), kind, &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);
context->resources.current_unit_file = previous_unit;
}
void* uce_alloc(size_t len)
@@ -745,6 +761,8 @@ int uce_wasm_apply_context(const char* buf, size_t len)
DValue* entry = decoded.key("entry_unit");
if(entry)
wasm_request.resources.current_unit_file = entry->to_string();
DValue* raw_in = decoded.key("in");
wasm_request.in = raw_in ? raw_in->to_string() : "";
return(0);
}
+9
View File
@@ -0,0 +1,9 @@
// Translation unit for the wasm backend object (wasm.o).
//
// Pulls in the runtime *declarations* (uce_lib.h) and then the backend
// definitions, so the heavy wasmtime.hh + worker.cpp only recompile when the
// wasm sources change — not on every native build. The symbols it references
// (String/DValue/config/connectors/unit_*/socket_*) are defined in the core
// (main) object and resolved at link.
#include "../lib/uce_lib.h"
#include "backend.cpp"
+42 -12
View File
@@ -84,6 +84,7 @@ struct WasmWorkerConfig
struct WasmResponse
{
bool ok = false;
bool handler_present = true; // false → unit has no handler for the requested kind (404)
String body;
DValue meta; // status / headers / cookies / session
String error; // collapsed trace or loader error when !ok
@@ -387,7 +388,8 @@ 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 };
enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2, RESOLVE_ONCE = 3,
RESOLVE_CLI = 4, RESOLVE_WEBSOCKET = 5, RESOLVE_SERVE_HTTP = 6 };
String birth()
{
@@ -480,35 +482,59 @@ public:
return("");
}
String render_entry(const String& entry_source_path)
// 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,
const String& handler_name = "")
{
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);
// a page with no RENDER block renders empty (native parity), not an error
if(!unit_func(unit_index, "__uce_render"))
// Base handler export for the kind, plus the named suffix (serve_http /
// render / component can route to a named entry, e.g. __uce_serve_http_x).
String symbol = kind == RESOLVE_CLI ? "__uce_cli"
: kind == RESOLVE_WEBSOCKET ? "__uce_websocket"
: kind == RESOLVE_SERVE_HTTP ? "__uce_serve_http"
: "__uce_render";
if(handler_name != "")
symbol += "_" + sanitize_symbol_suffix(handler_name);
bool present = (bool)unit_func(unit_index, symbol);
if(handler_present)
*handler_present = present;
if(!present)
return("");
// Render through the core so the entry gets the same ONCE() + dispatch
// The core parses a "path:name" target into the named suffix; the bare
// path above loaded the module, this drives the handler resolution.
String target = handler_name == "" ? entry_source_path : entry_source_path + ":" + handler_name;
// Invoke through the core so the entry gets the same ONCE() + dispatch
// path as components (unit is pre-loaded above; resolution is cached).
auto entry = core_func("uce_wasm_render_entry");
auto entry = core_func("uce_wasm_invoke_entry");
if(!entry)
return("core does not export uce_wasm_render_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);
error = call_core("uce_alloc", { (int32_t)target.size() }, &guest_ptr);
if(error != "" || guest_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)guest_ptr, target);
if(error != "")
return(error);
auto result = entry->call(ctx(), { wasmtime::Val(guest_ptr), wasmtime::Val((int32_t)entry_source_path.size()) });
auto result = entry->call(ctx(), { wasmtime::Val(guest_ptr), wasmtime::Val((int32_t)target.size()), wasmtime::Val(kind) });
call_core("uce_free", { guest_ptr }, 0);
if(!result)
return(trap_text(result.err()));
return("");
}
String render_entry(const String& entry_source_path)
{
return(invoke_entry(entry_source_path, RESOLVE_RENDER));
}
String collect(WasmResponse& response)
{
String error = call_core("uce_wasm_finish_output", {}, 0);
@@ -1055,6 +1081,9 @@ private:
}
String symbol = kind == RESOLVE_RENDER ? "__uce_render"
: kind == RESOLVE_ONCE ? "__uce_once"
: kind == RESOLVE_CLI ? "__uce_cli"
: kind == RESOLVE_WEBSOCKET ? "__uce_websocket"
: kind == RESOLVE_SERVE_HTTP ? "__uce_serve_http"
: "__uce_component";
if(render_name != "")
symbol += "_" + sanitize_symbol_suffix(render_name);
@@ -1735,7 +1764,8 @@ private:
// ---- public entry: one request through one workspace -----------------------
inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_tree, const String& entry_source_path)
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_name = "")
{
WasmResponse response;
WasmWorkspace workspace(worker);
@@ -1746,7 +1776,7 @@ inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_
if(error == "")
error = workspace.apply_context(context_tree);
if(error == "")
error = workspace.render_entry(entry_source_path);
error = workspace.invoke_entry(entry_source_path, kind, &response.handler_present, handler_name);
if(error == "")
error = workspace.collect(response);
response.workspace_birth_us = workspace.workspace_birth_us;