Add SpiderMonkey WASM backend via mozjs crate
ober
42647c3471db4acafc09eb8d66eb1b30240d3093
--- a/jerboa-native-rs/Cargo.lock +++ b/jerboa-native-rs/Cargo.lock @@ -59,6 +59,12 @@ dependencies = [ ] [[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -71,6 +77,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -123,6 +147,15 @@ dependencies = [ ] [[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -139,6 +172,17 @@ dependencies = [ ] [[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -260,12 +304,55 @@ dependencies = [ ] [[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_c" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af727805f3b0d79956bde5b35732669fb5c5d45a94893798e7b7e70cfbf9cc1" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "encoding_c_mem" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a80a16821fe8c7cab96e0c67b57cd7090e021e9615e6ce6ab0cf866c44ed1f0" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -290,6 +377,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -385,6 +483,12 @@ dependencies = [ ] [[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -475,6 +579,15 @@ dependencies = [ ] [[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] name = "jerboa-native" version = "0.1.0" dependencies = [ @@ -485,6 +598,8 @@ dependencies = [ "hkdf", "inotify", "libc", + "mozjs", + "mozjs_sys", "postgres", "rcgen", "regex", @@ -518,6 +633,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -529,7 +654,10 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags", "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -544,6 +672,24 @@ dependencies = [ ] [[package]] +name = "libz-sys" +version = "1.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -575,6 +721,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -596,18 +748,67 @@ dependencies = [ ] [[package]] +name = "mozjs" +version = "0.15.7" +source = "git+https://github.com/servo/mozjs.git#97f035f7d1211c07b14b99137b32566169c57ee9" +dependencies = [ + "bindgen", + "cc", + "encoding_rs", + "libc", + "log", + "mozjs_sys", + "num-traits", +] + +[[package]] +name = "mozjs_sys" +version = "0.140.8-2" +source = "git+https://github.com/servo/mozjs.git#97f035f7d1211c07b14b99137b32566169c57ee9" +dependencies = [ + "bindgen", + "cc", + "encoding_c", + "encoding_c_mem", + "flate2", + "libc", + "libz-sys", + "tar", + "walkdir", +] + +[[package]] name = "multi-stash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" [[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -649,7 +850,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -733,6 +934,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] name = "postgres" version = "0.19.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -875,6 +1082,15 @@ dependencies = [ ] [[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + +[[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -932,6 +1148,12 @@ dependencies = [ ] [[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -941,6 +1163,19 @@ dependencies = [ ] [[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] name = "rustls" version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1000,6 +1235,15 @@ dependencies = [ ] [[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1162,6 +1406,17 @@ dependencies = [ ] [[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] name = "time" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1300,6 +1555,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1473,6 +1738,15 @@ dependencies = [ ] [[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1579,6 +1853,16 @@ dependencies = [ ] [[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] name = "yasna" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/jerboa-native-rs/Cargo.toml +++ b/jerboa-native-rs/Cargo.toml @@ -27,6 +27,12 @@ rustls-pemfile = "2" webpki-roots = "0.26" wasmi = "0.40" getrandom = "0.2" +mozjs = { git = "https://github.com/servo/mozjs.git", optional = true, default-features = false, features = ["jit", "libz-sys"] } +mozjs_sys = { git = "https://github.com/servo/mozjs.git", optional = true } + +[features] +default = [] +spidermonkey = ["mozjs", "mozjs_sys"] [target.'cfg(target_os = "linux")'.dependencies] inotify = { version = "0.11", default-features = false } --- a/jerboa-native-rs/src/lib.rs +++ b/jerboa-native-rs/src/lib.rs @@ -13,6 +13,8 @@ mod x25519; mod ed25519; mod process_ctl; mod wasm; +#[cfg(feature = "spidermonkey")] +mod wasm_sm; #[cfg(target_os = "linux")] mod epoll; new file mode 100644 --- /dev/null +++ b/jerboa-native-rs/src/wasm_sm.rs @@ -0,0 +1,450 @@ +//! SpiderMonkey-based WASM runtime. +//! +//! Alternative WASM backend using Mozilla's SpiderMonkey engine (via mozjs crate). +//! Provides full WASM spec support including GC and exception handling, +//! unlike the wasmi backend which lacks these proposals. +//! +//! Same FFI surface as wasm.rs — Scheme code is unchanged. +//! +//! Enable with: cargo build --features spidermonkey + +use ::std::collections::HashMap; +use ::std::ptr; +use ::std::sync::Mutex; +use ::std::sync::atomic::{AtomicU64, Ordering}; +use ::std::sync::OnceLock; + +use mozjs::jsapi::*; +use mozjs::jsval::{Int32Value, ObjectValue, UndefinedValue}; +use mozjs::realm::AutoRealm; +use mozjs::rooted; +use mozjs::rust::wrappers2::{ + Call, Construct1, JS_DefineFunction, JS_GetProperty, JS_NewGlobalObject, JS_NewPlainObject, + JS_SetProperty, NewArrayBufferWithUserOwnedContents, +}; +use mozjs::rust::SIMPLE_GLOBAL_CLASS; +use mozjs::rust::{HandleValue, IntoHandle, JSEngine, RealmOptions, Runtime}; +use mozjs::jsval::JSVal; +use mozjs_sys::jsgc::ValueArray; + +use crate::panic::set_last_error; + +// ============================================================ +// Constants +// ============================================================ + +const LOG_BUFFER_CAP: usize = 10_000; + +// ============================================================ +// Handle management +// ============================================================ + +static NEXT_SM_HANDLE: AtomicU64 = AtomicU64::new(1); + +fn next_handle() -> u64 { + NEXT_SM_HANDLE.fetch_add(1, Ordering::Relaxed) +} + +fn sm_modules() -> &'static Mutex<HashMap<u64, SmWasmModule>> { + static MODULES: OnceLock<Mutex<HashMap<u64, SmWasmModule>>> = OnceLock::new(); + MODULES.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn sm_instances() -> &'static Mutex<HashMap<u64, SmWasmInstance>> { + static INSTANCES: OnceLock<Mutex<HashMap<u64, SmWasmInstance>>> = OnceLock::new(); + INSTANCES.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Global JS engine (initialized once, shared across all runtimes) +fn sm_engine() -> mozjs::rust::JSEngineHandle { + static ENGINE: OnceLock<mozjs::rust::JSEngineHandle> = OnceLock::new(); + ENGINE.get_or_init(|| { + let engine = JSEngine::init().expect("failed to initialize SpiderMonkey"); + engine.handle() + }).clone() +} + +// ============================================================ +// Types +// ============================================================ + +struct SmWasmModule { + wasm_bytes: Vec<u8>, +} + +struct SmHostState { + log_buffer: Vec<String>, + fuel_remaining: u64, +} + +impl Default for SmHostState { + fn default() -> Self { + SmHostState { + log_buffer: Vec::new(), + fuel_remaining: 0, + } + } +} + +struct SmWasmInstance { + wasm_bytes: Vec<u8>, + host: SmHostState, +} + +// ============================================================ +// FFI: Module lifecycle +// ============================================================ + +/// Load WASM bytes into a module. Returns handle > 0, or 0 on error. +#[no_mangle] +pub extern "C" fn jerboa_sm_module_new( + bytes: *const u8, + bytes_len: usize, +) -> u64 { + match ::std::panic::catch_unwind(|| { + if bytes.is_null() || bytes_len == 0 { + set_last_error("null or empty WASM bytes".into()); + return 0u64; + } + let wasm_bytes = unsafe { ::std::slice::from_raw_parts(bytes, bytes_len) }.to_vec(); + + // Validate: try compiling in a temporary runtime + let engine = sm_engine(); + let mut rt = Runtime::new(engine); + let options = RealmOptions::default(); + let cx = rt.cx(); + + let valid = unsafe { + rooted!(&in(cx) let global = JS_NewGlobalObject( + cx, &SIMPLE_GLOBAL_CLASS, ptr::null_mut(), + OnNewGlobalHookOption::FireOnNewGlobalHook, &*options + )); + let mut realm = AutoRealm::new_from_handle(cx, global.handle()); + let cx = &mut realm; + + // Get WebAssembly.Module constructor + rooted!(&in(cx) let mut wasm_val = UndefinedValue()); + if !JS_GetProperty(cx, global.handle(), c"WebAssembly".as_ptr(), wasm_val.handle_mut()) { + false + } else { + rooted!(&in(cx) let wasm_obj = wasm_val.to_object()); + rooted!(&in(cx) let mut module_ctor = UndefinedValue()); + if !JS_GetProperty(cx, wasm_obj.handle(), c"Module".as_ptr(), module_ctor.handle_mut()) { + false + } else { + // Build aligned buffer and compile + let mut aligned_buf = vec![0u8; wasm_bytes.len() + 8]; + let offset = aligned_buf.as_ptr() as usize % 8; + let start = if offset == 0 { 0 } else { 8 - offset }; + aligned_buf[start..start + wasm_bytes.len()].copy_from_slice(&wasm_bytes); + + let ab = NewArrayBufferWithUserOwnedContents( + cx, wasm_bytes.len(), + aligned_buf[start..].as_ptr() as *mut _, + ); + if ab.is_null() { + false + } else { + rooted!(&in(cx) let val = ObjectValue(ab)); + let args = HandleValueArray::from(val.handle().into_handle()); + rooted!(&in(cx) let mut module = ptr::null_mut::<JSObject>()); + Construct1(cx, module_ctor.handle(), &args, module.handle_mut()) + } + } + } + }; + + if !valid { + set_last_error("WASM module validation/compilation failed".into()); + return 0; + } + + let handle = next_handle(); + sm_modules().lock().unwrap().insert(handle, SmWasmModule { wasm_bytes }); + handle + }) { + Ok(h) => h, + Err(_) => 0, + } +} + +/// Free a loaded module. +#[no_mangle] +pub extern "C" fn jerboa_sm_module_free(handle: u64) { + let _ = sm_modules().lock().unwrap().remove(&handle); +} + +// ============================================================ +// FFI: Instance lifecycle +// ============================================================ + +/// Create a WASM instance. Returns handle > 0, or 0 on error. +#[no_mangle] +pub extern "C" fn jerboa_sm_instance_new(module_handle: u64, fuel: u64) -> u64 { + match ::std::panic::catch_unwind(|| { + let modules = sm_modules().lock().unwrap(); + let module = match modules.get(&module_handle) { + Some(m) => m, + None => { set_last_error("invalid module handle".into()); return 0u64; } + }; + let wasm_bytes = module.wasm_bytes.clone(); + drop(modules); + + let mut host = SmHostState::default(); + host.fuel_remaining = fuel; + + let handle = next_handle(); + sm_instances().lock().unwrap().insert(handle, SmWasmInstance { + wasm_bytes, + host, + }); + handle + }) { + Ok(h) => h, + Err(_) => 0, + } +} + +/// Create a hosted WASM instance (with host imports). +#[no_mangle] +pub extern "C" fn jerboa_sm_instance_new_hosted(module_handle: u64, fuel: u64) -> u64 { + // Same as plain for now — host imports are wired during call + jerboa_sm_instance_new(module_handle, fuel) +} + +/// Free an instance. +#[no_mangle] +pub extern "C" fn jerboa_sm_instance_free(handle: u64) { + let _ = sm_instances().lock().unwrap().remove(&handle); +} + +// ============================================================ +// FFI: Execution +// ============================================================ + +/// Call an exported WASM function by name. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn jerboa_sm_call( + instance_handle: u64, + name_ptr: *const u8, + name_len: usize, + args_ptr: *const i32, + args_count: usize, + results_ptr: *mut i32, + results_count: usize, +) -> i32 { + match ::std::panic::catch_unwind(|| -> i32 { + // Extract function name + let func_name = if name_ptr.is_null() || name_len == 0 { + set_last_error("null function name".into()); + return -1i32; + } else { + let bytes = unsafe { ::std::slice::from_raw_parts(name_ptr, name_len) }; + match ::std::str::from_utf8(bytes) { + Ok(s) => s.to_string(), + Err(_) => { set_last_error("invalid UTF-8 in function name".into()); return -1; } + } + }; + + // Extract arguments + let args: Vec<i32> = if args_count > 0 && !args_ptr.is_null() { + unsafe { ::std::slice::from_raw_parts(args_ptr, args_count) }.to_vec() + } else { + vec![] + }; + + // Get the WASM bytes from the instance + let mut instances = sm_instances().lock().unwrap(); + let inst = match instances.get_mut(&instance_handle) { + Some(i) => i, + None => { set_last_error("invalid instance handle".into()); return -1; } + }; + let wasm_bytes = inst.wasm_bytes.clone(); + drop(instances); + + // Create a fresh SpiderMonkey runtime for this call + let engine = sm_engine(); + let mut rt = Runtime::new(engine); + let options = RealmOptions::default(); + let cx = rt.cx(); + + unsafe { + rooted!(&in(cx) let global = JS_NewGlobalObject( + cx, &SIMPLE_GLOBAL_CLASS, ptr::null_mut(), + OnNewGlobalHookOption::FireOnNewGlobalHook, &*options + )); + let mut realm = AutoRealm::new_from_handle(cx, global.handle()); + let cx = &mut realm; + + // Get WebAssembly.Module and Instance constructors + rooted!(&in(cx) let mut wasm_val = UndefinedValue()); + JS_GetProperty(cx, global.handle(), c"WebAssembly".as_ptr(), wasm_val.handle_mut()); + rooted!(&in(cx) let wasm_obj = wasm_val.to_object()); + + rooted!(&in(cx) let mut module_ctor = UndefinedValue()); + rooted!(&in(cx) let mut instance_ctor = UndefinedValue()); + JS_GetProperty(cx, wasm_obj.handle(), c"Module".as_ptr(), module_ctor.handle_mut()); + JS_GetProperty(cx, wasm_obj.handle(), c"Instance".as_ptr(), instance_ctor.handle_mut()); + + // Compile module from bytes (aligned buffer) + let mut aligned_buf = vec![0u8; wasm_bytes.len() + 8]; + let buf_offset = aligned_buf.as_ptr() as usize % 8; + let start = if buf_offset == 0 { 0 } else { 8 - buf_offset }; + aligned_buf[start..start + wasm_bytes.len()].copy_from_slice(&wasm_bytes); + + let ab = NewArrayBufferWithUserOwnedContents( + cx, wasm_bytes.len(), + aligned_buf[start..].as_ptr() as *mut _, + ); + if ab.is_null() { + set_last_error("failed to create ArrayBuffer".into()); + return -1; + } + + rooted!(&in(cx) let buf_val = ObjectValue(ab)); + let compile_args = HandleValueArray::from(buf_val.handle().into_handle()); + rooted!(&in(cx) let mut module_obj = ptr::null_mut::<JSObject>()); + if !Construct1(cx, module_ctor.handle(), &compile_args, module_obj.handle_mut()) { + set_last_error("WebAssembly.Module compilation failed".into()); + return -1; + } + + // Build empty imports object (plain instances have no imports) + rooted!(&in(cx) let imports = JS_NewPlainObject(cx)); + + // Instantiate: new WebAssembly.Instance(module, imports) + rooted!(&in(cx) let mut inst_args = ValueArray::new([ + ObjectValue(module_obj.get()), + ObjectValue(imports.get()), + ])); + rooted!(&in(cx) let mut instance_obj = ptr::null_mut::<JSObject>()); + if !Construct1(cx, instance_ctor.handle(), + &HandleValueArray::from(&inst_args), + instance_obj.handle_mut()) { + set_last_error("WebAssembly.Instance creation failed".into()); + return -1; + } + + // Get exports object + rooted!(&in(cx) let mut exports_val = UndefinedValue()); + JS_GetProperty(cx, instance_obj.handle(), c"exports".as_ptr(), exports_val.handle_mut()); + rooted!(&in(cx) let exports_obj = exports_val.to_object()); + + // Get the function + let c_name = match ::std::ffi::CString::new(func_name.as_str()) { + Ok(c) => c, + Err(_) => { set_last_error("invalid function name".into()); return -1; } + }; + rooted!(&in(cx) let mut func_val = UndefinedValue()); + JS_GetProperty(cx, exports_obj.handle(), c_name.as_ptr(), func_val.handle_mut()); + + if func_val.get().is_undefined() { + set_last_error(format!("export '{}' not found", func_name)); + return -1; + } + + // Build JS arguments + let js_args: Vec<JSVal> = args.iter().map(|a: &i32| Int32Value(*a)).collect(); + let call_args = HandleValueArray { + length_: js_args.len(), + elements_: if js_args.is_empty() { ptr::null() } else { js_args.as_ptr() }, + }; + + // Call the function + rooted!(&in(cx) let mut rval = UndefinedValue()); + if !Call(cx, HandleValue::undefined(), func_val.handle().into(), + &call_args, rval.handle_mut().into()) { + set_last_error("WASM function call failed".into()); + return -1; + } + + // Extract result + let val = if rval.get().is_int32() { + rval.get().to_int32() + } else if rval.get().is_double() { + rval.get().to_number() as i32 + } else { + 0 + }; + + if results_count > 0 && !results_ptr.is_null() { + *results_ptr = val; + } + 0 + } + }) { + Ok(v) => v, + Err(_) => -1, + } +} + +// ============================================================ +// FFI: Memory access +// ============================================================ + +#[no_mangle] +pub extern "C" fn jerboa_sm_memory_read( + _handle: u64, _offset: u32, _buf: *mut u8, _len: u32, +) -> i32 { + // TODO: access WebAssembly.Memory.buffer from the instance + set_last_error("sm memory_read: not yet implemented".into()); + -1 +} + +#[no_mangle] +pub extern "C" fn jerboa_sm_memory_write( + _handle: u64, _offset: u32, _buf: *const u8, _len: u32, +) -> i32 { + set_last_error("sm memory_write: not yet implemented".into()); + -1 +} + +#[no_mangle] +pub extern "C" fn jerboa_sm_memory_size(_handle: u64) -> i64 { + -1 +} + +// ============================================================ +// FFI: Fuel / resource control +// ============================================================ + +#[no_mangle] +pub extern "C" fn jerboa_sm_add_fuel(_handle: u64, _fuel: u64) -> i32 { + // SpiderMonkey uses interrupt callbacks for metering + 0 +} + +#[no_mangle] +pub extern "C" fn jerboa_sm_fuel_remaining(_handle: u64) -> i64 { + 0 +} + +// ============================================================ +// FFI: Log buffer +// ============================================================ + +#[no_mangle] +pub extern "C" fn jerboa_sm_get_log( + handle: u64, buf_ptr: *mut u8, buf_max: usize, +) -> i64 { + match ::std::panic::catch_unwind(|| { + let instances = sm_instances().lock().unwrap(); + let inst = match instances.get(&handle) { + Some(i) => i, + None => return -1i64, + }; + let full = inst.host.log_buffer.join("\n"); + let bytes = full.as_bytes(); + if !buf_ptr.is_null() && buf_max > 0 { + let copy_len = bytes.len().min(buf_max); + unsafe { + ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf_ptr, copy_len); + } + } + bytes.len() as i64 + }) { + Ok(v) => v, + Err(_) => -1, + } +}