feat: membrane remaining wasm host surfaces

This commit is contained in:
root
2026-06-13 20:10:00 +00:00
parent c84fc86e6c
commit fe83c52411
5 changed files with 758 additions and 36 deletions
+6 -7
View File
@@ -117,14 +117,13 @@ static bool wasm_backend_native_fallback_uncached(Request* context, const String
// - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in),
// - unit_render()/component() (host resolver).
// What remains is genuinely host-owned / native-only for now:
// - zip, sockets/custom servers, memcache, mysql;
// - unit_call + compiler/unit introspection (need the native toolchain).
// Background tasks and sleep/usleep are host-owned but now have membrane
// hostcalls, so they intentionally do not fallback here.
// - zip pages that use C++ try/catch around zip_* error cases;
// - direct compiler_load_shared_unit() access to native SharedUnit internals.
// Background tasks, sleep/usleep, sockets/custom servers, memcache, mysql,
// and unit_call/unit_compile/unit_info/units_list are host-owned but now have
// membrane hostcalls, so they intentionally do not fallback here.
StringList native_only_tokens = {
"zip_", "socket_", "server_start_http(", "server_stop(",
"memcache_", "mysql_", "unit_call(",
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit("
"zip_", "compiler_load_shared_unit("
};
for(auto& token : native_only_tokens)
if(source.find(token) != String::npos)
+309 -17
View File
@@ -10,32 +10,323 @@
#include "../lib/mysql-connector.h"
#include "../lib/sqlite-connector.h"
// ---- W3 connector membrane stubs -------------------------------------------
// Generated units reference the connector class surface through uce_lib.h, so
// the core must define it. Until the real hostcall connectors land (W5 parity
// scope: the starter uses no database), every operation fails cleanly with an
// explanatory error instead of trapping.
// ---- W3 connector membranes -----------------------------------------------
// sqlite/mysql run host-side (the host links the native connectors and owns the
// connections in per-workspace handle tables). UCEB1-marshalled hostcalls carry
// operation requests/responses; `connection` holds the host handle (>0).
static const char* WASM_DB_UNAVAILABLE =
"database connectors are not yet available in the wasm workspace";
"database connector is not available in the wasm workspace";
extern "C" size_t uce_host_mysql(const char* in, size_t in_len, char* out, size_t cap);
extern "C" size_t uce_host_zip(const char* in, size_t in_len, char* out, size_t cap);
extern "C" size_t uce_host_units(const char* in, size_t in_len, char* out, size_t cap);
static DValue wasm_sized_hostcall(DValue request, size_t (*hostcall)(const char*, size_t, char*, size_t))
{
String encoded = ucb_encode(request);
size_t need = hostcall(encoded.data(), encoded.size(), 0, 0);
if(need == 0)
return(DValue());
String buffer(need, 0);
size_t got = hostcall(encoded.data(), encoded.size(), &buffer[0], need);
if(got == 0 || got > need)
return(DValue());
DValue response;
String error;
ucb_decode(String(buffer.data(), got), response, &error);
return(response);
}
static DValue wasm_zip_call(DValue request)
{
DValue response = wasm_sized_hostcall(request, uce_host_zip);
return(response);
}
DValue zip_list(String zip_file_name)
{
DValue request;
request["op"] = "list";
request["path"] = zip_file_name;
DValue response = wasm_zip_call(request);
DValue* result = response.key("result");
return(result ? *result : DValue());
}
String zip_read(String zip_file_name, String entry_name)
{
DValue request;
request["op"] = "read";
request["path"] = zip_file_name;
request["entry"] = entry_name;
DValue response = wasm_zip_call(request);
return(response["result"].to_string());
}
bool zip_create(String zip_file_name, DValue entries)
{
DValue request;
request["op"] = "create";
request["path"] = zip_file_name;
request["entries"] = entries;
DValue response = wasm_zip_call(request);
return(response["ok"].to_bool());
}
bool zip_extract(String zip_file_name, String destination_directory)
{
DValue request;
request["op"] = "extract";
request["path"] = zip_file_name;
request["destination"] = destination_directory;
DValue response = wasm_zip_call(request);
return(response["ok"].to_bool());
}
String gz_compress(String src)
{
DValue request;
request["op"] = "gz_compress";
request["src"] = src;
DValue response = wasm_zip_call(request);
return(response["result"].to_string());
}
String gz_uncompress(String compressed)
{
DValue request;
request["op"] = "gz_uncompress";
request["src"] = compressed;
DValue response = wasm_zip_call(request);
return(response["result"].to_string());
}
static DValue wasm_units_call(DValue request)
{
return(wasm_sized_hostcall(request, uce_host_units));
}
DValue unit_info(String path)
{
DValue request;
request["op"] = "info";
request["path"] = path;
DValue response = wasm_units_call(request);
DValue* result = response.key("result");
return(result ? *result : DValue());
}
StringList units_list()
{
DValue request;
request["op"] = "list";
DValue response = wasm_units_call(request);
StringList result;
DValue* items = response.key("result");
if(items)
items->each([&](const DValue& value, String) { result.push_back(value.to_string()); });
return(result);
}
bool unit_compile(String path)
{
DValue request;
request["op"] = "compile";
request["path"] = path;
DValue response = wasm_units_call(request);
return(response["ok"].to_bool());
}
static DValue wasm_unit_call_result;
DValue* unit_call(String file_name, String function_name, DValue* call_param)
{
DValue request;
request["op"] = "call";
request["file"] = file_name;
request["function"] = function_name;
if(call_param)
request["param"] = *call_param;
DValue response = wasm_units_call(request);
String output = response["output"].to_string();
if(output != "")
print(output);
DValue* result = response.key("result");
if(result)
{
wasm_unit_call_result = *result;
return(&wasm_unit_call_result);
}
return(0);
}
SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String current_path, bool opt_so_optional)
{
(void)context; (void)file_name; (void)current_path; (void)opt_so_optional;
return(0);
}
static DValue wasm_mysql_call(DValue request)
{
return(wasm_sized_hostcall(request, uce_host_mysql));
}
bool MySQL::connect(String host, String username, String password)
{
(void)host; (void)username; (void)password;
connection = 0;
statement_info = WASM_DB_UNAVAILABLE;
DValue request;
request["op"] = "connect";
request["host"] = host;
request["username"] = username;
request["password"] = password;
DValue response = wasm_mysql_call(request);
u64 handle = response["handle"].to_u64();
connection = (void*)(uintptr_t)handle;
_preload_next_error_code = (u32)response["error_code"].to_u64();
statement_info = response["statement_info"].to_string();
return(handle != 0 && _preload_next_error_code == 0);
}
void MySQL::disconnect()
{
if(connection)
{
DValue request;
request["op"] = "disconnect";
request["handle"] = (f64)(uintptr_t)connection;
wasm_mysql_call(request);
connection = 0;
}
}
String MySQL::error()
{
String result = statement_info;
statement_info = "";
_preload_next_error_code = 0;
return(result);
}
String MySQL::escape(String raw, char quote_char) { return(mysql_escape(raw, quote_char)); }
String mysql_escape(String raw, char quote_char)
{
DValue request;
request["op"] = "escape";
request["raw"] = raw;
request["quote_char"] = String(quote_char > 0 ? 1 : 0, quote_char);
DValue response = wasm_mysql_call(request);
DValue* result = response.key("result");
return(result ? result->to_string() : raw);
}
String MySQL::parse_query_parameters(String query, StringMap map)
{
String result;
query.append(1, ' ');
u8 mode = 0;
char quote = 0;
String identifier;
for(u32 i = 0; i < query.length(); i++)
{
char c = query[i];
if(mode == 0)
{
if(c == ':')
{
mode = 1;
identifier = "";
}
else if(c == '"' || c == '\'')
{
result.append(1, c);
mode = 2;
quote = c;
}
else
result.append(1, c);
}
else if(mode == 1)
{
if(isalnum(c))
identifier.append(1, c);
else
{
result.append(escape(map[identifier]));
result.append(1, c);
mode = 0;
}
}
else if(mode == 2)
{
if(c == quote)
mode = 0;
result.append(1, c);
}
}
return(result);
}
static bool wasm_mysql_has_unquoted_positional_placeholder(String query)
{
bool quoted = false;
char quote = 0;
bool escaped = false;
for(u32 i = 0; i < query.length(); i++)
{
char c = query[i];
if(quoted)
{
if(escaped)
{
escaped = false;
continue;
}
if(c == '\\')
{
escaped = true;
continue;
}
if(c == quote)
quoted = false;
continue;
}
if(c == '\'' || c == '"')
{
quoted = true;
quote = c;
continue;
}
if(c == '?')
return(true);
}
return(false);
}
void MySQL::disconnect() { connection = 0; }
String MySQL::error() { return(WASM_DB_UNAVAILABLE); }
String MySQL::escape(String raw, char quote_char) { (void)quote_char; return(raw); }
String MySQL::parse_query_parameters(String query, StringMap m) { (void)m; return(query); }
DValue MySQL::query(String q) { (void)q; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
DValue MySQL::query(String q, StringMap params) { (void)q; (void)params; statement_info = WASM_DB_UNAVAILABLE; return(DValue()); }
DValue MySQL::get_pending_result() { return(DValue()); }
DValue MySQL::query(String q)
{
if(wasm_mysql_has_unquoted_positional_placeholder(q))
{
_preload_next_error_code = 2000;
statement_info = "mysql positional ? placeholders are not supported; use named :name placeholders";
return(DValue());
}
DValue request;
request["op"] = "query";
request["handle"] = (f64)(uintptr_t)connection;
request["query"] = q;
DValue response = wasm_mysql_call(request);
insert_id = response["insert_id"].to_u64();
affected_rows = (u32)response["affected"].to_u64();
_preload_next_error_code = (u32)response["error_code"].to_u64();
statement_info = response["statement_info"].to_string();
DValue* result = response.key("result");
return(result ? *result : DValue());
}
String mysql_escape(String raw, char quote_char) { (void)quote_char; return(raw); }
DValue MySQL::query(String q, StringMap params) { return(query(parse_query_parameters(q, params))); }
DValue MySQL::get_pending_result() { return(DValue()); }
// sqlite runs host-side (the host links libsqlite and owns the connections in
// a per-workspace handle table). One UCEB1-marshalled hostcall carries
@@ -129,6 +420,7 @@ DValue sqlite_query(SQLite* db, String q, const StringMap& params) { return(db ?
u64 sqlite_insert_id(SQLite* db) { return(db ? db->insert_id : 0); }
u32 sqlite_affected_rows(SQLite* db) { return(db ? db->affected_rows : 0); }
void cleanup_sqlite_connections() { }
void cleanup_mysql_connections() { }
static ServerState wasm_server;
static Request wasm_request;
+10
View File
@@ -7,6 +7,16 @@ uce_host_task_spawn
uce_host_task_pid
uce_host_task_kill
uce_host_sleep_us
uce_host_socket_connect
uce_host_socket_close
uce_host_socket_write
uce_host_socket_read
uce_host_server_start_http
uce_host_server_stop
uce_host_memcache_command
uce_host_mysql
uce_host_zip
uce_host_units
uce_host_component_resolve
uce_host_file_exists
uce_host_file_read
+322
View File
@@ -35,6 +35,9 @@
#include <memory>
#include <optional>
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/time.h>
@@ -345,11 +348,15 @@ public:
// Host-owned resource handle table (§3.1): connections opened by the guest
// live here and are closed when the workspace drops at request end.
std::vector<SQLite*> sqlite_handles;
std::vector<MySQL*> mysql_handles;
~WasmWorkspace()
{
for(auto* db : sqlite_handles)
if(db)
delete db; // ~SQLite disconnects
for(auto* db : mysql_handles)
if(db)
delete db; // ~MySQL disconnects
}
#endif
@@ -359,6 +366,10 @@ public:
// 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)
@@ -1193,6 +1204,120 @@ private:
results[0] = Val(ok ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_zip")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
String out;
if(!self->hostcall_staged(encoded, out))
{
DValue request, response;
String decode_error;
try
{
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "list")
{
String path = self->resolve_guest_file(request["path"].to_string());
if(path == "") throw std::runtime_error("zip_list: path is outside wasm file policy");
response["result"] = zip_list(path);
}
else if(op == "read")
{
String path = self->resolve_guest_file(request["path"].to_string());
if(path == "") throw std::runtime_error("zip_read: path is outside wasm file policy");
response["result"] = zip_read(path, request["entry"].to_string());
}
else if(op == "create")
{
String path = self->resolve_guest_write(request["path"].to_string(), "");
if(path == "") throw std::runtime_error("zip_create: path is outside wasm file policy");
DValue* entries = request.key("entries");
response["ok"].set_bool(zip_create(path, entries ? *entries : DValue()));
}
else if(op == "extract")
{
String path = self->resolve_guest_file(request["path"].to_string());
String destination = self->resolve_guest_write(request["destination"].to_string(), "");
if(path == "" || destination == "") throw std::runtime_error("zip_extract: path is outside wasm file policy");
response["ok"].set_bool(zip_extract(path, destination));
}
else if(op == "gz_compress")
response["result"] = gz_compress(request["src"].to_string());
else if(op == "gz_uncompress")
response["result"] = gz_uncompress(request["src"].to_string());
}
}
catch(const std::exception& e)
{
response["error"] = e.what();
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_units")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
String out;
if(!self->hostcall_staged(encoded, out))
{
DValue request, response;
String decode_error;
try
{
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "info")
response["result"] = unit_info(request["path"].to_string());
else if(op == "list")
{
StringList paths = units_list();
for(auto& path : paths)
{
DValue item;
item = path;
response["result"].push(item);
}
}
else if(op == "compile")
response["ok"].set_bool(unit_compile(request["path"].to_string()));
else if(op == "call")
{
DValue* param = request.key("param");
ob_start();
DValue* result = unit_call(request["file"].to_string(), request["function"].to_string(), param);
response["output"] = ob_get_close();
if(result)
response["result"] = *result;
}
}
}
catch(const std::exception& e)
{
response["error"] = e.what();
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
#ifdef UCE_WASM_HOST_CONNECTORS
if(mod == "env" && name == "uce_host_sqlite")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
@@ -1260,6 +1385,99 @@ private:
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_memcache_command")
return(add([self](Caller caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String command;
self->hostcall_read(args[1].i32(), args[2].i32(), command);
String key = "memcache:" + std::to_string((u64)args[0].i64()) + ":" + command;
u32 cap = (u32)args[4].i32();
int32_t buf = args[3].i32();
String out;
if(buf != 0 && self->staged_memcache_key == key)
{
out = self->staged_memcache_result;
self->staged_memcache_key = "";
self->staged_memcache_result = "";
}
else
{
::socket_write((u64)args[0].i64(), command + "\r\n");
out = ::socket_read((u64)args[0].i64());
if(buf == 0)
{
self->staged_memcache_key = key;
self->staged_memcache_result = out;
}
}
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_mysql")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
String out;
if(!self->hostcall_staged(encoded, out))
{
DValue request, response;
String decode_error;
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "connect")
{
MySQL* db = new MySQL();
bool ok = db->connect(request["host"].to_string(), request["username"].to_string(), request["password"].to_string());
u64 handle = 0;
if(ok && db->connection)
{
self->mysql_handles.push_back(db);
handle = self->mysql_handles.size();
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->_preload_next_error_code;
response["statement_info"] = db->error();
if(handle == 0)
delete db;
}
else if(op == "escape")
{
String quote = request["quote_char"].to_string();
response["result"] = mysql_escape(request["raw"].to_string(), quote.size() ? quote[0] : 0);
}
else
{
u64 handle = request["handle"].to_u64();
MySQL* db = (handle >= 1 && handle <= self->mysql_handles.size())
? self->mysql_handles[(size_t)handle - 1] : 0;
if(op == "query" && db)
{
response["result"] = db->query(request["query"].to_string());
response["insert_id"] = (f64)db->insert_id;
response["affected"] = (f64)db->affected_rows;
response["error_code"] = (f64)db->_preload_next_error_code;
response["statement_info"] = db->error();
}
else if(op == "disconnect" && db)
{
delete db;
self->mysql_handles[(size_t)handle - 1] = 0;
}
}
}
out = ucb_encode(response);
self->hostcall_stage(encoded, out);
}
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
#endif
if(mod == "env" && name == "uce_host_file_unlink")
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
@@ -1271,6 +1489,110 @@ private:
::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;