uce/src/wasm/worker.cpp

2752 lines
108 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 (production 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 <filesystem>
#include <map>
#include <set>
#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>
#include <sys/wait.h>
#include <signal.h>
#include <poll.h>
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;
// uce_host_* names (bare, without the "uce_host_" prefix) the sysadmin has
// disabled via UCE_HOSTCALL_BLOCKLIST. A blocked hostcall resolves to a trap
// stub at workspace birth (see make_host_import); empty = feature off.
std::set<String> hostcall_blocklist;
};
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;
};
static u64 wasm_file_lock_timeout_ms()
{
const char* raw = getenv("UCE_FILE_LOCK_TIMEOUT_MS");
if(!raw || !*raw)
return(2000);
char* end = 0;
unsigned long long parsed = strtoull(raw, &end, 10);
return(end == raw ? 2000 : (u64)parsed);
}
static u64 wasm_monotonic_ms()
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return((u64)ts.tv_sec * 1000ull + (u64)ts.tv_nsec / 1000000ull);
}
static int wasm_open_locked_file(const String& file_name, int flags, int lock_type, bool truncate_after_lock)
{
int fd = open(file_name.c_str(), flags, 0644);
if(fd < 0)
return(-1);
fcntl(fd, F_SETFD, FD_CLOEXEC);
u64 timeout = wasm_file_lock_timeout_ms();
u64 deadline = wasm_monotonic_ms() + timeout;
while(true)
{
if(flock(fd, lock_type | LOCK_NB) == 0)
{
if(truncate_after_lock && ftruncate(fd, 0) != 0)
{
flock(fd, LOCK_UN);
close(fd);
return(-1);
}
return(fd);
}
if(errno != EWOULDBLOCK && errno != EAGAIN && errno != EINTR)
{
close(fd);
return(-1);
}
if(timeout == 0 || wasm_monotonic_ms() >= deadline)
{
fprintf(stderr, "[wasm] file lock timeout after %llums: %s\n", (unsigned long long)timeout, file_name.c_str());
close(fd);
return(-1);
}
usleep(10000);
}
}
static bool wasm_fd_write_all(int fd, const char* data, size_t remaining, u64* written_out)
{
u64 total = 0;
while(remaining > 0)
{
ssize_t n = write(fd, data, remaining);
if(n < 0)
{
if(errno == EINTR)
continue;
return(false);
}
if(n == 0)
return(false);
data += n;
remaining -= (size_t)n;
total += (u64)n;
}
if(written_out)
*written_out = total;
return(true);
}
// ---- file-backed async job registry + bounded process execution ----------
static String uce_job_root()
{
const char* env = getenv("UCE_JOB_ROOT");
String root = (env && *env) ? String(env) : String("/run/uce/jobs");
std::error_code ec;
std::filesystem::create_directories(root, ec);
if(ec)
{
root = "/tmp/uce/jobs";
std::filesystem::create_directories(root, ec);
}
return(root);
}
static String uce_job_path(u64 id) { return(uce_job_root() + "/" + std::to_string(id)); }
static String uce_read_text(const String& path) { std::ifstream in(path, std::ios::binary); if(!in) return(""); std::ostringstream ss; ss << in.rdbuf(); return(ss.str()); }
static void uce_write_text(const String& path, const String& data) { std::ofstream out(path, std::ios::binary|std::ios::trunc); out.write(data.data(), (std::streamsize)data.size()); }
static u64 uce_job_new(const String& kind)
{
String root = uce_job_root();
std::error_code ec;
std::filesystem::create_directories(root, ec);
u64 seed = ((u64)time(0) << 32) ^ ((u64)getpid() << 16) ^ (u64)rand();
for(int i = 0; i < 100; i++)
{
u64 id = seed ^ (wasm_monotonic_ms() + (u64)i * 0x9e3779b97f4a7c15ull);
String dir = root + "/" + std::to_string(id);
if(mkdir(dir.c_str(), 0700) == 0)
{
uce_write_text(dir + "/kind", kind);
uce_write_text(dir + "/created", std::to_string((u64)time(0)));
uce_write_text(dir + "/state", "pending");
return(id);
}
}
return(0);
}
static void uce_job_reap()
{
String root = uce_job_root();
u64 now = (u64)time(0);
u64 ttl = 3600;
if(const char* raw = getenv("UCE_JOB_TTL_SECONDS")) { char* e=0; unsigned long long v=strtoull(raw,&e,10); if(e!=raw && v>0) ttl=(u64)v; }
std::error_code ec;
for(auto& e : std::filesystem::directory_iterator(root, ec))
{
if(!e.is_directory()) continue;
u64 created = strtoull(uce_read_text(e.path().string()+"/created").c_str(), 0, 10);
if(created > 0 && now > created + ttl)
std::filesystem::remove_all(e.path(), ec);
}
}
static DValue uce_process_exec(String cmd, String input, StringMap env, u64 timeout_ms)
{
DValue r;
r["exit_code"] = (f64)-1;
r["stdout"] = "";
r["stderr"] = "";
r["timed_out"].set_bool(false);
if(timeout_ms == 0) timeout_ms = 5000;
int inpipe[2], outpipe[2], errpipe[2];
if(pipe(inpipe) || pipe(outpipe) || pipe(errpipe)) { r["stderr"]="pipe failed"; return(r); }
pid_t pid = fork();
if(pid == 0)
{
dup2(inpipe[0], 0); dup2(outpipe[1], 1); dup2(errpipe[1], 2);
close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); close(errpipe[0]); close(errpipe[1]);
for(auto& kv : env) setenv(kv.first.c_str(), kv.second.c_str(), 1);
execl("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0);
_exit(127);
}
close(inpipe[0]); close(outpipe[1]); close(errpipe[1]);
fcntl(outpipe[0], F_SETFL, fcntl(outpipe[0], F_GETFL, 0) | O_NONBLOCK);
fcntl(errpipe[0], F_SETFL, fcntl(errpipe[0], F_GETFL, 0) | O_NONBLOCK);
size_t input_off = 0; bool in_open = true, out_open = true, err_open = true; int status = 0; bool exited = false;
u64 deadline = wasm_monotonic_ms() + timeout_ms;
while(out_open || err_open || !exited)
{
if(!exited)
{
pid_t w = waitpid(pid, &status, WNOHANG);
if(w == pid) exited = true;
}
if(in_open)
{
if(input_off < input.size()) { ssize_t n=write(inpipe[1], input.data()+input_off, input.size()-input_off); if(n>0) input_off += (size_t)n; else if(n<0 && errno!=EINTR) { close(inpipe[1]); in_open=false; } }
else { close(inpipe[1]); in_open=false; }
}
char buf[4096];
ssize_t n;
while((n=read(outpipe[0], buf, sizeof(buf))) > 0) r["stdout"] = r["stdout"].to_string() + String(buf, n);
if(n == 0 && out_open) { close(outpipe[0]); out_open=false; }
while((n=read(errpipe[0], buf, sizeof(buf))) > 0) r["stderr"] = r["stderr"].to_string() + String(buf, n);
if(n == 0 && err_open) { close(errpipe[0]); err_open=false; }
if(!exited && wasm_monotonic_ms() >= deadline)
{
r["timed_out"].set_bool(true);
kill(pid, SIGKILL);
waitpid(pid, &status, 0);
exited = true;
}
if((out_open || err_open || !exited)) usleep(10000);
}
if(WIFEXITED(status)) { r["exit_code"] = (f64)WEXITSTATUS(status); r["timed_out"].set_bool(false); }
else if(WIFSIGNALED(status)) r["exit_code"] = (f64)(128 + WTERMSIG(status));
return(r);
}
static DValue uce_shell_exec_spec(const DValue& spec)
{
return(uce_process_exec(spec.key("cmd") ? spec.key("cmd")->to_string() : String(""), spec.key("stdin") ? spec.key("stdin")->to_string() : String(""), spec.key("env") ? spec.key("env")->to_stringmap() : StringMap(), spec.key("timeout_ms") ? spec.key("timeout_ms")->to_u64(5000) : 5000));
}
static void uce_job_finish(u64 id, DValue result, String final_state="done")
{
String dir = uce_job_path(id);
uce_write_text(dir + "/result.tmp", ucb_encode(result));
rename((dir + "/result.tmp").c_str(), (dir + "/result").c_str());
uce_write_text(dir + "/state", final_state);
}
static u64 uce_shell_spawn_spec(const DValue& spec)
{
uce_job_reap();
u64 id = uce_job_new("shell");
if(!id) return(0);
pid_t pid = fork();
if(pid == 0)
{
setsid();
uce_write_text(uce_job_path(id) + "/worker_pid", std::to_string((long long)getpid()));
uce_write_text(uce_job_path(id) + "/state", "running");
DValue result = uce_shell_exec_spec(spec);
uce_job_finish(id, result, "done");
_exit(0);
}
if(pid < 0) { DValue r; r["error"]="fork failed"; uce_job_finish(id,r,"failed"); return(id); }
uce_write_text(uce_job_path(id) + "/worker_pid", std::to_string((long long)pid));
uce_write_text(uce_job_path(id) + "/state", "running");
return(id);
}
static DValue uce_exec_argv_capture(std::vector<String> argv, String input, u64 timeout_ms)
{
DValue r; r["exit_code"]=(f64)-1; r["stdout"]=""; r["stderr"]=""; r["timed_out"].set_bool(false);
if(argv.empty()) { r["stderr"]="empty argv"; return(r); }
if(timeout_ms == 0) timeout_ms = 5000;
int inpipe[2], outpipe[2], errpipe[2];
if(pipe(inpipe)||pipe(outpipe)||pipe(errpipe)) { r["stderr"]="pipe failed"; return(r); }
pid_t pid=fork();
if(pid==0)
{
dup2(inpipe[0],0); dup2(outpipe[1],1); dup2(errpipe[1],2);
close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); close(errpipe[0]); close(errpipe[1]);
std::vector<char*> args; for(auto& a: argv) args.push_back((char*)a.c_str()); args.push_back(0);
execvp(args[0], args.data()); _exit(127);
}
close(inpipe[0]); close(outpipe[1]); close(errpipe[1]);
fcntl(outpipe[0], F_SETFL, fcntl(outpipe[0], F_GETFL, 0)|O_NONBLOCK); fcntl(errpipe[0], F_SETFL, fcntl(errpipe[0], F_GETFL, 0)|O_NONBLOCK);
size_t input_off=0; bool in_open=true,out_open=true,err_open=true,exited=false; int status=0; u64 deadline=wasm_monotonic_ms()+timeout_ms;
while(out_open || err_open || !exited)
{
if(!exited) { pid_t w=waitpid(pid,&status,WNOHANG); if(w==pid) exited=true; }
if(in_open) { if(input_off<input.size()) { ssize_t n=write(inpipe[1], input.data()+input_off, input.size()-input_off); if(n>0) input_off+=(size_t)n; else if(n<0 && errno!=EINTR) { close(inpipe[1]); in_open=false; } } else { close(inpipe[1]); in_open=false; } }
char buf[4096]; ssize_t n; while((n=read(outpipe[0],buf,sizeof(buf)))>0) r["stdout"] = r["stdout"].to_string()+String(buf,n); if(n==0&&out_open){close(outpipe[0]);out_open=false;}
while((n=read(errpipe[0],buf,sizeof(buf)))>0) r["stderr"] = r["stderr"].to_string()+String(buf,n); if(n==0&&err_open){close(errpipe[0]);err_open=false;}
if(!exited && wasm_monotonic_ms() >= deadline) { r["timed_out"].set_bool(true); kill(pid,SIGKILL); waitpid(pid,&status,0); exited=true; }
if(out_open || err_open || !exited) usleep(10000);
}
if(WIFEXITED(status)) { r["exit_code"]=(f64)WEXITSTATUS(status); r["timed_out"].set_bool(false); } else if(WIFSIGNALED(status)) r["exit_code"]=(f64)(128+WTERMSIG(status));
return(r);
}
static bool uce_header_name_safe(String name)
{
if(name=="") return(false);
for(unsigned char c: name) if(!(isalnum(c)||c=='-'||c=='_')) return(false);
return(true);
}
static DValue uce_http_request_value(const DValue& req)
{
DValue r; r["status"]=(f64)0; r["headers"].set_array(); r["body"]=""; r["error"]="";
const DValue* method_value = req.key("method");
const DValue* url_value = req.key("url");
const DValue* timeout_value = req.key("timeout_ms");
const DValue* follow_redirects_value = req.key("follow_redirects");
const DValue* headers_value = req.key("headers");
const DValue* body_value = req.key("body");
String method = method_value ? to_upper(method_value->to_string()) : String("GET");
String url = url_value ? url_value->to_string() : String("");
if(url=="" || url.find('\0')!=String::npos) { r["error"]="missing url"; return(r); }
if(method=="") method="GET";
u64 timeout_ms = timeout_value ? timeout_value->to_u64(5000) : 5000;
std::vector<String> argv = {"curl", "-sS", "--http1.0", "-X", method, "-D", "-", "-w", "\nUCE_HTTP_STATUS:%{http_code}", "--max-time", std::to_string(std::max<u64>(1, timeout_ms / 1000))};
if(follow_redirects_value && follow_redirects_value->to_bool()) argv.push_back("-L");
if(headers_value) headers_value->each([&](const DValue& v, String k){ if(uce_header_name_safe(k)) { argv.push_back("-H"); argv.push_back(k + ": " + replace(replace(v.to_string(), "\r", " "), "\n", " ")); } });
String body = body_value ? body_value->to_string() : String("");
if(body_value) { argv.push_back("--data-binary"); argv.push_back("@-"); }
argv.push_back(url);
if(access("/usr/bin/curl", X_OK)!=0 && access("/bin/curl", X_OK)!=0) { r["error"]="curl binary not found in runtime PATH"; return(r); }
DValue pr = uce_exec_argv_capture(argv, body, timeout_ms);
String out = pr["stdout"].to_string();
String marker="\nUCE_HTTP_STATUS:"; size_t mp=out.rfind(marker);
if(mp!=String::npos) { r["status"]=(f64)strtoull(out.c_str()+mp+marker.size(),0,10); out=out.substr(0,mp); }
else r["error"]="curl did not report status";
String sep="\r\n\r\n"; size_t hp=out.rfind(sep); size_t sep_len=4; if(hp==String::npos) { sep="\n\n"; hp=out.rfind(sep); sep_len=2; }
String hdrs = hp==String::npos ? String("") : out.substr(0,hp); r["body"] = hp==String::npos ? out : out.substr(hp+sep_len);
DValue headers;
for(String line: split(replace(hdrs,"\r",""), "\n")) { size_t c=line.find(':'); if(c!=String::npos) headers[trim(line.substr(0,c))] = trim(line.substr(c+1)); }
r["headers"] = headers;
if(pr["exit_code"].to_s64() != 0 && r["error"].to_string()=="") r["error"] = trim(pr["stderr"].to_string());
return(r);
}
static u64 uce_http_spawn_spec(const DValue& req)
{
uce_job_reap(); u64 id=uce_job_new("http"); if(!id) return(0);
pid_t pid=fork();
if(pid==0) { setsid(); uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)getpid())); uce_write_text(uce_job_path(id)+"/state", "running"); DValue result=uce_http_request_value(req); uce_job_finish(id,result,result["error"].to_string()==""?"done":"failed"); _exit(0); }
if(pid<0) { DValue r; r["error"]="fork failed"; uce_job_finish(id,r,"failed"); return(id); }
uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)pid)); uce_write_text(uce_job_path(id)+"/state", "running"); return(id);
}
static DValue uce_job_status_value(u64 id)
{
DValue r; String dir=uce_job_path(id); r["job_id"]=(f64)id;
if(id==0 || !std::filesystem::is_directory(dir)) { r["state"]="missing"; return(r); }
String state=trim(uce_read_text(dir+"/state")); if(state=="") state="pending"; r["state"]=state;
r["kind"]=trim(uce_read_text(dir+"/kind")); r["pid"]=(f64)strtoull(uce_read_text(dir+"/worker_pid").c_str(),0,10);
r["done"].set_bool(state=="done"||state=="failed"||state=="cancelled");
return(r);
}
static DValue uce_job_result_value(u64 id, u64 timeout_ms)
{
u64 deadline = wasm_monotonic_ms() + timeout_ms;
while(timeout_ms > 0 && wasm_monotonic_ms() < deadline)
{
DValue st = uce_job_status_value(id);
if(st["done"].to_bool()) break;
usleep(10000);
}
DValue r = uce_job_status_value(id);
String encoded = uce_read_text(uce_job_path(id)+"/result");
if(encoded != "") { DValue decoded; String err; if(ucb_decode(encoded, decoded, &err)) r["result"] = decoded; }
return(r);
}
static bool uce_job_cancel_value(u64 id)
{
DValue st = uce_job_status_value(id);
if(st["state"].to_string()=="missing") return(false);
String state = st["state"].to_string();
if(state == "done" || state == "failed" || state == "cancelled")
return(false);
pid_t pid = (pid_t)st["pid"].to_u64(0);
if(pid > 0) kill(-pid, SIGKILL);
DValue result; result["cancelled"].set_bool(true);
uce_job_finish(id, result, "cancelled");
return(true);
}
// ---- 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();
if(n < 0)
return(false);
in.seekg(0, std::ios::beg);
out.resize((size_t)n);
if(n == 0)
return(true);
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 FileHandle
{
int fd = -1;
bool writable = false;
};
std::vector<FileHandle> file_handles;
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;
}
#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. This is
// the wasm-side enforcement of request-scoped DB lifecycle; app code should
// never cache these opaque handles across requests.
std::vector<SQLite*> sqlite_handles;
std::vector<MySQL*> mysql_handles;
#endif
~WasmWorkspace()
{
for(auto& h : file_handles)
{
if(h.fd >= 0)
{
flock(h.fd, LOCK_UN);
close(h.fd);
h.fd = -1;
}
}
#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;
staged_hostcall_input = "";
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 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));
}
// Keep cwd host behavior local to this process but guard it with the same
// write-root policy we use for file writes (plus a single parity fallback).
String resolve_guest_cwd_set(const String& raw)
{
if(raw == "" || raw.find('\0') != String::npos)
return("");
String raw_target = raw;
if(raw.rfind("/", 0) != 0)
{
String cwd = ::cwd_get();
if(cwd == "")
return("");
raw_target = cwd + "/" + raw;
}
char resolved[PATH_MAX];
if(!realpath(raw_target.c_str(), resolved))
return("");
String resolved_target(resolved);
if(!dir_exists_host(resolved_target))
return("");
// Policy: allow only roots we already expose for writable filesystem access.
std::vector<String> roots;
roots.push_back(worker.cfg.site_root);
for(auto& root : worker.cfg.write_roots)
roots.push_back(root);
for(auto& root : roots)
{
if(root == "")
continue;
char root_real[PATH_MAX];
if(!realpath(root.c_str(), root_real))
continue;
String canonical_root(root_real);
if(resolved_target == canonical_root)
return(resolved_target);
if(canonical_root != "/" && resolved_target.rfind(canonical_root + "/", 0) == 0)
return(resolved_target);
}
// Parity/fallback: allow returning to the process start directory so
// legacy behavior is not silently broken for existing native/cached flows.
String start_directory = ::process_start_directory();
if(start_directory != "" && resolved_target == start_directory)
return(resolved_target);
return("");
}
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 == prefix || 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());
};
// Hostcall blocklist (UCE_HOSTCALL_BLOCKLIST): a sysadmin-disabled hostcall
// resolves to a trap stub instead of its real implementation, so a unit
// invoking it fails at runtime into the configurable error page. The
// decision is made once per import at workspace birth — no per-call cost,
// and zero cost when nothing is blocked. A small core set stays exempt so
// the runtime itself cannot be bricked.
if(mod == "env" && !worker.cfg.hostcall_blocklist.empty() && name.rfind("uce_host_", 0) == 0)
{
static const std::set<String> non_blockable = { "component_resolve" };
String bare = name.substr(9);
if(worker.cfg.hostcall_blocklist.count(bare) && !non_blockable.count(bare))
{
std::string blocked(name);
return(add([blocked](Caller, Span<const Val>, Span<Val>) -> Result<std::monostate, Trap> {
return(Trap("UCE_POLICY_BLOCKED:" + blocked));
}));
}
}
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_sha256")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=sha256_native(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_sha256_hex")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=sha256_hex_native(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_hmac_sha256")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String key,in; self->hostcall_read(args[0].i32(), args[1].i32(), key); self->hostcall_read(args[2].i32(), args[3].i32(), in); String out=hmac_sha256_native(key,in); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_hmac_sha256_hex")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String key,in; self->hostcall_read(args[0].i32(), args[1].i32(), key); self->hostcall_read(args[2].i32(), args[3].i32(), in); String out=hmac_sha256_hex_native(key,in); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_base64_encode")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=base64_encode(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_base64_decode")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); bool ok=false; String out=base64_decode(in, ok); if(!ok) out=""; u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_crypto_equal")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String a,b; self->hostcall_read(args[0].i32(), args[1].i32(), a); self->hostcall_read(args[2].i32(), args[3].i32(), b); results[0]=Val((int32_t)(crypto_equal_native(a,b)?1:0)); 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);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String out;
String stage_key = "shell:" + cmd;
if(!self->hostcall_staged(stage_key, out))
{
out = ::shell_exec(cmd);
if(buf == 0)
self->hostcall_stage(stage_key, 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_http_request")
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); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); String out; String stage_key="http:"+encoded;
if(!self->hostcall_staged(stage_key,out)) { DValue req,response; String err; if(ucb_decode(encoded,req,&err)) response=uce_http_request_value(req); else response["error"]="http_request decode failed: "+err; out=ucb_encode(response); if(buf==0) self->hostcall_stage(stage_key,out); }
if(buf&&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_http_request_async")
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); DValue req; String err; u64 id=0; if(ucb_decode(encoded,req,&err)) id=uce_http_spawn_spec(req); results[0]=Val((int64_t)id); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_shell_exec_dv")
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);
u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32();
String out; String stage_key="shell_dv:"+encoded;
if(!self->hostcall_staged(stage_key,out)) { DValue spec, response; String err; if(ucb_decode(encoded,spec,&err)) response=uce_shell_exec_spec(spec); else response["error"]="shell_exec spec decode failed: "+err; out=ucb_encode(response); if(buf==0) self->hostcall_stage(stage_key,out); }
if(buf&&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_shell_spawn")
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); DValue spec; String err; u64 id=0; if(ucb_decode(encoded,spec,&err)) id=uce_shell_spawn_spec(spec); results[0]=Val((int64_t)id); return(std::monostate());
}));
if(mod == "env" && name == "uce_host_job_status")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String out=ucb_encode(uce_job_status_value((u64)args[0].i64())); u32 cap=(u32)args[2].i32(); int32_t buf=args[1].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_job_result")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String out=ucb_encode(uce_job_result_value((u64)args[0].i64(), 100)); u32 cap=(u32)args[2].i32(); int32_t buf=args[1].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_job_await")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { u64 timeout=std::min<u64>((u64)args[1].i64(), 30000); String out=ucb_encode(uce_job_result_value((u64)args[0].i64(), timeout)); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&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_job_cancel")
return(add([](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { results[0]=Val((int32_t)(uce_job_cancel_value((u64)args[0].i64())?1:0)); 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);
String resolved = self->resolve_guest_cwd_set(path);
results[0] = Val(::chdir(resolved.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_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 stage_key = "file_read:" + path + "\0" + current;
String content;
if(!self->hostcall_staged(stage_key, content))
{
String resolved = self->resolve_guest_file(path, current);
content = resolved == "" ? String("") : ::file_get_contents(resolved);
self->hostcall_stage(stage_key, content);
}
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 >= content.size())
self->hostcall_write(buf, content);
results[0] = Val((int32_t)content.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(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_file_open")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current, mode;
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(), mode);
String resolved;
int flags = O_RDONLY;
int lock_type = LOCK_SH;
bool writable = false;
bool truncate_after_lock = false;
if(mode == "r")
resolved = self->resolve_guest_file(path, current);
else if(mode == "w")
{
resolved = self->resolve_guest_write(path, current);
flags = O_RDWR | O_CREAT;
lock_type = LOCK_EX;
writable = true;
truncate_after_lock = true;
}
else if(mode == "a")
{
resolved = self->resolve_guest_write(path, current);
flags = O_RDWR | O_CREAT | O_APPEND;
lock_type = LOCK_EX;
writable = true;
}
else if(mode == "r+")
{
resolved = self->resolve_guest_write(path, current);
flags = O_RDWR;
lock_type = LOCK_EX;
writable = true;
}
uint64_t handle = 0;
if(resolved != "")
{
int fd = wasm_open_locked_file(resolved, flags, lock_type, truncate_after_lock);
if(fd >= 0)
{
if(mode == "a")
lseek(fd, 0, SEEK_END);
self->file_handles.push_back({fd, writable});
handle = self->file_handles.size();
}
}
results[0] = Val((int64_t)handle);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_handle_read")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64();
u64 len = (u64)args[1].i64();
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String out;
String stage_key = "file_handle_read:" + std::to_string(handle) + ":" + std::to_string(len);
if(!self->hostcall_staged(stage_key, out))
{
if(handle >= 1 && handle <= self->file_handles.size())
{
int fd = self->file_handles[(size_t)handle - 1].fd;
if(fd >= 0 && len > 0)
{
out.resize((size_t)std::min<u64>(len, 16ull * 1024ull * 1024ull));
ssize_t n = read(fd, &out[0], out.size());
out.resize(n > 0 ? (size_t)n : 0);
}
}
if(buf == 0) self->hostcall_stage(stage_key, out);
}
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_file_handle_pread")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64();
u64 offset = (u64)args[1].i64();
u64 len = (u64)args[2].i64();
u32 cap = (u32)args[4].i32();
int32_t buf = args[3].i32();
String out;
String stage_key = "file_handle_pread:" + std::to_string(handle) + ":" + std::to_string(offset) + ":" + std::to_string(len);
if(!self->hostcall_staged(stage_key, out))
{
if(handle >= 1 && handle <= self->file_handles.size())
{
int fd = self->file_handles[(size_t)handle - 1].fd;
if(fd >= 0 && len > 0)
{
out.resize((size_t)std::min<u64>(len, 16ull * 1024ull * 1024ull));
ssize_t n = pread(fd, &out[0], out.size(), (off_t)offset);
out.resize(n > 0 ? (size_t)n : 0);
}
}
if(buf == 0) self->hostcall_stage(stage_key, out);
}
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_file_handle_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);
u64 handle = (u64)args[0].i64(); u64 written = 0;
if(handle >= 1 && handle <= self->file_handles.size())
{
auto& h = self->file_handles[(size_t)handle - 1];
if(h.fd >= 0 && h.writable) wasm_fd_write_all(h.fd, data.data(), data.size(), &written);
}
results[0] = Val((int64_t)written);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_handle_pwrite")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String data; self->hostcall_read(args[2].i32(), args[3].i32(), data);
u64 handle = (u64)args[0].i64(); u64 offset = (u64)args[1].i64(); u64 written = 0;
if(handle >= 1 && handle <= self->file_handles.size())
{
auto& h = self->file_handles[(size_t)handle - 1];
if(h.fd >= 0 && h.writable)
{
while(written < data.size())
{
ssize_t n = pwrite(h.fd, data.data() + written, data.size() - written, (off_t)(offset + written));
if(n < 0 && errno == EINTR) continue;
if(n <= 0) break;
written += (u64)n;
}
}
}
results[0] = Val((int64_t)written);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_handle_seek")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64(); s64 pos = -1;
if(handle >= 1 && handle <= self->file_handles.size())
{
int fd = self->file_handles[(size_t)handle - 1].fd;
if(fd >= 0) pos = (s64)lseek(fd, (off_t)args[1].i64(), args[2].i32());
}
results[0] = Val((int64_t)pos);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_handle_tell")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64(); s64 pos = -1;
if(handle >= 1 && handle <= self->file_handles.size())
{
int fd = self->file_handles[(size_t)handle - 1].fd;
if(fd >= 0) pos = (s64)lseek(fd, 0, SEEK_CUR);
}
results[0] = Val((int64_t)pos);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_handle_close")
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
u64 handle = (u64)args[0].i64();
if(handle >= 1 && handle <= self->file_handles.size())
{
auto& h = self->file_handles[(size_t)handle - 1];
if(h.fd >= 0) { flock(h.fd, LOCK_UN); close(h.fd); h.fd = -1; }
}
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_stat")
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); DValue r; struct stat st;
r["exists"].set_bool(resolved != "" && lstat(resolved.c_str(), &st) == 0);
if(r["exists"].to_bool()) { r["size"]=(f64)st.st_size; r["mtime"]=(f64)st.st_mtime; r["ctime"]=(f64)st.st_ctime; r["mode"]=(f64)(st.st_mode & 07777); r["is_dir"].set_bool(S_ISDIR(st.st_mode)); r["is_file"].set_bool(S_ISREG(st.st_mode)); r["is_symlink"].set_bool(S_ISLNK(st.st_mode)); }
else { r["size"]=(f64)0; r["mtime"]=(f64)0; r["ctime"]=(f64)0; r["mode"]=(f64)0; r["is_dir"].set_bool(false); r["is_file"].set_bool(false); r["is_symlink"].set_bool(false); }
String out = ucb_encode(r); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf && cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate());
}));
if(mod == "env" && name == "uce_host_dir_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); DValue list; list.set_array();
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); } std::sort(names.begin(), names.end()); for(auto& n:names) { String p=resolved+"/"+n; struct stat st; DValue item; item["name"]=n; if(lstat(p.c_str(), &st)==0) { item["size"]=(f64)st.st_size; item["mtime"]=(f64)st.st_mtime; item["type"]=S_ISDIR(st.st_mode)?"dir":S_ISLNK(st.st_mode)?"symlink":S_ISREG(st.st_mode)?"file":"other"; } list.push(item); } }
String out=ucb_encode(list); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_rename")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String from,to,current; self->hostcall_read(args[0].i32(),args[1].i32(),from); self->hostcall_read(args[2].i32(),args[3].i32(),to); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rf=self->resolve_guest_write(from,current), rt=self->resolve_guest_write(to,current); results[0]=Val((int32_t)(rf!=""&&rt!=""&&rename(rf.c_str(),rt.c_str())==0)); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_copy")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String from,to,current; self->hostcall_read(args[0].i32(),args[1].i32(),from); self->hostcall_read(args[2].i32(),args[3].i32(),to); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rf=self->resolve_guest_file(from,current), rt=self->resolve_guest_write(to,current); bool ok=false; if(rf!=""&&rt!="") { std::ifstream in(rf, std::ios::binary); std::ofstream out(rt, std::ios::binary|std::ios::trunc); out<<in.rdbuf(); struct stat st; if(in&&out) { ok=true; if(stat(rf.c_str(),&st)==0) chmod(rt.c_str(), st.st_mode & 07777); } } results[0]=Val((int32_t)ok); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_truncate")
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 r=self->resolve_guest_write(path,current); results[0]=Val((int32_t)(r!=""&&truncate(r.c_str(),(off_t)args[4].i64())==0)); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_dir_remove")
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 r=self->resolve_guest_write(path,current); bool rec=args[4].i32()!=0; bool ok=false; if(r!="") { if(rec) ok=std::filesystem::remove_all(r)>0; else ok=::rmdir(r.c_str())==0; } results[0]=Val((int32_t)ok); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_temp")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String prefix,current; self->hostcall_read(args[0].i32(),args[1].i32(),prefix); self->hostcall_read(args[2].i32(),args[3].i32(),current); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); String out; String stage_key="file_temp:"+prefix+"\0"+current; if(!self->hostcall_staged(stage_key,out)) { if(prefix=="") prefix="/tmp/uce-temp"; String templ=self->resolve_guest_write(prefix+"XXXXXX",current); if(templ!="") { std::vector<char> t(templ.begin(), templ.end()); t.push_back(0); int fd=mkstemp(t.data()); if(fd>=0) { close(fd); out=t.data(); } } if(buf==0) self->hostcall_stage(stage_key,out); } if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_chmod")
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 r=self->resolve_guest_write(path,current); results[0]=Val((int32_t)(r!=""&&chmod(r.c_str(),(mode_t)args[4].i32())==0)); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_symlink")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { String target,linkpath,current; self->hostcall_read(args[0].i32(),args[1].i32(),target); self->hostcall_read(args[2].i32(),args[3].i32(),linkpath); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rt=self->resolve_guest_file(target,current), rl=self->resolve_guest_write(linkpath,current); results[0]=Val((int32_t)(rt!=""&&rl!=""&&symlink(rt.c_str(),rl.c_str())==0)); return(std::monostate()); }));
if(mod == "env" && name == "uce_host_file_fsync")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> { u64 handle=(u64)args[0].i64(); bool ok=false; if(handle>=1&&handle<=self->file_handles.size()) { int fd=self->file_handles[(size_t)handle-1].fd; ok=fd>=0&&fsync(fd)==0; } results[0]=Val((int32_t)ok); 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);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String out;
String stage_key = "zip:" + encoded;
if(!self->hostcall_staged(stage_key, 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);
if(buf == 0)
self->hostcall_stage(stage_key, out);
}
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);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String out;
String stage_key = "units:" + encoded;
if(!self->hostcall_staged(stage_key, 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);
if(buf == 0)
self->hostcall_stage(stage_key, 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());
}));
#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 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);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String out;
String stage_key = "sqlite:" + encoded;
// run the op once across the length-query + fetch pair
if(!self->hostcall_staged(stage_key, 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);
if(buf == 0)
self->hostcall_stage(stage_key, out);
}
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);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
String out;
String stage_key = "mysql:" + encoded;
if(!self->hostcall_staged(stage_key, 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);
if(buf == 0)
self->hostcall_stage(stage_key, out);
}
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;
// task()/task_repeat() fork and invoke this lambda only in the child
// before the hostcall stack unwinds, so `self` points to the child's
// copy of this per-request workspace. The parent request can return and
// destroy its workspace without invalidating the child copy.
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 (UCEB2) → result out.
// PCRE2 lives host-side; this runs the 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);
}