diff --git a/scripts/build_linux.sh b/scripts/build_linux.sh
index 33c9823..b707daf 100755
--- a/scripts/build_linux.sh
+++ b/scripts/build_linux.sh
@@ -12,29 +12,70 @@ mkdir bin/assets > /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 (exports the binary's symbols so dlopen'd .uce
+# units resolve the runtime against it); the -c compiles below do not need it.
+FLAGS="-g -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
-# Wasmtime C API for the W4 wasm backend (src/wasm/backend.cpp).
+# 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\" $WASM_FLAGS"
+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, so the dlopen unit model is
+# unchanged. 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
+# 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
diff --git a/site/tests/components.uce b/site/tests/components.uce
index 9e809b6..4830382 100644
--- a/site/tests/components.uce
+++ b/site/tests/components.uce
@@ -24,6 +24,10 @@ RENDER(Request& context)
String resolved = component_resolve("components/panel");
String panel_markup = component("components/panel", props, context);
String alias_markup = component("components/props_alias", props, context);
+ // Named handler through the string-returning component("unit:NAME") form
+ // (distinct path from component_render below): resolves __uce_component_HEADER
+ // and must run ONLY that handler, not the default COMPONENT that emits all three.
+ String header_markup = component("components/panel:HEADER", props, context);
ob_start();
component_render("components/panel:FOOTER", props, context);
@@ -40,6 +44,7 @@ RENDER(Request& context)
check("component()", panel_markup.find("Component Output") != String::npos && panel_markup.find("This body comes from component()") != String::npos, panel_markup);
check("COMPONENT(Request& props) alias", alias_markup.find("alias=Component Output") != String::npos && alias_markup.find("body=This body comes from component()") != String::npos, alias_markup);
check("component_render() named handler", footer_markup.find("Named footer render") != String::npos, footer_markup);
+ check("component(\"unit:NAME\") named handler", header_markup.find("Component Output") != String::npos && header_markup.find("This body comes from component()") == String::npos, header_markup);
check("ob_start() / ob_get_close()", buffer_markup == "buffered-text", buffer_markup);
?>
diff --git a/src/lib/functionlib.h b/src/lib/functionlib.h
index d9c682f..a05a732 100644
--- a/src/lib/functionlib.h
+++ b/src/lib/functionlib.h
@@ -30,7 +30,7 @@ String join(StringList l, String delim = "\n");
String nibble(String& haystack, String delim);
void json_consume_space(String s, u32& i);
-String to_string(StringList l) {
+inline String to_string(StringList l) {
String result;
u32 i = 0;
for(auto& s : l)
@@ -43,7 +43,7 @@ String to_string(StringList l) {
return(result);
}
-String to_string(SharedUnit* u) {
+inline String to_string(SharedUnit* u) {
String result;
result += String("SharedUnit( \n")+
diff --git a/src/lib/mysql-connector.h b/src/lib/mysql-connector.h
index 4448820..c1e778b 100644
--- a/src/lib/mysql-connector.h
+++ b/src/lib/mysql-connector.h
@@ -39,7 +39,7 @@ struct MySQL {
};
-MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "")
+inline MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "")
{
MySQL* m = new MySQL();
m->request_cleanup_delete = true;
@@ -47,7 +47,7 @@ MySQL* mysql_connect(String host = "localhost", String username = "root", String
return(m);
}
-void mysql_disconnect(MySQL* m)
+inline void mysql_disconnect(MySQL* m)
{
if(!m)
return;
@@ -55,24 +55,24 @@ void mysql_disconnect(MySQL* m)
delete m;
}
-String mysql_error(MySQL* m)
+inline String mysql_error(MySQL* m)
{
return(m->error());
}
String mysql_escape(String raw, char quote_char);
-DValue mysql_query(MySQL* m, String q)
+inline DValue mysql_query(MySQL* m, String q)
{
return(m->query(q));
}
-DValue mysql_query(MySQL* m, String q, StringMap params)
+inline DValue mysql_query(MySQL* m, String q, StringMap params)
{
return(m->query(q, params));
}
-u64 mysql_insert_id(MySQL* m)
+inline u64 mysql_insert_id(MySQL* m)
{
return(m->insert_id);
}
diff --git a/src/lib/sys.cpp b/src/lib/sys.cpp
index 85fe0d1..a478b30 100644
--- a/src/lib/sys.cpp
+++ b/src/lib/sys.cpp
@@ -318,6 +318,10 @@ StringMap default_config()
#include "sys.h"
#include "hash.h"
+// Single definitions for the native split build (declared extern in sys.h).
+pid_t parent_pid = 0;
+pid_t my_pid = 0;
+
namespace {
constexpr f64 FILE_LOCK_WAIT_TIMEOUT_SECONDS = 3.0;
diff --git a/src/lib/sys.h b/src/lib/sys.h
index 2ea0a2f..e9863b8 100644
--- a/src/lib/sys.h
+++ b/src/lib/sys.h
@@ -100,8 +100,15 @@ bool memcache_delete(u64 connection, String key);
String memcache_get(u64 connection, String key, String default_value = "");
StringMap memcache_get_multiple(u64 connection, StringList keys);
+// Defined once in sys.cpp for the native split build (core/wasm/main); the wasm
+// core/unit builds keep the in-place definition (single TU / loader-resolved).
+#if defined(__UCE_WASM_CORE__) || defined(__UCE_WASM_UNIT__)
pid_t parent_pid = 0;
pid_t my_pid = 0;
+#else
+extern pid_t parent_pid;
+extern pid_t my_pid;
+#endif
void on_segfault(int sig);
int task_kill(pid_t pid, int sig = 0);
diff --git a/src/lib/types.cpp b/src/lib/types.cpp
index 71f190d..a39a374 100644
--- a/src/lib/types.cpp
+++ b/src/lib/types.cpp
@@ -16,6 +16,32 @@
#include "types.h"
+// Single definition of context for the native split build (the wasm core/unit
+// builds keep the in-place definition from types.h — see the guard there).
+#if !defined(__UCE_WASM_CORE__) && !defined(__UCE_WASM_UNIT__)
+Request* context = 0;
+#endif
+
+#ifndef __UCE_WASM_UNIT__
+void * operator new(decltype(sizeof(0)) n) noexcept(false)
+{
+ void* ptr = malloc(n);
+ if(context)
+ {
+ context->stats.mem_alloc += n;
+ if(context->stats.mem_alloc > context->stats.mem_high)
+ context->stats.mem_high = context->stats.mem_alloc;
+ }
+ return(ptr);
+}
+
+void operator delete(void * p) throw()
+{
+ //TO DO: track deallocations
+ free(p);
+}
+#endif
+
namespace {
String http_status_reason(s32 code)
diff --git a/src/lib/types.h b/src/lib/types.h
index 18df2bd..68d239c 100644
--- a/src/lib/types.h
+++ b/src/lib/types.h
@@ -22,27 +22,29 @@ typedef long long s64;
typedef std::string String;
-String operator+(String lhs, u64 rhs) {
+// inline: this header is included by every runtime object (core/wasm/main); a
+// non-inline definition here would be a duplicate symbol across them.
+inline String operator+(String lhs, u64 rhs) {
return(lhs + std::to_string(rhs));
}
-String operator+(String lhs, u32 rhs) {
+inline String operator+(String lhs, u32 rhs) {
return(lhs + std::to_string(rhs));
}
-String operator+(String lhs, s64 rhs) {
+inline String operator+(String lhs, s64 rhs) {
return(lhs + std::to_string(rhs));
}
-String operator+(String lhs, s32 rhs) {
+inline String operator+(String lhs, s32 rhs) {
return(lhs + std::to_string(rhs));
}
-String operator+(String lhs, f64 rhs) {
+inline String operator+(String lhs, f64 rhs) {
return(lhs + std::to_string(rhs));
}
-String operator+(String lhs, f32 rhs) {
+inline String operator+(String lhs, f32 rhs) {
return(lhs + std::to_string(rhs));
}
@@ -59,7 +61,7 @@ typedef void (*request_ref_handler)(Request& request);
typedef DValue* (*dv_call_handler)(DValue* call_param);
typedef void (*request_handler)(Request* request);
-String to_string(s64 v) { return(std::to_string(v)); }
+inline String to_string(s64 v) { return(std::to_string(v)); }
struct SharedUnit {
@@ -242,7 +244,15 @@ struct Request {
typedef Request FastCGIRequest;
+// Native runtime is split across objects (core/wasm/main), so context is
+// declared here and defined once in types.cpp. The wasm core is a single TU and
+// wasm units resolve context through the loader's GOT, so both keep the
+// in-place definition (unchanged ABI).
+#if defined(__UCE_WASM_CORE__) || defined(__UCE_WASM_UNIT__)
Request* context;
+#else
+extern Request* context;
+#endif
#include
@@ -268,22 +278,6 @@ inline String concat(Ts... args)
return(out.str());
}
-#ifndef __UCE_WASM_UNIT__
-void * operator new(decltype(sizeof(0)) n) noexcept(false)
-{
- void* ptr = malloc(n);
- if(context)
- {
- context->stats.mem_alloc += n;
- if(context->stats.mem_alloc > context->stats.mem_high)
- context->stats.mem_high = context->stats.mem_alloc;
- }
- return(ptr);
-}
-
-void operator delete(void * p) throw()
-{
- //TO DO: track deallocations
- free(p);
-}
-#endif
+// The global allocator override (request memory accounting) is defined once in
+// types.cpp so it lives in a single runtime object; a header definition would
+// duplicate it across core/wasm/main. Units (__UCE_WASM_UNIT__) import it.
diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp
index 928d92d..4fd9bb6 100644
--- a/src/linux_fastcgi.cpp
+++ b/src/linux_fastcgi.cpp
@@ -1,5 +1,8 @@
#include "lib/uce_lib.cpp"
-#include "wasm/backend.cpp"
+// The wasm backend is its own object (src/wasm/wasm_module.cpp → wasm.o); the
+// main object only needs its declarations, so editing the wasm runtime no
+// longer recompiles the whole native TU.
+#include "wasm/backend.h"
#include
#include
#include
@@ -853,7 +856,13 @@ int handle_cli_complete(FastCGIRequest& request)
request.params["SCRIPT_FILENAME"] = script_filename;
prepare_request_body_maps(request);
request.props = DValue();
- compiler_invoke_cli(&request, script_filename);
+ // The CLI socket path installs no native fault handler, so the
+ // wasm worker (whose traps are signal-based) can run directly.
+ String cli_unit = compiler_normalize_unit_path(&request, script_filename);
+ if(wasm_backend_should_handle(request, cli_unit))
+ wasm_backend_serve(request, cli_unit, wasm_kind::CLI);
+ else
+ compiler_invoke_cli(&request, script_filename);
}
}
}
@@ -937,18 +946,15 @@ int handle_complete(FastCGIRequest& request) {
{
prepare_request_body_maps(request);
request.props = DValue();
- if(request.resources.is_cli)
- compiler_invoke_cli(&request, request.params["SCRIPT_FILENAME"]);
- else if(request.params["UCE_SERVE_HTTP"] == "1")
- compiler_invoke_serve_http(&request, request.params["SCRIPT_FILENAME"], request.params["UCE_SERVE_HTTP_FUNCTION"]);
- else if(wasm_backend_should_handle(request, compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"])))
- {
- // W4/W5: Wasmtime uses host signals internally to implement guest
- // traps. The native SIGSEGV/SIGILL request recovery handler must not
- // intercept those, or clean guest traps become native fatal signals.
+
+ // Suspend the native SIGSEGV/SIGILL request recovery handler around a
+ // wasm invocation: Wasmtime uses host signals internally to implement
+ // guest traps, and the native handler would otherwise turn a clean
+ // guest trap into a native fatal signal. Sets failure_* on error.
+ auto serve_via_wasm = [&](const String& entry_unit, int32_t kind, const String& handler_name = "") {
request_fault_active = 0;
restore_request_fault_handlers();
- String wasm_error = wasm_backend_serve(request, compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"]));
+ String wasm_error = wasm_backend_serve(request, entry_unit, kind, handler_name);
install_request_fault_handlers();
request_fault_active = 1;
if(wasm_error != "")
@@ -957,7 +963,25 @@ int handle_complete(FastCGIRequest& request) {
failure_details = "";
failure_trace = wasm_error;
}
+ };
+
+ String entry_unit = compiler_normalize_unit_path(&request, request.params["SCRIPT_FILENAME"]);
+ if(request.resources.is_cli)
+ {
+ if(wasm_backend_should_handle(request, entry_unit))
+ serve_via_wasm(entry_unit, wasm_kind::CLI);
+ else
+ compiler_invoke_cli(&request, request.params["SCRIPT_FILENAME"]);
}
+ else if(request.params["UCE_SERVE_HTTP"] == "1")
+ // W7c pending: the custom-server dispatcher is forked from a worker
+ // that already holds a live Wasmtime engine, so re-creating an engine
+ // in that child is unsafe (engine must not cross fork). The fix is to
+ // have the broker forward to the worker pool rather than render in the
+ // fork; until then serve_http renders natively.
+ compiler_invoke_serve_http(&request, request.params["SCRIPT_FILENAME"], request.params["UCE_SERVE_HTTP_FUNCTION"]);
+ else if(wasm_backend_should_handle(request, entry_unit))
+ serve_via_wasm(entry_unit, wasm_kind::RENDER);
else
compiler_invoke(&request, request.params["SCRIPT_FILENAME"]);
}
@@ -1220,6 +1244,9 @@ void custom_server_http_dispatcher_loop(String key)
custom_server_dispatcher_key = key;
close_inherited_server_sockets();
install_process_fault_handlers();
+ // This broker is forked from a worker that may already hold a wasm engine;
+ // drop the inherited (unusable post-fork) engine so it re-inits its own.
+ wasm_backend_reset_after_fork();
StringMap cfg = custom_server_read_config(key);
if(cfg["type"] != "http" || cfg["bind"] == "")
diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp
index 212cebf..6668496 100644
--- a/src/wasm/backend.cpp
+++ b/src/wasm/backend.cpp
@@ -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();
+}
diff --git a/src/wasm/backend.h b/src/wasm/backend.h
new file mode 100644
index 0000000..37c4154
--- /dev/null
+++ b/src/wasm/backend.h
@@ -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
+
+// 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();
diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp
index 7a1d382..92d559a 100644
--- a/src/wasm/core.cpp
+++ b/src/wasm/core.cpp
@@ -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);
}
diff --git a/src/wasm/wasm_module.cpp b/src/wasm/wasm_module.cpp
new file mode 100644
index 0000000..827f970
--- /dev/null
+++ b/src/wasm/wasm_module.cpp
@@ -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"
diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp
index 84655d2..cc1941b 100644
--- a/src/wasm/worker.cpp
+++ b/src/wasm/worker.cpp
@@ -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;