phase 5
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# spikes/wasm-phase2 — core module + membrane scaffold
|
||||
|
||||
Phase 2 validates the next WASM step without the Phase 3 dynamic loader. The
|
||||
scaffold compiles a native UCE core subset (`Request`, `DValue`, UCEB1, print
|
||||
buffering) plus one statically linked real `.uce` page into a WASM reactor.
|
||||
A small Wasmtime C-API host provides the first membrane hostcalls, sends a UCEB1
|
||||
request context into the guest, invokes render, and reads the response from
|
||||
linear memory. `uce_host_ctx_read(ptr, cap)` follows a length-query contract:
|
||||
`ptr == 0` or `cap == 0` returns the required length; a short non-zero buffer
|
||||
also returns the required length without a partial copy; an adequately sized
|
||||
buffer receives the full context and returns the copied length.
|
||||
|
||||
This is intentionally not the final worker. It proves the Phase 2 membrane path:
|
||||
core-owned DValue/UCEB1 code runs in WASM and a `.uce` render entry sees a
|
||||
host-provided request context through the membrane. The checked-in page uses the
|
||||
same `RENDER(Request&)` entry shape as generated units, but it is included
|
||||
directly by the scaffold rather than emitted by the UCE preprocessor.
|
||||
|
||||
Run on `k-uce`:
|
||||
|
||||
```bash
|
||||
bash spikes/wasm-phase2/build_modules.sh
|
||||
bash spikes/wasm-phase2/build_loader.sh
|
||||
/tmp/uce/wasm-phase2/loader
|
||||
```
|
||||
|
||||
Expected final line:
|
||||
|
||||
```text
|
||||
PHASE2 EXIT CRITERION: PASS
|
||||
```
|
||||
|
||||
Files:
|
||||
|
||||
- `core.cpp` — WASM reactor core subset and membrane decode/render entry.
|
||||
- `page.uce` — real UCE page statically linked for Phase 2 only.
|
||||
- `loader.cpp` — host runner with `uce_host_ctx_read` and `uce_host_log`.
|
||||
Its small host-side UCEB1 encoder is spike-only; the production UCE server
|
||||
host should use the native `ucb_encode()` implementation to avoid format drift.
|
||||
- `build_modules.sh` — wasi-sdk build for `/tmp/uce/wasm-phase2/core.wasm`.
|
||||
- `build_loader.sh` — Wasmtime C-API build for `/tmp/uce/wasm-phase2/loader`.
|
||||
|
||||
Deferred to Phase 3:
|
||||
|
||||
- PIC side modules and dylink loader integration.
|
||||
- Lazy unit/component loading.
|
||||
- Full `uce_lib` hostcall surface beyond the minimal context/log membrane.
|
||||
- Generator-emitted units (`.uce` preprocessor output with literal markup,
|
||||
`__uce_set_current_request`, and generated includes) rendering through this
|
||||
membrane; Phase 0 only proved those generated units can compile as side
|
||||
modules.
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Phase 2: build the minimal host/membrane runner against Wasmtime's wasm-c-api.
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
OUT=/tmp/uce/wasm-phase2
|
||||
mkdir -p "$OUT"
|
||||
|
||||
c++ -std=c++17 -O2 loader.cpp \
|
||||
-I/opt/wasmtime/include \
|
||||
-L/opt/wasmtime/lib \
|
||||
-Wl,-rpath,/opt/wasmtime/lib \
|
||||
-lwasmtime \
|
||||
-o "$OUT/loader"
|
||||
|
||||
echo "built: $OUT/loader"
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Phase 2: build the native DValue/UCEB1 core subset plus one statically
|
||||
# linked real .uce page as a WASM reactor. Runs on k-uce; artifacts go under
|
||||
# /tmp/uce/wasm-phase2.
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
SDK=/opt/wasi-sdk
|
||||
OUT=/tmp/uce/wasm-phase2
|
||||
mkdir -p "$OUT"
|
||||
|
||||
"$SDK/bin/clang++" --target=wasm32-wasip1 -mexec-model=reactor \
|
||||
-O1 -fno-exceptions \
|
||||
-I../.. -I../../src/lib \
|
||||
core.cpp -o "$OUT/core.wasm" \
|
||||
-Wl,--export-all \
|
||||
-Wl,--export=__heap_base \
|
||||
-Wl,--allow-undefined-file=hostcalls.syms
|
||||
|
||||
echo "--- phase2 core.wasm ---"
|
||||
ls -la "$OUT/core.wasm"
|
||||
@@ -0,0 +1,142 @@
|
||||
// WASM-PROPOSAL Phase 2 — native UCE core subset compiled to WASM.
|
||||
//
|
||||
// This scaffold validates the Phase 2 membrane without the Phase 3 dynamic
|
||||
// loader: the core owns memory/libc++/DValue/UCEB1, imports a tiny hostcall
|
||||
// surface, decodes a host-provided UCEB1 request context, and invokes one
|
||||
// statically linked real .uce page.
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "../../src/lib/types.h"
|
||||
#include "../../src/lib/dvalue.cpp"
|
||||
|
||||
extern "C" {
|
||||
size_t uce_host_ctx_read(char* buf, size_t cap);
|
||||
void uce_host_log(int level, const char* buf, size_t len);
|
||||
}
|
||||
|
||||
#define RENDER(X) extern "C" void __uce_render(X)
|
||||
#include "page.uce"
|
||||
|
||||
static Request g_request;
|
||||
static ByteStream g_ob;
|
||||
static String g_output;
|
||||
|
||||
SharedUnit::~SharedUnit() {}
|
||||
|
||||
String nibble(String div, String& haystack)
|
||||
{
|
||||
auto pos = haystack.find(div);
|
||||
if(pos == String::npos)
|
||||
{
|
||||
auto result = haystack;
|
||||
haystack.clear();
|
||||
return(result);
|
||||
}
|
||||
auto result = haystack.substr(0, pos);
|
||||
haystack.erase(0, pos + div.length());
|
||||
return(result);
|
||||
}
|
||||
|
||||
void Request::ob_start()
|
||||
{
|
||||
ob_stack.push_back(new ByteStream());
|
||||
ob = ob_stack.back();
|
||||
}
|
||||
|
||||
void Request::set_status(s32 code, String reason)
|
||||
{
|
||||
if(reason == "")
|
||||
reason = code == 200 ? "OK" : "Status";
|
||||
response_code = "HTTP/1.1 " + std::to_string(code) + " " + reason;
|
||||
}
|
||||
|
||||
Request::~Request()
|
||||
{
|
||||
for(auto* stream : ob_stack)
|
||||
delete stream;
|
||||
ob_stack.clear();
|
||||
}
|
||||
|
||||
static void phase2_clear_ob_stack()
|
||||
{
|
||||
for(auto* stream : g_request.ob_stack)
|
||||
delete stream;
|
||||
g_request.ob_stack.clear();
|
||||
}
|
||||
|
||||
static void phase2_apply_context(DValue& root)
|
||||
{
|
||||
g_request.call = root;
|
||||
g_request.params.clear();
|
||||
DValue* params = root.key("params");
|
||||
if(params)
|
||||
{
|
||||
params->each([&](const DValue& item, String key) {
|
||||
g_request.params[key] = item.to_string();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
void* uce_alloc(size_t len)
|
||||
{
|
||||
return(malloc(len));
|
||||
}
|
||||
|
||||
void uce_free(void* ptr)
|
||||
{
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
int uce_phase2_render()
|
||||
{
|
||||
context = &g_request;
|
||||
phase2_clear_ob_stack();
|
||||
g_ob.str("");
|
||||
g_ob.clear();
|
||||
g_request.ob = &g_ob;
|
||||
g_request.out = "";
|
||||
g_output = "";
|
||||
|
||||
size_t ctx_required = uce_host_ctx_read(0, 0);
|
||||
if(ctx_required == 0)
|
||||
return(10);
|
||||
char* ctx_buf = (char*)malloc(ctx_required);
|
||||
if(ctx_buf == 0)
|
||||
return(11);
|
||||
size_t ctx_len = uce_host_ctx_read(ctx_buf, ctx_required);
|
||||
if(ctx_len != ctx_required)
|
||||
{
|
||||
free(ctx_buf);
|
||||
return(12);
|
||||
}
|
||||
DValue decoded;
|
||||
String error;
|
||||
bool ok = ucb_decode(String(ctx_buf, ctx_len), decoded, &error);
|
||||
free(ctx_buf);
|
||||
if(!ok)
|
||||
{
|
||||
uce_host_log(3, error.data(), error.size());
|
||||
return(20);
|
||||
}
|
||||
|
||||
phase2_apply_context(decoded);
|
||||
__uce_render(g_request);
|
||||
g_output = g_ob.str();
|
||||
return(0);
|
||||
}
|
||||
|
||||
const char* uce_phase2_output_data()
|
||||
{
|
||||
return(g_output.data());
|
||||
}
|
||||
|
||||
size_t uce_phase2_output_size()
|
||||
{
|
||||
return(g_output.size());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
uce_host_ctx_read
|
||||
uce_host_log
|
||||
@@ -0,0 +1,280 @@
|
||||
// WASM-PROPOSAL Phase 2 — minimal membrane host.
|
||||
// Instantiates the statically linked core/page module, serves a UCEB1 request
|
||||
// context via hostcall, invokes render, and reads the response from guest
|
||||
// memory. Dynamic linking is intentionally Phase 3 work.
|
||||
|
||||
#include <wasm.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
|
||||
#define CHECK(cond, ...) do { if(!(cond)) FAIL(__VA_ARGS__); } while(0)
|
||||
|
||||
static wasm_store_t* g_store = nullptr;
|
||||
static wasm_memory_t* g_memory = nullptr;
|
||||
static std::string g_context;
|
||||
|
||||
static std::vector<uint8_t> read_file(const char* fn)
|
||||
{
|
||||
FILE* f = fopen(fn, "rb");
|
||||
CHECK(f, "cannot open %s", fn);
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
std::vector<uint8_t> buf(n);
|
||||
CHECK(fread(buf.data(), 1, n, f) == (size_t)n, "short read on %s", fn);
|
||||
fclose(f);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void append_varuint(std::string& out, uint64_t value)
|
||||
{
|
||||
while(value >= 0x80)
|
||||
{
|
||||
out.push_back((char)((value & 0x7f) | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
out.push_back((char)value);
|
||||
}
|
||||
|
||||
struct Node
|
||||
{
|
||||
std::string scalar;
|
||||
bool is_list = false;
|
||||
std::vector<std::pair<std::string, Node>> children;
|
||||
};
|
||||
|
||||
static Node scalar(const char* value)
|
||||
{
|
||||
Node n;
|
||||
n.scalar = value;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void encode_node(std::string& out, const Node& node)
|
||||
{
|
||||
out.push_back(node.is_list ? 1 : 0);
|
||||
append_varuint(out, node.scalar.size());
|
||||
out.append(node.scalar);
|
||||
append_varuint(out, node.children.size());
|
||||
for(const auto& child : node.children)
|
||||
{
|
||||
append_varuint(out, child.first.size());
|
||||
out.append(child.first);
|
||||
encode_node(out, child.second);
|
||||
}
|
||||
}
|
||||
|
||||
static std::string make_context()
|
||||
{
|
||||
Node params;
|
||||
params.children.push_back({"HTTP_HOST", scalar("phase2.example.test")});
|
||||
params.children.push_back({"SCRIPT_URL", scalar("/spikes/wasm-phase2/page.uce")});
|
||||
|
||||
Node nested;
|
||||
nested.children.push_back({"answer", scalar("42")});
|
||||
|
||||
Node root;
|
||||
root.children.push_back({"params", params});
|
||||
root.children.push_back({"route", scalar("/spikes/wasm-phase2/page.uce")});
|
||||
root.children.push_back({"nested", nested});
|
||||
|
||||
std::string out = "UCEB";
|
||||
out.push_back((char)1);
|
||||
encode_node(out, root);
|
||||
return out;
|
||||
}
|
||||
|
||||
static wasm_trap_t* host_ctx_read(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
||||
{
|
||||
(void)env;
|
||||
uint32_t ptr = args->data[0].of.i32;
|
||||
uint32_t cap = args->data[1].of.i32;
|
||||
if(ptr == 0 || cap == 0 || cap < g_context.size())
|
||||
{
|
||||
results->data[0] = WASM_I32_VAL((int32_t)g_context.size());
|
||||
return nullptr;
|
||||
}
|
||||
CHECK(g_memory, "host ctx_read called before memory export was captured");
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
||||
size_t mem_size = wasm_memory_data_size(g_memory);
|
||||
CHECK((size_t)ptr <= mem_size, "ctx_read pointer outside memory");
|
||||
CHECK((size_t)ptr + g_context.size() <= mem_size, "ctx_read buffer outside memory");
|
||||
memcpy(mem + ptr, g_context.data(), g_context.size());
|
||||
results->data[0] = WASM_I32_VAL((int32_t)g_context.size());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static wasm_trap_t* host_log(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
||||
{
|
||||
(void)env; (void)results;
|
||||
int level = args->data[0].of.i32;
|
||||
uint32_t ptr = args->data[1].of.i32;
|
||||
uint32_t len = args->data[2].of.i32;
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
||||
size_t mem_size = wasm_memory_data_size(g_memory);
|
||||
if((size_t)ptr + len <= mem_size)
|
||||
fprintf(stderr, "[guest log %d] %.*s\n", level, (int)len, (const char*)mem + ptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static wasm_trap_t* stub_callback(void* env, const wasm_val_vec_t* args, wasm_val_vec_t* results)
|
||||
{
|
||||
(void)args; (void)results;
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "unimplemented import called: %s", (const char*)env);
|
||||
wasm_message_t message;
|
||||
wasm_byte_vec_new(&message, strlen(msg) + 1, msg);
|
||||
wasm_trap_t* trap = wasm_trap_new(g_store, &message);
|
||||
wasm_byte_vec_delete(&message);
|
||||
return trap;
|
||||
}
|
||||
|
||||
static wasm_func_t* make_func(wasm_store_t* store, std::vector<wasm_valkind_t> params, std::vector<wasm_valkind_t> results, wasm_func_callback_with_env_t cb, void* env = nullptr)
|
||||
{
|
||||
wasm_valtype_vec_t ps;
|
||||
wasm_valtype_vec_new_uninitialized(&ps, params.size());
|
||||
for(size_t i = 0; i < params.size(); i++)
|
||||
ps.data[i] = wasm_valtype_new(params[i]);
|
||||
wasm_valtype_vec_t rs;
|
||||
wasm_valtype_vec_new_uninitialized(&rs, results.size());
|
||||
for(size_t i = 0; i < results.size(); i++)
|
||||
rs.data[i] = wasm_valtype_new(results[i]);
|
||||
wasm_functype_t* ft = wasm_functype_new(&ps, &rs);
|
||||
wasm_func_t* fn = wasm_func_new_with_env(store, ft, cb, env, nullptr);
|
||||
wasm_functype_delete(ft);
|
||||
return fn;
|
||||
}
|
||||
|
||||
struct Instance
|
||||
{
|
||||
wasm_module_t* module = nullptr;
|
||||
wasm_instance_t* instance = nullptr;
|
||||
wasm_extern_vec_t exports = WASM_EMPTY_VEC;
|
||||
std::map<std::string, wasm_extern_t*> by_name;
|
||||
|
||||
void index_exports()
|
||||
{
|
||||
wasm_exporttype_vec_t types = WASM_EMPTY_VEC;
|
||||
wasm_module_exports(module, &types);
|
||||
wasm_instance_exports(instance, &exports);
|
||||
for(size_t i = 0; i < types.size; i++)
|
||||
{
|
||||
const wasm_name_t* nm = wasm_exporttype_name(types.data[i]);
|
||||
std::string key(nm->data, nm->size);
|
||||
while(!key.empty() && key.back() == '\0') key.pop_back();
|
||||
by_name[key] = exports.data[i];
|
||||
}
|
||||
wasm_exporttype_vec_delete(&types);
|
||||
}
|
||||
|
||||
wasm_func_t* func(const char* name)
|
||||
{
|
||||
auto it = by_name.find(name);
|
||||
return it == by_name.end() ? nullptr : wasm_extern_as_func(it->second);
|
||||
}
|
||||
};
|
||||
|
||||
static int32_t call_i32(Instance& inst, const char* name)
|
||||
{
|
||||
wasm_func_t* f = inst.func(name);
|
||||
CHECK(f, "missing export func %s", name);
|
||||
wasm_val_t result_buf[1] = { WASM_INIT_VAL };
|
||||
wasm_val_vec_t args = WASM_EMPTY_VEC;
|
||||
wasm_val_vec_t results = { wasm_func_result_arity(f), result_buf };
|
||||
wasm_val_vec_t no_results = WASM_EMPTY_VEC;
|
||||
wasm_trap_t* trap = wasm_func_call(f, &args, results.size ? &results : &no_results);
|
||||
if(trap)
|
||||
{
|
||||
wasm_message_t msg;
|
||||
wasm_trap_message(trap, &msg);
|
||||
FAIL("trap during %s: %.*s", name, (int)msg.size, msg.data);
|
||||
}
|
||||
return results.size ? result_buf[0].of.i32 : 0;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
const char* core_path = argc > 1 ? argv[1] : "/tmp/uce/wasm-phase2/core.wasm";
|
||||
g_context = make_context();
|
||||
|
||||
wasm_engine_t* engine = wasm_engine_new();
|
||||
CHECK(engine, "engine");
|
||||
g_store = wasm_store_new(engine);
|
||||
CHECK(g_store, "store");
|
||||
|
||||
std::vector<uint8_t> bytes = read_file(core_path);
|
||||
wasm_byte_vec_t bv;
|
||||
wasm_byte_vec_new(&bv, bytes.size(), (const char*)bytes.data());
|
||||
Instance core;
|
||||
core.module = wasm_module_new(g_store, &bv);
|
||||
wasm_byte_vec_delete(&bv);
|
||||
CHECK(core.module, "core module load failed");
|
||||
|
||||
wasm_importtype_vec_t imports = WASM_EMPTY_VEC;
|
||||
wasm_module_imports(core.module, &imports);
|
||||
std::vector<wasm_extern_t*> externs(imports.size);
|
||||
std::vector<wasm_func_t*> owned_funcs;
|
||||
for(size_t i = 0; i < imports.size; i++)
|
||||
{
|
||||
const wasm_name_t* mod_n = wasm_importtype_module(imports.data[i]);
|
||||
const wasm_name_t* name_n = wasm_importtype_name(imports.data[i]);
|
||||
std::string mod(mod_n->data, mod_n->size);
|
||||
std::string name(name_n->data, name_n->size);
|
||||
while(!mod.empty() && mod.back() == '\0') mod.pop_back();
|
||||
while(!name.empty() && name.back() == '\0') name.pop_back();
|
||||
const wasm_externtype_t* et = wasm_importtype_type(imports.data[i]);
|
||||
CHECK(wasm_externtype_kind(et) == WASM_EXTERN_FUNC, "unexpected non-func import %s.%s", mod.c_str(), name.c_str());
|
||||
|
||||
wasm_func_t* fn = nullptr;
|
||||
if(mod == "env" && name == "uce_host_ctx_read")
|
||||
fn = make_func(g_store, {WASM_I32, WASM_I32}, {WASM_I32}, host_ctx_read);
|
||||
else if(mod == "env" && name == "uce_host_log")
|
||||
fn = make_func(g_store, {WASM_I32, WASM_I32, WASM_I32}, {}, host_log);
|
||||
else
|
||||
{
|
||||
char* label = strdup((mod + "." + name).c_str());
|
||||
const wasm_functype_t* ft = wasm_externtype_as_functype_const(et);
|
||||
fn = wasm_func_new_with_env(g_store, ft, stub_callback, label, nullptr);
|
||||
}
|
||||
CHECK(fn, "failed to create import %s.%s", mod.c_str(), name.c_str());
|
||||
owned_funcs.push_back(fn);
|
||||
externs[i] = wasm_func_as_extern(fn);
|
||||
}
|
||||
|
||||
wasm_extern_vec_t iv = { externs.size(), externs.data() };
|
||||
wasm_trap_t* trap = nullptr;
|
||||
core.instance = wasm_instance_new(g_store, core.module, &iv, &trap);
|
||||
if(trap)
|
||||
{
|
||||
wasm_message_t msg;
|
||||
wasm_trap_message(trap, &msg);
|
||||
FAIL("trap during instantiation: %.*s", (int)msg.size, msg.data);
|
||||
}
|
||||
CHECK(core.instance, "instantiation failed");
|
||||
core.index_exports();
|
||||
g_memory = wasm_extern_as_memory(core.by_name.count("memory") ? core.by_name["memory"] : nullptr);
|
||||
CHECK(g_memory, "core does not export memory");
|
||||
if(core.func("_initialize"))
|
||||
call_i32(core, "_initialize");
|
||||
|
||||
int rc = call_i32(core, "uce_phase2_render");
|
||||
CHECK(rc == 0, "render returned %d", rc);
|
||||
int32_t data = call_i32(core, "uce_phase2_output_data");
|
||||
int32_t size = call_i32(core, "uce_phase2_output_size");
|
||||
uint8_t* mem = (uint8_t*)wasm_memory_data(g_memory);
|
||||
CHECK((size_t)data + (size_t)size <= wasm_memory_data_size(g_memory), "output outside memory");
|
||||
std::string output((const char*)mem + data, size);
|
||||
printf("---- phase2 output (%d bytes) ----\n%s", size, output.c_str());
|
||||
printf("----------------------------------\n");
|
||||
CHECK(output.find("PHASE2 PAGE OK") != std::string::npos, "missing page marker");
|
||||
CHECK(output.find("host=phase2.example.test") != std::string::npos, "missing context param");
|
||||
CHECK(output.find("answer=42") != std::string::npos, "missing nested context value");
|
||||
printf("PHASE2 EXIT CRITERION: PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// WASM-PROPOSAL Phase 2 scaffold page.
|
||||
// This is a real .uce source file with a normal RENDER entry point. The
|
||||
// Phase 2 core statically links it to validate the host-context membrane
|
||||
// before the dynamic loader is introduced in Phase 3.
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
print("PHASE2 PAGE OK\n");
|
||||
print("host=", context.params["HTTP_HOST"], "\n");
|
||||
print("route=", context.call["route"].to_string(), "\n");
|
||||
print("answer=", context.call["nested"]["answer"].to_string(), "\n");
|
||||
}
|
||||
Reference in New Issue
Block a user