mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
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:
1 parent
171b777265
commit
6fb2dcc4ae
4 files changed
+314
-8
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user