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;