This commit is contained in:
udo
2026-06-13 15:10:42 +00:00
parent afaa4dd7c0
commit 5a56d4f39e
14 changed files with 531 additions and 83 deletions
+77 -25
View File
@@ -10,6 +10,10 @@
// handle_complete() changes. CLI, serve_http, and websocket stay native.
#include "../lib/wasm_trace.h"
// The native server TU has the real connectors (sqlite/mysql) compiled in, so
// the worker's host-side connector hostcalls are available here. The W3 CLI
// driver does not define this and gets a named-trap stub for those imports.
#define UCE_WASM_HOST_CONNECTORS 1
#include "worker.cpp"
#include <atomic>
@@ -44,6 +48,12 @@ static String wasm_backend_ensure_started(Request* context)
path_join(cfg["COMPILER_SYS_PATH"], "bin/wasm/core.wasm"));
wc.site_root = path_join(cfg["COMPILER_SYS_PATH"], cfg["SITE_DIRECTORY"]);
wc.cache_root = cfg["BIN_DIRECTORY"];
// write membrane allowlist: the site tree plus the runtime scratch dirs
// pages legitimately write to (matches native reachable write targets).
wc.write_roots = { wc.site_root, "/tmp" };
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
if(cfg[key] != "")
wc.write_roots.push_back(cfg[key]);
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
@@ -75,31 +85,68 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit)
if(entry_unit == "")
return(false);
String wasm_path = context->server->config["BIN_DIRECTORY"] + entry_unit + ".wasm";
struct stat st;
return(stat(wasm_path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
struct stat wasm_st;
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
return(false);
// The wasm path bypasses the native JIT recompile, so a source edit would
// otherwise be served from a stale .wasm. Require the artifact to be newer
// than the source; if it is stale, fall back to native — which JIT-rebuilds
// both .so and .wasm — so the next request gets a fresh artifact.
struct stat src_st;
if(stat(entry_unit.c_str(), &src_st) == 0 && wasm_st.st_mtime < src_st.st_mtime)
return(false);
return(true);
}
// Decide, by source inspection, whether a page must stay on the native
// backend for now (W5 surfaces not yet behind the membrane).
//
// Caveats this deliberately accepts:
// - ENTRY-UNIT ONLY: a clean entry page that component()s into a native-only
// unit is still served by wasm. With signals_based_traps off (see
// make_engine) the worst case is a clean wasm error or a stubbed-empty
// result, never a worker crash — so this is a best-effort routing hint,
// not a guarantee. Full coverage is a W5 concern (un-stub the APIs).
// - SUBSTRING match, intentionally conservative: a token like "xml_" also
// matches an identifier or doc string containing it. That only ever
// over-routes to native (always correct), never the reverse.
static bool wasm_backend_native_fallback_uncached(Request* context, const String& entry_unit)
{
String source = file_get_contents(entry_unit);
// 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, sqlite, background tasks, filesystem writes, sleeps;
// - unit_call + compiler/unit introspection (need the native toolchain).
StringList native_only_tokens = {
"zip_", "task(", "task_repeat(",
"task_pid(", "task_kill(", "unit_call(",
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(",
"usleep(", "sleep("
};
for(auto& token : native_only_tokens)
if(source.find(token) != String::npos)
return(true);
return(false);
}
static bool wasm_backend_native_fallback_needed(Request* context, const String& entry_unit)
{
if(!context || !context->server || entry_unit == "")
return(true);
String site_root = path_join(context->server->config["COMPILER_SYS_PATH"], context->server->config["SITE_DIRECTORY"]);
// W5 keeps native as the reference backend for compiler/docs pages and the
// host-owned service surfaces that are not membrane APIs yet. This makes the
// default backend safe while W6 decides which fallbacks to retire vs keep.
if(entry_unit.rfind(path_join(site_root, "doc") + "/", 0) == 0)
return(true);
String source = file_get_contents(entry_unit);
StringList native_only_tokens = {
"markdown_to_", "zip_", "sqlite_", "regex_", "xml_", "yaml_", "task(", "task_repeat(",
"task_pid(", "task_kill(", "unit_call(", "unit_render(",
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(",
"file_put_contents(", "file_append(", "usleep(", "sleep("
};
for(auto& token : native_only_tokens)
if(source.find(token) != String::npos)
return(true);
return(false);
// Cache the verdict per process, keyed on path + mtime: the source scan is
// otherwise a file read + 20 substring searches on every page request.
struct CachedVerdict { time_t mtime; bool fallback; };
static std::map<String, CachedVerdict> cache;
struct stat st;
time_t mtime = (stat(entry_unit.c_str(), &st) == 0) ? st.st_mtime : 0;
auto it = cache.find(entry_unit);
if(it != cache.end() && it->second.mtime == mtime)
return(it->second.fallback);
bool fallback = wasm_backend_native_fallback_uncached(context, entry_unit);
cache[entry_unit] = { mtime, fallback };
return(fallback);
}
// True if this request should be served by the wasm backend. Falls through to
@@ -142,12 +189,17 @@ String wasm_backend_serve(Request& request, const String& entry_unit)
if(!response.ok)
return(response.error == "" ? String("wasm workspace failed") : response.error);
request.header["X-UCE-Backend"] = "wasm";
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
// 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))
{
request.header["X-UCE-Backend"] = "wasm";
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
}
// status line: keep the native default unless the unit set one
String status = response.meta["status"].to_string();