page: Mono's wasm JIT in the page (B2): provider_jit_reserve/instantiate/table

- cgen/libprov.rs: a reservation of the function table for the JIT, every Worker filled with
  the JIT-call placeholder as it catches up (wasm-encoder, as natively);
- gex.mjs: each JIT module compiled once per process (shared by content) and instantiated in
  the Worker that runs it, its exports at their slots; the slot queries, with the provider's
  own function types parsed from its module (JS has no type reflection); the s.* globals' types
  from the module's import section;
- gex.mjs: GEX_TRAP_STACK keeps V8's stack traces and puts the wasm frames in the trap message;
- run-node.mjs: GEX_WASM_DLLS.

tests/unity-dotnet/embed with GEXMONO_WASM_JIT=1 MONO_WASM_JIT_THRESHOLD=1: every line as natively
under node (wasm64 shim) and in Chromium; the interpreter-only run too.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_d2453757b8a84fdbb4ee7c9e093141df
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-07 22:33:06 -04:00
1 parent 171b777265
commit 6fb2dcc4ae
4 files changed
+314 -8

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ crate-type = ["cdylib"]
gex-abi.workspace = true
gex-kernel.workspace = true
gex-atomic.workspace = true
wasm-encoder = "0.219.2"
gex-wayland.workspace = true
gex-xwm.workspace = true
gex-xlate = { workspace = true, features = ["fjit"] }
+248 -6
View File
@@ -16,6 +16,120 @@ const m64 = () => !!globalThis.GEX_MEM64;
const LIMIT64_BYTES = 16 * 2 ** 30;
/** A value for an address-typed global: i64 (BigInt) in a wasm64 module. */
const addrVal = (v) => (m64() ? BigInt(v >>> 0) : v | 0);
/** The global imports of a wasm module, as `module.name` -> WebAssembly.Global descriptor
* (JS cannot ask a Module for its imports' types). */
function importedGlobals(bytes) {
const out = new Map();
let p = 8;
const leb = () => {
let r = 0, sh = 0, b;
do { b = bytes[p++]; r |= (b & 0x7f) * 2 ** sh; sh += 7; } while (b & 0x80);
return r;
};
const str = () => { const n = leb(); const s = new TextDecoder().decode(bytes.subarray(p, p + n)); p += n; return s; };
while (p < bytes.length) {
const id = bytes[p++];
const size = leb();
const end = p + size;
if (id === 2) {
const count = leb();
for (let i = 0; i < count; i++) {
const m = str(), n = str(), kind = bytes[p++];
if (kind === 0) leb();
else if (kind === 1) { p++; const f = leb(); leb(); if (f & 1) leb(); }
else if (kind === 2) { const f = leb(); leb(); if (f & 1) leb(); }
else if (kind === 3) {
const t = bytes[p++], mut = bytes[p++];
out.set(`${m}.${n}`, { value: { 0x7f: 'i32', 0x7e: 'i64', 0x7d: 'f32', 0x7c: 'f64' }[t] ?? 'i64', mutable: !!(mut & 1) });
} else if (kind === 4) { p++; leb(); }
}
return out;
}
p = end;
}
return out;
}
/** For each element of a PIC module's (first active) element segment, its function's type
* packed as the provider's JIT asks for it (`provider_jit_table` SIG, the native `wasmjit`):
* bits 0..4 the parameter count, 5..7 the result (0 none, 1 i32, 2 i64, 3 f32, 4 f64), then two
* bits per parameter from bit 8 (0 i32, 1 i64, 2 f32, 3 f64); -1 when not representable. */
function elementSignatures(bytes) {
let p = 8;
const leb = () => {
let r = 0, sh = 0, b;
do { b = bytes[p++]; r += (b & 0x7f) * 2 ** sh; sh += 7; } while (b & 0x80);
return r;
};
const skipStr = () => { const n = leb(); p += n; };
const vcode = { 0x7f: 0, 0x7e: 1, 0x7d: 2, 0x7c: 3 };
const types = [];
let importedFuncs = 0;
const funcTypes = [];
let elems = null;
while (p < bytes.length) {
const id = bytes[p++];
const size = leb();
const end = p + size;
if (id === 1) {
const n = leb();
for (let i = 0; i < n; i++) {
if (bytes[p] === 0x4e || bytes[p] === 0x4f || bytes[p] === 0x50) { types.push(-1); p = end; break; } // rec/sub: not ours
p++; // 0x60
const np = leb();
let packed = np > 28 ? -1 : np, ok = np <= 28;
for (let k = 0; k < np; k++) {
const c = vcode[bytes[p++]];
if (c === undefined) ok = false; else if (ok) packed += c * 2 ** (8 + 2 * k);
}
const nr = leb();
if (nr > 1) ok = false;
for (let k = 0; k < nr; k++) {
const c = vcode[bytes[p++]];
if (c === undefined) ok = false; else if (ok) packed += (c + 1) * 32;
}
types.push(ok ? packed : -1);
}
} else if (id === 2) {
const n = leb();
for (let i = 0; i < n; i++) {
skipStr(); skipStr();
const kind = bytes[p++];
if (kind === 0) { leb(); importedFuncs++; funcTypes.push(-2); }
else if (kind === 1) { p++; const f = leb(); leb(); if (f & 1) leb(); }
else if (kind === 2) { const f = leb(); leb(); if (f & 1) leb(); }
else if (kind === 3) { p += 2; }
else if (kind === 4) { p++; leb(); }
}
} else if (id === 3) {
const n = leb();
for (let i = 0; i < n; i++) funcTypes.push(leb());
} else if (id === 9 && !elems) {
const n = leb();
for (let i = 0; i < n && !elems; i++) {
const flags = leb();
if (flags === 0 || flags === 2) {
if (flags === 2) leb();
while (bytes[p] !== 0x0b) p++; // the offset expression
p++;
if (flags === 2) p++; // elemkind
const m = leb();
elems = [];
for (let k = 0; k < m; k++) elems.push(leb());
} else {
p = end; // other forms: not what a PIC module writes
}
}
}
p = end;
}
// Imported functions' types are not known here (their entries are -2 above): -1.
return (elems ?? []).map((fi) => {
const ti = funcTypes[fi];
return ti === undefined || ti < 0 ? -1 : (types[ti] ?? -1);
});
}
/** An index into the function table: a table64 (BigInt indices) beside a 64-bit memory. */
const tIdx = (v) => (m64() ? BigInt(v) : v);
@@ -2592,6 +2706,11 @@ export class Gex {
},
});
if (wasmprog ? e.gex_wasmprog_relocate(prov) : memBase && e.gex_provider_relocate(memBase)) inst.exports.__wasm_apply_data_relocs?.();
// A wasm64 provider's slots' function types, for its JIT's `provider_jit_table` SIG.
if (libprov && !this.#provSigs.some((x) => x.base === base)) {
const bytes = new Uint8Array(this.#memory.buffer, e.gex_cgen_pmod_wasm_ptr(prov) >>> 0, e.gex_cgen_pmod_wasm_len(prov) >>> 0);
this.#provSigs.push({ base, sigs: elementSignatures(bytes) });
}
const n = regsOf(0, 0) >>> 0;
const buf = e.gex_alloc(4 * Math.max(1, n)) >>> 0;
let regs;
@@ -2687,13 +2806,135 @@ export class Gex {
provider_park: () => e.gex_libprov_park(),
provider_stop_guest: () => e.gex_libprov_stop(),
provider_start_guest: () => e.gex_libprov_start(),
// The runtime's wasm JIT: not yet in the page (B2); it runs interpreted.
provider_jit_reserve: () => 0n,
provider_jit_instantiate: () => -1,
provider_jit_table: () => 0n,
// The runtime's wasm JIT (the native `wasmjit`): a range of the process's function
// table, and each method's module compiled once per process and instantiated in every
// Worker that runs it.
provider_jit_reserve: (n) => {
const base = e.gex_libprov_jit_reserve(BigInt(n));
this.#cgenCatchUp();
return base;
},
provider_jit_instantiate: (...a) => this.#jitInstantiate(...a),
provider_jit_table: (op, a, b) => {
a = BigInt(a);
if (op === 0) {
const f = this.#jitInstalled.get(a);
if (!f) return 0n;
ftab.set(a, f);
return 1n;
}
if (op === 1) {
const f = ftab.get(BigInt(b));
if (!f) throw new Error(`wasmjit: alias of an empty slot ${b}`);
ftab.set(a, f);
this.#jitInstalled.set(a, f);
return 0n;
}
if (op === 2) {
const f = ftab.get(a);
return f ? BigInt(f.length) : -1n;
}
// SIG: from the provider module's own types (JS cannot ask a function for them).
for (const { base, sigs } of this.#provSigs) {
const k = Number(a) - base;
if (k >= 0 && k < sigs.length) return BigInt(sigs[k]);
}
return -1n;
},
};
}
/** Wasm64 providers' table ranges and their slots' packed function types. */
#provSigs = [];
/** The functions this Worker's JIT instantiations installed, by table slot. */
#jitInstalled = new Map();
/** `gex.provider_jit_instantiate(bytes, len, cnames, consts, nconsts, slots, names, n,
* errbuf, errcap)`: compile (once per process, by content) and instantiate a JIT module in
* this Worker, installing its named exports at table slots. 1, or 0 with the reason in
* errbuf. */
#jitInstantiate(bytes, len, cnames, consts, nconsts, slots, names, n, errbuf, errcap) {
const mem = new Uint8Array(this.#memory.buffer);
const N = Number;
try {
const wasm = mem.slice(N(bytes), N(bytes) + N(len));
let h = 0x811c9dc5;
for (let i = 0; i < wasm.length; i++) h = Math.imul(h ^ wasm[i], 16777619) >>> 0;
const name = `wasmjit:${wasm.length}:${h.toString(16)}`;
let mod = this.#sharedModules.get(name);
if (!mod) {
mod = new WebAssembly.Module(wasm);
this.#sharedModules.set(name, mod);
this.#env.shareModule?.(name, mod);
}
const dv = new DataView(this.#memory.buffer);
const cv = new Map();
if (N(nconsts) > 0) {
const nb = mem.slice(N(cnames), N(cnames) + 256);
let at = 0;
for (let k = 0; k < N(nconsts); k++) {
let end = at;
while (end < nb.length && nb[end] !== 0) end++;
cv.set(new TextDecoder().decode(nb.subarray(at, end)), dv.getBigUint64(N(consts) + 8 * k, true));
at = end + 1;
}
}
const globals = importedGlobals(wasm);
const ftab = this.#cgen.ftab;
const imports = { m: { h: this.#memory }, f: { f: ftab }, s: {}, x: { e: this.#providerTag() }, h: {} };
for (const imp of WebAssembly.Module.imports(mod)) {
if (imp.module === 's') {
if (imp.name === 'p') {
imports.s.p = this.#providerStack64();
continue;
}
const v = cv.get(imp.name);
if (v === undefined) throw new Error(`no value for import s.${imp.name}`);
const g = globals.get(`s.${imp.name}`) ?? { value: 'i64', mutable: false };
imports.s[imp.name] = new WebAssembly.Global(g, g.value === 'i64' ? BigInt.asIntN(64, v) : Number(BigInt.asIntN(32, v)));
} else if (imp.module === 'h') {
const f = ftab.get(BigInt(imp.name));
if (!f) throw new Error(`import h.${imp.name}: table slot holds no function`);
imports.h[imp.name] = f;
}
}
const inst = new WebAssembly.Instance(mod, imports);
const k = N(n);
if (k > 0) {
const nb = mem.slice(N(names), N(names) + 64 * k);
const list = [];
let at = 0;
for (let i = 0; i < k; i++) {
let end = at;
while (end < nb.length && nb[end] !== 0) end++;
list.push(new TextDecoder().decode(nb.subarray(at, end)));
at = end + 1;
}
const todo = list.map((nm, i) => {
const f = inst.exports[nm];
if (typeof f !== 'function') throw new Error(`no export ${nm}`);
return [dv.getBigUint64(N(slots) + 8 * i, true), f];
});
for (const [slot, f] of todo) {
ftab.set(slot, f);
this.#jitInstalled.set(slot, f);
}
}
this.#jitInstances.push(inst);
return 1;
} catch (err) {
const cap = N(errcap);
if (N(errbuf) && cap > 0) {
const b = new TextEncoder().encode(String(err?.message ?? err)).subarray(0, cap - 1);
mem.set(b, N(errbuf));
mem[N(errbuf) + b.length] = 0;
}
return 0;
}
}
#jitInstances = [];
/** What a stateful provider imports from `gex` (the native `libprov`). */
#providerImports() {
const e = this.#shim.exports;
@@ -3167,7 +3408,7 @@ export class Gex {
// stack trace per trap that nothing here reads -- hundreds of thousands of
// them for a program with contended locks.
const limit = Error.stackTraceLimit;
if (!globalThis.__gexKeepTrace) Error.stackTraceLimit = 0;
if (!globalThis.__gexKeepTrace && !globalThis.GEX_TRAP_STACK) Error.stackTraceLimit = 0;
try {
return await this.#runLoop(entry, startCtx);
} finally {
@@ -3352,7 +3593,8 @@ export class Gex {
throw new Error(
`the gex runtime trapped inside the shim (${err.message}${at}${lr}${nr}` +
`${code}${heap}${where}). That is a crash in the runtime, not a guest exit; ` +
'anything printed above is all the guest managed to do first.',
'anything printed above is all the guest managed to do first.'
+ (globalThis.GEX_TRAP_STACK ? `\n${err.stack ?? ''}` : ''),
);
}
// A trap abandons the engine's frames without running the epilogues
+2
View File
@@ -307,6 +307,8 @@ const host = new ProcessHost({
dense: process.env.GEX_DENSE === '0' ? false : process.env.GEX_DENSE === '2' ? 2 : undefined,
denseDebug: !!process.env.GEX_DENSE_DEBUG,
mem64: MEM64,
// GEX_WASM_DLLS=soname=path,...: shared objects replaced by wasm64 providers (cgen/libprov.rs).
wasmDlls: process.env.GEX_WASM_DLLS || undefined,
calloutTrace: !!process.env.GEX_CALLOUT_TRACE,
inPlace: process.env.GEX_IN_PLACE === '0' ? 0 : undefined,
excTrace: !!process.env.GEX_EXC_TRACE,
+63 -2
View File
@@ -232,9 +232,70 @@ pub extern "C" fn gex_libprov_publish(u: u32, i: u32) -> i32 {
}
/// The module bytes of provider module `number` when it is a replaced
/// Linux library's (`gex_cgen_pmod_wasm_ptr`/`len`).
/// Linux library's, or a JIT reservation's placeholder (`gex_cgen_pmod_wasm_ptr`/`len`).
pub(crate) fn wasm(number: u32) -> Option<(u32, u32)> {
lock(&LIBS).iter().find(|l| l.number == Some(number)).map(|l| (l.wasm.as_ptr() as usize as u32, l.wasm.len() as u32))
if let Some(r) = lock(&LIBS).iter().find(|l| l.number == Some(number)).map(|l| (l.wasm.as_ptr() as usize as u32, l.wasm.len() as u32)) {
return Some(r);
}
lock(&RESERVES).iter().find(|(n, _)| *n == number).map(|(_, w)| (w.as_ptr() as usize as u32, w.len() as u32))
}
/// The JIT's reserved table ranges: (provider module number, placeholder module).
static RESERVES: Mutex<Vec<(u32, Vec<u8>)>> = Mutex::new(Vec::new());
/// `gex.provider_jit_reserve(n)` (docs/unity-dotnet.md P5 B2, the native
/// `wasmjit`): a range of the process's function table for the provider's
/// JIT, every Worker's filled with the JIT-call placeholder as it catches up.
/// Returns the range's first index.
#[no_mangle]
pub extern "C" fn gex_libprov_jit_reserve(n: u64) -> u64 {
let wasm = placeholder_module(n as u32);
let number = lock(&PROVIDERS).len() as u32;
let install = ftab(|t| t.publish(n as u32, PROVIDER | number, Tier::Unit, &[]));
lock(&PROVIDERS).push(ProviderModule { file: "wasmjit:reserve".into(), regs: Vec::new(), dirs: Vec::new(), slots: 0, memory_base: 0 });
lock(&RESERVES).push((number, wasm));
ftab(|t| t.install(install).base) as u64
}
/// A module of `n` copies of the JIT-call placeholder (the browser's
/// `mono_jiterp_placeholder_jit_call`, `(ret_sp, sp, ftndesc, thrown)`, which
/// stores 999 through `thrown`: "not here yet"), filling the function table
/// from `__table_base`, as the native host makes it.
fn placeholder_module(n: u32) -> Vec<u8> {
use wasm_encoder::*;
let mut m = wasm_encoder::Module::new();
let mut types = TypeSection::new();
types.ty().function([ValType::I64; 4], []);
m.section(&types);
let mut imports = ImportSection::new();
imports.import(
gex_abi::ftab::PIC_MODULE,
"memory",
MemoryType { minimum: 0, maximum: Some(gex_abi::LIMIT64_PAGES), memory64: true, shared: true, page_size_log2: None },
);
imports.import(
gex_abi::ftab::PIC_MODULE,
gex_abi::ftab::PIC_TABLE,
TableType { element_type: RefType::FUNCREF, table64: true, minimum: 0, maximum: None, shared: false },
);
imports.import(gex_abi::ftab::PIC_MODULE, gex_abi::ftab::PIC_TABLE_BASE, GlobalType { val_type: ValType::I64, mutable: false, shared: false });
m.section(&imports);
let mut funcs = FunctionSection::new();
funcs.function(0);
m.section(&funcs);
let mut elems = ElementSection::new();
let idx: Vec<u32> = vec![0; n as usize];
elems.active(Some(0), &ConstExpr::global_get(0), Elements::Functions(idx.into()));
m.section(&elems);
let mut code = CodeSection::new();
let mut f = Function::new([]);
f.instruction(&Instruction::LocalGet(3));
f.instruction(&Instruction::I32Const(999));
f.instruction(&Instruction::I32Store(MemArg { offset: 0, align: 2, memory_index: 0 }));
f.instruction(&Instruction::End);
code.function(&f);
m.section(&code);
m.finish()
}
/// Whether provider module `number` is a replaced Linux library's: its