uce/src/wasm/worker.cpp
2026-06-15 10:28:23 +00:00

2139 lines
73 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// W3 — production wasm workspace runtime + membrane (WASM-PROPOSAL §6, §7).
//
// One workspace per request: a fresh Wasmtime store holding the core module
// instance (real uce_lib carve-out, owns memory/allocator/DValue) plus unit
// PIC side modules loaded lazily — including mid-request via the
// uce_host_component_resolve hostcall, which is how component()/unit_render()
// inside the guest trigger dynamic loading.
//
// Designed for amalgamation-include (like uce_lib.cpp): the including TU must
// provide String/DValue/ucb_encode/ucb_decode (types.cpp + dvalue.cpp) and
// wasm_trace.h. W4 includes this from the FastCGI worker build; the W3 CLI
// driver (w3_driver.cpp) is the first consumer.
//
// Loader rules carried from the spike phases, now enforced as policy:
// - import discipline: units may import only env / GOT.mem / GOT.func
// (no WASI — the core is zero-WASI for UCE's own calls, and units get
// everything from the core); units defining allocator symbols are rejected
// - GOT.mem of a PIC module's data exports are __memory_base-relative
// offsets; the loader adds the owning module's base (Phase 0 erratum)
// - GOT.func resolves host-side: the target function's funcref is placed
// into the shared table and the slot index becomes the GOT value
// - export name collisions: core wins, then first-loading unit wins
// (mirrors native dlopen global-symbol interposition for identical
// vague-linkage template instantiations)
// - uce.abi stamp must match the core ABI version; fail loudly, never guess
#include <wasmtime.hh>
#include <chrono>
#include <cmath>
#include <cstring>
#include <ctime>
#include <fstream>
#include <map>
#include <cstdio>
#include <memory>
#include <optional>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <fcntl.h>
#include <limits.h>
#include <unistd.h>
#include <sys/time.h>
#include <vector>
#include <algorithm>
#include <dirent.h>
#include <cerrno>
struct WasmDylinkInfo
{
u32 mem_size = 0;
u32 mem_align = 0;
u32 table_size = 0;
u32 table_align = 0;
bool found = false;
};
struct WasmAbiInfo
{
u32 version = 0;
String toolchain;
bool found = false;
};
struct WasmUnitModule
{
String source_path; // absolute .uce path
String wasm_path; // artifact in the unit cache
time_t mtime = 0;
WasmDylinkInfo dylink;
WasmAbiInfo abi;
std::optional<wasmtime::Module> module;
};
struct WasmWorkerConfig
{
String core_wasm_path = "bin/wasm/core.wasm";
String site_root; // absolute
String cache_root = "/tmp/uce/work";
std::vector<String> write_roots; // absolute prefixes the write membrane allows
int64_t memory_limit = 512ll * 1024 * 1024;
u32 table_headroom = 4096;
u64 epoch_deadline_ticks = 200; // ticker period × ticks = CPU budget
bool verbose = false;
};
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
u64 workspace_birth_us = 0;
u64 component_resolve_count = 0;
u64 component_resolve_total_us = 0;
};
// ---- module byte parsing (hardened; carried from the phase 3 spike) -------
// included into both w3_driver.cpp and the native server TU (via backend.cpp);
// file-scope helpers are static so each TU gets its own copy with no clash
static bool wasm_read_uleb(const std::vector<u8>& buf, size_t& pos, size_t end, u64& out)
{
out = 0;
u32 shift = 0;
while(true)
{
if(pos >= end || shift >= 64)
return(false);
u8 byte = buf[pos++];
out |= ((u64)(byte & 0x7f)) << shift;
if((byte & 0x80) == 0)
return(true);
shift += 7;
}
}
static bool wasm_parse_sections(const std::vector<u8>& bytes, WasmDylinkInfo& dylink, WasmAbiInfo& abi, String& error)
{
if(bytes.size() < 8 || memcmp(bytes.data(), "\0asm", 4) != 0)
{
error = "not a wasm module";
return(false);
}
if(!(bytes[4] == 1 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0))
{
error = "unsupported wasm binary version";
return(false);
}
size_t pos = 8;
while(pos < bytes.size())
{
u8 section_id = bytes[pos++];
u64 size = 0;
if(!wasm_read_uleb(bytes, pos, bytes.size(), size) || size > bytes.size() - pos)
{
error = "malformed wasm section header";
return(false);
}
size_t end = pos + (size_t)size;
if(section_id == 0)
{
u64 name_len = 0;
size_t cursor = pos;
if(!wasm_read_uleb(bytes, cursor, end, name_len) || name_len > end - cursor)
{
error = "malformed custom section name";
return(false);
}
String name((const char*)bytes.data() + cursor, (size_t)name_len);
cursor += (size_t)name_len;
if(name == "dylink.0")
{
while(cursor < end)
{
u8 sub = bytes[cursor++];
u64 sub_len = 0;
if(!wasm_read_uleb(bytes, cursor, end, sub_len) || sub_len > end - cursor)
{
error = "malformed dylink.0 subsection";
return(false);
}
size_t sub_end = cursor + (size_t)sub_len;
if(sub == 1) // WASM_DYLINK_MEM_INFO
{
u64 v[4];
for(int i = 0; i < 4; i++)
if(!wasm_read_uleb(bytes, cursor, sub_end, v[i]))
{
error = "malformed dylink.0 mem_info";
return(false);
}
if(dylink.found)
{
error = "duplicate dylink.0 mem_info";
return(false);
}
if(v[1] >= 31 || v[3] >= 31)
{
error = "unsupported dylink alignment";
return(false);
}
dylink.mem_size = (u32)v[0];
dylink.mem_align = (u32)v[1];
dylink.table_size = (u32)v[2];
dylink.table_align = (u32)v[3];
dylink.found = true;
}
cursor = sub_end;
}
}
else if(name == "uce.abi")
{
String text((const char*)bytes.data() + cursor, end - cursor);
abi.found = true;
size_t line_start = 0;
while(line_start < text.size())
{
size_t line_end = text.find('\n', line_start);
if(line_end == String::npos)
line_end = text.size();
String line = text.substr(line_start, line_end - line_start);
if(line.rfind("unit_abi_version=", 0) == 0)
abi.version = (u32)strtoul(line.c_str() + 17, 0, 10);
if(line.rfind("toolchain=", 0) == 0)
abi.toolchain = line.substr(10);
line_start = line_end + 1;
}
}
}
pos = end;
}
return(true);
}
static bool wasm_read_file(const String& path, std::vector<u8>& out)
{
std::ifstream in(path, std::ios::binary);
if(!in)
return(false);
in.seekg(0, std::ios::end);
std::streamoff n = in.tellg();
in.seekg(0, std::ios::beg);
out.resize((size_t)n);
in.read((char*)out.data(), n);
return((bool)in);
}
// ---- worker (per process): engine + compiled module caches ----------------
class WasmWorker
{
public:
WasmWorkerConfig cfg;
explicit WasmWorker(WasmWorkerConfig config) : cfg(std::move(config)), engine(make_engine())
{
}
String init()
{
std::vector<u8> bytes;
if(!wasm_read_file(cfg.core_wasm_path, bytes))
return("cannot read core module: " + cfg.core_wasm_path);
WasmAbiInfo abi_ignored;
String parse_error;
if(!wasm_parse_sections(bytes, core_dylink, abi_ignored, parse_error))
return("core module: " + parse_error);
String core_cached_path = cached_wasm_path(cfg.core_wasm_path);
String compile_error;
auto compiled_or_cached = load_or_compile_cached_module(engine, core_cached_path, cfg.core_wasm_path, bytes, compile_error);
if(!compiled_or_cached)
return("core module compile failed: " + compile_error);
core_module.emplace(std::move(*compiled_or_cached));
return("");
}
wasmtime::Engine engine;
std::optional<wasmtime::Module> core_module;
WasmDylinkInfo core_dylink;
// unit artifact path in the W2 cache: cache_root + <abs source dir> + /<file>.wasm
String unit_wasm_path(const String& source_path) const
{
return(cfg.cache_root + source_path + ".wasm");
}
std::shared_ptr<WasmUnitModule> unit_module(const String& source_path, String& error)
{
String wasm_path = unit_wasm_path(source_path);
struct stat st;
if(stat(wasm_path.c_str(), &st) != 0)
{
error = "no wasm artifact for " + source_path + " (expected " + wasm_path + ")";
return(nullptr);
}
auto cached = module_cache.find(wasm_path);
if(cached != module_cache.end() && cached->second->mtime == st.st_mtime)
return(cached->second);
auto unit = std::make_shared<WasmUnitModule>();
unit->source_path = source_path;
unit->wasm_path = wasm_path;
unit->mtime = st.st_mtime;
std::vector<u8> bytes;
if(!wasm_read_file(wasm_path, bytes))
{
error = "cannot read " + wasm_path;
return(nullptr);
}
if(!wasm_parse_sections(bytes, unit->dylink, unit->abi, error))
{
error = wasm_path + ": " + error;
return(nullptr);
}
if(!unit->dylink.found)
{
error = wasm_path + ": missing dylink.0 mem_info (not a PIC side module)";
return(nullptr);
}
if(!unit->abi.found)
{
error = wasm_path + ": missing uce.abi stamp";
return(nullptr);
}
String unit_cached_path = cached_wasm_path(wasm_path);
String compile_error;
auto compiled_or_cached = load_or_compile_cached_module(engine, unit_cached_path, wasm_path, bytes, compile_error);
if(!compiled_or_cached)
{
error = wasm_path + ": compile failed: " + compile_error;
return(nullptr);
}
unit->module.emplace(std::move(*compiled_or_cached));
module_cache[wasm_path] = unit;
return(unit);
}
private:
static String cached_wasm_path(const String& wasm_path)
{
if(wasm_path.size() >= 5 && wasm_path.rfind(".wasm", wasm_path.size() - 5) == wasm_path.size() - 5)
return(wasm_path.substr(0, wasm_path.size() - 5) + ".cwasm");
return(wasm_path + ".cwasm");
}
static std::optional<wasmtime::Module> load_or_compile_cached_module(wasmtime::Engine& engine, const String& cached_path, const String& wasm_path,
std::vector<u8>& bytes, String& compile_error)
{
struct stat wasm_stat;
struct stat cached_stat;
if(stat(cached_path.c_str(), &cached_stat) == 0 && stat(wasm_path.c_str(), &wasm_stat) == 0)
{
if(cached_stat.st_mtime > wasm_stat.st_mtime)
{
auto deserialized = wasmtime::Module::deserialize_file(engine, cached_path);
if(deserialized)
return(deserialized.ok());
}
}
auto compiled = wasmtime::Module::compile(engine, bytes);
if(!compiled)
{
compile_error = String(compiled.err().message());
return(std::nullopt);
}
auto result = compiled.ok();
auto serialized = result.serialize();
if(serialized)
{
String tmp = cached_path + "." + std::to_string((long long)getpid()) + ".tmp";
auto data = serialized.ok();
{
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
if(out)
{
out.write((const char*)data.data(), (std::streamsize)data.size());
if(out)
{
out.flush();
out.close();
if(std::rename(tmp.c_str(), cached_path.c_str()) != 0)
(void)std::remove(tmp.c_str());
}
}
else
(void)std::remove(tmp.c_str());
}
}
return(result);
}
static wasmtime::Engine make_engine()
{
wasmtime::Config config;
config.epoch_interruption(true);
// CRITICAL: the host (linux_fastcgi.cpp) installs its own SIGSEGV/SIGILL/
// SIGBUS handlers per request (install_request_fault_handlers) with plain
// signal(), which clobbers Wasmtime's trap-handling handlers. With
// signals_based_traps on, a guest `unreachable`/OOB surfaces as a host
// signal that the native on_segfault catches and abort()s the worker —
// a 502 instead of a clean wasm trap. Disabling it makes Cranelift emit
// explicit trap checks: guest traps stay pure wasm traps returned as
// Result errors, never a host signal, so the two never collide.
config.signals_based_traps(false);
return(wasmtime::Engine(std::move(config)));
}
std::map<String, std::shared_ptr<WasmUnitModule>> module_cache;
};
// ---- workspace (per request) ----------------------------------------------
class WasmWorkspace
{
public:
WasmWorker& worker;
wasmtime::Store store;
u64 workspace_birth_us = 0;
u64 component_resolve_count = 0;
u64 component_resolve_total_us = 0;
struct RequestPerfSnapshot
{
u64 worker_pid = 0;
u64 parent_pid = 0;
u64 request_count = 0;
f64 time_init = 0;
f64 time_start = 0;
bool active = false;
} request_perf;
explicit WasmWorkspace(WasmWorker& w) : worker(w), store(w.engine)
{
}
void set_perf_snapshot(u64 worker_pid, u64 parent_pid, u64 request_count, f64 time_init, f64 time_start)
{
request_perf.worker_pid = worker_pid;
request_perf.parent_pid = parent_pid;
request_perf.request_count = request_count;
request_perf.time_init = time_init;
request_perf.time_start = time_start;
request_perf.active = true;
}
// Host-owned opaque fd handles opened by wasm file_open_locked().
std::vector<int> locked_file_handles;
#ifdef UCE_WASM_HOST_CONNECTORS
// Host-owned resource handle table (§3.1): connections opened by the guest
// live here and are closed when the workspace drops at request end.
std::vector<SQLite*> sqlite_handles;
std::vector<MySQL*> mysql_handles;
#endif
~WasmWorkspace()
{
for(int fd : locked_file_handles)
if(fd >= 0)
{
flock(fd, LOCK_UN);
::close(fd);
}
#ifdef UCE_WASM_HOST_CONNECTORS
for(auto* db : sqlite_handles)
if(db)
delete db; // ~SQLite disconnects
for(auto* db : mysql_handles)
if(db)
delete db; // ~MySQL disconnects
#endif
}
// The guest calls a sized hostcall twice (buf=0 to learn the length, then
// to fetch). For side-effecting ops (sqlite) re-executing on the fetch is
// wrong, so the result is staged on the first call (keyed on the exact
// input bytes) and replayed on the second without re-running the op.
String staged_hostcall_input;
String staged_hostcall_result;
String staged_socket_read_key;
String staged_socket_read_result;
String staged_memcache_key;
String staged_memcache_result;
bool hostcall_staged(const String& input, String& out)
{
if(!staged_hostcall_input.empty() && input == staged_hostcall_input)
{
out = staged_hostcall_result;
return(true);
}
return(false);
}
void hostcall_stage(const String& input, const String& out)
{
staged_hostcall_input = input;
staged_hostcall_result = out;
}
// resolve-kind values shared with the guest core (src/wasm/core.cpp)
// must match WasmResolveKind in src/wasm/core.cpp
String birth()
{
auto cx = ctx();
store.limiter(worker.cfg.memory_limit, -1, -1, -1, -1);
cx.set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
auto& module = *worker.core_module;
std::vector<wasmtime::Extern> imports;
for(auto import_type : module.imports())
{
String mod(import_type.module());
String name(import_type.name());
auto extern_type = wasmtime::ExternType::from_import(import_type);
if(auto* table_ref = std::get_if<wasmtime::TableType::Ref>(&extern_type))
{
if(name.rfind("__indirect_function_table", 0) != 0)
return("core imports unexpected table " + mod + "." + name);
u32 core_min = table_ref->min();
u32 total = core_min + worker.cfg.table_headroom;
wasmtime::TableType table_type(wasmtime::ValType::funcref(), total, total);
auto created = wasmtime::Table::create(cx, table_type, wasmtime::Val(std::optional<wasmtime::Func>()));
if(!created)
return("table create failed: " + String(created.err().message()));
table.emplace(created.ok());
table_next_free = core_min;
imports.push_back(*table);
continue;
}
auto* func_ref = std::get_if<wasmtime::FuncType::Ref>(&extern_type);
if(!func_ref)
return("core has unexpected non-func import " + mod + "." + name);
wasmtime::FuncType func_type(*func_ref);
imports.push_back(make_host_import(cx, mod, name, func_type));
}
if(!table)
return("core does not import __indirect_function_table — rebuild core with --import-table");
auto created = wasmtime::Instance::create(cx, module, imports);
if(!created)
return("core instantiation failed: " + trap_text(created.err()));
core.emplace(created.ok());
auto exported_memory = core->get(cx, "memory");
if(!exported_memory || !std::get_if<wasmtime::Memory>(&*exported_memory))
return("core does not export memory");
memory.emplace(std::get<wasmtime::Memory>(*exported_memory));
String error = call_core("_initialize", {}, 0);
if(error != "")
return(error);
int32_t rc = 0;
error = call_core("uce_wasm_core_init", {}, &rc);
if(error != "")
return(error);
if(rc != 0)
return("uce_wasm_core_init returned " + std::to_string(rc));
int32_t core_abi = 0;
error = call_core("uce_wasm_core_abi_version", {}, &core_abi);
if(error != "")
return(error);
abi_version = (u32)core_abi;
error = call_core("uce_wasm_request", {}, &request_ptr);
if(error != "")
return(error);
if(request_ptr == 0)
return("core returned null Request*");
return("");
}
String apply_context(const DValue& context_tree)
{
String encoded = ucb_encode(context_tree);
int32_t guest_ptr = 0;
String error = call_core("uce_alloc", { (int32_t)encoded.size() }, &guest_ptr);
if(error != "")
return(error);
if(guest_ptr == 0)
return("guest uce_alloc failed for context buffer");
error = guest_write((u32)guest_ptr, encoded);
if(error != "")
return(error);
int32_t rc = 0;
error = call_core("uce_wasm_apply_context", { guest_ptr, (int32_t)encoded.size() }, &rc);
if(error != "")
return(error);
call_core("uce_free", { guest_ptr }, 0);
if(rc != 0)
return("uce_wasm_apply_context returned " + std::to_string(rc));
return("");
}
// Invoke the entry unit through a named handler ("render"/"cli"/"websocket"/
// "serve_http:named"). The handler is just an export name — same resolve +
// ONCE + dispatch path as a component. handler_present, when provided,
// reports whether the unit exports it (caller maps a missing render to an
// empty body, a missing cli/serve handler to a 404).
String invoke_entry(const String& entry_source_path, const String& handler, bool* handler_present = 0)
{
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);
bool present = (bool)unit_func(unit_index, handler_export_symbol(handler));
if(handler_present)
*handler_present = present;
if(!present)
return("");
// Invoke through the core: it resolves the same handler and runs ONCE +
// dispatch (unit is pre-loaded above; resolution is cached). The core
// entry takes two guest buffers — the unit path and the handler name.
auto entry = core_func("uce_wasm_invoke_entry");
if(!entry)
return("core does not export uce_wasm_invoke_entry");
int32_t path_ptr = 0, handler_ptr = 0;
error = call_core("uce_alloc", { (int32_t)entry_source_path.size() }, &path_ptr);
if(error != "" || path_ptr == 0)
return(error == "" ? String("guest uce_alloc failed for entry path") : error);
error = guest_write((u32)path_ptr, entry_source_path);
if(error == "")
{
error = call_core("uce_alloc", { (int32_t)handler.size() }, &handler_ptr);
if(error == "" && handler_ptr == 0)
error = "guest uce_alloc failed for handler";
}
if(error == "")
error = guest_write((u32)handler_ptr, handler);
if(error != "")
{
if(path_ptr) call_core("uce_free", { path_ptr }, 0);
if(handler_ptr) call_core("uce_free", { handler_ptr }, 0);
return(error);
}
auto result = entry->call(ctx(), { wasmtime::Val(path_ptr), wasmtime::Val((int32_t)entry_source_path.size()),
wasmtime::Val(handler_ptr), wasmtime::Val((int32_t)handler.size()) });
call_core("uce_free", { path_ptr }, 0);
call_core("uce_free", { handler_ptr }, 0);
if(!result)
return(trap_text(result.err()));
return("");
}
String render_entry(const String& entry_source_path)
{
return(invoke_entry(entry_source_path, "render"));
}
String collect(WasmResponse& response)
{
String error = call_core("uce_wasm_finish_output", {}, 0);
if(error != "")
return(error);
error = call_core("uce_wasm_finish_response_meta", {}, 0);
if(error != "")
return(error);
int32_t body_ptr = 0, body_len = 0, meta_ptr = 0, meta_len = 0;
if((error = call_core("uce_wasm_output_data", {}, &body_ptr)) != "") return(error);
if((error = call_core("uce_wasm_output_size", {}, &body_len)) != "") return(error);
if((error = call_core("uce_wasm_response_meta_data", {}, &meta_ptr)) != "") return(error);
if((error = call_core("uce_wasm_response_meta_size", {}, &meta_len)) != "") return(error);
error = guest_read((u32)body_ptr, (u32)body_len, response.body);
if(error != "")
return(error);
String meta_encoded;
error = guest_read((u32)meta_ptr, (u32)meta_len, meta_encoded);
if(error != "")
return(error);
String decode_error;
if(!ucb_decode(meta_encoded, response.meta, &decode_error))
return("response meta decode failed: " + decode_error);
return("");
}
private:
std::optional<wasmtime::Instance> core;
std::optional<wasmtime::Memory> memory;
std::optional<wasmtime::Table> table;
u32 table_next_free = 0;
u32 abi_version = 0;
int32_t request_ptr = 0;
String entry_dir;
struct LoadedUnit
{
std::shared_ptr<WasmUnitModule> mod;
std::optional<wasmtime::Instance> instance;
u32 memory_base = 0;
};
std::vector<LoadedUnit> units;
std::map<String, size_t> units_by_source;
std::map<String, u32> handler_slots; // source + ":" + symbol → table slot
std::vector<wasmtime::Func> host_funcs; // keep host imports alive
wasmtime::Store::Context ctx()
{
return(wasmtime::Store::Context(store));
}
static String dir_of(const String& path)
{
auto pos = path.find_last_of('/');
return(pos == String::npos ? String("") : path.substr(0, pos));
}
static String trap_text(const wasmtime::TrapError& error)
{
return(wasm_trace_collapse(String(error.message())));
}
// ---- guest memory access (pointer re-derived per call: it moves) ------
String guest_write(u32 ptr, const String& data)
{
auto span = memory->data(ctx());
if((size_t)ptr + data.size() > span.size())
return("guest write out of bounds");
memcpy(span.data() + ptr, data.data(), data.size());
return("");
}
String guest_read(u32 ptr, u32 len, String& out)
{
auto span = memory->data(ctx());
if((size_t)ptr + len > span.size())
return("guest read out of bounds");
out.assign((const char*)span.data() + ptr, len);
return("");
}
// ---- calls -------------------------------------------------------------
std::optional<wasmtime::Func> core_func(const String& name)
{
auto exported = core->get(ctx(), std::string_view(name));
if(!exported)
return(std::nullopt);
if(auto* func = std::get_if<wasmtime::Func>(&*exported))
return(*func);
return(std::nullopt);
}
std::optional<wasmtime::Func> unit_func(size_t unit_index, const String& name)
{
auto exported = units[unit_index].instance->get(ctx(), std::string_view(name));
if(!exported)
return(std::nullopt);
if(auto* func = std::get_if<wasmtime::Func>(&*exported))
return(*func);
return(std::nullopt);
}
String call_core(const String& name, std::vector<int32_t> argv, int32_t* result_out)
{
auto func = core_func(name);
if(!func)
return("core does not export " + name);
std::vector<wasmtime::Val> args;
for(auto value : argv)
args.push_back(wasmtime::Val(value));
auto result = func->call(ctx(), args);
if(!result)
return(trap_text(result.err()));
auto values = result.ok();
if(result_out)
*result_out = values.empty() ? 0 : values[0].i32();
return("");
}
// ---- symbol registry (core first, then units in load order) -----------
std::optional<wasmtime::Func> resolve_func(const String& name)
{
if(auto func = core_func(name))
return(func);
for(size_t i = 0; i < units.size(); i++)
if(auto func = unit_func(i, name))
return(func);
return(std::nullopt);
}
// data symbol: value of the exported i32 global + owning module's base
// (core is non-PIC → base 0; PIC units export __memory_base-relative)
bool resolve_data(const String& name, u32& address_out)
{
auto from_core = core->get(ctx(), std::string_view(name));
if(from_core)
if(auto* global = std::get_if<wasmtime::Global>(&*from_core))
{
address_out = (u32)global->get(ctx()).i32();
return(true);
}
for(size_t i = 0; i < units.size(); i++)
{
auto exported = units[i].instance->get(ctx(), std::string_view(name));
if(exported)
if(auto* global = std::get_if<wasmtime::Global>(&*exported))
{
address_out = units[i].memory_base + (u32)global->get(ctx()).i32();
return(true);
}
}
return(false);
}
String place_funcref(const wasmtime::Func& func, u32& slot_out)
{
auto cx = ctx();
if(table_next_free >= table->size(cx))
return("funcref table headroom exhausted");
auto set = table->set(cx, table_next_free, wasmtime::Val(std::optional<wasmtime::Func>(func)));
if(!set)
return("table set failed: " + String(set.err().message()));
slot_out = table_next_free++;
return("");
}
// ---- unit loading (the §6 sequence) ------------------------------------
String load_unit(const String& source_path, size_t& unit_index_out)
{
auto known = units_by_source.find(source_path);
if(known != units_by_source.end())
{
unit_index_out = known->second;
return("");
}
String error;
auto mod = worker.unit_module(source_path, error);
if(!mod)
return(error);
if(mod->abi.version != abi_version)
return(mod->wasm_path + ": uce.abi version " + std::to_string(mod->abi.version)
+ " does not match core ABI " + std::to_string(abi_version));
auto cx = ctx();
auto& module = *mod->module;
// base allocation
u32 align = 1u << mod->dylink.mem_align;
int32_t raw_base = 0;
error = call_core("malloc", { (int32_t)(mod->dylink.mem_size + align) }, &raw_base);
if(error != "")
return(error);
if(raw_base == 0)
return("core malloc failed for unit data segment");
u32 memory_base = ((u32)raw_base + (align - 1)) & ~(align - 1);
u32 table_base = table_next_free;
if(mod->dylink.table_size > table->size(cx) - table_next_free)
return("funcref table headroom exhausted by " + source_path);
table_next_free += mod->dylink.table_size;
// import resolution
std::vector<wasmtime::Extern> imports;
std::vector<std::pair<String, wasmtime::Global>> deferred_got;
for(auto import_type : module.imports())
{
String mod_name(import_type.module());
String name(import_type.name());
auto extern_type = wasmtime::ExternType::from_import(import_type);
bool is_func_import = std::get_if<wasmtime::FuncType::Ref>(&extern_type) != 0;
if(mod_name == "env" && name == "memory")
{
imports.push_back(*memory);
continue;
}
if(mod_name == "env" && name == "__indirect_function_table")
{
imports.push_back(*table);
continue;
}
if(mod_name == "env" && name == "__stack_pointer")
{
auto sp = core->get(cx, "__stack_pointer");
if(!sp || !std::get_if<wasmtime::Global>(&*sp))
return("core does not export __stack_pointer");
imports.push_back(std::get<wasmtime::Global>(*sp));
continue;
}
if(mod_name == "env" && (name == "__memory_base" || name == "__table_base"))
{
int32_t value = name == "__memory_base" ? (int32_t)memory_base : (int32_t)table_base;
wasmtime::GlobalType global_type(wasmtime::ValType::i32(), false);
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val(value));
if(!global)
return("global create failed: " + String(global.err().message()));
imports.push_back(global.ok());
continue;
}
if(mod_name == "env" && is_func_import)
{
auto func = resolve_func(name);
if(!func)
return(source_path + ": unresolved import env." + wasm_trace_demangle(name));
imports.push_back(*func);
continue;
}
if(mod_name == "GOT.mem")
{
wasmtime::GlobalType global_type(wasmtime::ValType::i32(), true);
u32 address = 0;
if(resolve_data(name, address))
{
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)address));
if(!global)
return("global create failed: " + String(global.err().message()));
imports.push_back(global.ok());
}
else
{
// provisional; self-resolved from the unit's own export
// (plus its memory base) after instantiation
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)0));
if(!global)
return("global create failed: " + String(global.err().message()));
deferred_got.push_back({ name, global.ok() });
imports.push_back(deferred_got.back().second);
}
continue;
}
if(mod_name == "GOT.func")
{
auto func = resolve_func(name);
if(!func)
return(source_path + ": unresolved GOT.func." + wasm_trace_demangle(name));
u32 slot = 0;
error = place_funcref(*func, slot);
if(error != "")
return(error);
wasmtime::GlobalType global_type(wasmtime::ValType::i32(), true);
auto global = wasmtime::Global::create(cx, global_type, wasmtime::Val((int32_t)slot));
if(!global)
return("global create failed: " + String(global.err().message()));
imports.push_back(global.ok());
continue;
}
return(source_path + ": import policy violation: " + mod_name + "." + name);
}
auto created = wasmtime::Instance::create(cx, module, imports);
if(!created)
return(source_path + ": instantiation failed: " + trap_text(created.err()));
LoadedUnit unit;
unit.mod = mod;
unit.instance.emplace(created.ok());
unit.memory_base = memory_base;
units.push_back(std::move(unit));
size_t unit_index = units.size() - 1;
units_by_source[source_path] = unit_index;
unit_index_out = unit_index;
// deferred GOT: the unit's own data exports are module-relative —
// add this unit's memory base (Phase 0 FINDINGS erratum)
for(auto& [name, got] : deferred_got)
{
auto own = units[unit_index].instance->get(cx, std::string_view(name));
if(!own || !std::get_if<wasmtime::Global>(&*own))
return(source_path + ": GOT.mem." + name + " defined neither by core nor by any unit");
u32 offset = (u32)std::get<wasmtime::Global>(*own).get(cx).i32();
auto set = got.set(cx, wasmtime::Val((int32_t)(memory_base + offset)));
if(!set)
return("GOT patch failed: " + String(set.err().message()));
}
// init sequence, then bind this unit's context to the request
if(auto relocs = unit_func(unit_index, "__wasm_apply_data_relocs"))
{
auto result = relocs->call(ctx(), {});
if(!result)
return(trap_text(result.err()));
}
if(auto ctors = unit_func(unit_index, "__wasm_call_ctors"))
{
auto result = ctors->call(ctx(), {});
if(!result)
return(trap_text(result.err()));
}
if(auto set_request = unit_func(unit_index, "__uce_set_current_request"))
{
auto result = set_request->call(ctx(), { wasmtime::Val(request_ptr) });
if(!result)
return(trap_text(result.err()));
}
if(worker.cfg.verbose)
fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n",
source_path.c_str(), memory_base, table_base);
// The epoch budget is a guest-CPU watchdog, but the ticker is wall-clock
// and host-side module compilation here (lazy, mid-render, possibly many
// units) burns it without the guest running. Reset the deadline after a
// load so the budget measures guest execution between membrane crossings,
// not our compile time. A genuine runaway loop makes no loads, so it
// still trips the deadline.
ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
return("");
}
// ---- component target resolution (host side of the membrane) ----------
static String normalize_component_path(String name)
{
// mirrors component_normalize_path in compiler.cpp / core.cpp
if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce")
return(name);
return(name + ".uce");
}
static bool file_exists_host(const String& path)
{
struct stat st;
return(stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
}
static bool dir_exists_host(const String& path)
{
struct stat st;
return(stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
}
String resolve_source_path(const String& file_name, const String& current_unit)
{
std::vector<String> bases;
if(file_name.rfind("/", 0) == 0)
bases.push_back(""); // absolute target
if(entry_dir != "")
bases.push_back(entry_dir + "/");
if(current_unit != "")
{
String current_dir = dir_of(current_unit);
if(current_dir != "" && current_dir != entry_dir)
bases.push_back(current_dir + "/");
}
bases.push_back(worker.cfg.site_root + "/");
for(auto& base : bases)
{
std::vector<String> candidates;
candidates.push_back(base + file_name);
candidates.push_back(base + normalize_component_path(file_name));
if(file_name.rfind("components/", 0) != 0 && base != "")
{
candidates.push_back(base + "components/" + file_name);
candidates.push_back(base + "components/" + normalize_component_path(file_name));
}
for(auto& candidate : candidates)
if(file_exists_host(candidate))
return(candidate);
}
return("");
}
// guest file access policy: only inside the site tree, resolved against
// the entry unit's directory first (the native cwd convention), then the
// site root; containment checked on the canonicalized path
String resolve_guest_file(const String& raw, const String& current_unit = "", bool allow_dir = false)
{
if(raw == "" || raw.find('\0') != String::npos)
return("");
std::vector<String> candidates;
if(raw.rfind("/", 0) == 0)
candidates.push_back(raw);
else
{
String current_dir = current_unit != "" ? dir_of(current_unit) : String("");
if(current_dir != "")
candidates.push_back(current_dir + "/" + raw);
if(entry_dir != "" && entry_dir != current_dir)
candidates.push_back(entry_dir + "/" + raw);
candidates.push_back(worker.cfg.site_root + "/" + raw);
}
// readable roots = the site tree plus the writable scratch dirs (a page
// can read back what it is allowed to write, e.g. /tmp), canonicalized.
std::vector<String> read_roots;
read_roots.push_back(worker.cfg.site_root);
for(auto& root : worker.cfg.write_roots)
read_roots.push_back(root);
std::vector<String> root_prefixes;
for(auto& root : read_roots)
{
char root_real[4096];
if(root != "" && realpath(root.c_str(), root_real))
root_prefixes.push_back(String(root_real) + "/");
}
for(auto& candidate : candidates)
{
char resolved[4096];
if(!realpath(candidate.c_str(), resolved))
continue;
String path(resolved);
bool allowed = false;
for(auto& prefix : root_prefixes)
if(path.rfind(prefix, 0) == 0)
{
allowed = true;
break;
}
if(!allowed)
continue;
if(file_exists_host(path) || (allow_dir && dir_exists_host(path)))
return(path);
}
return("");
}
// write membrane policy: resolve the target (absolute, or relative to the
// current unit / site root) and allow it only if its parent directory
// canonicalizes under one of the configured write roots (site tree + the
// runtime scratch dirs). The file itself need not exist yet.
String resolve_guest_write(const String& raw, const String& current_unit)
{
if(raw == "" || raw.find('\0') != String::npos)
return("");
String target;
if(raw.rfind("/", 0) == 0)
target = raw;
else
{
String current_dir = current_unit != "" ? dir_of(current_unit) : String("");
target = (current_dir != "" ? current_dir : worker.cfg.site_root) + "/" + raw;
}
String parent = dir_of(target);
String base = parent.size() < target.size() ? target.substr(parent.size() + 1) : String("");
if(base == "" || base == "." || base == "..")
return("");
char parent_real[4096];
if(!realpath(parent.c_str(), parent_real))
return("");
String resolved_parent(parent_real);
for(auto& root : worker.cfg.write_roots)
{
char root_real[4096];
if(root == "" || !realpath(root.c_str(), root_real))
continue;
String root_prefix(root_real);
if(resolved_parent == root_prefix || resolved_parent.rfind(root_prefix + "/", 0) == 0)
return(resolved_parent + "/" + base);
}
return("");
}
static String sanitize_symbol_suffix(const String& raw)
{
// mirrors ascii_safe_name in functionlib.cpp
String result;
for(auto c : raw)
if(isalnum((unsigned char)c) || c == '_')
result.push_back(c);
return(result);
}
// __uce_<base>[_<suffix>] for a handler spec like "component:CARD" / "render".
static String handler_export_symbol(const String& handler)
{
String export_prefix = "export:";
if(handler.rfind(export_prefix, 0) == 0)
return(handler.substr(export_prefix.size()));
auto colon = handler.find(":");
String symbol = "__uce_" + (colon == String::npos ? handler : handler.substr(0, colon));
if(colon != String::npos)
symbol += "_" + sanitize_symbol_suffix(handler.substr(colon + 1));
return(symbol);
}
// hostcall body: uce_host_component_resolve(unit, handler, current) → slot.
// `handler` names the export ("render", "component:CARD", "cli",
// "serve_http:named", "once") or is "exists" (probe only, loads nothing).
int32_t component_resolve(const String& target, const String& handler, const String& current_unit, String& resolved_out)
{
auto probe_start = std::chrono::steady_clock::now();
auto record_probe = [&]() {
component_resolve_count += 1;
component_resolve_total_us += (u64)std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - probe_start).count();
};
String file_name = target;
if(file_name == "" && current_unit != "")
file_name = current_unit;
if(file_name == "")
{
record_probe();
return(0);
}
String resolved = resolve_source_path(file_name, current_unit);
if(resolved == "")
{
record_probe();
return(0);
}
resolved_out = resolved;
if(handler == "exists")
{
record_probe();
return(1);
}
if(!file_exists_host(worker.unit_wasm_path(resolved)) || compiler_unit_needs_recompile(context, resolved, 0))
get_shared_unit(context, resolved);
size_t unit_index = 0;
String error = load_unit(resolved, unit_index);
if(error != "")
{
fprintf(stderr, "[wasm] component load failed: %s\n", error.c_str());
record_probe();
return(0);
}
String symbol = handler_export_symbol(handler);
String slot_key = resolved + ":" + symbol;
auto cached = handler_slots.find(slot_key);
if(cached != handler_slots.end())
{
record_probe();
return((int32_t)cached->second);
}
auto handler_fn = unit_func(unit_index, symbol);
if(!handler_fn)
{
// ONCE is optional per unit; a missing __uce_once is not an error
if(handler != "once")
fprintf(stderr, "[wasm] %s does not export %s\n", resolved.c_str(), symbol.c_str());
record_probe();
return(0);
}
u32 slot = 0;
error = place_funcref(*handler_fn, slot);
if(error != "")
{
fprintf(stderr, "[wasm] %s\n", error.c_str());
record_probe();
return(0);
}
handler_slots[slot_key] = slot;
record_probe();
return((int32_t)slot);
}
String run_task_callback(u64 callback_id)
{
auto runner = core_func("uce_wasm_task_run");
if(!runner)
return("core does not export uce_wasm_task_run");
auto result = runner->call(ctx(), { wasmtime::Val((int64_t)callback_id) });
if(!result)
return(trap_text(result.err()));
return("");
}
// ---- host imports for the core -----------------------------------------
wasmtime::Extern make_host_import(wasmtime::Store::Context cx, const String& mod, const String& name, const wasmtime::FuncType& func_type)
{
using namespace wasmtime;
WasmWorkspace* self = this;
auto add = [&](auto&& callback) -> Extern {
Func func(cx, func_type, callback);
host_funcs.push_back(func);
return(host_funcs.back());
};
if(mod == "env" && name == "uce_host_time")
return(add([](Caller, Span<const Val>, Span<Val> results) -> Result<std::monostate, Trap> {
results[0] = Val((int64_t)::time(0));
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_time_precise")
return(add([](Caller, Span<const Val>, Span<Val> results) -> Result<std::monostate, Trap> {
results[0] = Val(time_precise());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_request_perf")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
DValue response;
if(self->request_perf.active)
{
f64 now = time_precise();
response["worker_pid"] = (f64)self->request_perf.worker_pid;
response["parent_pid"] = (f64)self->request_perf.parent_pid;
response["request_count"] = (f64)self->request_perf.request_count;
if(self->request_perf.time_start > 0 && self->request_perf.time_init > 0)
response["accept_us"] = (f64)((self->request_perf.time_start - self->request_perf.time_init) * 1000000.0);
if(self->request_perf.time_start > 0)
response["running_us"] = (f64)((now - self->request_perf.time_start) * 1000000.0);
if(self->request_perf.time_init > 0)
response["total_us"] = (f64)((now - self->request_perf.time_init) * 1000000.0);
if(self->workspace_birth_us > 0)
response["workspace_birth_us"] = (f64)self->workspace_birth_us;
}
String encoded = ucb_encode(response);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= encoded.size())
self->hostcall_write(buf, encoded);
results[0] = Val((int32_t)encoded.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_env")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String key, value;
if(self->hostcall_read(args[0].i32(), args[1].i32(), key) == "")
if(const char* raw = getenv(key.c_str()))
value = raw;
u32 cap = (u32)args[3].i32();
if(value.size() && cap >= value.size())
self->hostcall_write(args[2].i32(), value);
results[0] = Val((int32_t)value.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_random")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u32 len = (u32)args[1].i32();
String bytes(len, 0);
FILE* urandom = fopen("/dev/urandom", "rb");
if(urandom)
{
size_t got = fread(&bytes[0], 1, len, urandom);
fclose(urandom);
bytes.resize(got);
}
self->hostcall_write(args[0].i32(), bytes);
results[0] = Val((int32_t)bytes.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_log")
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
String text;
self->hostcall_read(args[1].i32(), args[2].i32(), text);
fprintf(stderr, "[guest log %d] %.*s\n", args[0].i32(), (int)text.size(), text.data());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_shell_exec")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String cmd;
self->hostcall_read(args[0].i32(), args[1].i32(), cmd);
String out;
if(!self->hostcall_staged("shell:" + cmd, out))
{
out = ::shell_exec(cmd);
self->hostcall_stage("shell:" + cmd, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_path_real")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
String resolved = ::path_real(path);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= resolved.size())
self->hostcall_write(buf, resolved);
results[0] = Val((int32_t)resolved.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_path_is_within")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, root;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), root);
results[0] = Val(::path_is_within(path, root) ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_cwd_get")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String cwd = ::cwd_get();
u32 cap = (u32)args[1].i32();
int32_t buf = args[0].i32();
if(buf != 0 && cap >= cwd.size())
self->hostcall_write(buf, cwd);
results[0] = Val((int32_t)cwd.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_cwd_set")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
results[0] = Val(::chdir(path.c_str()) == 0 ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_process_start_directory")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String cwd = ::process_start_directory();
u32 cap = (u32)args[1].i32();
int32_t buf = args[0].i32();
if(buf != 0 && cap >= cwd.size())
self->hostcall_write(buf, cwd);
results[0] = Val((int32_t)cwd.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_last_trap_trace")
return(add([](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
(void)args;
results[0] = Val((int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_exists")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_file(path, current);
if(self->worker.cfg.verbose)
fprintf(stderr, "[wasm] file_exists(%s, current=%s) -> %s\n", path.c_str(), current.c_str(), resolved.c_str());
results[0] = Val(resolved != "" ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_mkdir")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_write(path, current);
int ok = 0;
if(resolved != "")
ok = (::mkdir(resolved.c_str(), 0777) == 0 || errno == EEXIST) ? 1 : 0;
results[0] = Val((int32_t)ok);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_mtime")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_file(path, current);
int64_t mtime = 0;
struct stat st;
if(resolved != "" && stat(resolved.c_str(), &st) == 0)
mtime = (int64_t)st.st_mtime;
results[0] = Val((int64_t)mtime);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_open_locked")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, purpose;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[6].i32(), args[7].i32(), purpose);
int fd = ::file_open_locked(path, args[2].i32(), args[3].i32(), args[4].i32(), args[5].f64(), purpose);
int handle = -1;
if(fd >= 0)
{
for(size_t i = 0; i < self->locked_file_handles.size(); i++)
if(self->locked_file_handles[i] < 0)
{
self->locked_file_handles[i] = fd;
handle = (int)i + 1;
break;
}
if(handle < 0)
{
self->locked_file_handles.push_back(fd);
handle = (int)self->locked_file_handles.size();
}
}
results[0] = Val((int32_t)handle);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_close_locked")
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
int handle = args[0].i32();
if(handle >= 1 && (size_t)handle <= self->locked_file_handles.size())
{
int& fd = self->locked_file_handles[(size_t)handle - 1];
if(fd >= 0)
{
::file_close_locked(fd);
fd = -1;
}
}
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_release_process_locks")
return(add([self](Caller, Span<const Val>, Span<Val>) -> Result<std::monostate, Trap> {
for(int& fd : self->locked_file_handles)
if(fd >= 0)
{
::file_close_locked(fd);
fd = -1;
}
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_read_locked_fd")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
int handle = args[0].i32();
String content;
if(handle >= 1 && (size_t)handle <= self->locked_file_handles.size())
{
int fd = self->locked_file_handles[(size_t)handle - 1];
if(fd >= 0)
content = ::file_get_contents_locked_fd(fd);
}
u32 cap = (u32)args[2].i32();
int32_t buf = args[1].i32();
if(buf != 0 && cap >= content.size())
self->hostcall_write(buf, content);
results[0] = Val((int32_t)content.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_write_locked_fd")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
int handle = args[0].i32();
String content;
self->hostcall_read(args[1].i32(), args[2].i32(), content);
bool ok = false;
if(handle >= 1 && (size_t)handle <= self->locked_file_handles.size())
{
int fd = self->locked_file_handles[(size_t)handle - 1];
if(fd >= 0)
ok = ::file_put_contents_locked_fd(fd, content);
}
results[0] = Val(ok ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_read")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_file(path, current);
std::vector<u8> bytes;
if(resolved == "" || !wasm_read_file(resolved, bytes))
{
results[0] = Val((int32_t)0);
return(std::monostate());
}
u32 cap = (u32)args[5].i32();
int32_t buf = args[4].i32();
// length-query convention: no copy unless the buffer fits
if(buf != 0 && cap >= bytes.size())
self->hostcall_write(buf, String((const char*)bytes.data(), bytes.size()));
results[0] = Val((int32_t)bytes.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_list")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_file(path, current, true /*allow_dir*/);
String listing;
if(resolved != "")
{
std::vector<String> names;
if(DIR* d = opendir(resolved.c_str()))
{
while(struct dirent* e = readdir(d))
{
String n = e->d_name;
if(n != "." && n != "..")
names.push_back(n);
}
closedir(d);
}
// match the native ls -1 convention: bare names, sorted
std::sort(names.begin(), names.end());
listing = join(names, "\n");
}
u32 cap = (u32)args[5].i32();
int32_t buf = args[4].i32();
// length-query convention: no copy unless the buffer fits
if(buf != 0 && cap >= listing.size())
self->hostcall_write(buf, listing);
results[0] = Val((int32_t)listing.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_write")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current, content;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
self->hostcall_read(args[4].i32(), args[5].i32(), content);
bool append = args[6].i32() != 0;
String resolved = self->resolve_guest_write(path, current);
bool ok = false;
if(resolved != "")
ok = append ? file_append_contents(resolved, content) : file_put_contents(resolved, content);
else if(self->worker.cfg.verbose)
fprintf(stderr, "[wasm] file_write denied: %s\n", path.c_str());
results[0] = Val(ok ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_zip")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
String out;
if(!self->hostcall_staged(encoded, out))
{
DValue request, response;
String decode_error;
try
{
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "list")
{
String path = self->resolve_guest_file(request["path"].to_string());
if(path == "") throw std::runtime_error("zip_list: path is outside wasm file policy");
response["result"] = zip_list(path);
}
else if(op == "read")
{
String path = self->resolve_guest_file(request["path"].to_string());
if(path == "") throw std::runtime_error("zip_read: path is outside wasm file policy");
response["result"] = zip_read(path, request["entry"].to_string());
}
else if(op == "create")
{
String path = self->resolve_guest_write(request["path"].to_string(), "");
if(path == "") throw std::runtime_error("zip_create: path is outside wasm file policy");
DValue* entries = request.key("entries");
response["ok"].set_bool(zip_create(path, entries ? *entries : DValue()));
}
else if(op == "extract")
{
String path = self->resolve_guest_file(request["path"].to_string());
String destination = self->resolve_guest_write(request["destination"].to_string(), "");
if(path == "" || destination == "") throw std::runtime_error("zip_extract: path is outside wasm file policy");
response["ok"].set_bool(zip_extract(path, destination));
}
else if(op == "gz_compress")
response["result"] = gz_compress(request["src"].to_string());
else if(op == "gz_uncompress")
response["result"] = gz_uncompress(request["src"].to_string());
}
}
catch(const std::exception& e)
{
response["error"] = e.what();
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_units")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
String out;
if(!self->hostcall_staged(encoded, out))
{
DValue request, response;
String decode_error;
try
{
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "info")
response["result"] = unit_info(request["path"].to_string());
else if(op == "list")
{
StringList paths = units_list();
for(auto& path : paths)
{
DValue item;
item = path;
response["result"].push(item);
}
}
else if(op == "compile")
response["ok"].set_bool(unit_compile(request["path"].to_string()));
else if(op == "call")
{
DValue* param = request.key("param");
ob_start();
DValue* result = unit_call(request["file"].to_string(), request["function"].to_string(), param);
response["output"] = ob_get_close();
if(result)
response["result"] = *result;
}
}
}
catch(const std::exception& e)
{
response["error"] = e.what();
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
#ifdef UCE_WASM_HOST_CONNECTORS
if(mod == "env" && name == "uce_host_sqlite")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
// {op,handle,path,query,params} in → result out. The real native
// connector runs host-side; connections live in the workspace
// handle table (handle = 1-based index).
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
String out;
// run the op once across the length-query + fetch pair
if(!self->hostcall_staged(encoded, out))
{
DValue request, response;
String decode_error;
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "connect")
{
SQLite* db = new SQLite();
db->connect(request["path"].to_string());
u64 handle = 0;
if(db->connection)
{
self->sqlite_handles.push_back(db);
handle = self->sqlite_handles.size();
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->error_code;
response["statement_info"] = db->error();
if(handle == 0)
delete db;
}
else
{
u64 handle = request["handle"].to_u64();
SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size())
? self->sqlite_handles[(size_t)handle - 1] : 0;
if(op == "query" && db)
{
StringMap params;
DValue* p = request.key("params");
if(p)
p->each([&](const DValue& value, String key) { params[key] = value.to_string(); });
response["result"] = db->query(request["query"].to_string(), params);
response["insert_id"] = (f64)db->insert_id;
response["affected"] = (f64)db->affected_rows;
response["error_code"] = (f64)db->error_code;
response["statement_info"] = db->error();
}
else if(op == "disconnect" && db)
{
delete db;
self->sqlite_handles[(size_t)handle - 1] = 0;
}
}
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_memcache_command")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String command;
self->hostcall_read(args[1].i32(), args[2].i32(), command);
String key = "memcache:" + std::to_string((u64)args[0].i64()) + ":" + command;
u32 cap = (u32)args[4].i32();
int32_t buf = args[3].i32();
String out;
if(buf != 0 && self->staged_memcache_key == key)
{
out = self->staged_memcache_result;
self->staged_memcache_key = "";
self->staged_memcache_result = "";
}
else
{
::socket_write((u64)args[0].i64(), command + "\r\n");
out = ::socket_read((u64)args[0].i64());
if(buf == 0)
{
self->staged_memcache_key = key;
self->staged_memcache_result = out;
}
}
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_mysql")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
String out;
if(!self->hostcall_staged(encoded, out))
{
DValue request, response;
String decode_error;
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "connect")
{
MySQL* db = new MySQL();
bool ok = db->connect(request["host"].to_string(), request["username"].to_string(), request["password"].to_string());
u64 handle = 0;
if(ok && db->connection)
{
self->mysql_handles.push_back(db);
handle = self->mysql_handles.size();
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->_preload_next_error_code;
response["statement_info"] = db->error();
if(handle == 0)
delete db;
}
else if(op == "escape")
{
String quote = request["quote_char"].to_string();
response["result"] = mysql_escape(request["raw"].to_string(), quote.size() ? quote[0] : 0);
}
else
{
u64 handle = request["handle"].to_u64();
MySQL* db = (handle >= 1 && handle <= self->mysql_handles.size())
? self->mysql_handles[(size_t)handle - 1] : 0;
if(op == "query" && db)
{
response["result"] = db->query(request["query"].to_string());
response["insert_id"] = (f64)db->insert_id;
response["affected"] = (f64)db->affected_rows;
response["error_code"] = (f64)db->_preload_next_error_code;
response["statement_info"] = db->error();
}
else if(op == "disconnect" && db)
{
delete db;
self->mysql_handles[(size_t)handle - 1] = 0;
}
}
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
#endif
if(mod == "env" && name == "uce_host_file_unlink")
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_write(path, current);
if(resolved != "")
::unlink(resolved.c_str());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_socket_connect")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String host;
self->hostcall_read(args[0].i32(), args[1].i32(), host);
int fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(fd >= 0)
{
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons((short)args[2].i32());
addr.sin_addr.s_addr = inet_addr(host.c_str());
if(::connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
{
::close(fd);
fd = -1;
}
else if(fd == 0)
{
int moved = ::dup(fd);
::close(fd);
fd = moved;
}
if(fd > 0 && context)
context->resources.sockets.push_back(fd);
}
results[0] = Val((int64_t)(fd > 0 ? fd : 0));
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_socket_close")
return(add([](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
::socket_close((u64)args[0].i64());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_socket_write")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String data;
self->hostcall_read(args[1].i32(), args[2].i32(), data);
results[0] = Val(::socket_write((u64)args[0].i64(), data) ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_socket_read")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 sockfd = (u64)args[0].i64();
u32 max_length = (u32)args[1].i32();
u32 timeout = (u32)args[2].i32();
int32_t buf = args[3].i32();
u32 cap = (u32)args[4].i32();
String key = std::to_string(sockfd) + ":" + std::to_string(max_length) + ":" + std::to_string(timeout);
String out;
if(buf != 0 && self->staged_socket_read_key == key)
{
out = self->staged_socket_read_result;
self->staged_socket_read_key = "";
self->staged_socket_read_result = "";
}
else
{
out = ::socket_read(sockfd, max_length, timeout);
if(buf == 0)
{
self->staged_socket_read_key = key;
self->staged_socket_read_result = out;
}
}
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_server_start_http")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String key, bind, file, function, current;
self->hostcall_read(args[0].i32(), args[1].i32(), key);
self->hostcall_read(args[2].i32(), args[3].i32(), bind);
self->hostcall_read(args[4].i32(), args[5].i32(), file);
self->hostcall_read(args[6].i32(), args[7].i32(), function);
self->hostcall_read(args[8].i32(), args[9].i32(), current);
String resolved = self->resolve_guest_file(file, current);
pid_t pid = 0;
try
{
if(resolved != "")
pid = ::server_start_http(key, bind, resolved, function);
}
catch(const std::exception& e)
{
fprintf(stderr, "[wasm server] start failed for key '%s': %s\n", key.c_str(), e.what());
}
catch(...)
{
fprintf(stderr, "[wasm server] start failed for key '%s'\n", key.c_str());
}
results[0] = Val((int32_t)pid);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_server_stop")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String key;
self->hostcall_read(args[0].i32(), args[1].i32(), key);
results[0] = Val(::server_stop(key) ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_task_spawn")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String key;
self->hostcall_read(args[0].i32(), args[1].i32(), key);
u64 callback_id = (u64)args[2].i64();
f64 interval = args[3].f64();
u64 timeout = (u64)args[4].i64();
bool repeat = args[5].i32() != 0;
auto run_callback = [self, callback_id]() {
String error = self->run_task_callback(callback_id);
if(error != "")
fprintf(stderr, "[wasm task] callback failed: %s\n", error.c_str());
};
pid_t pid = 0;
try
{
if(!repeat || (interval > 0 && std::isfinite(interval)))
pid = repeat
? ::task_repeat(key, interval, run_callback, timeout)
: ::task(key, run_callback, timeout);
}
catch(const std::exception& e)
{
fprintf(stderr, "[wasm task] spawn failed for key '%s': %s\n", key.c_str(), e.what());
}
catch(...)
{
fprintf(stderr, "[wasm task] spawn failed for key '%s'\n", key.c_str());
}
results[0] = Val((int32_t)pid);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_task_pid")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String key;
self->hostcall_read(args[0].i32(), args[1].i32(), key);
results[0] = Val((int32_t)::task_pid(key));
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_task_kill")
return(add([](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
results[0] = Val((int32_t)::task_kill((pid_t)args[0].i32(), args[1].i32()));
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_sleep_us")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 usec = (u64)args[0].i64();
while(usec >= 1000000ull)
{
unsigned int remaining = ::sleep((unsigned int)(usec / 1000000ull));
if(remaining != 0)
{
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)remaining);
return(std::monostate());
}
usec %= 1000000ull;
}
if(usec > 0)
::usleep((useconds_t)usec);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_regex")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
// {op,pattern,subject,flags,replacement} in (UCEB1) → result out.
// PCRE2 lives host-side; this runs the real native regex_*.
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
DValue request;
String decode_error;
DValue response;
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
String pattern = request["pattern"].to_string();
String subject = request["subject"].to_string();
String flags = request["flags"].to_string();
if(op == "match")
response["bool"].set_bool(regex_match(pattern, subject, flags));
else if(op == "search")
response["tree"] = regex_search(pattern, subject, flags);
else if(op == "search_all")
response["tree"] = regex_search_all(pattern, subject, flags);
else if(op == "replace")
response["text"] = regex_replace(pattern, request["replacement"].to_string(), subject, flags);
else if(op == "split")
for(auto& part : regex_split(pattern, subject, flags))
{
DValue value;
value = part;
response["list"].push(value);
}
}
String out = ucb_encode(response);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_component_resolve")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String target, handler, current, resolved;
self->hostcall_read(args[0].i32(), args[1].i32(), target);
self->hostcall_read(args[2].i32(), args[3].i32(), handler);
self->hostcall_read(args[4].i32(), args[5].i32(), current);
int32_t slot = self->component_resolve(target, handler, current, resolved);
u32 cap = (u32)args[7].i32();
if(cap > 0)
{
if(resolved.size() >= cap)
resolved = resolved.substr(0, cap - 1);
resolved.push_back('\0');
self->hostcall_write(args[6].i32(), resolved);
}
results[0] = Val(slot);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
return(std::monostate());
}));
// anything else (wasi-libc residue): a named trap — tolerated as long
// as it is never called
String label = mod + "." + name;
return(add([label](Caller, Span<const Val>, Span<Val>) -> Result<std::monostate, Trap> {
return(wasmtime::Trap("unimplemented host import called: " + std::string(label)));
}));
}
String hostcall_read(int32_t ptr, int32_t len, String& out)
{
return(guest_read((u32)ptr, (u32)len, out));
}
void hostcall_write(int32_t ptr, const String& data)
{
guest_write((u32)ptr, data);
}
};
// ---- public entry: one request through one workspace -----------------------
inline WasmResponse wasm_worker_serve(WasmWorker& worker, const DValue& context_tree, const String& entry_source_path,
const String& handler = "render", const Request* request = 0)
{
WasmResponse response;
WasmWorkspace workspace(worker);
if(request)
workspace.set_perf_snapshot(my_pid, (u64)parent_pid, request->server ? request->server->request_count : 0, request->stats.time_init, request->stats.time_start);
auto birth_start = std::chrono::steady_clock::now();
String error = workspace.birth();
workspace.workspace_birth_us = (u64)std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - birth_start).count();
if(error == "")
error = workspace.apply_context(context_tree);
if(error == "")
error = workspace.invoke_entry(entry_source_path, handler, &response.handler_present);
if(error == "")
error = workspace.collect(response);
response.workspace_birth_us = workspace.workspace_birth_us;
response.component_resolve_count = workspace.component_resolve_count;
response.component_resolve_total_us = workspace.component_resolve_total_us;
if(error != "")
{
response.ok = false;
response.error = error;
return(response);
}
response.ok = true;
return(response);
}