1036 lines
32 KiB
C++
1036 lines
32 KiB
C++
// Production WASM W1 core entrypoint.
|
|
//
|
|
// This file deliberately includes the production UCE runtime amalgamation with
|
|
// __UCE_WASM_CORE__ enabled. Native-only pieces are carved out in the runtime
|
|
// sources, while the workspace-owned DValue ABI and output plumbing are built
|
|
// into core.wasm.
|
|
|
|
#define __UCE_WASM_CORE__ 1
|
|
#include "abi.h"
|
|
#include "../lib/uce_lib.cpp"
|
|
|
|
#include "../lib/mysql-connector.h"
|
|
#include "../lib/sqlite-connector.h"
|
|
|
|
// ---- W3 connector membranes -----------------------------------------------
|
|
// sqlite/mysql run host-side (the host links the native connectors and owns the
|
|
// connections in per-workspace handle tables). UCEB2-marshalled hostcalls carry
|
|
// operation requests/responses; `connection` holds the host handle (>0).
|
|
|
|
static const char* WASM_DB_UNAVAILABLE =
|
|
"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;
|
|
|
|
static DValue wasm_mysql_call(DValue request)
|
|
{
|
|
return(wasm_sized_hostcall(request, uce_host_mysql));
|
|
}
|
|
|
|
bool MySQL::connect(String host, String username, String password, String database)
|
|
{
|
|
request_host = host;
|
|
request_username = username;
|
|
request_password = password;
|
|
request_database = database;
|
|
DValue request;
|
|
request["op"] = "connect";
|
|
request["host"] = host;
|
|
request["username"] = username;
|
|
request["password"] = password;
|
|
request["database"] = database;
|
|
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) || 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);
|
|
}
|
|
|
|
DValue MySQL::query(String q)
|
|
{
|
|
affected_rows = 0;
|
|
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());
|
|
}
|
|
if(!connection)
|
|
{
|
|
if(_preload_next_error_code == 0)
|
|
_preload_next_error_code = 2000;
|
|
if(statement_info == "")
|
|
statement_info = "mysql connection is not open";
|
|
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());
|
|
}
|
|
|
|
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 UCEB2-marshalled hostcall carries
|
|
// {op,handle,path,query,params} in and {handle,result,insert_id,affected,
|
|
// error_code,statement_info} out. `connection` holds the host handle (>0).
|
|
extern "C" size_t uce_host_sqlite(const char* in, size_t in_len, char* out, size_t cap);
|
|
|
|
static DValue wasm_sqlite_call(DValue request)
|
|
{
|
|
String encoded = ucb_encode(request);
|
|
size_t need = uce_host_sqlite(encoded.data(), encoded.size(), 0, 0);
|
|
if(need == 0)
|
|
return(DValue());
|
|
String buffer(need, 0);
|
|
size_t got = uce_host_sqlite(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);
|
|
}
|
|
|
|
void SQLite::set_error(s32 code, String info) { error_code = code; statement_info = info; }
|
|
|
|
bool SQLite::connect(String path)
|
|
{
|
|
this->path = path;
|
|
DValue request;
|
|
request["op"] = "connect";
|
|
request["path"] = path;
|
|
DValue response = wasm_sqlite_call(request);
|
|
u64 handle = response["handle"].to_u64();
|
|
connection = (void*)(uintptr_t)handle;
|
|
error_code = (s32)response["error_code"].to_s64();
|
|
statement_info = response["statement_info"].to_string();
|
|
return(handle != 0 && error_code == 0);
|
|
}
|
|
|
|
void SQLite::disconnect()
|
|
{
|
|
if(connection)
|
|
{
|
|
DValue request;
|
|
request["op"] = "disconnect";
|
|
request["handle"] = (f64)(uintptr_t)connection;
|
|
wasm_sqlite_call(request);
|
|
connection = 0;
|
|
}
|
|
}
|
|
|
|
String SQLite::error()
|
|
{
|
|
return(statement_info);
|
|
}
|
|
|
|
DValue SQLite::query(String q, const StringMap& params)
|
|
{
|
|
DValue request;
|
|
request["op"] = "query";
|
|
request["handle"] = (f64)(uintptr_t)connection;
|
|
request["query"] = q;
|
|
for(auto& entry : params)
|
|
request["params"][entry.first] = entry.second;
|
|
DValue response = wasm_sqlite_call(request);
|
|
insert_id = response["insert_id"].to_u64();
|
|
affected_rows = (u32)response["affected"].to_u64();
|
|
error_code = (s32)response["error_code"].to_s64();
|
|
statement_info = response["statement_info"].to_string();
|
|
DValue* result = response.key("result");
|
|
return(result ? *result : DValue());
|
|
}
|
|
|
|
DValue SQLite::query(String q) { return(query(q, StringMap())); }
|
|
bool SQLite::apply_default_pragmas() { return(true); }
|
|
bool SQLite::bind_params(void* statement, const StringMap& params) { (void)statement; (void)params; return(true); }
|
|
DValue SQLite::collect_rows(void* statement) { (void)statement; return(DValue()); }
|
|
|
|
SQLite* sqlite_connect(String path)
|
|
{
|
|
SQLite* db = new SQLite();
|
|
db->request_cleanup_delete = true;
|
|
db->connect(path);
|
|
return(db);
|
|
}
|
|
|
|
void sqlite_disconnect(SQLite* db) { if(db) { db->disconnect(); if(db->request_cleanup_delete) delete db; } }
|
|
String sqlite_error(SQLite* db) { return(db ? db->error() : String(WASM_DB_UNAVAILABLE)); }
|
|
DValue sqlite_query(SQLite* db, String q) { return(db ? db->query(q) : DValue()); }
|
|
DValue sqlite_query(SQLite* db, String q, const StringMap& params) { return(db ? db->query(q, params) : DValue()); }
|
|
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;
|
|
static String wasm_output;
|
|
static String wasm_response_meta;
|
|
|
|
// ---- vague-linkage link anchors --------------------------------------------
|
|
// Units import libc++ template instantiations they use; --export-all only
|
|
// exports what the core itself instantiated. Some libc++ internals lack the
|
|
// hide-from-ABI attribute (the Phase 0 lambda finding), so units emit them as
|
|
// imports rather than binding locally. This function exists purely to make
|
|
// the core instantiate — and therefore export — the ones the site tree needs.
|
|
// Extend it when the loader reports "unresolved import env.<libc++ symbol>".
|
|
extern "C" void uce_wasm_link_anchors()
|
|
{
|
|
StringMap string_map;
|
|
string_map["k"] = "v";
|
|
string_map.erase(String("k")); // __tree::__erase_unique<String>
|
|
std::map<String, DValue> dvalue_map;
|
|
dvalue_map["k"] = DValue();
|
|
dvalue_map.erase(String("k"));
|
|
std::vector<String> string_list = { "a", "b" };
|
|
string_list.erase(string_list.begin());
|
|
std::set<String> string_set;
|
|
string_set.insert("k");
|
|
string_set.erase(String("k"));
|
|
|
|
// libc functions units may call that the core itself never references —
|
|
// taking their address forces them into the link (and --export-all)
|
|
static void* volatile libc_anchors[] = {
|
|
(void*)&atof, (void*)&atoi, (void*)&atol, (void*)&atoll,
|
|
(void*)&strtol, (void*)&strtoul, (void*)&strtoll, (void*)&strtoull,
|
|
(void*)&strtod, (void*)&strtof,
|
|
(void*)&qsort, (void*)&bsearch,
|
|
(void*)&snprintf, (void*)&sscanf,
|
|
(void*)&memmove, (void*)&strncmp, (void*)&strncpy,
|
|
// memchr/strchr/strrchr/strstr are C++-overloaded; cast to the C shape
|
|
(void*)(const void* (*)(const void*, int, size_t))&memchr,
|
|
(void*)(const char* (*)(const char*, int))&strchr,
|
|
(void*)(const char* (*)(const char*, int))&strrchr,
|
|
(void*)(const char* (*)(const char*, const char*))&strstr,
|
|
// ctype family (int(int)); units use these directly
|
|
(void*)&isalnum, (void*)&isalpha, (void*)&isblank, (void*)&iscntrl,
|
|
(void*)&isdigit, (void*)&isgraph, (void*)&islower, (void*)&isprint,
|
|
(void*)&ispunct, (void*)&isspace, (void*)&isupper, (void*)&isxdigit,
|
|
(void*)&tolower, (void*)&toupper,
|
|
// math functions used by side modules but not otherwise retained by core
|
|
(void*)(double (*)(double))&cos,
|
|
(void*)(double (*)(double))&sin,
|
|
(void*)(double (*)(double))&round,
|
|
};
|
|
(void)libc_anchors;
|
|
}
|
|
|
|
// W3 membrane: the host resolves component/render targets to funcref-table
|
|
// slots (loading units lazily) and writes the resolved unit path back so
|
|
// nested relative component resolution keeps working.
|
|
extern "C" int32_t uce_host_component_resolve(
|
|
const char* target, size_t target_len,
|
|
const char* handler, size_t handler_len,
|
|
const char* current_unit, size_t current_unit_len,
|
|
char* resolved_buf, size_t resolved_cap,
|
|
int32_t* once_slot_out);
|
|
|
|
// target → table slot, reset per request (workspaces die with the request,
|
|
// but a single workspace can render the same component many times)
|
|
static std::map<String, s32> wasm_component_slots;
|
|
static std::map<String, String> wasm_component_paths;
|
|
|
|
// These mirror small page-runtime pieces that cannot include compiler.cpp in
|
|
// the wasm core (compiler.cpp owns parser/clang/cache bookkeeping for the host
|
|
// build). Kept byte-identical where possible.
|
|
String component_normalize_path(String name)
|
|
{
|
|
name = trim(name);
|
|
if(name.length() >= 4 && name.substr(name.length() - 4) == ".uce")
|
|
return(name);
|
|
return(name + ".uce");
|
|
}
|
|
|
|
void component_parse_target(String target, String& file_name, String& render_name)
|
|
{
|
|
target = trim(target);
|
|
render_name = "";
|
|
auto render_split_pos = target.find(":");
|
|
if(render_split_pos != String::npos)
|
|
{
|
|
render_name = trim(target.substr(render_split_pos + 1));
|
|
target = trim(target.substr(0, render_split_pos));
|
|
}
|
|
file_name = target;
|
|
}
|
|
|
|
String component_error_banner(String message)
|
|
{
|
|
return("<div class=\"banner\">" + html_escape(message) + "</div>");
|
|
}
|
|
|
|
struct RequestPropsScope
|
|
{
|
|
Request* context = 0;
|
|
DValue previous_props;
|
|
|
|
static void swap_value(DValue& left, DValue& right)
|
|
{
|
|
std::swap(left.type, right.type);
|
|
left._String.swap(right._String);
|
|
std::swap(left._float, right._float);
|
|
std::swap(left._array_index, right._array_index);
|
|
std::swap(left._bool, right._bool);
|
|
std::swap(left._list_mode, right._list_mode);
|
|
std::swap(left._ptr, right._ptr);
|
|
left._map.swap(right._map);
|
|
}
|
|
|
|
RequestPropsScope(Request* context, DValue& props)
|
|
{
|
|
this->context = context;
|
|
if(this->context)
|
|
{
|
|
swap_value(previous_props, this->context->props);
|
|
swap_value(this->context->props, props);
|
|
}
|
|
}
|
|
|
|
~RequestPropsScope()
|
|
{
|
|
if(context)
|
|
swap_value(context->props, previous_props);
|
|
}
|
|
};
|
|
|
|
// A unit is a bag of exported handlers; invoking any of them is one operation —
|
|
// the host resolves __uce_<handler> in the loaded module to a funcref slot. The
|
|
// handler is just a string: "render", "component:CARD", "render:VARIANT",
|
|
// "once", "cli", "websocket", "serve_http:named" — or "exists" (an existence
|
|
// probe that loads nothing). No per-mode kinds.
|
|
static s32 wasm_resolve_target(String unit_target, String handler, String* resolved_out = 0)
|
|
{
|
|
String current = context ? context->resources.current_unit_file : "";
|
|
String cache_key = current + "\t" + handler + "\t" + unit_target;
|
|
bool is_exists = (handler == "exists");
|
|
auto cached = wasm_component_slots.find(cache_key);
|
|
if(cached != wasm_component_slots.end() && !is_exists)
|
|
{
|
|
if(resolved_out)
|
|
*resolved_out = wasm_component_paths[cache_key];
|
|
return(cached->second);
|
|
}
|
|
char resolved[512];
|
|
s32 once_slot = 0;
|
|
s32 slot = uce_host_component_resolve(
|
|
unit_target.data(), unit_target.size(), handler.data(), handler.size(),
|
|
current.data(), current.size(),
|
|
resolved, sizeof(resolved), &once_slot);
|
|
String resolved_path = slot ? String(resolved, strnlen(resolved, sizeof(resolved))) : String("");
|
|
if(resolved_out && slot)
|
|
*resolved_out = resolved_path;
|
|
if(!is_exists)
|
|
{
|
|
wasm_component_slots[cache_key] = slot;
|
|
wasm_component_paths[cache_key] = resolved_path;
|
|
bool runs_once = handler == "render" || handler.rfind("render:", 0) == 0 ||
|
|
handler == "component" || handler.rfind("component:", 0) == 0;
|
|
if(slot && runs_once)
|
|
{
|
|
String once_key = current + "\t" + "once" + "\t" + resolved_path;
|
|
wasm_component_slots[once_key] = once_slot;
|
|
wasm_component_paths[once_key] = once_slot ? resolved_path : String("");
|
|
}
|
|
}
|
|
return(slot);
|
|
}
|
|
|
|
String component_resolve(String name)
|
|
{
|
|
String file_name, render_name;
|
|
component_parse_target(trim(name), file_name, render_name);
|
|
String resolved;
|
|
if(wasm_resolve_target(file_name, "exists", &resolved))
|
|
return(resolved);
|
|
return("");
|
|
}
|
|
|
|
bool component_exists(String name)
|
|
{
|
|
return(component_resolve(name) != "");
|
|
}
|
|
|
|
// Run a unit's ONCE() handler at most once per request (native
|
|
// compiler_run_unit_once_if_needed semantics): dedup on the resolved unit
|
|
// path via request.once_units. The handler emits head assets, etc.
|
|
static void wasm_run_once(const String& resolved, Request& request)
|
|
{
|
|
if(resolved == "")
|
|
return;
|
|
if(request.once_units.find(resolved) != request.once_units.end())
|
|
return;
|
|
request.once_units.insert(resolved);
|
|
s32 once_slot = wasm_resolve_target(resolved, "once");
|
|
if(once_slot == 0)
|
|
return;
|
|
String previous_unit = request.resources.current_unit_file;
|
|
request.resources.current_unit_file = resolved;
|
|
WasmRequestHandler once_handler = (WasmRequestHandler)(uintptr_t)once_slot;
|
|
once_handler(request);
|
|
request.resources.current_unit_file = previous_unit;
|
|
}
|
|
|
|
DValue* unit_call(String file_name, String function_name, DValue* call_param)
|
|
{
|
|
String macro = to_upper(trim(function_name));
|
|
String handler = "";
|
|
if(macro == "RENDER" || macro.rfind("RENDER:", 0) == 0)
|
|
handler = "render" + (macro.length() > 7 ? ":" + trim(function_name.substr(function_name.find(":") + 1)) : String(""));
|
|
else if(macro == "COMPONENT" || macro.rfind("COMPONENT:", 0) == 0)
|
|
handler = "component" + (macro.length() > 10 ? ":" + trim(function_name.substr(function_name.find(":") + 1)) : String(""));
|
|
else if(macro == "ONCE")
|
|
handler = "once";
|
|
else if(macro == "INIT")
|
|
handler = "init";
|
|
|
|
if(handler != "")
|
|
{
|
|
String resolved;
|
|
s32 slot = wasm_resolve_target(file_name, handler, &resolved);
|
|
if(!slot)
|
|
{
|
|
print("Error: unit_call() function '", function_name, "' not found");
|
|
return(0);
|
|
}
|
|
DValue props = call_param ? *call_param : DValue();
|
|
RequestPropsScope props_scope(context, props);
|
|
if((handler == "render" || handler.rfind("render:", 0) == 0 || handler == "component" || handler.rfind("component:", 0) == 0) && resolved != "")
|
|
wasm_run_once(resolved, *context);
|
|
String previous_unit = context->resources.current_unit_file;
|
|
if(resolved != "")
|
|
context->resources.current_unit_file = resolved;
|
|
WasmRequestHandler handler_fn = (WasmRequestHandler)(uintptr_t)slot;
|
|
handler_fn(*context);
|
|
context->resources.current_unit_file = previous_unit;
|
|
return(0);
|
|
}
|
|
|
|
String resolved;
|
|
s32 slot = wasm_resolve_target(file_name, "export:" + function_name, &resolved);
|
|
if(!slot)
|
|
{
|
|
print("Error: unit_call() function '", function_name, "' not found");
|
|
return(0);
|
|
}
|
|
|
|
String previous_unit = context->resources.current_unit_file;
|
|
if(resolved != "")
|
|
context->resources.current_unit_file = resolved;
|
|
WasmDValueCallHandler handler_fn = (WasmDValueCallHandler)(uintptr_t)slot;
|
|
DValue* result = handler_fn(call_param);
|
|
context->resources.current_unit_file = previous_unit;
|
|
if(result)
|
|
{
|
|
wasm_unit_call_result = *result;
|
|
return(&wasm_unit_call_result);
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
static void component_render_with_props(String name, DValue& props, Request& request)
|
|
{
|
|
String file_name, render_name;
|
|
component_parse_target(trim(name), file_name, render_name);
|
|
String handler = render_name == "" ? String("component") : "component:" + render_name;
|
|
String resolved;
|
|
s32 slot = wasm_resolve_target(file_name, handler, &resolved);
|
|
if(!slot)
|
|
{
|
|
request.set_status(500, "Internal Server Error");
|
|
print(component_error_banner("component not found: " + trim(name)));
|
|
return;
|
|
}
|
|
wasm_run_once(resolved, request);
|
|
RequestPropsScope props_scope(&request, props);
|
|
String previous_unit = request.resources.current_unit_file;
|
|
if(resolved != "")
|
|
request.resources.current_unit_file = resolved;
|
|
// a wasm function pointer is its index in the shared funcref table; the
|
|
// host returned the handler's slot, so this is a plain call_indirect
|
|
WasmRequestHandler handler_fn = (WasmRequestHandler)(uintptr_t)slot;
|
|
handler_fn(request);
|
|
request.resources.current_unit_file = previous_unit;
|
|
}
|
|
|
|
void component_render(String name, DValue props, Request& request) { component_render_with_props(name, props, request); }
|
|
void component_render(String name) { DValue props; component_render_with_props(name, props, *context); }
|
|
void component_render(String name, Request& request) { DValue props; component_render_with_props(name, props, request); }
|
|
void component_render(String name, DValue props) { component_render_with_props(name, props, *context); }
|
|
|
|
String component(String name, DValue props, Request& request)
|
|
{
|
|
ob_start();
|
|
component_render_with_props(name, props, request);
|
|
return(ob_get_close());
|
|
}
|
|
|
|
String component(String name) { DValue props; return(component(name, props, *context)); }
|
|
String component(String name, Request& request) { DValue props; return(component(name, props, request)); }
|
|
String component(String name, DValue props) { return(component(name, props, *context)); }
|
|
|
|
void unit_render(String file_name, Request& request)
|
|
{
|
|
String unit_name, render_name;
|
|
component_parse_target(trim(file_name), unit_name, render_name);
|
|
String handler = render_name == "" ? String("render") : "render:" + render_name;
|
|
String resolved;
|
|
s32 slot = wasm_resolve_target(unit_name, handler, &resolved);
|
|
if(!slot)
|
|
{
|
|
request.set_status(500, "Internal Server Error");
|
|
print(component_error_banner("unit not found: " + trim(file_name)));
|
|
return;
|
|
}
|
|
wasm_run_once(resolved, request);
|
|
String previous_unit = request.resources.current_unit_file;
|
|
if(resolved != "")
|
|
request.resources.current_unit_file = resolved;
|
|
WasmRequestHandler handler_fn = (WasmRequestHandler)(uintptr_t)slot;
|
|
handler_fn(request);
|
|
request.resources.current_unit_file = previous_unit;
|
|
}
|
|
|
|
void unit_render(String file_name) { unit_render(file_name, *context); }
|
|
|
|
struct WasmRequestEnvelopeSegment
|
|
{
|
|
const char* data = 0;
|
|
size_t size = 0;
|
|
};
|
|
|
|
static bool wasm_decode_request_envelope(const char* encoded, size_t encoded_size,
|
|
WasmRequestEnvelopeSegment (&segments)[12], String& error)
|
|
{
|
|
if(encoded_size < 6 || memcmp(encoded, "UCER", 4) != 0)
|
|
{
|
|
error = "missing UCE request-envelope header";
|
|
return(false);
|
|
}
|
|
if((u8)encoded[4] != 1 || (u8)encoded[5] != 12)
|
|
{
|
|
error = "unsupported UCE request-envelope version or segment count";
|
|
return(false);
|
|
}
|
|
size_t offset = 6;
|
|
for(u32 i = 0; i < 12; i++)
|
|
{
|
|
u64 segment_size = 0;
|
|
if(!ucb_read_varint(encoded, encoded_size, offset, segment_size) || segment_size > encoded_size - offset)
|
|
{
|
|
error = "invalid UCE request-envelope segment " + std::to_string(i);
|
|
return(false);
|
|
}
|
|
segments[i].data = encoded + offset;
|
|
segments[i].size = (size_t)segment_size;
|
|
offset += (size_t)segment_size;
|
|
}
|
|
if(offset != encoded_size)
|
|
{
|
|
error = "trailing bytes after UCE request envelope";
|
|
return(false);
|
|
}
|
|
return(true);
|
|
}
|
|
|
|
extern "C" {
|
|
|
|
// The host has already loaded the request's entry unit and can place its
|
|
// exports directly in the shared table. This avoids resolving that same unit
|
|
// back through a hostcall while preserving per-request ONCE deduplication.
|
|
void uce_wasm_invoke_loaded_entry(s32 handler_slot, s32 once_slot)
|
|
{
|
|
String resolved = context->resources.current_unit_file;
|
|
if(once_slot && resolved != "" && context->once_units.find(resolved) == context->once_units.end())
|
|
{
|
|
context->once_units.insert(resolved);
|
|
WasmRequestHandler once_handler = (WasmRequestHandler)(uintptr_t)once_slot;
|
|
once_handler(*context);
|
|
}
|
|
WasmRequestHandler handler_fn = (WasmRequestHandler)(uintptr_t)handler_slot;
|
|
handler_fn(*context);
|
|
}
|
|
|
|
void* uce_alloc(size_t len)
|
|
{
|
|
return(malloc(len));
|
|
}
|
|
|
|
void uce_free(void* ptr)
|
|
{
|
|
free(ptr);
|
|
}
|
|
|
|
u32 uce_wasm_core_abi_version()
|
|
{
|
|
return(UCE_WASM_CORE_ABI_VERSION);
|
|
}
|
|
|
|
int uce_wasm_core_init()
|
|
{
|
|
wasm_server.config = default_config();
|
|
wasm_request.server = &wasm_server;
|
|
// the primary output stream must live ON ob_stack (native semantics):
|
|
// ob_get_close()/ob_close() pop and rebalance against the stack, so a
|
|
// stream outside it would be orphaned by the first component() capture
|
|
if(wasm_request.ob_stack.empty())
|
|
wasm_request.ob_start();
|
|
context = &wasm_request;
|
|
return(0);
|
|
}
|
|
|
|
void uce_wasm_core_reset_request()
|
|
{
|
|
if(context == 0)
|
|
uce_wasm_core_init();
|
|
wasm_request.call = DValue();
|
|
wasm_request.props = DValue();
|
|
wasm_request.params.clear();
|
|
wasm_request.get.clear();
|
|
wasm_request.post.clear();
|
|
wasm_request.header.clear();
|
|
wasm_request.set_cookies.clear();
|
|
wasm_request.response_code = "HTTP/1.1 200 OK";
|
|
wasm_request.flags = Request::Flags();
|
|
wasm_request.stats = Request::Stats();
|
|
for(auto* stream : wasm_request.ob_stack)
|
|
delete stream;
|
|
wasm_request.ob_stack.clear();
|
|
wasm_request.ob_start();
|
|
wasm_output = "";
|
|
wasm_response_meta = "";
|
|
wasm_request.cookies.clear();
|
|
wasm_request.session.clear();
|
|
wasm_request.session_id = "";
|
|
wasm_request.session_name = "";
|
|
wasm_request.session_loaded_hash = "";
|
|
wasm_request.out = "";
|
|
wasm_request.resources.current_unit_file = "";
|
|
wasm_component_slots.clear();
|
|
wasm_component_paths.clear();
|
|
}
|
|
|
|
// Host pushes the worker-cached immutable configuration followed by the
|
|
// dynamic UCEB2 request context into one guest buffer.
|
|
int uce_wasm_apply_context(const char* config_buf, size_t config_len, const char* context_buf, size_t context_len)
|
|
{
|
|
if(context == 0)
|
|
uce_wasm_core_init();
|
|
StringMap decoded_config;
|
|
StringMap decoded_params;
|
|
StringMap decoded_get;
|
|
StringMap decoded_post;
|
|
StringMap decoded_cookies;
|
|
StringMap decoded_session;
|
|
DValue decoded_call;
|
|
DValue decoded_ws;
|
|
WasmRequestEnvelopeSegment segments[12];
|
|
String error;
|
|
if(!ucb_decode_flat_string_map(config_buf, config_len, decoded_config, &error))
|
|
{
|
|
uce_host_log(3, error.data(), error.size());
|
|
return(1);
|
|
}
|
|
if(!wasm_decode_request_envelope(context_buf, context_len, segments, error))
|
|
{
|
|
uce_host_log(3, error.data(), error.size());
|
|
return(2);
|
|
}
|
|
auto decode_tree = [&](u32 index, DValue& target, const char* name) {
|
|
if(ucb_decode(String(segments[index].data, segments[index].size), target, &error))
|
|
return(true);
|
|
error = String(name) + ": " + error;
|
|
return(false);
|
|
};
|
|
auto decode_map = [&](u32 index, StringMap& target, const char* name) {
|
|
if(ucb_decode_flat_string_map(segments[index].data, segments[index].size, target, &error))
|
|
return(true);
|
|
error = String(name) + ": " + error;
|
|
return(false);
|
|
};
|
|
if(!decode_tree(0, decoded_call, "request call") ||
|
|
!decode_map(1, decoded_params, "request params") ||
|
|
!decode_map(2, decoded_get, "request get") ||
|
|
!decode_map(3, decoded_post, "request post") ||
|
|
!decode_map(4, decoded_cookies, "request cookies") ||
|
|
!decode_map(5, decoded_session, "request session"))
|
|
{
|
|
uce_host_log(3, error.data(), error.size());
|
|
return(3);
|
|
}
|
|
if(segments[11].size && !decode_tree(11, decoded_ws, "request websocket"))
|
|
{
|
|
uce_host_log(3, error.data(), error.size());
|
|
return(4);
|
|
}
|
|
wasm_server.config = std::move(decoded_config);
|
|
wasm_request.call = std::move(decoded_call);
|
|
wasm_request.params = std::move(decoded_params);
|
|
wasm_request.get = std::move(decoded_get);
|
|
wasm_request.post = std::move(decoded_post);
|
|
wasm_request.cookies = std::move(decoded_cookies);
|
|
wasm_request.session = std::move(decoded_session);
|
|
wasm_request.response_code = wasm_request.params["GATEWAY_INTERFACE"] != "" ?
|
|
"Status: 200 OK" : "HTTP/1.1 200 OK";
|
|
wasm_request.session_id.assign(segments[6].data, segments[6].size);
|
|
wasm_request.session_name.assign(segments[7].data, segments[7].size);
|
|
wasm_request.session_loaded_hash.assign(segments[8].data, segments[8].size);
|
|
wasm_request.resources.current_unit_file.assign(segments[9].data, segments[9].size);
|
|
wasm_request.in.assign(segments[10].data, segments[10].size);
|
|
// websocket event context: ws_send()/ws_close() capture into the dispatch
|
|
// list (the workspace owns no connections), which collect() carries back to
|
|
// the broker. Reset per invocation.
|
|
wasm_request.connection = DValue();
|
|
wasm_request.resources.websocket_connection_state_before = DValue();
|
|
wasm_request.resources.websocket_dispatch_commands = DValue();
|
|
wasm_request.resources.websocket_dispatch_capture = false;
|
|
if(segments[11].size)
|
|
{
|
|
wasm_request.resources.websocket_connection_id = decoded_ws["connection_id"].to_string();
|
|
wasm_request.resources.websocket_scope = decoded_ws["scope"].to_string();
|
|
wasm_request.resources.websocket_opcode = (u8)decoded_ws["opcode"].to_u64();
|
|
wasm_request.resources.websocket_is_binary = decoded_ws["binary"].to_bool();
|
|
wasm_request.resources.websocket_scope_connection_ids.clear();
|
|
if(DValue* conns = decoded_ws.key("connections"))
|
|
conns->each([&](const DValue& v, String) {
|
|
wasm_request.resources.websocket_scope_connection_ids.push_back(v.to_string());
|
|
});
|
|
wasm_request.resources.websocket_dispatch_capture = true;
|
|
if(DValue* cstate = decoded_ws.key("connection_state"))
|
|
wasm_request.connection = *cstate;
|
|
wasm_request.resources.websocket_connection_state_before = wasm_request.connection;
|
|
}
|
|
return(0);
|
|
}
|
|
|
|
Request* uce_wasm_request()
|
|
{
|
|
if(context == 0)
|
|
uce_wasm_core_init();
|
|
return(&wasm_request);
|
|
}
|
|
|
|
// After render: response metadata (status line, headers, cookies, session)
|
|
// goes back to the host as UCEB2.
|
|
void uce_wasm_finish_response_meta()
|
|
{
|
|
DValue meta;
|
|
meta["status"] = wasm_request.response_code;
|
|
for(auto& header : wasm_request.header)
|
|
meta["headers"][header.first] = header.second;
|
|
for(auto& cookie : wasm_request.set_cookies)
|
|
{
|
|
DValue cookie_value;
|
|
cookie_value = cookie;
|
|
meta["cookies"].push(cookie_value);
|
|
}
|
|
for(auto& entry : wasm_request.session)
|
|
meta["session"][entry.first] = entry.second;
|
|
meta["session_id"] = wasm_request.session_id;
|
|
meta["session_name"] = wasm_request.session_name;
|
|
meta["session_loaded_hash"] = wasm_request.session_loaded_hash;
|
|
bool ws_has_commands = !wasm_request.resources.websocket_dispatch_commands._map.empty();
|
|
bool ws_state_changed = false;
|
|
if(wasm_request.resources.websocket_dispatch_capture)
|
|
{
|
|
String prior_state = ucb_encode(wasm_request.resources.websocket_connection_state_before);
|
|
String current_state = ucb_encode(wasm_request.connection);
|
|
ws_state_changed = (prior_state != current_state);
|
|
}
|
|
// Any unit code (not just WS handlers) may call ws_send/ws_close; whenever the
|
|
// dispatch list is non-empty, carry it back so the worker can flush it to the
|
|
// broker. If only connection state changed, flush a command-less state-only
|
|
// batch so the broker can persist the connection mutation.
|
|
if(ws_has_commands)
|
|
meta["ws_commands"] = wasm_request.resources.websocket_dispatch_commands;
|
|
if(ws_state_changed)
|
|
meta["ws_connection_state"] = wasm_request.connection;
|
|
wasm_response_meta = ucb_encode(meta);
|
|
}
|
|
|
|
const char* uce_wasm_response_meta_data()
|
|
{
|
|
return(wasm_response_meta.data());
|
|
}
|
|
|
|
size_t uce_wasm_response_meta_size()
|
|
{
|
|
return(wasm_response_meta.size());
|
|
}
|
|
|
|
void uce_print_bytes(const char* data, size_t len)
|
|
{
|
|
if(context == 0)
|
|
uce_wasm_core_init();
|
|
if(context->ob && data && len)
|
|
context->ob->write(data, len);
|
|
}
|
|
|
|
void uce_wasm_finish_output()
|
|
{
|
|
// ob_stack[0] is the request's primary stream; nested captures above it
|
|
// belong to unbalanced ob_start() calls and are intentionally ignored
|
|
wasm_output = wasm_request.ob_stack.empty() ? String("") : wasm_request.ob_stack[0]->str();
|
|
}
|
|
|
|
const char* uce_wasm_output_data()
|
|
{
|
|
return(wasm_output.data());
|
|
}
|
|
|
|
size_t uce_wasm_output_size()
|
|
{
|
|
return(wasm_output.size());
|
|
}
|
|
|
|
}
|