This commit is contained in:
root
2026-06-15 21:42:50 +00:00
parent 34a97e2577
commit 99cd92fb4a
126 changed files with 1615 additions and 1057 deletions
+9 -9
View File
@@ -76,15 +76,15 @@ struct TransportLimits {
TransportLimits transport_limits()
{
TransportLimits limits;
limits.max_client_connections = config_u64("TRANSPORT_MAX_CLIENT_CONNECTIONS", limits.max_client_connections);
limits.max_http_header_bytes = config_u64("TRANSPORT_MAX_HTTP_HEADER_BYTES", limits.max_http_header_bytes);
limits.max_http_body_bytes = config_u64("TRANSPORT_MAX_HTTP_BODY_BYTES", limits.max_http_body_bytes);
limits.max_websocket_frame_bytes = config_u64("TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES", limits.max_websocket_frame_bytes);
limits.max_websocket_message_bytes = config_u64("TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES", limits.max_websocket_message_bytes);
limits.max_websocket_output_bytes = config_u64("TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES", limits.max_websocket_output_bytes);
limits.max_response_bytes = config_u64("TRANSPORT_MAX_RESPONSE_BYTES", limits.max_response_bytes);
limits.http_request_timeout_seconds = config_f64("TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS", limits.http_request_timeout_seconds);
limits.connection_idle_timeout_seconds = config_f64("TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS", limits.connection_idle_timeout_seconds);
limits.max_client_connections = to_u64(server_state.config["TRANSPORT_MAX_CLIENT_CONNECTIONS"], limits.max_client_connections);
limits.max_http_header_bytes = to_u64(server_state.config["TRANSPORT_MAX_HTTP_HEADER_BYTES"], limits.max_http_header_bytes);
limits.max_http_body_bytes = to_u64(server_state.config["TRANSPORT_MAX_HTTP_BODY_BYTES"], limits.max_http_body_bytes);
limits.max_websocket_frame_bytes = to_u64(server_state.config["TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES"], limits.max_websocket_frame_bytes);
limits.max_websocket_message_bytes = to_u64(server_state.config["TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES"], limits.max_websocket_message_bytes);
limits.max_websocket_output_bytes = to_u64(server_state.config["TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES"], limits.max_websocket_output_bytes);
limits.max_response_bytes = to_u64(server_state.config["TRANSPORT_MAX_RESPONSE_BYTES"], limits.max_response_bytes);
limits.http_request_timeout_seconds = to_f64(server_state.config["TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS"], limits.http_request_timeout_seconds);
limits.connection_idle_timeout_seconds = to_f64(server_state.config["TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS"], limits.connection_idle_timeout_seconds);
return(limits);
}
+4 -2
View File
@@ -10,7 +10,7 @@
namespace {
const u64 UCE_UNIT_ABI_VERSION = 6;
const u64 UCE_UNIT_ABI_VERSION = 7;
struct SharedUnitFilesystemState
{
@@ -46,7 +46,7 @@ bool compiler_jit_compile_on_request_enabled(Request* context)
{
if(!context || !context->server)
return(true);
return(config_bool("JIT_COMPILE_ON_REQUEST", true));
return(to_bool(context->server->config["JIT_COMPILE_ON_REQUEST"], true));
}
bool compiler_is_u64_string(String value)
@@ -150,6 +150,8 @@ bool compiler_failure_retry_deferred(Request* context, SharedUnit* su, const Sha
return(false);
if(state.source_time == 0)
return(false);
if(!state.metadata_exists || !state.metadata_parsed || !state.abi_compatible || !state.input_signature_matches)
return(false);
auto required_failure_inputs_time = std::max({
state.source_time,
state.setup_template_time,
+157 -17
View File
@@ -174,6 +174,74 @@ void DValue::each(std::function <void (const DValue& t, String key)> f) const
}
}
StringList DValue::keys() const
{
StringList result;
each([&](const DValue& item, String key) {
(void)item;
if(key != "")
result.push_back(key);
});
return(result);
}
DValue DValue::values() const
{
DValue result;
result.set_array();
each([&](const DValue& item, String key) {
(void)key;
result.push(item);
});
return(result);
}
DValue DValue::filter(StringList keys) const
{
DValue result;
const DValue& target = deref();
for(auto key : keys)
{
const DValue* item = target.key(key);
if(item)
result[key] = *item;
}
return(result);
}
DValue DValue::filter(std::function<bool (const DValue&, String)> f) const
{
DValue result;
bool input_is_list = is_list();
if(input_is_list)
result.set_array();
each([&](const DValue& item, String key) {
if(!f(item, key))
return;
if(key != "" && !input_is_list)
result[key] = item;
else
result.push(item);
});
return(result);
}
DValue DValue::map(std::function<DValue (const DValue&, String)> f) const
{
DValue result;
bool input_is_list = is_list();
if(input_is_list)
result.set_array();
each([&](const DValue& item, String key) {
DValue mapped = f(item, key);
if(key != "" && !input_is_list)
result[key] = mapped;
else
result.push(mapped);
});
return(result);
}
bool DValue::is_array() const
{
return(deref().type == 'M');
@@ -798,8 +866,9 @@ void DValue::clear()
namespace {
const char* UCEB_MAGIC = "UCEB";
const u8 UCEB_VERSION = 1;
const u8 UCEB_VERSION = 2;
const u8 UCEB_FLAG_LIST = 1;
const u32 UCEB_MAX_NESTING_DEPTH = 64;
thread_local String uce_dv_last_error_text;
thread_local String uce_dv_value_result;
@@ -831,6 +900,23 @@ bool ucb_read_varint(const String& src, size_t& offset, u64& value_out)
return(false);
}
char ucb_node_type(const DValue& value)
{
const DValue& target = value.deref();
switch(target.type)
{
case('M'):
case('S'):
case('F'):
case('B'):
return(target.type);
default:
// Raw pointers/references are not meaningful across the native/wasm
// membrane; preserve the historical wire behavior as an empty scalar.
return('S');
}
}
String ucb_node_scalar(const DValue& value)
{
const DValue& target = value.deref();
@@ -845,7 +931,7 @@ String ucb_node_scalar(const DValue& value)
return(out.str());
}
case('B'):
return(target._bool ? "(true)" : "(false)");
return(target._bool ? "1" : "0");
case('P'):
return("");
default:
@@ -853,11 +939,50 @@ String ucb_node_scalar(const DValue& value)
}
}
bool ucb_decode_scalar(char node_type, const String& scalar, DValue& out, String& error)
{
switch(node_type)
{
case('S'):
out = scalar;
return(true);
case('F'):
{
const char* begin = scalar.c_str();
char* end = 0;
f64 value = strtod(begin, &end);
if(end == begin || end != begin + scalar.size() || !std::isfinite(value))
{
error = "invalid UCEB2 f64 scalar";
return(false);
}
out = value;
return(true);
}
case('B'):
if(scalar == "1" || scalar == "true" || scalar == "(true)")
{
out.set_bool(true);
return(true);
}
if(scalar == "0" || scalar == "false" || scalar == "(false)")
{
out.set_bool(false);
return(true);
}
error = "invalid UCEB2 bool scalar";
return(false);
}
error = "invalid UCEB2 scalar type tag";
return(false);
}
void ucb_encode_node(String& out, const DValue& value)
{
const DValue& target = value.deref();
u8 flags = target.is_list() ? UCEB_FLAG_LIST : 0;
out.push_back((char)flags);
out.push_back(ucb_node_type(target));
String scalar = ucb_node_scalar(target);
ucb_append_varint(out, scalar.size());
out.append(scalar.data(), scalar.size());
@@ -877,26 +1002,32 @@ void ucb_encode_node(String& out, const DValue& value)
bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& error, u32 depth = 0)
{
if(depth > 1024)
if(depth >= UCEB_MAX_NESTING_DEPTH)
{
error = "UCEB1 nesting limit exceeded";
error = "UCEB2 nesting limit exceeded";
return(false);
}
if(offset >= src.size())
if(offset > src.size() || src.size() - offset < 2)
{
error = "unexpected end of UCEB1 node";
error = "unexpected end of UCEB2 node";
return(false);
}
u8 flags = (u8)src[offset++];
char node_type = src[offset++];
if(node_type != 'M' && node_type != 'S' && node_type != 'F' && node_type != 'B')
{
error = "invalid UCEB2 node type tag";
return(false);
}
u64 scalar_len = 0;
if(!ucb_read_varint(src, offset, scalar_len))
{
error = "invalid UCEB1 scalar length";
error = "invalid UCEB2 scalar length";
return(false);
}
if(scalar_len > src.size() - offset)
if(offset > src.size() || scalar_len > src.size() - offset)
{
error = "UCEB1 scalar length exceeds input";
error = "UCEB2 scalar length exceeds input";
return(false);
}
String scalar(src.data() + offset, (size_t)scalar_len);
@@ -905,15 +1036,19 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err
u64 child_count = 0;
if(!ucb_read_varint(src, offset, child_count))
{
error = "invalid UCEB1 child count";
error = "invalid UCEB2 child count";
return(false);
}
out.clear();
if(child_count == 0 && (flags & UCEB_FLAG_LIST) == 0)
if(node_type != 'M')
{
out = scalar;
return(true);
if(child_count != 0 || (flags & UCEB_FLAG_LIST) != 0)
{
error = "UCEB2 scalar node cannot have children or list flag";
return(false);
}
return(ucb_decode_scalar(node_type, scalar, out, error));
}
if((flags & UCEB_FLAG_LIST) != 0)
out.set_array();
@@ -923,16 +1058,21 @@ bool ucb_decode_node(const String& src, size_t& offset, DValue& out, String& err
u64 key_len = 0;
if(!ucb_read_varint(src, offset, key_len))
{
error = "invalid UCEB1 child key length";
error = "invalid UCEB2 child key length";
return(false);
}
if(key_len > src.size() - offset)
if(offset > src.size() || key_len > src.size() - offset)
{
error = "UCEB1 child key length exceeds input";
error = "UCEB2 child key length exceeds input";
return(false);
}
String key(src.data() + offset, (size_t)key_len);
offset += (size_t)key_len;
if(depth + 1 >= UCEB_MAX_NESTING_DEPTH)
{
error = "UCEB2 nesting limit exceeded";
return(false);
}
DValue child;
if(!ucb_decode_node(src, offset, child, error, depth + 1))
return(false);
@@ -992,7 +1132,7 @@ bool ucb_decode(const String& encoded, DValue& out, String* error_out)
return(true);
}
if(error == "")
error = "trailing bytes after UCEB1 document";
error = "trailing bytes after UCEB2 document";
}
if(error_out)
*error_out = error;
+5
View File
@@ -23,6 +23,11 @@ struct DValue {
// conversions take an optional default that is returned when the value is
// missing (empty) or cannot be converted to the requested type.
void each(std::function <void (const DValue& t, String key)> f) const;
StringList keys() const;
DValue values() const;
DValue filter(StringList keys) const;
DValue filter(std::function<bool (const DValue&, String)> f) const;
DValue map(std::function<DValue (const DValue&, String)> f) const;
bool is_array() const;
bool is_list() const;
String to_string(String default_value = "") const;
+157 -98
View File
@@ -112,95 +112,6 @@ String list_find(StringList items, std::function<bool (String)> f, String fallba
return(fallback);
}
StringList dv_keys(DValue tree)
{
StringList result;
tree.each([&](const DValue& item, String key) {
if(key != "")
result.push_back(key);
});
return(result);
}
DValue dv_values(DValue tree)
{
DValue result;
result.set_array();
tree.each([&](const DValue& item, String key) {
result.push(item);
});
return(result);
}
DValue dv_pick(DValue tree, StringList keys)
{
DValue result;
for(auto key : keys)
{
DValue* item = tree.key(key);
if(item)
result[key] = *item;
}
return(result);
}
DValue dv_omit(DValue tree, StringList keys)
{
DValue result;
std::set<String> omitted(keys.begin(), keys.end());
tree.each([&](const DValue& item, String key) {
if(key != "" && omitted.find(key) == omitted.end())
result[key] = item;
});
return(result);
}
DValue dv_map(DValue tree, std::function<DValue (const DValue&, String)> f)
{
DValue result;
bool input_is_list = tree.is_list();
if(input_is_list)
result.set_array();
tree.each([&](const DValue& item, String key) {
DValue mapped = f(item, key);
if(key != "" && !input_is_list)
result[key] = mapped;
else
result.push(mapped);
});
return(result);
}
DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f)
{
DValue result;
bool input_is_list = tree.is_list();
if(input_is_list)
result.set_array();
tree.each([&](const DValue& item, String key) {
if(!f(item, key))
return;
if(key != "" && !input_is_list)
result[key] = item;
else
result.push(item);
});
return(result);
}
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f)
{
DValue result;
tree.each([&](const DValue& item, String key) {
String group = f(item, key);
DValue* group_items = result.get_or_create(group);
if(!group_items->is_array())
group_items->set_array();
group_items->push(item);
});
return(result);
}
String substr(String s, s64 start_pos)
{
s64 len = s.length();
@@ -643,7 +554,7 @@ StringList regex_split(String pattern, String subject, String flags)
#else
// PCRE2 is not compiled into the wasm core; regex runs host-side (the host
// already links libpcre2). One UCEB1-marshalled hostcall carries the request
// already links libpcre2). One UCEB2-marshalled hostcall carries the request
// {op,pattern,subject,flags,replacement} in and the result tree out — the host
// runs the native regex_* and packs the answer. See uce_host_regex in
// src/wasm/worker.cpp.
@@ -1063,6 +974,48 @@ f64 float_val(String s)
return(strtod(s.c_str(), 0));
}
u64 to_u64(String s, u64 fallback)
{
String raw = trim(s);
if(raw == "")
return(fallback);
char* end = 0;
unsigned long long value = strtoull(raw.c_str(), &end, 10);
return(end && *end == 0 ? (u64)value : fallback);
}
s64 to_s64(String s, s64 fallback)
{
String raw = trim(s);
if(raw == "")
return(fallback);
char* end = 0;
long long value = strtoll(raw.c_str(), &end, 10);
return(end && *end == 0 ? (s64)value : fallback);
}
f64 to_f64(String s, f64 fallback)
{
String raw = trim(s);
if(raw == "")
return(fallback);
char* end = 0;
double value = strtod(raw.c_str(), &end);
return(end && *end == 0 ? (f64)value : fallback);
}
bool to_bool(String s, bool fallback)
{
String raw = trim(to_lower(s));
if(raw == "")
return(fallback);
if(raw == "1" || raw == "true" || raw == "yes" || raw == "on")
return(true);
if(raw == "0" || raw == "false" || raw == "no" || raw == "off")
return(false);
return(fallback);
}
String nibble(String& haystack, String delim)
{
auto idx = haystack.find(delim);
@@ -2255,6 +2208,58 @@ DValue yaml_decode(String s)
return(parser.parse_document());
}
static bool json_hex_to_u32(String s, u32 start, u32& value)
{
value = 0;
if(start > s.length() || s.length() - start < 4)
return(false);
for(u32 i = 0; i < 4; i += 1)
{
char c = s[start + i];
if(c >= '0' && c <= '9')
value = (value << 4) | (c - '0');
else if(c >= 'a' && c <= 'f')
value = (value << 4) | (c - 'a' + 10);
else if(c >= 'A' && c <= 'F')
value = (value << 4) | (c - 'A' + 10);
else
return(false);
}
return(true);
}
static bool json_decode_unicode_escape(String s, u32& i, String& result)
{
u32 codepoint = 0;
if(i >= s.length() || s[i] != 'u' || !json_hex_to_u32(s, i + 1, codepoint))
return(false);
u32 next_sequence_start = i + 5;
if(codepoint >= 0xd800 && codepoint <= 0xdbff)
{
u32 low = 0;
if(next_sequence_start <= s.length() && s.length() - next_sequence_start >= 6 && s[next_sequence_start] == '\\' && s[next_sequence_start + 1] == 'u' && json_hex_to_u32(s, next_sequence_start + 2, low) && low >= 0xdc00 && low <= 0xdfff)
{
codepoint = 0x10000 + (((codepoint - 0xd800) << 10) | (low - 0xdc00));
String utf8 = xml_utf8_from_codepoint(codepoint);
if(utf8 != "")
result += utf8;
i += 10;
return(true);
}
return(false);
}
if(codepoint >= 0xdc00 && codepoint <= 0xdfff)
return(false);
String utf8 = xml_utf8_from_codepoint(codepoint);
if(utf8 == "")
return(false);
result += utf8;
i += 4;
return(true);
}
// https://i.stack.imgur.com/SHLOB.gif
String json_decode_String(String s, u32& i, char termination_char)
{
@@ -2271,6 +2276,9 @@ String json_decode_String(String s, u32& i, char termination_char)
}
else if(c == '\\')
{
if(i + 1 >= s.length())
return(result);
i += 1;
c = s[i];
switch(c)
@@ -2293,9 +2301,16 @@ String json_decode_String(String s, u32& i, char termination_char)
case('f'):
result.append(1, '\f');
break;
case('u'):
// todo decode
case('"'):
result.append(1, '"');
break;
case('\''):
result.append(1, '\'');
break;
case('u'):
if(i <= s.length() && s.length() - i >= 5 && json_decode_unicode_escape(s, i, result))
break;
return(result);
default:
result.append(1, c);
break;
@@ -2342,19 +2357,58 @@ String json_decode_keyword(String s, u32& i)
String json_decode_number(String s, u32& i)
{
String result;
bool saw_dot = false;
bool saw_exponent = false;
json_consume_space(s, i);
if(i < s.length() && s[i] == '-')
{
result.append(1, '-');
i += 1;
}
if(i >= s.length() || !isdigit(s[i]))
return("");
while(i < s.length())
{
char c = s[i];
if(isdigit(c) || c == '.')
if(isdigit(c))
{
result.append(1, c);
i += 1;
continue;
}
else
if(c == '.' && !saw_dot && !saw_exponent)
{
return(result);
result.append(1, c);
saw_dot = true;
i += 1;
continue;
}
i += 1;
if((c == 'e' || c == 'E') && !saw_exponent)
{
if(i + 1 >= s.length())
break;
u32 exponent_start = i;
u32 next = i + 1;
if(s[next] == '+' || s[next] == '-')
next += 1;
if(next < s.length() && isdigit(s[next]))
{
result.append(1, s[i]);
i = i + 1;
if(i < s.length() && (s[i] == '+' || s[i] == '-'))
{
result.append(1, s[i]);
i += 1;
}
saw_exponent = true;
continue;
}
i = exponent_start;
break;
}
break;
}
return(result);
}
@@ -2364,6 +2418,9 @@ DValue json_decode_value(String s, u32& i)
DValue result;
String value = "";
json_consume_space(s, i);
if(i >= s.length())
return(result);
char c = s[i];
//print("json_decode_value " + s.substr(i) + "\n");
if(c == '"' || c == '\'') // String value
@@ -2373,7 +2430,7 @@ DValue json_decode_value(String s, u32& i)
result._String = json_decode_String(s, i, s[i-1]);
return(result);
}
else if(isdigit(c))
else if(c == '-' || isdigit(c))
{
result.type = 'S';
result._String = json_decode_number(s, i);
@@ -2426,9 +2483,11 @@ DValue json_decode_map(String s, u32& i)
else if(c == '"' || c == '\'')
{
i += 1;
if(i >= s.length())
return(result);
key = json_decode_String(s, i, s[i-1]);
json_consume_space(s, i);
if(s[i] != ':')
if(i >= s.length() || s[i] != ':')
return(result); // malformed
i += 1;
DValue v = json_decode_value(s, i);
+4 -7
View File
@@ -5,6 +5,10 @@ u8 char_to_u8(char input);
u8 hex_to_u8(String src);
u64 int_val(String s, u32 base = 10);
f64 float_val(String s);
u64 to_u64(String s, u64 fallback = 0);
s64 to_s64(String s, s64 fallback = 0);
f64 to_f64(String s, f64 fallback = 0);
bool to_bool(String s, bool fallback = false);
String to_lower(String s);
String to_upper(String s);
String substr(String s, s64 start_pos);
@@ -95,13 +99,6 @@ StringList list_sort(StringList items);
bool list_some(StringList items, std::function<bool (String)> f);
bool list_every(StringList items, std::function<bool (String)> f);
String list_find(StringList items, std::function<bool (String)> f, String fallback = "");
StringList dv_keys(DValue tree);
DValue dv_values(DValue tree);
DValue dv_pick(DValue tree, StringList keys);
DValue dv_omit(DValue tree, StringList keys);
DValue dv_map(DValue tree, std::function<DValue (const DValue&, String)> f);
DValue dv_filter(DValue tree, std::function<bool (const DValue&, String)> f);
DValue dv_group_by(DValue tree, std::function<String (const DValue&, String)> f);
template <class ...Args>
inline String first(Args... args)
+1 -61
View File
@@ -193,13 +193,6 @@ StringList ls(String dir)
return(StringList());
return(split(listing, "\n"));
}
u64 config_map_u64(StringMap& cfg, String key, u64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; unsigned long long v = strtoull(raw.c_str(), &end, 10); return(end && *end == 0 ? (u64)v : fallback); }
f64 config_map_f64(StringMap& cfg, String key, f64 fallback) { String raw = first(cfg[key], std::to_string(fallback)); char* end = 0; double v = strtod(raw.c_str(), &end); return(end && *end == 0 ? (f64)v : fallback); }
bool config_bool_value(String raw, bool fallback) { if(raw == "") return(fallback); return(raw != "0" && raw != "false" && raw != "no" && raw != "off"); }
bool config_map_bool(StringMap& cfg, String key, bool fallback) { return(config_bool_value(cfg[key], fallback)); }
u64 config_u64(String key, u64 fallback) { return(context ? config_map_u64(context->server->config, key, fallback) : fallback); }
f64 config_f64(String key, f64 fallback) { return(context ? config_map_f64(context->server->config, key, fallback) : fallback); }
bool config_bool(String key, bool fallback) { return(context ? config_map_bool(context->server->config, key, fallback) : fallback); }
DValue request_perf()
{
size_t required = uce_host_request_perf("", 0, 0, 0);
@@ -1412,60 +1405,6 @@ StringList ls(String dir)
return(split(trim(shell_exec("ls -1 "+shell_escape(dir))), "\n"));
}
u64 config_map_u64(StringMap& cfg, String key, u64 fallback)
{
String raw = trim(cfg[key]);
if(raw == "")
return(fallback);
return(int_val(raw));
}
f64 config_map_f64(StringMap& cfg, String key, f64 fallback)
{
String raw = trim(cfg[key]);
if(raw == "")
return(fallback);
return(float_val(raw));
}
bool config_bool_value(String raw, bool fallback)
{
raw = trim(to_lower(raw));
if(raw == "")
return(fallback);
if(raw == "1" || raw == "true" || raw == "yes" || raw == "on")
return(true);
if(raw == "0" || raw == "false" || raw == "no" || raw == "off")
return(false);
return(fallback);
}
bool config_map_bool(StringMap& cfg, String key, bool fallback)
{
return(config_bool_value(cfg[key], fallback));
}
u64 config_u64(String key, u64 fallback)
{
if(!context || !context->server)
return(fallback);
return(config_map_u64(context->server->config, key, fallback));
}
f64 config_f64(String key, f64 fallback)
{
if(!context || !context->server)
return(fallback);
return(config_map_f64(context->server->config, key, fallback));
}
bool config_bool(String key, bool fallback)
{
if(!context || !context->server)
return(fallback);
return(config_map_bool(context->server->config, key, fallback));
}
StringMap make_server_settings()
{
StringMap cfg;
@@ -1485,6 +1424,7 @@ StringMap make_server_settings()
// Command socket the WS broker listens on; workers flush ws_* dispatch
// command batches here at workspace teardown.
cfg["WS_BROKER_SOCKET_PATH"] = "/run/uce/ws-broker.sock";
cfg["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"] = "30";
cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads";
cfg["SESSION_PATH"] = "/tmp/uce/sessions";
cfg["COMPILER_SYS_PATH"] = ".";
-8
View File
@@ -56,14 +56,6 @@ time_t file_mtime(String file_name);
void file_unlink(String file_name);
String expand_path(String path, String relative_to_path = "");
StringList ls(String dir);
u64 config_map_u64(StringMap& cfg, String key, u64 fallback);
f64 config_map_f64(StringMap& cfg, String key, f64 fallback);
bool config_bool_value(String raw, bool fallback = true);
bool config_map_bool(StringMap& cfg, String key, bool fallback = true);
u64 config_u64(String key, u64 fallback);
f64 config_f64(String key, f64 fallback);
bool config_bool(String key, bool fallback = true);
f64 time_precise();
u64 time();
String time_format_local(String format = "", u64 timestamp = 0);
+54 -1
View File
@@ -8,6 +8,7 @@
#include <sstream>
#include <atomic>
#include <set>
#include <utility>
typedef unsigned char u8;
typedef signed char s8;
@@ -51,7 +52,58 @@ inline String operator+(String lhs, f32 rhs) {
#define DEBUG_MEMORY_OFF
typedef std::map<String, String> StringMap;
typedef std::vector<String> StringList;
struct StringList : std::vector<String> {
using std::vector<String>::vector;
using std::vector<String>::operator=;
StringList() = default;
StringList(const StringList&) = default;
StringList(StringList&&) = default;
StringList& operator=(const StringList&) = default;
StringList& operator=(StringList&&) = default;
StringList(const std::vector<String>& source) : std::vector<String>(source) {}
StringList(std::vector<String>&& source) : std::vector<String>(std::move(source)) {}
StringList& operator=(const std::vector<String>& source) { std::vector<String>::operator=(source); return(*this); }
StringList& operator=(std::vector<String>&& source) { std::vector<String>::operator=(std::move(source)); return(*this); }
template<typename F>
StringList filter(F f) const
{
StringList result;
for(const auto& item : *this)
{
if(f(item))
result.push_back(item);
}
return(result);
}
template<typename F>
StringList map(F f) const
{
StringList result;
for(const auto& item : *this)
result.push_back(f(item));
return(result);
}
StringList keys() const
{
StringList result;
for(size_t i = 0; i < size(); i++)
result.push_back(std::to_string(i));
return(result);
}
template<typename F>
void each(F f) const
{
for(const auto& item : *this)
f(item);
}
};
typedef std::ostringstream ByteStream;
struct Request;
@@ -203,6 +255,7 @@ struct Request {
String websocket_scope = "";
DValue* websocket_connection_state = 0;
StringList websocket_scope_connection_ids;
DValue websocket_connection_state_before;
DValue websocket_dispatch_commands;
bool websocket_dispatch_capture = false;
u8 websocket_opcode = 0;
+2 -2
View File
@@ -16,7 +16,7 @@ String zip_error(String api, String detail)
void archive_check_size(String api, String label, u64 size, String config_key, u64 fallback)
{
u64 limit = config_u64(config_key, fallback);
u64 limit = to_u64(context->server->config[config_key], fallback);
if(limit > 0 && size > limit)
throw std::runtime_error(zip_error(api, label + " exceeds configured limit"));
}
@@ -326,7 +326,7 @@ String gz_uncompress(String compressed)
if(!out)
throw std::runtime_error("gz_uncompress(): decompression failed");
u64 output_limit = config_u64("ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
u64 output_limit = to_u64(context->server->config["ARCHIVE_MAX_OUTPUT_BYTES"], 64 * 1024 * 1024);
if(output_limit > 0 && out_len > output_limit)
{
mz_free(out);
+29 -14
View File
@@ -20,10 +20,16 @@ pid_t proactive_compiler_pid = 0;
// The central WS broker process: owns the WS port + every connection, forwards
// renders to the worker pool over uce.sock, and applies ws_* commands flushed
// back from workers. ws_broker_outbound holds in-flight async render
// connections (fd -> bytes still to write; empty value means draining the reply).
// connections and their enqueue timestamps.
struct WsBrokerOutbound
{
String pending;
f64 started_at;
};
FastCGIServer ws_broker;
pid_t ws_broker_pid = 0;
std::map<int, String> ws_broker_outbound;
std::map<int, WsBrokerOutbound> ws_broker_outbound;
static sigjmp_buf request_fault_jmp;
static volatile sig_atomic_t request_fault_active = 0;
static volatile sig_atomic_t request_fault_signal = 0;
@@ -768,11 +774,11 @@ int custom_server_bind_http(FastCGIServer& dispatcher, String bind)
if(custom_server_is_numeric_port(bind))
{
u64 port = int_val(bind);
u64 min_port = config_map_u64(server_state.config, "CUSTOM_SERVER_MIN_PORT", 1024);
u64 max_port = config_map_u64(server_state.config, "CUSTOM_SERVER_MAX_PORT", 65535);
u64 min_port = to_u64(server_state.config["CUSTOM_SERVER_MIN_PORT"], 1024);
u64 max_port = to_u64(server_state.config["CUSTOM_SERVER_MAX_PORT"], 65535);
if(port < min_port || port > max_port)
throw std::runtime_error("server_start_http(): TCP port is outside configured custom server range");
String bind_address = config_map_bool(server_state.config, "CUSTOM_SERVER_ALLOW_PUBLIC_BIND", false) ? "0.0.0.0" : "127.0.0.1";
String bind_address = to_bool(server_state.config["CUSTOM_SERVER_ALLOW_PUBLIC_BIND"], false) ? "0.0.0.0" : "127.0.0.1";
return(dispatcher.listen_http((unsigned)port, bind_address));
}
String socket_prefix = first(server_state.config["CUSTOM_SERVER_UNIX_SOCKET_PREFIX"], "/tmp/uce/custom-servers/");
@@ -836,7 +842,7 @@ int custom_server_http_complete(FastCGIRequest& request)
request.params["UCE_SERVE_HTTP_BIND"] = cfg["bind"];
request.params["UCE_SERVE_HTTP_FUNCTION"] = cfg["function"];
request.params["SCRIPT_FILENAME"] = cfg["file"];
u64 timeout = config_map_u64(server_state.config, "CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS", 30);
u64 timeout = to_u64(server_state.config["CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS"], 30);
return(forward_request_to_worker(request, timeout > 0 ? (u32)timeout : 30));
}
@@ -942,20 +948,28 @@ int ws_broker_ws_message(FastCGIRequest& request, const String& message, u8 opco
int fd = ws_broker_connect_unix(first(server_state.config["FCGI_SOCKET_PATH"], "/run/uce.sock"));
if(fd < 0)
return(0);
ws_broker_outbound[fd] = fcgi_build_request(params, "");
ws_broker_outbound[fd] = {fcgi_build_request(params, ""), time_precise()};
return(0);
}
// Drive in-flight render forwards non-blocking: finish writing the request, then
// drain/discard the reply (the unit's ws_* came back via the command socket).
void ws_broker_drain_outbound()
void ws_broker_drain_outbound(u64 timeout_seconds)
{
f64 now = time_precise();
for(auto it = ws_broker_outbound.begin(); it != ws_broker_outbound.end(); )
{
int fd = it->first;
String& pending = it->second;
WsBrokerOutbound& outbound = it->second;
String& pending = outbound.pending;
bool done = false;
if(!pending.empty())
bool timed_out = timeout_seconds > 0 && now - outbound.started_at > (f64)timeout_seconds;
if(timed_out)
{
fprintf(stderr, "ws broker dropping stale outbound forward fd=%i after %.1fs\n", fd, now - outbound.started_at);
done = true;
}
if(!done && !pending.empty())
{
ssize_t n = ::send(fd, pending.data(), pending.size(), MSG_NOSIGNAL | MSG_DONTWAIT);
if(n > 0)
@@ -997,6 +1011,7 @@ void run_ws_broker()
ws_broker.on_websocket_message = &ws_broker_ws_message;
if(server_state.config["HTTP_PORT"] != "")
ws_broker.listen_http(int_val(server_state.config["HTTP_PORT"]));
u64 ws_broker_outbound_timeout_seconds = to_u64(server_state.config["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"], 30);
if(server_state.config["WS_BROKER_SOCKET_PATH"] != "")
{
ws_broker.listen(server_state.config["WS_BROKER_SOCKET_PATH"]);
@@ -1005,7 +1020,7 @@ void run_ws_broker()
for(;;)
{
ws_broker.process(50);
ws_broker_drain_outbound();
ws_broker_drain_outbound(ws_broker_outbound_timeout_seconds);
}
}
@@ -1094,7 +1109,7 @@ pid_t server_start_http(String key, String socket_fn_or_port, String call_uce_fi
previous_config = custom_server_config_decode(file_get_contents(config_file));
String new_config = custom_server_config_encode(key, "http", socket_fn_or_port, call_uce_filename, call_function);
String task_key = custom_server_task_key(key);
u64 max_servers = config_map_u64(server_state.config, "CUSTOM_SERVER_MAX_SERVERS", 16);
u64 max_servers = to_u64(server_state.config["CUSTOM_SERVER_MAX_SERVERS"], 16);
if(!file_exists(config_file) && max_servers > 0 && custom_server_registry_count() >= max_servers)
throw std::runtime_error("server_start_http(): custom server quota exceeded");
pid_t existing_pid = task_pid(task_key);
@@ -1146,7 +1161,7 @@ void run_proactive_compiler()
StringList compile_queue;
background_context.server = &server_state;
set_active_request(background_context);
if(!config_map_bool(server_state.config, "PROACTIVE_COMPILE_ENABLED", true))
if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true))
return;
f64 check_interval = float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]);
f64 failure_retry_interval = 0;
@@ -1276,7 +1291,7 @@ bool proactive_compiler_alive()
void ensure_proactive_compiler()
{
if(!config_map_bool(server_state.config, "PROACTIVE_COMPILE_ENABLED", true))
if(!to_bool(server_state.config["PROACTIVE_COMPILE_ENABLED"], true))
return;
if(float_val(server_state.config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) <= 0)
return;
+23 -20
View File
@@ -1,7 +1,7 @@
// W4 — FastCGI backend glue for the W3 wasm workspace runtime.
//
// Included into the native server TU (src/linux_fastcgi.cpp) after uce_lib.cpp,
// so it shares String/DValue/config and the UCEB1 codec. Provides a
// so it shares String/DValue/config and the UCEB2 codec. Provides a
// Always-on wasm backend: every unit request is served through a per-request
// wasm workspace. The legacy native dlopen execution path has been removed.
@@ -50,9 +50,9 @@ static String wasm_backend_ensure_started(Request* context)
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
if(cfg[key] != "")
wc.write_roots.push_back(cfg[key]);
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
wc.memory_limit = (int64_t)to_u64(cfg["WASM_MEMORY_LIMIT_BYTES"], 512ull * 1024 * 1024);
wc.epoch_deadline_ticks = to_u64(cfg["WASM_EPOCH_DEADLINE_TICKS"], 200);
wc.verbose = to_bool(cfg["WASM_BACKEND_VERBOSE"], false);
g_wasm_worker = new WasmWorker(wc);
g_wasm_init_error = g_wasm_worker->init();
@@ -65,7 +65,7 @@ static String wasm_backend_ensure_started(Request* context)
g_wasm_epoch_running.store(true);
WasmWorker* worker = g_wasm_worker;
u64 period_ms = config_u64("WASM_EPOCH_PERIOD_MS", 50);
u64 period_ms = to_u64(cfg["WASM_EPOCH_PERIOD_MS"], 50);
g_wasm_epoch_ticker = new std::thread([worker, period_ms] {
while(g_wasm_epoch_running.load())
{
@@ -84,12 +84,13 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit)
struct stat wasm_st;
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
return(false);
// Require the artifact to be newer than the source. If it is stale,
// dispatch compiles the unit on demand and then rechecks this predicate.
struct stat src_st;
if(stat(entry_unit.c_str(), &src_st) != 0 || !S_ISREG(src_st.st_mode))
// Require the artifact to satisfy the full compiler freshness check. Source
// mtime alone misses runtime/unit ABI changes, setup-template changes, and
// metadata mismatches, which can leave stale wasm with old imports.
bool source_missing = false;
if(compiler_unit_needs_recompile(context, entry_unit, &source_missing))
return(false);
if(wasm_st.st_mtime < src_st.st_mtime)
if(source_missing)
return(false);
return(true);
}
@@ -151,23 +152,25 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri
if(!response.ok)
return(response.error == "" ? String("wasm workspace failed") : response.error);
// Any handler may have called ws_send/ws_close (not just WS handlers). If it
// left dispatch commands, flush the batch to the central WS broker, which owns
// all connections and resolves each command's target (id/scope/broadcast)
// against the full registry. This is the only path WS data takes out — the
// workspace owns no connections.
if(DValue* cmds = response.meta.key("ws_commands"))
// Any handler may have called ws_send/ws_close (not just WS handlers). Flush the
// websocket batch whenever either command frames or an updated per-connection
// state are present. This is the only path WS data takes out; the workspace
// owns no connections.
DValue* cmds = response.meta.key("ws_commands");
DValue* cstate = response.meta.key("ws_connection_state");
if(cmds || cstate)
{
DValue batch;
batch["commands"] = *cmds;
if(DValue* cstate = response.meta.key("ws_connection_state"))
if(cmds)
batch["commands"] = *cmds;
if(cstate)
{
batch["connection_id"] = request.resources.websocket_connection_id;
batch["connection_state"] = *cstate;
}
StringMap dispatch_params;
dispatch_params["UCE_WS_DISPATCH"] = "1";
String broker_socket = first(request.server ? request.server->config["WS_BROKER_SOCKET_PATH"] : String(),
String broker_socket = first(request.server->config["WS_BROKER_SOCKET_PATH"],
"/run/uce/ws-broker.sock");
fcgi_forward_request(broker_socket, dispatch_params, ucb_encode(batch), 5);
}
@@ -182,7 +185,7 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri
// Diagnostic timing headers are opt-in: they leak workspace internals and
// belong to the W5 benchmark harness, not public responses.
if(config_bool("WASM_BACKEND_VERBOSE", false))
if(to_bool(request.server->config["WASM_BACKEND_VERBOSE"], false))
{
request.header["X-UCE-Backend"] = "wasm";
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
+19 -8
View File
@@ -12,7 +12,7 @@
// ---- 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
// 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 =
@@ -303,7 +303,7 @@ DValue MySQL::query(String q, StringMap params) { return(query(parse_query_param
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
// 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);
@@ -766,7 +766,7 @@ void uce_wasm_core_reset_request()
wasm_component_slots.clear();
}
// Host pushes the UCEB1-encoded request context into a guest buffer
// Host pushes the UCEB2-encoded request context into a guest buffer
// (uce_alloc) and applies it here; mirrors the native param population.
int uce_wasm_apply_context(const char* buf, size_t len)
{
@@ -800,6 +800,8 @@ int uce_wasm_apply_context(const char* buf, size_t len)
// 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;
DValue* ws = decoded.key("ws");
@@ -817,6 +819,7 @@ int uce_wasm_apply_context(const char* buf, size_t len)
wasm_request.resources.websocket_dispatch_capture = true;
if(DValue* cstate = ws->key("connection_state"))
wasm_request.connection = *cstate;
wasm_request.resources.websocket_connection_state_before = wasm_request.connection;
}
return(0);
}
@@ -829,7 +832,7 @@ Request* uce_wasm_request()
}
// After render: response metadata (status line, headers, cookies, session)
// goes back to the host as UCEB1.
// goes back to the host as UCEB2.
void uce_wasm_finish_response_meta()
{
DValue meta;
@@ -844,14 +847,22 @@ void uce_wasm_finish_response_meta()
}
for(auto& entry : wasm_request.session)
meta["session"][entry.first] = entry.second;
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. ws_connection_state rides along for stateful WS handlers.
if(!wasm_request.resources.websocket_dispatch_commands._map.empty())
{
// 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);
}
+3 -3
View File
@@ -280,7 +280,7 @@ int main(int argc, char** argv)
int32_t encoded_ptr = call_i32(core, "uce_alloc", { encoded_len });
CHECK(call_i32(core, "uce_dv_encode", { root, encoded_ptr, encoded_len }) == encoded_len, "encode length mismatch");
std::string encoded = read_bytes(memory, encoded_ptr, encoded_len);
CHECK(encoded.rfind("UCEB\x01", 0) == 0, "UCEB1 header missing");
CHECK(encoded.size() >= 5 && encoded.compare(0, 4, "UCEB") == 0 && (unsigned char)encoded[4] == 2, "UCEB2 header missing");
int32_t decoded = call_i32(core, "uce_dv_decode", { encoded_ptr, encoded_len });
CHECK(decoded != 0, "uce_dv_decode failed");
CHECK(call_i32(core, "uce_dv_count", { decoded }) == 1, "decoded root count mismatch");
@@ -290,8 +290,8 @@ int main(int argc, char** argv)
std::string bad = "bad";
int32_t bad_ptr = call_i32(core, "uce_alloc", { (int32_t)bad.size() });
write_bytes(memory, bad_ptr, bad);
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB1 decode unexpectedly succeeded");
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB1 decode did not set error");
CHECK(call_i32(core, "uce_dv_decode", { bad_ptr, (int32_t)bad.size() }) == 0, "bad UCEB2 decode unexpectedly succeeded");
CHECK(read_cstr(memory, call_i32(core, "uce_dv_last_error")) != "", "bad UCEB2 decode did not set error");
std::string out = "W1 output";
int32_t out_ptr = call_i32(core, "uce_alloc", { (int32_t)out.size() });
+1 -1
View File
@@ -1,7 +1,7 @@
// W3 CLI driver — the exit gate for WASM-PROPOSAL §9.1 W3.
//
// Serves requests through the production workspace runtime
// (src/wasm/worker.cpp): UCEB1 context in → core + lazily loaded
// (src/wasm/worker.cpp): UCEB2 context in → core + lazily loaded
// generated units → body/response-meta out. Each --repeat gets a fresh
// workspace, proving birth/drop. An epoch ticker thread enforces the CPU
// budget; the store limiter enforces memory; traps come back as collapsed
+57 -2
View File
@@ -1012,6 +1012,56 @@ private:
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;
@@ -1378,7 +1428,8 @@ private:
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
results[0] = Val(::chdir(path.c_str()) == 0 ? (int32_t)1 : (int32_t)0);
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")
@@ -1991,6 +2042,10 @@ private:
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 != "")
@@ -2049,7 +2104,7 @@ private:
}));
if(mod == "env" && name == "uce_host_regex")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
// {op,pattern,subject,flags,replacement} in (UCEB1) → result out.
// {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);