diff --git a/CLAUDE.md b/CLAUDE.md index 8cbfcbcfdd..eb7cf596f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1540 +**Current Version:** 0.5.1541 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index fe75bff02f..ca65cd0e20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5695,7 +5695,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "base64 0.22.1", @@ -5759,7 +5759,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-dispatch", "serde", @@ -5767,7 +5767,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "cc", "libc", @@ -5776,7 +5776,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "aho-corasick", "anyhow", @@ -5794,7 +5794,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-hir", @@ -5802,7 +5802,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-hir", @@ -5810,7 +5810,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-dispatch", @@ -5819,7 +5819,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-hir", @@ -5827,7 +5827,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "base64 0.22.1", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-hir", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "async-trait", @@ -5876,14 +5876,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "serde", "serde_json", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1540" +version = "0.5.1541" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5902,7 +5902,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "clap", @@ -5917,7 +5917,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "block2", "objc2", @@ -5927,7 +5927,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "argon2", "perry-ffi", @@ -5936,7 +5936,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "reqwest", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "bcrypt", "perry-ffi", @@ -5953,7 +5953,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "rusqlite", @@ -5961,7 +5961,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "scraper", @@ -5969,7 +5969,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "perry-runtime", @@ -5977,7 +5977,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "chrono", "cron", @@ -5987,7 +5987,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "chrono", "perry-ffi", @@ -5995,7 +5995,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "rust_decimal", @@ -6003,7 +6003,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "serde_json", @@ -6011,7 +6011,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -6019,7 +6019,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "perry-runtime", @@ -6027,14 +6027,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "bytes", "http-body-util", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "bytes", "lazy_static", @@ -6065,7 +6065,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "bytes", @@ -6097,7 +6097,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "lazy_static", "perry-ffi", @@ -6107,7 +6107,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6118,7 +6118,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "lru", "perry-ffi", @@ -6127,7 +6127,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "chrono", "perry-ffi", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "bson", "futures-util", @@ -6147,7 +6147,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "chrono", "perry-ffi", @@ -6159,7 +6159,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "nanoid", "perry-ffi", @@ -6168,7 +6168,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "bytes", "perry-ffi", @@ -6183,7 +6183,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6202,7 +6202,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "lettre", "perry-ffi", @@ -6212,7 +6212,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "fancy-regex", "notify", @@ -6224,7 +6224,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "printpdf", @@ -6232,7 +6232,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "sqlx", @@ -6241,7 +6241,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "perry-runtime", @@ -6250,7 +6250,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "governor", "perry-ffi", @@ -6258,7 +6258,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "fast_image_resize", "image", @@ -6269,7 +6269,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "lazy_static", "perry-ffi", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-ffi", @@ -6298,7 +6298,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "perry-runtime", @@ -6307,7 +6307,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "uuid", @@ -6315,7 +6315,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "regex", @@ -6325,7 +6325,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "futures-util", "lazy_static", @@ -6338,7 +6338,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "brotli", "flate2", @@ -6348,7 +6348,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6358,7 +6358,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-api-manifest", @@ -6379,11 +6379,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1540" +version = "0.5.1541" [[package]] name = "perry-parser" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-diagnostics", @@ -6397,7 +6397,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "ahash", "anyhow", @@ -6460,14 +6460,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6563,14 +6563,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "perry-hir", @@ -6579,7 +6579,7 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "perry-ffi", "perry-ui-model", @@ -6587,7 +6587,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "itoa", @@ -6605,7 +6605,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "rand 0.10.2", "serde", @@ -6615,7 +6615,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6638,7 +6638,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "block2", @@ -6655,7 +6655,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "block2", @@ -6672,7 +6672,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1540" +version = "0.5.1541" [[package]] name = "perry-ui-test" @@ -6683,11 +6683,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1540" +version = "0.5.1541" [[package]] name = "perry-ui-tvos" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "block2", @@ -6704,7 +6704,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "block2", @@ -6721,7 +6721,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "block2", "libc", @@ -6735,7 +6735,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "libc", @@ -6754,7 +6754,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "base64 0.22.1", "libc", @@ -6767,7 +6767,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "anyhow", "base64 0.22.1", @@ -6783,7 +6783,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1540" +version = "0.5.1541" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 81b509c61d..de0e094ba3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -336,7 +336,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1540" +version = "0.5.1541" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/10102-wasm-main-side-modules.md b/changelog.d/10102-wasm-main-side-modules.md new file mode 100644 index 0000000000..ad3297912c --- /dev/null +++ b/changelog.d/10102-wasm-main-side-modules.md @@ -0,0 +1,12 @@ +### Fixed + +WebAssembly modules compiled with `--enable-wasm-runtime` can now share imported +functions, tables, memories, and mutable globals across Emscripten main/side +module instances, including imports resolved through a JavaScript `Proxy`. +`WebAssembly.Table`, `WebAssembly.Global`, and host-backed `WebAssembly.Memory` +constructors now expose linkable resources, wasm-bindgen externrefs cross import +callbacks intact, i64 values preserve their exact `BigInt` bits, and byte/file +dynamic imports with `{ with: { type: "wasm" | "file" } }` embed their assets. + +Set `PERRY_WASM_TRACE=1` or `PERRY_WASM_DIAGNOSTICS=1` to report module byte +sizes plus import/export names while diagnosing standalone wasm loading. diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 6c3dddcfce..1af55af496 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -28,7 +28,9 @@ pub(crate) use global_this_webassembly::{ // extraction are reachable only under that feature. #[cfg(feature = "wasm-host")] pub(crate) use global_this_webassembly::{ - register_module_wrapper as register_wasm_module_wrapper, registered_module_handle, + register_extern_wrapper as register_wasm_extern_wrapper, + register_module_wrapper as register_wasm_module_wrapper, registered_extern_handle, + registered_module_handle, }; // Topical sub-modules split out of the original monolithic `global_this.rs` diff --git a/crates/perry-runtime/src/object/global_this_webassembly.rs b/crates/perry-runtime/src/object/global_this_webassembly.rs index ce27d282a9..ec7def9da0 100644 --- a/crates/perry-runtime/src/object/global_this_webassembly.rs +++ b/crates/perry-runtime/src/object/global_this_webassembly.rs @@ -158,6 +158,14 @@ fn module_wrappers() -> &'static std::sync::Mutex>; + +fn extern_wrappers() -> &'static ExternWrapperRegistry { + static REG: std::sync::OnceLock = std::sync::OnceLock::new(); + REG.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())) +} + /// Fast-path latch for the GC hooks. Most programs never construct a wasm /// module, so their ordinary-object move/death path pays only one atomic load /// and never initializes or locks the registry. @@ -178,6 +186,29 @@ pub(crate) fn register_module_wrapper(wrapper: usize, host_handle: usize) { } } +/// Bind a genuine Memory/Table/Global wrapper identity to the opaque host +/// external used when another WebAssembly module imports it. +#[cfg(any(test, feature = "wasm-host"))] +pub(crate) fn register_extern_wrapper(wrapper: usize, kind: &'static [u8], host_handle: usize) { + if wrapper != 0 && host_handle != 0 { + if let Ok(mut wrappers) = extern_wrappers().lock() { + wrappers.insert(wrapper, (kind, host_handle)); + module_wrapper_registry_used().store(true, std::sync::atomic::Ordering::Release); + } + } +} + +pub(crate) fn registered_extern_handle(wrapper: usize, expected_kind: &[u8]) -> Option { + if wrapper == 0 || !module_wrapper_registry_used().load(std::sync::atomic::Ordering::Acquire) { + return None; + } + extern_wrappers().lock().ok().and_then(|wrappers| { + wrappers + .get(&wrapper) + .and_then(|(kind, handle)| (*kind == expected_kind).then_some(*handle)) + }) +} + /// Return the trusted host handle for a registered wrapper identity. A /// poisoned lock or unknown address fails closed. pub(crate) fn registered_module_handle(wrapper: usize) -> Option { @@ -202,6 +233,11 @@ pub(crate) fn module_wrapper_owner_moved(old_wrapper: usize, new_wrapper: usize) wrappers.insert(new_wrapper, host_handle); } } + if let Ok(mut wrappers) = extern_wrappers().lock() { + if let Some(host_handle) = wrappers.remove(&old_wrapper) { + wrappers.insert(new_wrapper, host_handle); + } + } } /// Clear the identity before a dead wrapper's address can be reused. @@ -212,6 +248,9 @@ pub(crate) fn clear_module_wrapper_for_dead_ptr(wrapper: usize) { if let Ok(mut wrappers) = module_wrappers().lock() { wrappers.remove(&wrapper); } + if let Ok(mut wrappers) = extern_wrappers().lock() { + wrappers.remove(&wrapper); + } } // ──────────────────────────────────────────────────────────────────────── @@ -415,14 +454,16 @@ extern "C" fn webassembly_compile_thunk( extern "C" fn webassembly_instantiate_thunk( _closure: *const crate::closure::ClosureHeader, bytes: f64, + imports: f64, ) -> f64 { - crate::webassembly::js_webassembly_instantiate(bytes, undefined()) + crate::webassembly::js_webassembly_instantiate(bytes, imports) } #[cfg(not(feature = "wasm-host"))] extern "C" fn webassembly_instantiate_thunk( _closure: *const crate::closure::ClosureHeader, _bytes: f64, + _imports: f64, ) -> f64 { wasm_unsupported_rejection("WebAssembly.instantiate") } @@ -518,25 +559,49 @@ extern "C" fn webassembly_instance_ctor_thunk( extern "C" fn webassembly_table_ctor_thunk( closure: *const crate::closure::ClosureHeader, - _descriptor: f64, + descriptor: f64, ) -> f64 { if !invoked_as_constructor(closure) { throw_requires_new("WebAssembly.Table"); } - crate::exception::js_throw(wasm_unsupported_error(b"RuntimeError", "WebAssembly.Table")); + #[cfg(feature = "wasm-host")] + { + crate::webassembly::js_webassembly_table_new( + descriptor, + crate::object::js_implicit_this_get(), + ) + } + #[cfg(not(feature = "wasm-host"))] + { + let _ = descriptor; + crate::exception::js_throw(wasm_unsupported_error(b"RuntimeError", "WebAssembly.Table")); + } } extern "C" fn webassembly_global_ctor_thunk( closure: *const crate::closure::ClosureHeader, - _descriptor: f64, + descriptor: f64, + initial: f64, ) -> f64 { if !invoked_as_constructor(closure) { throw_requires_new("WebAssembly.Global"); } - crate::exception::js_throw(wasm_unsupported_error( - b"RuntimeError", - "WebAssembly.Global", - )); + #[cfg(feature = "wasm-host")] + { + crate::webassembly::js_webassembly_global_new( + descriptor, + initial, + crate::object::js_implicit_this_get(), + ) + } + #[cfg(not(feature = "wasm-host"))] + { + let _ = (descriptor, initial); + crate::exception::js_throw(wasm_unsupported_error( + b"RuntimeError", + "WebAssembly.Global", + )); + } } // ── Memory: minimally functional (real ArrayBuffer backing) ───────────── @@ -586,6 +651,21 @@ fn wasm_memory_descriptor_pages(descriptor: f64) -> Result Ok(pages) } +fn wasm_memory_descriptor_maximum(descriptor: f64) -> u32 { + let Some(obj) = value_object_ptr(descriptor) else { + return u32::MAX; + }; + let maximum = crate::value::JSValue::from_bits( + js_object_get_field_by_name_f64(obj, named_key(b"maximum")).to_bits(), + ); + if maximum.is_undefined() { + u32::MAX + } else { + maximum.to_number().trunc().max(0.0).min(u32::MAX as f64) as u32 + } +} + +#[cfg(not(feature = "wasm-host"))] fn wasm_memory_new_buffer(pages: u32) -> f64 { let buf = crate::buffer::js_array_buffer_new((pages * WASM_PAGE_BYTES) as i32); crate::value::js_nanbox_pointer(buf as i64) @@ -609,22 +689,33 @@ extern "C" fn webassembly_memory_ctor_thunk( crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64)); } }; - let buffer = wasm_memory_new_buffer(pages); // The dynamic construct path pre-allocated the receiver with // `Memory.prototype` linked (so `instanceof` works); fill it in place. let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get())); - if let Some(this_obj) = value_object_ptr(this) { - js_object_set_field_by_name(this_obj, named_key(b"buffer"), buffer); - undefined() - } else { - // Reached only from a non-construct dispatch that faked new.target; - // still return a usable standalone instance rather than crashing. - let obj = js_object_alloc(0, 1); - if obj.is_null() { - return undefined(); + #[cfg(feature = "wasm-host")] + { + crate::webassembly::js_webassembly_memory_new( + pages, + wasm_memory_descriptor_maximum(descriptor), + this, + ) + } + #[cfg(not(feature = "wasm-host"))] + { + let buffer = wasm_memory_new_buffer(pages); + if let Some(this_obj) = value_object_ptr(this) { + js_object_set_field_by_name(this_obj, named_key(b"buffer"), buffer); + undefined() + } else { + // Reached only from a non-construct dispatch that faked new.target; + // still return a usable standalone instance rather than crashing. + let obj = js_object_alloc(0, 1); + if obj.is_null() { + return undefined(); + } + js_object_set_field_by_name(obj, named_key(b"buffer"), buffer); + crate::value::js_nanbox_pointer(obj as i64) } - js_object_set_field_by_name(obj, named_key(b"buffer"), buffer); - crate::value::js_nanbox_pointer(obj as i64) } } @@ -642,6 +733,23 @@ fn memory_buffer_ptr(value: f64) -> Option<*mut crate::buffer::BufferHeader> { /// copy (the spec detaches the old buffer; perry's baseline leaves the old /// buffer intact — stale aliases keep reading the pre-grow bytes). fn wasm_memory_grow_on(this: f64, delta: f64) -> Result { + #[cfg(feature = "wasm-host")] + if let Some(object) = value_object_ptr(this) { + if registered_extern_handle(object as usize, b"memory").is_some() { + if !delta.is_finite() || delta < 0.0 { + return Err(MemoryCtorError::Type( + "WebAssembly.Memory.grow(): argument must be a non-negative number", + )); + } + let old = crate::webassembly::js_webassembly_memory_grow(this, delta.trunc() as u32); + if old >= 0.0 { + return Ok(old as u32); + } + return Err(MemoryCtorError::Range( + "WebAssembly.Memory.grow(): could not grow memory", + )); + } + } let Some(this_obj) = value_object_ptr(this) else { return Err(MemoryCtorError::Type( "WebAssembly.Memory.prototype.grow called on an incompatible receiver", @@ -866,6 +974,7 @@ pub(super) fn create_webassembly_namespace() -> f64 { "Global", webassembly_global_ctor_thunk as *const u8, ); + crate::closure::js_register_closure_arity(webassembly_global_ctor_thunk as *const u8, 2); install_webassembly_proto_data(global_ctor, "value", undefined()); install_webassembly_proto_method(global_ctor, "valueOf", 0); @@ -901,6 +1010,7 @@ pub(super) fn create_webassembly_namespace() -> f64 { 1, true, ); + crate::closure::js_register_closure_arity(webassembly_instantiate_thunk as *const u8, 2); install_webassembly_static_fn( ns_obj, "validate", @@ -1302,7 +1412,7 @@ mod tests { "WebAssembly.compile", ); assert_rejected_with_compile_error( - webassembly_instantiate_thunk(closure, undefined()), + webassembly_instantiate_thunk(closure, undefined(), undefined()), "WebAssembly.instantiate", ); assert_rejected_with_compile_error( @@ -1507,6 +1617,10 @@ mod tests { assert_eq!(registered_module_handle(new_wrapper), None); } + // Exercises the ArrayBuffer-backed memory fallback, which #10138 gated to + // `not(wasm-host)` — with the feature on, memory comes from the real host + // and `wasm_memory_new_buffer` does not exist. + #[cfg(not(feature = "wasm-host"))] #[test] fn memory_descriptor_validation_and_buffer_backing() { // Valid: 1 page → 65536-byte zero-filled ArrayBuffer. @@ -1561,6 +1675,7 @@ mod tests { )); } + #[cfg(not(feature = "wasm-host"))] #[test] fn memory_grow_replaces_buffer_and_returns_old_page_count() { let instance = js_object_alloc(0, 1); diff --git a/crates/perry-runtime/src/webassembly.rs b/crates/perry-runtime/src/webassembly.rs index 4c2935e897..4af8fc234d 100644 --- a/crates/perry-runtime/src/webassembly.rs +++ b/crates/perry-runtime/src/webassembly.rs @@ -27,13 +27,20 @@ //! `new WebAssembly.Module(bytes)`, `WebAssembly.compile(bytes)`, and //! `WebAssembly.Module.{exports,imports,customSections}`. //! -//! Numeric args only (i32/i64/f32/f64). Standard surface tracked as -//! follow-up work in the issue thread. +//! Export calls support the four numeric types, while imported callbacks also +//! preserve `externref` JavaScript values and shared `funcref` table entries. use std::ffi::{c_char, c_void}; use crate::value::{JSValue, TAG_UNDEFINED}; +#[path = "webassembly_host.rs"] +mod host; +use host::*; +#[path = "webassembly_calls.rs"] +mod calls; +use calls::*; + const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; @@ -99,8 +106,7 @@ fn extract_bytes(jsval: f64) -> Option<(*const u8, usize)> { { let header = addr as *const crate::buffer::BufferHeader; let len = unsafe { (*header).length as usize }; - let data = - unsafe { crate::buffer::buffer_data(header as *const crate::buffer::BufferHeader) }; + let data = crate::buffer::buffer_data(header as *const crate::buffer::BufferHeader); return Some((data, len)); } @@ -122,142 +128,6 @@ fn extract_string_bytes(jsval: f64) -> Option<(*const u8, usize)> { Some((data, byte_len)) } -// ──────────────────────────────────────────────────────────────────────── -// Forward declarations of the C ABI from perry-wasm-host. These symbols -// only need to resolve at link time when the user's program actually calls -// a `js_webassembly_*` function — otherwise the linker strips this whole -// translation unit. -// ──────────────────────────────────────────────────────────────────────── - -const WASM_VAL_KIND_I32: u8 = 0; -const WASM_VAL_KIND_I64: u8 = 1; -const WASM_VAL_KIND_F32: u8 = 2; -const WASM_VAL_KIND_F64: u8 = 3; -const WASM_VAL_KIND_NONE: u8 = 0xFF; -const WASM_EXTERN_KIND_FUNCTION: u8 = 0; -const WASM_EXTERN_KIND_TABLE: u8 = 1; -const WASM_EXTERN_KIND_MEMORY: u8 = 2; -const WASM_EXTERN_KIND_GLOBAL: u8 = 3; - -type WasmImportCallback = unsafe extern "C" fn( - context: u64, - module: *const u8, - module_len: usize, - name: *const u8, - name_len: usize, - arg_kinds: *const u8, - arg_bits: *const u64, - arg_count: usize, - result_kinds: *const u8, - result_bits: *mut u64, - result_count: usize, -) -> i32; - -extern "C" { - fn perry_wasm_host_string_free(s: *mut c_char); - fn perry_wasm_host_validate(bytes: *const u8, len: usize) -> i32; - fn perry_wasm_host_module_new( - bytes: *const u8, - len: usize, - out_err: *mut *mut c_char, - ) -> *mut c_void; - fn perry_wasm_host_module_drop(module: *mut c_void); - fn perry_wasm_host_module_exports_len(module: *mut c_void) -> usize; - fn perry_wasm_host_module_export_at( - module: *mut c_void, - index: usize, - out_name: *mut *const c_char, - out_name_len: *mut usize, - out_kind: *mut u8, - ) -> i32; - fn perry_wasm_host_module_export_func_arity(module: *mut c_void, index: usize) -> usize; - fn perry_wasm_host_module_imports_len(module: *mut c_void) -> usize; - fn perry_wasm_host_module_import_at( - module: *mut c_void, - index: usize, - out_module: *mut *const c_char, - out_module_len: *mut usize, - out_name: *mut *const c_char, - out_name_len: *mut usize, - out_kind: *mut u8, - ) -> i32; - fn perry_wasm_host_module_custom_sections_len( - module: *mut c_void, - name: *const c_char, - name_len: usize, - ) -> usize; - fn perry_wasm_host_module_custom_section_at( - module: *mut c_void, - name: *const c_char, - name_len: usize, - nth: usize, - out_data: *mut *const u8, - out_data_len: *mut usize, - ) -> i32; - fn perry_wasm_host_instance_new( - module: *mut c_void, - import_callback: Option, - import_context: u64, - out_err: *mut *mut c_char, - ) -> *mut c_void; - #[allow(dead_code)] - fn perry_wasm_host_instance_drop(inst: *mut c_void); - fn perry_wasm_host_instance_memory_span(inst: *mut c_void, out_len: *mut usize) -> *mut u8; - fn perry_wasm_host_instance_export_handle( - inst: *mut c_void, - name: *const c_char, - name_len: usize, - ) -> usize; - fn perry_wasm_host_instance_table_len( - inst: *mut c_void, - name: *const c_char, - name_len: usize, - ) -> usize; - fn perry_wasm_host_instance_table_set( - inst: *mut c_void, - name: *const c_char, - name_len: usize, - index: usize, - bits: u64, - is_null: i32, - ) -> i32; - fn perry_wasm_host_instance_table_grow( - inst: *mut c_void, - name: *const c_char, - name_len: usize, - delta: usize, - bits: u64, - is_null: i32, - out_old_len: *mut usize, - ) -> i32; - fn perry_wasm_host_instance_take_exit_code(inst: *mut c_void, out_code: *mut i32) -> i32; - fn perry_wasm_host_call_export( - inst: *mut c_void, - name: *const c_char, - name_len: usize, - arg_kinds: *const u8, - arg_bits: *const u64, - arg_count: usize, - out_kinds: *mut u8, - out_bits: *mut u64, - out_capacity: usize, - out_count: *mut usize, - out_err: *mut *mut c_char, - ) -> i32; - fn perry_wasm_host_call_export_by_handle( - inst: *mut c_void, - handle: usize, - arg_kinds: *const u8, - arg_bits: *const u64, - arg_count: usize, - out_kinds: *mut u8, - out_bits: *mut u64, - out_capacity: usize, - out_count: *mut usize, - out_err: *mut *mut c_char, - ) -> i32; -} - fn emit_error_to_stderr(prefix: &str, err: *mut c_char) { if !err.is_null() { let cs = unsafe { std::ffi::CStr::from_ptr(err) }; @@ -345,12 +215,12 @@ fn object_set( let obj = scope.root_raw_mut_ptr(obj); let value = scope.root_nanbox_f64(value); let key = scope.root_string_ptr(named_key(key)); - crate::object::js_object_set_field_by_name( - obj.get_raw_mut_ptr::(), - key.get_raw_const_ptr::(), - value.get_nanbox_f64(), - ); - obj.get_raw_mut_ptr::() + obj.with_mut_ptr(|o: *mut crate::object::ObjectHeader| { + key.with_const_ptr(|k: *const crate::string::StringHeader| { + crate::object::js_object_set_field_by_name(o, k, value.get_nanbox_f64()) + }) + }); + obj.with_mut_ptr(|o: *mut crate::object::ObjectHeader| o) } fn object_set_string( @@ -936,6 +806,85 @@ fn resync_memory_after_call(inst: *mut c_void, memory: f64) { } } +fn wasm_import_value( + context: u64, + module: *const u8, + module_len: usize, + name: *const u8, + name_len: usize, +) -> f64 { + if module.is_null() || name.is_null() { + return nanbox_undefined(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let imports = scope.root_nanbox_f64(instance_imports(context)); + let imports_value = JSValue::from_bits(imports.get_nanbox_f64().to_bits()); + if !imports_value.is_pointer() { + return nanbox_undefined(); + } + let module_bytes = unsafe { std::slice::from_raw_parts(module, module_len) }; + let module_key = scope.root_string_ptr(named_key(module_bytes)); + let module_value = scope.root_nanbox_f64(module_key.with_const_ptr( + |k: *const crate::string::StringHeader| { + crate::object::js_object_get_field_by_name_f64( + imports_value.as_pointer::(), + k, + ) + }, + )); + let module_object = JSValue::from_bits(module_value.get_nanbox_f64().to_bits()); + if !module_object.is_pointer() { + return nanbox_undefined(); + } + let name_bytes = unsafe { std::slice::from_raw_parts(name, name_len) }; + let name_key = scope.root_string_ptr(named_key(name_bytes)); + name_key.with_const_ptr(|k: *const crate::string::StringHeader| { + crate::object::js_object_get_field_by_name_f64( + module_object.as_pointer::(), + k, + ) + }) +} + +unsafe extern "C" fn resolve_wasm_import( + context: u64, + module: *const u8, + module_len: usize, + name: *const u8, + name_len: usize, + kind: u8, +) -> *mut c_void { + let value = wasm_import_value(context, module, module_len, name, name_len); + let js = JSValue::from_bits(value.to_bits()); + if !js.is_pointer() { + return std::ptr::null_mut(); + } + let pointer = js.as_pointer::() as usize; + if kind == WASM_EXTERN_KIND_FUNCTION { + let Some(header) = crate::value::addr_class::try_read_gc_header(pointer) else { + return std::ptr::null_mut(); + }; + if header.obj_type != crate::gc::GC_TYPE_CLOSURE { + return std::ptr::null_mut(); + } + let closure = pointer as *const crate::closure::ClosureHeader; + let fp = (*closure).func_ptr; + if is_wasm_export_call_shim(fp) { + return crate::closure::js_closure_get_capture_f64(closure, 6) as usize as *mut c_void; + } + return std::ptr::null_mut(); + } + let expected_kind: &[u8] = match kind { + WASM_EXTERN_KIND_TABLE => b"table", + WASM_EXTERN_KIND_MEMORY => b"memory", + WASM_EXTERN_KIND_GLOBAL => b"global", + _ => return std::ptr::null_mut(), + }; + crate::object::registered_extern_handle(pointer, expected_kind) + .map(|handle| handle as *mut c_void) + .unwrap_or(std::ptr::null_mut()) +} + unsafe extern "C" fn call_wasm_import( context: u64, module: *const u8, @@ -965,28 +914,8 @@ unsafe extern "C" fn call_wasm_import( // `context` is an import TOKEN, not an object address: the host cannot be // told that a collection moved the imports object, so it never holds one. let scope = crate::gc::RuntimeHandleScope::new(); - let imports = scope.root_nanbox_f64(instance_imports(context)); - let imports_value = JSValue::from_bits(imports.get_nanbox_f64().to_bits()); - if !imports_value.is_pointer() { - return 0; - } - - let module_bytes = std::slice::from_raw_parts(module, module_len); - let module_key = scope.root_string_ptr(named_key(module_bytes)); - let module_value = scope.root_nanbox_f64(crate::object::js_object_get_field_by_name_f64( - imports_value.as_pointer::(), - module_key.get_raw_const_ptr::(), - )); - let module_object = JSValue::from_bits(module_value.get_nanbox_f64().to_bits()); - if !module_object.is_pointer() { - return 0; - } - - let name_bytes = std::slice::from_raw_parts(name, name_len); - let name_key = scope.root_string_ptr(named_key(name_bytes)); - let callback = scope.root_nanbox_f64(crate::object::js_object_get_field_by_name_f64( - module_object.as_pointer::(), - name_key.get_raw_const_ptr::(), + let callback = scope.root_nanbox_f64(wasm_import_value( + context, module, module_len, name, name_len, )); let kinds = if arg_count == 0 { @@ -1004,9 +933,10 @@ unsafe extern "C" fn call_wasm_import( .zip(bits.iter()) .map(|(kind, bits)| match *kind { WASM_VAL_KIND_I32 => (*bits as u32 as i32) as f64, - WASM_VAL_KIND_I64 => (*bits as i64) as f64, + WASM_VAL_KIND_I64 => wasm_i64_to_js(*bits), WASM_VAL_KIND_F32 => f32::from_bits(*bits as u32) as f64, WASM_VAL_KIND_F64 => f64::from_bits(*bits), + WASM_VAL_KIND_EXTERNREF => f64::from_bits(*bits), _ => f64::from_bits(TAG_UNDEFINED), }) .collect(); @@ -1026,9 +956,10 @@ unsafe extern "C" fn call_wasm_import( for (kind, bits) in result_kinds.iter().zip(result_bits.iter_mut()) { *bits = match *kind { WASM_VAL_KIND_I32 => result as i32 as u32 as u64, - WASM_VAL_KIND_I64 => result as i64 as u64, + WASM_VAL_KIND_I64 => js_to_wasm_i64_bits(result).unwrap_or(result as i64 as u64), WASM_VAL_KIND_F32 => (result as f32).to_bits() as u64, WASM_VAL_KIND_F64 => result.to_bits(), + WASM_VAL_KIND_EXTERNREF => result.to_bits(), _ => 0, }; } @@ -1043,10 +974,13 @@ fn call_captured_wasm_export(closure: *const crate::closure::ClosureHeader, args let instance = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(closure, 3)); let imports = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(closure, 4)); let handle = crate::closure::js_closure_get_capture_f64(closure, 5) as usize; + let external = crate::closure::js_closure_get_capture_f64(closure, 6) as usize as *mut c_void; // No per-call import-context store: the token the host holds was fixed at // instantiation and cannot go stale, which is the bug this replaced. let _ = &imports; - let result = { + let result = if inst.is_null() && !external.is_null() { + call_external_function(external, args) + } else { let _active = ActiveInstanceGuard::enter(inst); if handle != 0 { call_export_by_handle(inst, handle, args) @@ -1072,41 +1006,6 @@ fn call_captured_wasm_export(closure: *const crate::closure::ClosureHeader, args } } -extern "C" fn js_wasm_export_call_0(closure: *const crate::closure::ClosureHeader) -> f64 { - call_captured_wasm_export(closure, &[]) -} - -extern "C" fn js_wasm_export_call_1(closure: *const crate::closure::ClosureHeader, a: f64) -> f64 { - call_captured_wasm_export(closure, &[a]) -} - -extern "C" fn js_wasm_export_call_2( - closure: *const crate::closure::ClosureHeader, - a: f64, - b: f64, -) -> f64 { - call_captured_wasm_export(closure, &[a, b]) -} - -extern "C" fn js_wasm_export_call_3( - closure: *const crate::closure::ClosureHeader, - a: f64, - b: f64, - c: f64, -) -> f64 { - call_captured_wasm_export(closure, &[a, b, c]) -} - -extern "C" fn js_wasm_export_call_4( - closure: *const crate::closure::ClosureHeader, - a: f64, - b: f64, - c: f64, - d: f64, -) -> f64 { - call_captured_wasm_export(closure, &[a, b, c, d]) -} - fn make_export_function( inst: *mut c_void, name: &[u8], @@ -1119,20 +1018,17 @@ fn make_export_function( let memory = scope.root_nanbox_f64(memory); let instance = scope.root_nanbox_f64(instance); let imports = scope.root_nanbox_f64(imports); - let (func_ptr, declared_arity) = match arity { - 0 => (js_wasm_export_call_0 as *const u8, 0), - 1 => (js_wasm_export_call_1 as *const u8, 1), - 2 => (js_wasm_export_call_2 as *const u8, 2), - 3 => (js_wasm_export_call_3 as *const u8, 3), - _ => (js_wasm_export_call_4 as *const u8, 4), - }; + let (func_ptr, declared_arity) = wasm_export_call_shim_for_arity(arity); // Resolve the export ONCE here rather than by name on every call: the // per-call `get_func` probe plus `FuncType` clone was the sub-microsecond // floor left under the linear-memory copy this binding removed (#9611). let handle = unsafe { perry_wasm_host_instance_export_handle(inst, name.as_ptr() as *const c_char, name.len()) }; - let closure = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc(func_ptr, 6)); + let external = unsafe { + perry_wasm_host_instance_export_extern(inst, name.as_ptr() as *const c_char, name.len()) + }; + let closure = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc(func_ptr, 7)); if closure .get_raw_mut_ptr::() .is_null() @@ -1148,6 +1044,7 @@ fn make_export_function( crate::closure::js_closure_set_capture_f64(closure_ptr, 3, instance.get_nanbox_f64()); crate::closure::js_closure_set_capture_f64(closure_ptr, 4, imports.get_nanbox_f64()); crate::closure::js_closure_set_capture_f64(closure_ptr, 5, handle as f64); + crate::closure::js_closure_set_capture_f64(closure_ptr, 6, external as usize as f64); crate::object::set_bound_native_closure_name( closure_ptr, std::str::from_utf8(name).unwrap_or("wasm"), @@ -1159,14 +1056,16 @@ fn table_method_context<'scope>( scope: &'scope crate::gc::RuntimeHandleScope, closure: *const crate::closure::ClosureHeader, ) -> ( + *mut c_void, *mut c_void, crate::gc::RuntimeHandle<'scope>, crate::gc::RuntimeHandle<'scope>, ) { - let inst = crate::closure::js_closure_get_capture_f64(closure, 0) as usize as *mut c_void; - let name = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(closure, 1)); - let table = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(closure, 2)); - (inst, name, table) + let external = crate::closure::js_closure_get_capture_f64(closure, 0) as usize as *mut c_void; + let inst = crate::closure::js_closure_get_capture_f64(closure, 1) as usize as *mut c_void; + let name = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(closure, 2)); + let table = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(closure, 3)); + (external, inst, name, table) } fn table_values(table: f64) -> f64 { @@ -1186,18 +1085,84 @@ fn table_values(table: f64) -> f64 { }) } +fn wasm_function_external(value: f64) -> *mut c_void { + let value = JSValue::from_bits(value.to_bits()); + if !value.is_pointer() { + return std::ptr::null_mut(); + } + let closure = value.as_pointer::(); + let Some(header) = (unsafe { crate::value::addr_class::try_read_gc_header(closure as usize) }) + else { + return std::ptr::null_mut(); + }; + if header.obj_type != crate::gc::GC_TYPE_CLOSURE { + return std::ptr::null_mut(); + } + let fp = unsafe { (*closure).func_ptr }; + if !is_wasm_export_call_shim(fp) { + return std::ptr::null_mut(); + } + crate::closure::js_closure_get_capture_f64(closure, 6) as usize as *mut c_void +} + extern "C" fn js_wasm_table_get(closure: *const crate::closure::ClosureHeader, index: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let (_inst, _name, table) = table_method_context(&scope, closure); + let (external, inst, name, table) = table_method_context(&scope, closure); let values = scope.root_nanbox_f64(table_values(table.get_nanbox_f64())); - let values = JSValue::from_bits(values.get_nanbox_f64().to_bits()); - if !values.is_pointer() || !index.is_finite() || index < 0.0 { + let values_value = JSValue::from_bits(values.get_nanbox_f64().to_bits()); + if !values_value.is_pointer() || !index.is_finite() || index < 0.0 { return nanbox_undefined(); } - crate::array::js_array_get_f64( - values.as_pointer::(), + let index = index as usize; + let cached = crate::array::js_array_get_f64( + values_value.as_pointer::(), index as u32, - ) + ); + if cached.to_bits() != crate::value::TAG_NULL { + return cached; + } + let mut bits = 0u64; + let mut is_null = 0i32; + let mut function_external = std::ptr::null_mut(); + let ok = if inst.is_null() { + unsafe { + perry_wasm_host_table_get( + external, + index, + &mut bits, + &mut is_null, + &mut function_external, + ) + } + } else if let Some((name_ptr, name_len)) = extract_string_bytes(name.get_nanbox_f64()) { + unsafe { + perry_wasm_host_instance_table_get( + inst, + name_ptr.cast(), + name_len, + index, + &mut bits, + &mut is_null, + &mut function_external, + ) + } + } else { + 0 + }; + if ok == 0 { + return nanbox_undefined(); + } + let value = if is_null != 0 { + f64::from_bits(crate::value::TAG_NULL) + } else if !function_external.is_null() { + make_table_function(function_external) + } else { + f64::from_bits(bits) + }; + let values_ptr = + values_value.as_pointer::() as *mut crate::array::ArrayHeader; + crate::array::js_array_set_f64(values_ptr, index as u32, value); + value } extern "C" fn js_wasm_table_set( @@ -1206,20 +1171,26 @@ extern "C" fn js_wasm_table_set( value: f64, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let (inst, name, table) = table_method_context(&scope, closure); - let Some((name_ptr, name_len)) = extract_string_bytes(name.get_nanbox_f64()) else { - return nanbox_undefined(); - }; + let (external, inst, name, table) = table_method_context(&scope, closure); let is_null = (value.to_bits() == crate::value::TAG_NULL) as i32; - let ok = unsafe { - perry_wasm_host_instance_table_set( - inst, - name_ptr as *const c_char, - name_len, - index.max(0.0) as usize, - value.to_bits(), - is_null, - ) + let index = index.max(0.0) as usize; + let function = wasm_function_external(value); + let ok = if inst.is_null() { + unsafe { perry_wasm_host_table_set(external, index, value.to_bits(), is_null, function) } + } else if let Some((name_ptr, name_len)) = extract_string_bytes(name.get_nanbox_f64()) { + unsafe { + perry_wasm_host_instance_table_set( + inst, + name_ptr.cast(), + name_len, + index, + value.to_bits(), + is_null, + function, + ) + } + } else { + 0 }; if ok != 0 { let values = scope.root_nanbox_f64(table_values(table.get_nanbox_f64())); @@ -1227,8 +1198,12 @@ extern "C" fn js_wasm_table_set( if values_value.is_pointer() { let values_ptr = values_value.as_pointer::() as *mut crate::array::ArrayHeader; - crate::array::js_array_set_f64(values_ptr, index.max(0.0) as u32, value); + crate::array::js_array_set_f64(values_ptr, index as u32, value); } + } else { + crate::exception::js_throw(wasm_type_error_value( + "WebAssembly.Table.set(): value is not a WebAssembly function", + )); } nanbox_undefined() } @@ -1239,26 +1214,34 @@ extern "C" fn js_wasm_table_grow( value: f64, ) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); - let (inst, name, table) = table_method_context(&scope, closure); - let Some((name_ptr, name_len)) = extract_string_bytes(name.get_nanbox_f64()) else { - return nanbox_undefined(); - }; + let (external, inst, name, table) = table_method_context(&scope, closure); let value_bits = value.to_bits(); let is_null = matches!( value_bits, crate::value::TAG_NULL | crate::value::TAG_UNDEFINED ) as i32; let mut old_len = 0usize; - let ok = unsafe { - perry_wasm_host_instance_table_grow( - inst, - name_ptr as *const c_char, - name_len, - delta.max(0.0) as usize, - value_bits, - is_null, - &mut old_len, - ) + let delta = delta.max(0.0) as usize; + let function = wasm_function_external(value); + let ok = if inst.is_null() { + unsafe { + perry_wasm_host_table_grow(external, delta, value_bits, is_null, function, &mut old_len) + } + } else if let Some((name_ptr, name_len)) = extract_string_bytes(name.get_nanbox_f64()) { + unsafe { + perry_wasm_host_instance_table_grow( + inst, + name_ptr.cast(), + name_len, + delta, + value_bits, + is_null, + function, + &mut old_len, + ) + } + } else { + 0 }; if ok == 0 { return nanbox_undefined(); @@ -1275,7 +1258,7 @@ extern "C" fn js_wasm_table_grow( } else { value }; - for _ in 0..delta.max(0.0) as usize { + for _ in 0..delta { values_ptr = crate::array::js_array_push_f64(values_ptr, fill); values.set_nanbox_f64(array_value(values_ptr)); } @@ -1285,13 +1268,14 @@ extern "C" fn js_wasm_table_grow( table_value.as_pointer::() as *mut crate::object::ObjectHeader, b"length", - old_len.saturating_add(delta.max(0.0) as usize) as f64, + old_len.saturating_add(delta) as f64, ); } old_len as f64 } fn make_table_method( + external: *mut c_void, inst: *mut c_void, name: f64, table: f64, @@ -1302,19 +1286,22 @@ fn make_table_method( let scope = crate::gc::RuntimeHandleScope::new(); let name = scope.root_nanbox_f64(name); let table = scope.root_nanbox_f64(table); - let closure = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc(func_ptr, 3)); + let closure = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc(func_ptr, 4)); if closure.with_mut_ptr(|closure: *mut crate::closure::ClosureHeader| closure.is_null()) { return nanbox_undefined(); } crate::closure::js_register_closure_arity(func_ptr, arity); closure.with_mut_ptr(|closure: *mut crate::closure::ClosureHeader| { - crate::closure::js_closure_set_capture_f64(closure, 0, inst as usize as f64) + crate::closure::js_closure_set_capture_f64(closure, 0, external as usize as f64) + }); + closure.with_mut_ptr(|closure: *mut crate::closure::ClosureHeader| { + crate::closure::js_closure_set_capture_f64(closure, 1, inst as usize as f64) }); closure.with_mut_ptr(|closure: *mut crate::closure::ClosureHeader| { - crate::closure::js_closure_set_capture_f64(closure, 1, name.get_nanbox_f64()) + crate::closure::js_closure_set_capture_f64(closure, 2, name.get_nanbox_f64()) }); closure.with_mut_ptr(|closure: *mut crate::closure::ClosureHeader| { - crate::closure::js_closure_set_capture_f64(closure, 2, table.get_nanbox_f64()) + crate::closure::js_closure_set_capture_f64(closure, 3, table.get_nanbox_f64()) }); closure.with_mut_ptr(|closure: *mut crate::closure::ClosureHeader| { crate::object::set_bound_native_closure_name(closure, display_name) @@ -1324,15 +1311,47 @@ fn make_table_method( }) } -fn make_export_table(inst: *mut c_void, name: &[u8]) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let table = scope.root_nanbox_f64(object_value(crate::object::js_object_alloc(0, 0))); - let name_value = scope.root_nanbox_f64(string_value(name)); - let Some((name_ptr, name_len)) = extract_string_bytes(name_value.get_nanbox_f64()) else { +fn make_table_function(external: *mut c_void) -> f64 { + let arity = unsafe { perry_wasm_host_func_arity(external) }; + if arity == usize::MAX { return nanbox_undefined(); + } + let (func_ptr, declared_arity) = wasm_export_call_shim_for_arity(arity); + let closure = crate::closure::js_closure_alloc(func_ptr, 7); + if closure.is_null() { + return nanbox_undefined(); + } + crate::closure::js_register_closure_arity(func_ptr, declared_arity); + for index in 0..6 { + crate::closure::js_closure_set_capture_f64(closure, index, nanbox_undefined()); + } + crate::closure::js_closure_set_capture_f64(closure, 0, 0.0); + crate::closure::js_closure_set_capture_f64(closure, 5, 0.0); + crate::closure::js_closure_set_capture_f64(closure, 6, external as usize as f64); + crate::object::set_bound_native_closure_name(closure, "wasm-table-function"); + crate::value::js_nanbox_pointer(closure as i64) +} + +fn make_table_object(external: *mut c_void, inst: *mut c_void, name: f64, receiver: f64) -> f64 { + if external.is_null() { + return nanbox_undefined(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let name = scope.root_nanbox_f64(name); + let receiver = JSValue::from_bits(receiver.to_bits()); + let table_ptr = if receiver.is_pointer() { + receiver.as_pointer::() as *mut crate::object::ObjectHeader + } else { + crate::object::js_object_alloc(0, 0) + }; + let table = scope.root_nanbox_f64(object_value(table_ptr)); + let len = if inst.is_null() { + unsafe { perry_wasm_host_table_len(external) } + } else if let Some((name_ptr, name_len)) = extract_string_bytes(name.get_nanbox_f64()) { + unsafe { perry_wasm_host_instance_table_len(inst, name_ptr.cast(), name_len) } + } else { + usize::MAX }; - let len = - unsafe { perry_wasm_host_instance_table_len(inst, name_ptr as *const c_char, name_len) }; if len == usize::MAX { return nanbox_undefined(); } @@ -1356,8 +1375,9 @@ fn make_export_table(inst: *mut c_void, name: &[u8]) -> f64 { ]; for (method_name, func_ptr, arity) in methods { let method = scope.root_nanbox_f64(make_table_method( + external, inst, - name_value.get_nanbox_f64(), + name.get_nanbox_f64(), table.get_nanbox_f64(), func_ptr, arity, @@ -1372,9 +1392,309 @@ fn make_export_table(inst: *mut c_void, name: &[u8]) -> f64 { .as_pointer::() as *mut crate::object::ObjectHeader; let _ = object_set(table_ptr, b"length", len as f64); + let table_ptr = JSValue::from_bits(table.get_nanbox_f64().to_bits()) + .as_pointer::(); + crate::object::register_wasm_extern_wrapper(table_ptr as usize, b"table", external as usize); table.get_nanbox_f64() } +fn make_export_table(inst: *mut c_void, name: &[u8]) -> f64 { + let name_value = string_value(name); + let external = + unsafe { perry_wasm_host_instance_export_extern(inst, name.as_ptr().cast(), name.len()) }; + make_table_object(external, inst, name_value, nanbox_undefined()) +} + +fn global_handle_from_receiver() -> Option<*mut c_void> { + let receiver = JSValue::from_bits(crate::object::js_implicit_this_get().to_bits()); + if !receiver.is_pointer() { + return None; + } + crate::object::registered_extern_handle( + receiver.as_pointer::() as usize, + b"global", + ) + .map(|handle| handle as *mut c_void) +} + +extern "C" fn js_wasm_global_get(_closure: *const crate::closure::ClosureHeader) -> f64 { + let Some(handle) = global_handle_from_receiver() else { + return nanbox_undefined(); + }; + let mut kind = WASM_VAL_KIND_NONE; + let mut bits = 0u64; + if unsafe { perry_wasm_host_global_get(handle, &mut kind, &mut bits) } == 0 { + return nanbox_undefined(); + } + decode_wasm_value(kind, bits) +} + +extern "C" fn js_wasm_global_set( + _closure: *const crate::closure::ClosureHeader, + value: f64, +) -> f64 { + let Some(handle) = global_handle_from_receiver() else { + return nanbox_undefined(); + }; + let mut kind = WASM_VAL_KIND_NONE; + let mut previous = 0u64; + if unsafe { perry_wasm_host_global_get(handle, &mut kind, &mut previous) } == 0 { + return nanbox_undefined(); + } + let bits = match kind { + WASM_VAL_KIND_I32 => value as i32 as u32 as u64, + WASM_VAL_KIND_I64 => js_to_wasm_i64_bits(value).unwrap_or(value as i64 as u64), + WASM_VAL_KIND_F32 => (value as f32).to_bits() as u64, + WASM_VAL_KIND_F64 => value.to_bits(), + _ => return nanbox_undefined(), + }; + let _ = unsafe { perry_wasm_host_global_set(handle, kind, bits) }; + nanbox_undefined() +} + +fn make_global_object(external: *mut c_void, receiver: f64) -> f64 { + if external.is_null() { + return nanbox_undefined(); + } + let receiver_value = JSValue::from_bits(receiver.to_bits()); + let object = if receiver_value.is_pointer() { + receiver_value.as_pointer::() + as *mut crate::object::ObjectHeader + } else { + crate::object::js_object_alloc(0, 0) + }; + if object.is_null() { + return nanbox_undefined(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let object = scope.root_raw_mut_ptr(object); + let placeholder = scope.root_string_ptr(named_key(b"value")); + crate::object::js_object_set_field_by_name( + object.get_raw_mut_ptr::(), + placeholder.get_raw_const_ptr::(), + nanbox_undefined(), + ); + let getter_fp = js_wasm_global_get as *const u8; + let setter_fp = js_wasm_global_set as *const u8; + crate::closure::js_register_closure_arity(getter_fp, 0); + crate::closure::js_register_closure_arity(setter_fp, 1); + let getter = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc(getter_fp, 0)); + let setter = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc(setter_fp, 0)); + let object_ptr = object.get_raw_mut_ptr::(); + crate::object::set_builtin_accessor_descriptor( + object_ptr as usize, + "value".to_string(), + crate::object::AccessorDescriptor { + get: crate::value::js_nanbox_pointer( + getter.get_raw_mut_ptr::() as i64, + ) + .to_bits(), + set: crate::value::js_nanbox_pointer( + setter.get_raw_mut_ptr::() as i64, + ) + .to_bits(), + }, + crate::object::PropertyAttrs::new(true, false, false), + ); + crate::object::register_wasm_extern_wrapper(object_ptr as usize, b"global", external as usize); + object_value(object_ptr) +} + +fn make_export_global(inst: *mut c_void, name: &[u8]) -> f64 { + let external = + unsafe { perry_wasm_host_instance_export_extern(inst, name.as_ptr().cast(), name.len()) }; + make_global_object(external, nanbox_undefined()) +} + +fn descriptor_object(value: f64) -> Option<*mut crate::object::ObjectHeader> { + let value = JSValue::from_bits(value.to_bits()); + if !value.is_pointer() { + return None; + } + let object = + value.as_pointer::() as *mut crate::object::ObjectHeader; + let header = unsafe { crate::value::addr_class::try_read_gc_header(object as usize)? }; + (header.obj_type == crate::gc::GC_TYPE_OBJECT).then_some(object) +} + +fn descriptor_field(object: *mut crate::object::ObjectHeader, name: &[u8]) -> f64 { + crate::object::js_object_get_field_by_name_f64(object, named_key(name)) +} + +fn descriptor_string(object: *mut crate::object::ObjectHeader, name: &[u8]) -> Option> { + let value = descriptor_field(object, name); + let (ptr, len) = extract_string_bytes(value)?; + Some(unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec()) +} + +fn wasm_numeric_kind(name: &[u8]) -> Option { + match name { + b"i32" => Some(WASM_VAL_KIND_I32), + b"i64" => Some(WASM_VAL_KIND_I64), + b"f32" => Some(WASM_VAL_KIND_F32), + b"f64" => Some(WASM_VAL_KIND_F64), + _ => None, + } +} + +#[no_mangle] +pub extern "C" fn js_webassembly_table_new(descriptor: f64, receiver: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let descriptor = scope.root_nanbox_f64(descriptor); + let receiver = scope.root_nanbox_f64(receiver); + let Some(object) = descriptor_object(descriptor.get_nanbox_f64()) else { + crate::exception::js_throw(wasm_type_error_value( + "WebAssembly.Table(): argument must be a table descriptor object", + )); + }; + let element = match descriptor_string(object, b"element").as_deref() { + Some(b"anyfunc" | b"funcref") => 0, + Some(b"externref") => 1, + _ => crate::exception::js_throw(wasm_type_error_value( + "WebAssembly.Table(): descriptor property 'element' must be anyfunc, funcref, or externref", + )), + }; + let initial = JSValue::from_bits(descriptor_field(object, b"initial").to_bits()).to_number(); + if !initial.is_finite() || initial < 0.0 || initial > u32::MAX as f64 { + crate::exception::js_throw(wasm_type_error_value( + "WebAssembly.Table(): descriptor property 'initial' must be a non-negative number", + )); + } + let maximum_value = JSValue::from_bits(descriptor_field(object, b"maximum").to_bits()); + let maximum = if maximum_value.is_undefined() { + u32::MAX + } else { + let maximum = maximum_value.to_number(); + if !maximum.is_finite() + || maximum < initial.trunc() + || maximum < 0.0 + || maximum > u32::MAX as f64 + { + crate::exception::js_throw(wasm_type_error_value( + "WebAssembly.Table(): 'maximum' must be at least 'initial'", + )); + } + maximum.trunc() as u32 + }; + let external = unsafe { perry_wasm_host_table_new(element, initial.trunc() as u32, maximum) }; + if external.is_null() { + crate::exception::js_throw(wasm_error_value_from_host( + b"RuntimeError", + std::ptr::null_mut(), + "WebAssembly.Table(): allocation failed", + )); + } + make_table_object( + external, + std::ptr::null_mut(), + nanbox_undefined(), + receiver.get_nanbox_f64(), + ) +} + +#[no_mangle] +pub extern "C" fn js_webassembly_global_new(descriptor: f64, initial: f64, receiver: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let descriptor = scope.root_nanbox_f64(descriptor); + let receiver = scope.root_nanbox_f64(receiver); + let Some(object) = descriptor_object(descriptor.get_nanbox_f64()) else { + crate::exception::js_throw(wasm_type_error_value( + "WebAssembly.Global(): argument must be a global descriptor object", + )); + }; + let Some(kind) = descriptor_string(object, b"value").and_then(|name| wasm_numeric_kind(&name)) + else { + crate::exception::js_throw(wasm_type_error_value( + "WebAssembly.Global(): descriptor property 'value' must be a numeric value type", + )); + }; + let mutable = crate::value::js_is_truthy(descriptor_field(object, b"mutable")) != 0; + let bits = match kind { + WASM_VAL_KIND_I32 => initial as i32 as u32 as u64, + WASM_VAL_KIND_I64 => js_to_wasm_i64_bits(initial).unwrap_or(initial as i64 as u64), + WASM_VAL_KIND_F32 => (initial as f32).to_bits() as u64, + WASM_VAL_KIND_F64 => initial.to_bits(), + _ => 0, + }; + let external = unsafe { perry_wasm_host_global_new(kind, mutable as i32, bits) }; + if external.is_null() { + crate::exception::js_throw(wasm_error_value_from_host( + b"RuntimeError", + std::ptr::null_mut(), + "WebAssembly.Global(): allocation failed", + )); + } + make_global_object(external, receiver.get_nanbox_f64()) +} + +#[no_mangle] +pub extern "C" fn js_webassembly_memory_new(initial: u32, maximum: u32, receiver: f64) -> f64 { + let external = unsafe { perry_wasm_host_memory_new(initial, maximum) }; + if external.is_null() { + crate::exception::js_throw(wasm_error_value_from_host( + b"RuntimeError", + std::ptr::null_mut(), + "WebAssembly.Memory(): allocation failed", + )); + } + let mut len = 0usize; + let data = unsafe { perry_wasm_host_memory_span(external, &mut len) }; + let buffer = crate::buffer::buffer_alloc_foreign(data, len.min(u32::MAX as usize) as u32); + if buffer.is_null() { + return nanbox_undefined(); + } + crate::buffer::mark_as_array_buffer(buffer as usize); + let receiver_value = JSValue::from_bits(receiver.to_bits()); + let object = if receiver_value.is_pointer() { + receiver_value.as_pointer::() + as *mut crate::object::ObjectHeader + } else { + crate::object::js_object_alloc(0, 0) + }; + let object = object_set( + object, + b"buffer", + crate::value::js_nanbox_pointer(buffer as i64), + ); + crate::object::register_wasm_extern_wrapper(object as usize, b"memory", external as usize); + object_value(object) +} + +#[no_mangle] +pub extern "C" fn js_webassembly_memory_grow(memory_value: f64, delta: u32) -> f64 { + let value = JSValue::from_bits(memory_value.to_bits()); + if !value.is_pointer() { + return -1.0; + } + let object = + value.as_pointer::() as *mut crate::object::ObjectHeader; + let Some(handle) = crate::object::registered_extern_handle(object as usize, b"memory") else { + return -1.0; + }; + let old_pages = unsafe { perry_wasm_host_memory_grow(handle as *mut c_void, delta) }; + if old_pages < 0 { + return -1.0; + } + let previous = crate::object::js_object_get_field_by_name_f64(object, named_key(b"buffer")); + let previous = unbox_pointer(previous) as usize; + let mut len = 0usize; + let data = unsafe { perry_wasm_host_memory_span(handle as *mut c_void, &mut len) }; + let buffer = crate::buffer::buffer_alloc_foreign(data, len.min(u32::MAX as usize) as u32); + if buffer.is_null() { + return -1.0; + } + crate::buffer::mark_as_array_buffer(buffer as usize); + let _ = object_set( + object, + b"buffer", + crate::value::js_nanbox_pointer(buffer as i64), + ); + if previous != 0 && previous != buffer as usize { + crate::buffer::detach_array_buffer(previous); + } + old_pages as f64 +} + fn make_instance_value(module: *mut c_void, inst: *mut c_void, imports: f64, receiver: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let imports = scope.root_nanbox_f64(imports); @@ -1387,7 +1707,11 @@ fn make_instance_value(module: *mut c_void, inst: *mut c_void, imports: f64, rec let object = object.with_mut_ptr(|object: *mut crate::object::ObjectHeader| { object_set(object, b"buffer", buffer.get_nanbox_f64()) }); - scope.root_nanbox_f64(object_value(object)) + let object_value = scope.root_nanbox_f64(object_value(object)); + let external = + unsafe { perry_wasm_host_instance_export_extern(inst, b"memory".as_ptr().cast(), 6) }; + crate::object::register_wasm_extern_wrapper(object as usize, b"memory", external as usize); + object_value }; // `new WebAssembly.Instance(...)` arrives with a receiver whose // [[Prototype]] was already linked to `WebAssembly.Instance.prototype` by @@ -1426,6 +1750,7 @@ fn make_instance_value(module: *mut c_void, inst: *mut c_void, imports: f64, rec ), WASM_EXTERN_KIND_MEMORY => memory.get_nanbox_f64(), WASM_EXTERN_KIND_TABLE => make_export_table(inst, name), + WASM_EXTERN_KIND_GLOBAL => make_export_global(inst, name), _ => nanbox_undefined(), }); let exports_ptr = object_set( @@ -1448,30 +1773,6 @@ fn make_instance_value(module: *mut c_void, inst: *mut c_void, imports: f64, rec instance.get_nanbox_f64() } -fn make_instance_result(module: *mut c_void, inst: *mut c_void, imports: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let instance = scope.root_nanbox_f64(make_instance_value( - module, - inst, - imports, - nanbox_undefined(), - )); - - let result = scope.root_raw_mut_ptr(crate::object::js_object_alloc(0, 0)); - let module_value = scope.root_nanbox_f64(make_module_object(module)); - let result_ptr = result.with_mut_ptr(|r: *mut crate::object::ObjectHeader| { - object_set(r, b"module", module_value.get_nanbox_f64()) - }); - result.set_raw_mut_ptr(result_ptr); - let result_ptr = object_set( - result.get_raw_mut_ptr::(), - b"instance", - instance.get_nanbox_f64(), - ); - result.set_raw_mut_ptr(result_ptr); - result.with_mut_ptr(|r: *mut crate::object::ObjectHeader| object_value(r)) -} - /// `new WebAssembly.Instance(module, imports?)` — synchronously instantiate a /// previously compiled module. This is the shape emitted by wasm-bindgen's /// Node glue (including `@silvia-odwyer/photon-node`). Unlike the async @@ -1498,6 +1799,7 @@ pub extern "C" fn js_webassembly_instance_new( perry_wasm_host_instance_new( module, Some(call_wasm_import), + Some(resolve_wasm_import), register_instance_imports(imports.get_nanbox_f64()), &mut err, ) @@ -1525,6 +1827,26 @@ pub extern "C" fn js_webassembly_instance_new( pub extern "C" fn js_webassembly_instantiate(bytes_jsval: f64, imports_jsval: f64) -> f64 { let scope = crate::gc::RuntimeHandleScope::new(); let imports = scope.root_nanbox_f64(imports_jsval); + if let Some(module) = extract_module_handle(bytes_jsval) { + let mut err: *mut c_char = std::ptr::null_mut(); + let inst = unsafe { + perry_wasm_host_instance_new( + module, + Some(call_wasm_import), + Some(resolve_wasm_import), + register_instance_imports(imports.get_nanbox_f64()), + &mut err, + ) + }; + if inst.is_null() { + return rejected_promise_value(wasm_error_value_from_host( + b"LinkError", + err, + "WebAssembly.instantiate(): instantiation failed", + )); + } + return make_instance_value(module, inst, imports.get_nanbox_f64(), nanbox_undefined()); + } let Some((ptr, len)) = extract_bytes(bytes_jsval) else { return rejected_promise_value(wasm_type_error_value( "WebAssembly.instantiate: argument must be a Uint8Array or ArrayBuffer", @@ -1544,6 +1866,7 @@ pub extern "C" fn js_webassembly_instantiate(bytes_jsval: f64, imports_jsval: f6 perry_wasm_host_instance_new( module, Some(call_wasm_import), + Some(resolve_wasm_import), register_instance_imports(imports.get_nanbox_f64()), &mut err2, ) @@ -1622,154 +1945,3 @@ pub extern "C" fn js_webassembly_call_export_4( ) -> f64 { call_export_named_rebinding(inst_jsval, name_jsval, &[a, b, c, d]) } - -/// The most arguments any exported-function shim forwards -/// (`js_wasm_export_call_4` / `js_webassembly_call_export_4`). Keeping the -/// marshalling buffers on the stack keeps a Wasm call allocation-free. -const MAX_WASM_ARGS: usize = 4; -const MAX_WASM_RESULTS: usize = 16; - -/// Encode JS numbers as wasm values. -/// -/// Every input arg arrives as an f64. An f64 that round-trips through i32 -/// exactly is sent as an i32 — that covers `add(2, 3)` on an i32 export -/// without making the caller think about wasm signatures — and everything else -/// is sent as an f64. The host re-coerces against the export's real signature. -fn encode_wasm_args( - args: &[f64], - kinds: &mut [u8; MAX_WASM_ARGS], - bits: &mut [u64; MAX_WASM_ARGS], -) { - for (index, v) in args.iter().take(MAX_WASM_ARGS).enumerate() { - let as_i32 = *v as i32; - if (as_i32 as f64) == *v && v.is_finite() { - kinds[index] = WASM_VAL_KIND_I32; - bits[index] = as_i32 as u32 as u64; - } else { - kinds[index] = WASM_VAL_KIND_F64; - bits[index] = v.to_bits(); - } - } -} - -fn decode_wasm_value(kind: u8, bits: u64) -> f64 { - match kind { - WASM_VAL_KIND_I32 => (bits as u32 as i32) as f64, - WASM_VAL_KIND_I64 => (bits as i64) as f64, - WASM_VAL_KIND_F32 => f32::from_bits(bits as u32) as f64, - WASM_VAL_KIND_F64 => f64::from_bits(bits), - _ => nanbox_undefined(), - } -} - -/// Decode a call's results: no result is `undefined`, one is the value itself, -/// and several become an array (wasm multi-value). -fn decode_wasm_results( - out_kinds: &[u8; MAX_WASM_RESULTS], - out_bits: &[u64; MAX_WASM_RESULTS], - out_count: usize, -) -> f64 { - match out_count { - 0 => nanbox_undefined(), - 1 => decode_wasm_value(out_kinds[0], out_bits[0]), - count => { - let scope = crate::gc::RuntimeHandleScope::new(); - let array = scope.root_nanbox_f64(array_value(crate::array::js_array_alloc( - count.min(MAX_WASM_RESULTS) as u32, - ))); - for index in 0..count.min(MAX_WASM_RESULTS) { - let array_ptr = JSValue::from_bits(array.get_nanbox_f64().to_bits()) - .as_pointer::() - as *mut crate::array::ArrayHeader; - let array_ptr = crate::array::js_array_push_f64( - array_ptr, - decode_wasm_value(out_kinds[index], out_bits[index]), - ); - array.set_nanbox_f64(array_value(array_ptr)); - } - array.get_nanbox_f64() - } - } -} - -/// Call an export the instance resolved at construction time (#9611). Same -/// marshalling as [`call_export_n`], without the per-call name lookup. -fn call_export_by_handle(inst: *mut c_void, handle: usize, args: &[f64]) -> f64 { - let mut kinds = [WASM_VAL_KIND_NONE; MAX_WASM_ARGS]; - let mut bits = [0u64; MAX_WASM_ARGS]; - encode_wasm_args(args, &mut kinds, &mut bits); - let arg_count = args.len().min(MAX_WASM_ARGS); - - let mut out_kinds = [WASM_VAL_KIND_NONE; MAX_WASM_RESULTS]; - let mut out_bits = [0u64; MAX_WASM_RESULTS]; - let mut out_count = 0usize; - let mut err: *mut c_char = std::ptr::null_mut(); - let ok = unsafe { - perry_wasm_host_call_export_by_handle( - inst, - handle, - kinds.as_ptr(), - bits.as_ptr(), - arg_count, - out_kinds.as_mut_ptr(), - out_bits.as_mut_ptr(), - MAX_WASM_RESULTS, - &mut out_count, - &mut err, - ) - }; - if ok == 0 { - emit_error_to_stderr("WebAssembly.RuntimeError", err); - return nanbox_undefined(); - } - if !err.is_null() { - unsafe { perry_wasm_host_string_free(err) }; - } - decode_wasm_results(&out_kinds, &out_bits, out_count) -} - -fn call_export_n(inst_jsval: f64, name_jsval: f64, args: &[f64]) -> f64 { - let inst = unbox_pointer(inst_jsval); - if inst.is_null() { - eprintln!("WebAssembly.callExport: instance handle is null/undefined"); - return nanbox_undefined(); - } - let Some((name_ptr, name_len)) = extract_string_bytes(name_jsval) else { - eprintln!("WebAssembly.callExport: export name must be a string"); - return nanbox_undefined(); - }; - - let mut kinds = [WASM_VAL_KIND_NONE; MAX_WASM_ARGS]; - let mut bits = [0u64; MAX_WASM_ARGS]; - encode_wasm_args(args, &mut kinds, &mut bits); - let arg_count = args.len().min(MAX_WASM_ARGS); - - let mut out_kinds = [WASM_VAL_KIND_NONE; MAX_WASM_RESULTS]; - let mut out_bits = [0u64; MAX_WASM_RESULTS]; - let mut out_count = 0usize; - let mut err: *mut c_char = std::ptr::null_mut(); - let ok = unsafe { - perry_wasm_host_call_export( - inst, - name_ptr as *const c_char, - name_len, - kinds.as_ptr(), - bits.as_ptr(), - arg_count, - out_kinds.as_mut_ptr(), - out_bits.as_mut_ptr(), - MAX_WASM_RESULTS, - &mut out_count, - &mut err, - ) - }; - if ok == 0 { - emit_error_to_stderr("WebAssembly.RuntimeError", err); - return nanbox_undefined(); - } - // Avoid leaking the unused err buffer on success. - if !err.is_null() { - unsafe { perry_wasm_host_string_free(err) }; - } - decode_wasm_results(&out_kinds, &out_bits, out_count) -} diff --git a/crates/perry-runtime/src/webassembly_calls.rs b/crates/perry-runtime/src/webassembly_calls.rs new file mode 100644 index 0000000000..73f8826c19 --- /dev/null +++ b/crates/perry-runtime/src/webassembly_calls.rs @@ -0,0 +1,447 @@ +//! Numeric argument/result marshalling for Wasm function calls. + +use super::*; + +extern "C" fn js_wasm_instance_result_then( + closure: *const crate::closure::ClosureHeader, + on_fulfilled: f64, + _on_rejected: f64, +) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let value = scope.root_nanbox_f64(crate::closure::js_closure_get_capture_f64(closure, 0)); + let on_fulfilled = scope.root_nanbox_f64(on_fulfilled); + let outcome = crate::exception::catch_js_throw(|| unsafe { + crate::closure::js_native_call_value( + on_fulfilled.get_nanbox_f64(), + [value.get_nanbox_f64()].as_ptr(), + 1, + ) + }); + let promise = scope.root_raw_mut_ptr(crate::promise::js_promise_new()); + promise.with_mut_ptr(|p: *mut crate::promise::Promise| match outcome { + Ok(result) => crate::promise::js_promise_resolve(p, result), + Err(reason) => crate::promise::js_promise_reject(p, reason), + }); + promise + .with_mut_ptr(|p: *mut crate::promise::Promise| crate::value::js_nanbox_pointer(p as i64)) +} + +fn instance_result_object(module: f64, instance: f64) -> f64 { + let object = crate::object::js_object_alloc(0, 0); + let object = object_set(object, b"module", module); + let object = object_set(object, b"instance", instance); + object_value(object) +} + +macro_rules! wasm_export_call_shim { + ($name:ident $(, $arg:ident)*) => { + pub(super) extern "C" fn $name( + closure: *const crate::closure::ClosureHeader, + $($arg: f64),* + ) -> f64 { + call_captured_wasm_export(closure, &[$($arg),*]) + } + }; +} + +wasm_export_call_shim!(js_wasm_export_call_0); +wasm_export_call_shim!(js_wasm_export_call_1, a0); +wasm_export_call_shim!(js_wasm_export_call_2, a0, a1); +wasm_export_call_shim!(js_wasm_export_call_3, a0, a1, a2); +wasm_export_call_shim!(js_wasm_export_call_4, a0, a1, a2, a3); +wasm_export_call_shim!(js_wasm_export_call_5, a0, a1, a2, a3, a4); +wasm_export_call_shim!(js_wasm_export_call_6, a0, a1, a2, a3, a4, a5); +wasm_export_call_shim!(js_wasm_export_call_7, a0, a1, a2, a3, a4, a5, a6); +wasm_export_call_shim!(js_wasm_export_call_8, a0, a1, a2, a3, a4, a5, a6, a7); +wasm_export_call_shim!(js_wasm_export_call_9, a0, a1, a2, a3, a4, a5, a6, a7, a8); +wasm_export_call_shim!( + js_wasm_export_call_10, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9 +); +wasm_export_call_shim!( + js_wasm_export_call_11, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10 +); +wasm_export_call_shim!( + js_wasm_export_call_12, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11 +); +wasm_export_call_shim!( + js_wasm_export_call_13, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12 +); +wasm_export_call_shim!( + js_wasm_export_call_14, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13 +); +wasm_export_call_shim!( + js_wasm_export_call_15, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14 +); +wasm_export_call_shim!( + js_wasm_export_call_16, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15 +); + +pub(super) fn wasm_export_call_shim_for_arity(arity: usize) -> (*const u8, u32) { + match arity { + 0 => (js_wasm_export_call_0 as *const u8, 0), + 1 => (js_wasm_export_call_1 as *const u8, 1), + 2 => (js_wasm_export_call_2 as *const u8, 2), + 3 => (js_wasm_export_call_3 as *const u8, 3), + 4 => (js_wasm_export_call_4 as *const u8, 4), + 5 => (js_wasm_export_call_5 as *const u8, 5), + 6 => (js_wasm_export_call_6 as *const u8, 6), + 7 => (js_wasm_export_call_7 as *const u8, 7), + 8 => (js_wasm_export_call_8 as *const u8, 8), + 9 => (js_wasm_export_call_9 as *const u8, 9), + 10 => (js_wasm_export_call_10 as *const u8, 10), + 11 => (js_wasm_export_call_11 as *const u8, 11), + 12 => (js_wasm_export_call_12 as *const u8, 12), + 13 => (js_wasm_export_call_13 as *const u8, 13), + 14 => (js_wasm_export_call_14 as *const u8, 14), + 15 => (js_wasm_export_call_15 as *const u8, 15), + _ => (js_wasm_export_call_16 as *const u8, 16), + } +} + +pub(super) fn is_wasm_export_call_shim(function: *const u8) -> bool { + matches!( + function, + f if f == js_wasm_export_call_0 as *const u8 + || f == js_wasm_export_call_1 as *const u8 + || f == js_wasm_export_call_2 as *const u8 + || f == js_wasm_export_call_3 as *const u8 + || f == js_wasm_export_call_4 as *const u8 + || f == js_wasm_export_call_5 as *const u8 + || f == js_wasm_export_call_6 as *const u8 + || f == js_wasm_export_call_7 as *const u8 + || f == js_wasm_export_call_8 as *const u8 + || f == js_wasm_export_call_9 as *const u8 + || f == js_wasm_export_call_10 as *const u8 + || f == js_wasm_export_call_11 as *const u8 + || f == js_wasm_export_call_12 as *const u8 + || f == js_wasm_export_call_13 as *const u8 + || f == js_wasm_export_call_14 as *const u8 + || f == js_wasm_export_call_15 as *const u8 + || f == js_wasm_export_call_16 as *const u8 + ) +} + +/// Perry's synchronous wasm adapter reads `.instance` immediately, while +/// Emscripten's async side-module path calls `.then(...)`. Publish both views: +/// the direct result is a thenable whose fulfillment value is a plain result +/// object, avoiding recursive thenable assimilation. +pub(super) fn make_instance_result(module: *mut c_void, inst: *mut c_void, imports: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let instance = scope.root_nanbox_f64(make_instance_value( + module, + inst, + imports, + nanbox_undefined(), + )); + let module = scope.root_nanbox_f64(make_module_object(module)); + let settlement = scope.root_nanbox_f64(instance_result_object( + module.get_nanbox_f64(), + instance.get_nanbox_f64(), + )); + let direct = scope.root_nanbox_f64(instance_result_object( + module.get_nanbox_f64(), + instance.get_nanbox_f64(), + )); + let function = js_wasm_instance_result_then as *const u8; + crate::closure::js_register_closure_arity(function, 2); + let then = scope.root_raw_mut_ptr(crate::closure::js_closure_alloc(function, 1)); + then.with_mut_ptr(|t: *mut crate::closure::ClosureHeader| { + crate::closure::js_closure_set_capture_f64(t, 0, settlement.get_nanbox_f64()) + }); + let direct_ptr = JSValue::from_bits(direct.get_nanbox_f64().to_bits()) + .as_pointer::() + as *mut crate::object::ObjectHeader; + let direct_ptr = object_set( + direct_ptr, + b"then", + then.with_mut_ptr(|t: *mut crate::closure::ClosureHeader| { + crate::value::js_nanbox_pointer(t as i64) + }), + ); + object_value(direct_ptr) +} + +/// The most arguments any exported-function shim forwards, matching Perry's +/// existing `js_closure_call0..16` dynamic-call ABI. Keeping the +/// marshalling buffers on the stack keeps a Wasm call allocation-free. +const MAX_WASM_ARGS: usize = 16; +const MAX_WASM_RESULTS: usize = 16; + +fn encode_wasm_args( + args: &[f64], + kinds: &mut [u8; MAX_WASM_ARGS], + bits: &mut [u64; MAX_WASM_ARGS], +) { + for (index, value) in args.iter().take(MAX_WASM_ARGS).enumerate() { + if let Some(i64_bits) = js_to_wasm_i64_bits(*value) { + kinds[index] = WASM_VAL_KIND_I64; + bits[index] = i64_bits; + continue; + } + let as_i32 = *value as i32; + if (as_i32 as f64) == *value && value.is_finite() { + kinds[index] = WASM_VAL_KIND_I32; + bits[index] = as_i32 as u32 as u64; + } else { + kinds[index] = WASM_VAL_KIND_F64; + bits[index] = value.to_bits(); + } + } +} + +pub(super) fn js_to_wasm_i64_bits(value: f64) -> Option { + let bits = value.to_bits(); + let js = JSValue::from_bits(bits); + let raw = (bits & POINTER_MASK) as *const crate::bigint::BigIntHeader; + let is_bigint = js.is_bigint() + || (js.is_pointer() + && unsafe { crate::value::addr_class::try_read_gc_header(raw as usize) } + .is_some_and(|header| header.obj_type == crate::gc::GC_TYPE_BIGINT)); + if !is_bigint { + return None; + } + let bigint = crate::bigint::clean_bigint_ptr(raw); + (!bigint.is_null()).then(|| unsafe { (*bigint).limbs[0] }) +} + +pub(super) fn wasm_i64_to_js(bits: u64) -> f64 { + let bigint = crate::bigint::js_bigint_from_i64(bits as i64); + f64::from_bits(JSValue::bigint_ptr(bigint).bits()) +} + +pub(super) fn decode_wasm_value(kind: u8, bits: u64) -> f64 { + match kind { + WASM_VAL_KIND_I32 => (bits as u32 as i32) as f64, + WASM_VAL_KIND_I64 => wasm_i64_to_js(bits), + WASM_VAL_KIND_F32 => f32::from_bits(bits as u32) as f64, + WASM_VAL_KIND_F64 => f64::from_bits(bits), + _ => nanbox_undefined(), + } +} + +fn decode_wasm_results( + out_kinds: &[u8; MAX_WASM_RESULTS], + out_bits: &[u64; MAX_WASM_RESULTS], + out_count: usize, +) -> f64 { + match out_count { + 0 => nanbox_undefined(), + 1 => decode_wasm_value(out_kinds[0], out_bits[0]), + count => { + let scope = crate::gc::RuntimeHandleScope::new(); + let array = scope.root_nanbox_f64(array_value(crate::array::js_array_alloc( + count.min(MAX_WASM_RESULTS) as u32, + ))); + for index in 0..count.min(MAX_WASM_RESULTS) { + let array_ptr = JSValue::from_bits(array.get_nanbox_f64().to_bits()) + .as_pointer::() + as *mut crate::array::ArrayHeader; + let array_ptr = crate::array::js_array_push_f64( + array_ptr, + decode_wasm_value(out_kinds[index], out_bits[index]), + ); + array.set_nanbox_f64(array_value(array_ptr)); + } + array.get_nanbox_f64() + } + } +} + +pub(super) fn call_export_by_handle(inst: *mut c_void, handle: usize, args: &[f64]) -> f64 { + let mut kinds = [WASM_VAL_KIND_NONE; MAX_WASM_ARGS]; + let mut bits = [0u64; MAX_WASM_ARGS]; + encode_wasm_args(args, &mut kinds, &mut bits); + let arg_count = args.len().min(MAX_WASM_ARGS); + let mut out_kinds = [WASM_VAL_KIND_NONE; MAX_WASM_RESULTS]; + let mut out_bits = [0u64; MAX_WASM_RESULTS]; + let mut out_count = 0usize; + let mut err: *mut c_char = std::ptr::null_mut(); + let ok = unsafe { + perry_wasm_host_call_export_by_handle( + inst, + handle, + kinds.as_ptr(), + bits.as_ptr(), + arg_count, + out_kinds.as_mut_ptr(), + out_bits.as_mut_ptr(), + MAX_WASM_RESULTS, + &mut out_count, + &mut err, + ) + }; + if ok == 0 { + emit_error_to_stderr("WebAssembly.RuntimeError", err); + return nanbox_undefined(); + } + if !err.is_null() { + unsafe { perry_wasm_host_string_free(err) }; + } + decode_wasm_results(&out_kinds, &out_bits, out_count) +} + +pub(super) fn call_external_function(external: *mut c_void, args: &[f64]) -> f64 { + let mut kinds = [WASM_VAL_KIND_NONE; MAX_WASM_ARGS]; + let mut bits = [0u64; MAX_WASM_ARGS]; + encode_wasm_args(args, &mut kinds, &mut bits); + let arg_count = args.len().min(MAX_WASM_ARGS); + let mut out_kinds = [WASM_VAL_KIND_NONE; MAX_WASM_RESULTS]; + let mut out_bits = [0u64; MAX_WASM_RESULTS]; + let mut out_count = 0usize; + let mut err: *mut c_char = std::ptr::null_mut(); + let ok = unsafe { + perry_wasm_host_func_call( + external, + kinds.as_ptr(), + bits.as_ptr(), + arg_count, + out_kinds.as_mut_ptr(), + out_bits.as_mut_ptr(), + MAX_WASM_RESULTS, + &mut out_count, + &mut err, + ) + }; + if ok == 0 { + emit_error_to_stderr("WebAssembly.RuntimeError", err); + return nanbox_undefined(); + } + if !err.is_null() { + unsafe { perry_wasm_host_string_free(err) }; + } + decode_wasm_results(&out_kinds, &out_bits, out_count) +} + +pub(super) fn call_export_n(inst_jsval: f64, name_jsval: f64, args: &[f64]) -> f64 { + let inst = unbox_pointer(inst_jsval); + if inst.is_null() { + eprintln!("WebAssembly.callExport: instance handle is null/undefined"); + return nanbox_undefined(); + } + let Some((name_ptr, name_len)) = extract_string_bytes(name_jsval) else { + eprintln!("WebAssembly.callExport: export name must be a string"); + return nanbox_undefined(); + }; + let mut kinds = [WASM_VAL_KIND_NONE; MAX_WASM_ARGS]; + let mut bits = [0u64; MAX_WASM_ARGS]; + encode_wasm_args(args, &mut kinds, &mut bits); + let arg_count = args.len().min(MAX_WASM_ARGS); + let mut out_kinds = [WASM_VAL_KIND_NONE; MAX_WASM_RESULTS]; + let mut out_bits = [0u64; MAX_WASM_RESULTS]; + let mut out_count = 0usize; + let mut err: *mut c_char = std::ptr::null_mut(); + let ok = unsafe { + perry_wasm_host_call_export( + inst, + name_ptr as *const c_char, + name_len, + kinds.as_ptr(), + bits.as_ptr(), + arg_count, + out_kinds.as_mut_ptr(), + out_bits.as_mut_ptr(), + MAX_WASM_RESULTS, + &mut out_count, + &mut err, + ) + }; + if ok == 0 { + emit_error_to_stderr("WebAssembly.RuntimeError", err); + return nanbox_undefined(); + } + if !err.is_null() { + unsafe { perry_wasm_host_string_free(err) }; + } + decode_wasm_results(&out_kinds, &out_bits, out_count) +} diff --git a/crates/perry-runtime/src/webassembly_host.rs b/crates/perry-runtime/src/webassembly_host.rs new file mode 100644 index 0000000000..d33233b389 --- /dev/null +++ b/crates/perry-runtime/src/webassembly_host.rs @@ -0,0 +1,218 @@ +//! C ABI declarations for the optional `perry-wasm-host` archive. + +use std::ffi::{c_char, c_void}; + +pub(super) const WASM_VAL_KIND_I32: u8 = 0; +pub(super) const WASM_VAL_KIND_I64: u8 = 1; +pub(super) const WASM_VAL_KIND_F32: u8 = 2; +pub(super) const WASM_VAL_KIND_F64: u8 = 3; +pub(super) const WASM_VAL_KIND_EXTERNREF: u8 = 4; +pub(super) const WASM_VAL_KIND_NONE: u8 = 0xFF; +pub(super) const WASM_EXTERN_KIND_FUNCTION: u8 = 0; +pub(super) const WASM_EXTERN_KIND_TABLE: u8 = 1; +pub(super) const WASM_EXTERN_KIND_MEMORY: u8 = 2; +pub(super) const WASM_EXTERN_KIND_GLOBAL: u8 = 3; + +pub(super) type WasmImportCallback = unsafe extern "C" fn( + context: u64, + module: *const u8, + module_len: usize, + name: *const u8, + name_len: usize, + arg_kinds: *const u8, + arg_bits: *const u64, + arg_count: usize, + result_kinds: *const u8, + result_bits: *mut u64, + result_count: usize, +) -> i32; + +pub(super) type WasmImportResolverCallback = unsafe extern "C" fn( + context: u64, + module: *const u8, + module_len: usize, + name: *const u8, + name_len: usize, + kind: u8, +) -> *mut c_void; + +extern "C" { + pub(super) fn perry_wasm_host_string_free(s: *mut c_char); + pub(super) fn perry_wasm_host_validate(bytes: *const u8, len: usize) -> i32; + pub(super) fn perry_wasm_host_module_new( + bytes: *const u8, + len: usize, + out_err: *mut *mut c_char, + ) -> *mut c_void; + pub(super) fn perry_wasm_host_module_drop(module: *mut c_void); + pub(super) fn perry_wasm_host_module_exports_len(module: *mut c_void) -> usize; + pub(super) fn perry_wasm_host_module_export_at( + module: *mut c_void, + index: usize, + out_name: *mut *const c_char, + out_name_len: *mut usize, + out_kind: *mut u8, + ) -> i32; + pub(super) fn perry_wasm_host_module_export_func_arity( + module: *mut c_void, + index: usize, + ) -> usize; + pub(super) fn perry_wasm_host_module_imports_len(module: *mut c_void) -> usize; + pub(super) fn perry_wasm_host_module_import_at( + module: *mut c_void, + index: usize, + out_module: *mut *const c_char, + out_module_len: *mut usize, + out_name: *mut *const c_char, + out_name_len: *mut usize, + out_kind: *mut u8, + ) -> i32; + pub(super) fn perry_wasm_host_module_custom_sections_len( + module: *mut c_void, + name: *const c_char, + name_len: usize, + ) -> usize; + pub(super) fn perry_wasm_host_module_custom_section_at( + module: *mut c_void, + name: *const c_char, + name_len: usize, + nth: usize, + out_data: *mut *const u8, + out_data_len: *mut usize, + ) -> i32; + pub(super) fn perry_wasm_host_instance_new( + module: *mut c_void, + import_callback: Option, + import_resolver: Option, + import_context: u64, + out_err: *mut *mut c_char, + ) -> *mut c_void; + #[allow(dead_code)] + pub(super) fn perry_wasm_host_instance_drop(inst: *mut c_void); + pub(super) fn perry_wasm_host_instance_memory_span( + inst: *mut c_void, + out_len: *mut usize, + ) -> *mut u8; + pub(super) fn perry_wasm_host_instance_table_len( + inst: *mut c_void, + name: *const c_char, + name_len: usize, + ) -> usize; + pub(super) fn perry_wasm_host_instance_table_get( + inst: *mut c_void, + name: *const c_char, + name_len: usize, + index: usize, + out_bits: *mut u64, + out_is_null: *mut i32, + out_external: *mut *mut c_void, + ) -> i32; + pub(super) fn perry_wasm_host_instance_table_set( + inst: *mut c_void, + name: *const c_char, + name_len: usize, + index: usize, + bits: u64, + is_null: i32, + external: *mut c_void, + ) -> i32; + pub(super) fn perry_wasm_host_instance_table_grow( + inst: *mut c_void, + name: *const c_char, + name_len: usize, + delta: usize, + bits: u64, + is_null: i32, + external: *mut c_void, + out_old_len: *mut usize, + ) -> i32; + pub(super) fn perry_wasm_host_instance_export_handle( + inst: *mut c_void, + name: *const c_char, + name_len: usize, + ) -> usize; + pub(super) fn perry_wasm_host_instance_export_extern( + inst: *mut c_void, + name: *const c_char, + name_len: usize, + ) -> *mut c_void; + pub(super) fn perry_wasm_host_instance_take_exit_code( + inst: *mut c_void, + out_code: *mut i32, + ) -> i32; + pub(super) fn perry_wasm_host_global_new(kind: u8, mutable: i32, bits: u64) -> *mut c_void; + pub(super) fn perry_wasm_host_global_get( + handle: *mut c_void, + out_kind: *mut u8, + out_bits: *mut u64, + ) -> i32; + pub(super) fn perry_wasm_host_global_set(handle: *mut c_void, kind: u8, bits: u64) -> i32; + pub(super) fn perry_wasm_host_memory_new(initial: u32, maximum: u32) -> *mut c_void; + pub(super) fn perry_wasm_host_memory_span(handle: *mut c_void, out_len: *mut usize) -> *mut u8; + pub(super) fn perry_wasm_host_memory_grow(handle: *mut c_void, delta: u32) -> i64; + pub(super) fn perry_wasm_host_table_new( + element_kind: u8, + initial: u32, + maximum: u32, + ) -> *mut c_void; + pub(super) fn perry_wasm_host_table_len(handle: *mut c_void) -> usize; + pub(super) fn perry_wasm_host_table_get( + handle: *mut c_void, + index: usize, + out_bits: *mut u64, + out_is_null: *mut i32, + out_external: *mut *mut c_void, + ) -> i32; + pub(super) fn perry_wasm_host_table_set( + handle: *mut c_void, + index: usize, + bits: u64, + is_null: i32, + external: *mut c_void, + ) -> i32; + pub(super) fn perry_wasm_host_table_grow( + handle: *mut c_void, + delta: usize, + bits: u64, + is_null: i32, + external: *mut c_void, + out_old_len: *mut usize, + ) -> i32; + pub(super) fn perry_wasm_host_func_arity(handle: *mut c_void) -> usize; + pub(super) fn perry_wasm_host_func_call( + handle: *mut c_void, + arg_kinds: *const u8, + arg_bits: *const u64, + arg_count: usize, + out_kinds: *mut u8, + out_bits: *mut u64, + out_capacity: usize, + out_count: *mut usize, + out_err: *mut *mut c_char, + ) -> i32; + pub(super) fn perry_wasm_host_call_export( + inst: *mut c_void, + name: *const c_char, + name_len: usize, + arg_kinds: *const u8, + arg_bits: *const u64, + arg_count: usize, + out_kinds: *mut u8, + out_bits: *mut u64, + out_capacity: usize, + out_count: *mut usize, + out_err: *mut *mut c_char, + ) -> i32; + pub(super) fn perry_wasm_host_call_export_by_handle( + inst: *mut c_void, + handle: usize, + arg_kinds: *const u8, + arg_bits: *const u64, + arg_count: usize, + out_kinds: *mut u8, + out_bits: *mut u64, + out_capacity: usize, + out_count: *mut usize, + out_err: *mut *mut c_char, + ) -> i32; +} diff --git a/crates/perry-wasm-host/src/externals.rs b/crates/perry-wasm-host/src/externals.rs new file mode 100644 index 0000000000..97296b6485 --- /dev/null +++ b/crates/perry-wasm-host/src/externals.rs @@ -0,0 +1,380 @@ +//! Shared WebAssembly externals used by Emscripten main/side-module graphs. + +use super::*; + +/// Return an opaque, store-stable handle for an instance export. It is used +/// when another WebAssembly instance imports that JS-visible export. +#[no_mangle] +pub extern "C" fn perry_wasm_host_instance_export_extern( + inst: *mut WasmInstanceHandle, + name: *const c_char, + name_len: usize, +) -> *mut c_void { + let Some(inst) = (unsafe { inst.as_ref() }) else { + return std::ptr::null_mut(); + }; + let Some(name) = utf8_arg(name, name_len) else { + return std::ptr::null_mut(); + }; + inst.inner + .instance + .get_export(inst.store(), name) + .map(extern_handle) + .unwrap_or(std::ptr::null_mut()) +} + +/// Construct a standalone mutable/immutable numeric WebAssembly.Global in +/// this worker's shared store. +#[no_mangle] +pub extern "C" fn perry_wasm_host_global_new(kind: u8, mutable: i32, bits: u64) -> *mut c_void { + let value = match kind { + WASM_VAL_KIND_I32 => Val::I32(bits as u32 as i32), + WASM_VAL_KIND_I64 => Val::I64(bits as i64), + WASM_VAL_KIND_F32 => Val::F32(f32::from_bits(bits as u32).into()), + WASM_VAL_KIND_F64 => Val::F64(f64::from_bits(bits).into()), + _ => return std::ptr::null_mut(), + }; + with_host_runtime(|runtime| { + let global = Global::new( + &mut runtime.store, + value, + if mutable != 0 { + Mutability::Var + } else { + Mutability::Const + }, + ); + extern_handle(global.into()) + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_global_get( + handle: *mut c_void, + out_kind: *mut u8, + out_bits: *mut u64, +) -> i32 { + if out_kind.is_null() || out_bits.is_null() { + return 0; + } + let Some(Extern::Global(global)) = extern_from_handle(handle) else { + return 0; + }; + with_host_runtime(|runtime| { + let Some((kind, bits)) = val_kind_bits(&global.get(&runtime.store)) else { + return 0; + }; + unsafe { + *out_kind = kind; + *out_bits = bits; + } + 1 + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_global_set(handle: *mut c_void, kind: u8, bits: u64) -> i32 { + let Some(Extern::Global(global)) = extern_from_handle(handle) else { + return 0; + }; + let value = val_from_kind_bits(kind, bits); + with_host_runtime(|runtime| global.set(&mut runtime.store, value).is_ok() as i32) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_memory_new(initial: u32, maximum: u32) -> *mut c_void { + let maximum = (maximum != u32::MAX).then_some(maximum); + let ty = MemoryType::new(initial, maximum); + with_host_runtime(|runtime| { + Memory::new(&mut runtime.store, ty) + .map(|memory| extern_handle(memory.into())) + .unwrap_or(std::ptr::null_mut()) + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_memory_span(handle: *mut c_void, out_len: *mut usize) -> *mut u8 { + if !out_len.is_null() { + unsafe { *out_len = 0 }; + } + let Some(Extern::Memory(memory)) = extern_from_handle(handle) else { + return std::ptr::null_mut(); + }; + with_host_runtime(|runtime| { + if !out_len.is_null() { + unsafe { *out_len = memory.data_size(&runtime.store) }; + } + memory.data_ptr(&runtime.store) + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_memory_grow(handle: *mut c_void, delta: u32) -> i64 { + let Some(Extern::Memory(memory)) = extern_from_handle(handle) else { + return -1; + }; + with_host_runtime(|runtime| { + memory + .grow(&mut runtime.store, u64::from(delta)) + .map(|pages| pages as i64) + .unwrap_or(-1) + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_table_new( + element_kind: u8, + initial: u32, + maximum: u32, +) -> *mut c_void { + let element = match element_kind { + 0 => ValType::FuncRef, + 1 => ValType::ExternRef, + _ => return std::ptr::null_mut(), + }; + let maximum = (maximum != u32::MAX).then_some(maximum); + let ty = TableType::new(element, initial, maximum); + with_host_runtime(|runtime| { + Table::new(&mut runtime.store, ty, Val::default(element)) + .map(|table| extern_handle(table.into())) + .unwrap_or(std::ptr::null_mut()) + }) +} + +fn table_from_handle(handle: *mut c_void) -> Option { + match extern_from_handle(handle) { + Some(Extern::Table(table)) => Some(table), + _ => None, + } +} + +fn table_value_for_store( + store: &mut Store<()>, + table: Table, + bits: u64, + is_null: i32, + external: *mut c_void, +) -> Option { + let element = table.ty(&*store).element(); + if is_null != 0 { + return Some(Val::default(element)); + } + match element { + ValType::ExternRef => Some(Val::from(ExternRef::new(store, bits))), + ValType::FuncRef => match extern_from_handle(external) { + Some(Extern::Func(function)) => Some(Val::FuncRef(Ref::Val(function))), + _ => None, + }, + _ => None, + } +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_table_len(handle: *mut c_void) -> usize { + let Some(table) = table_from_handle(handle) else { + return usize::MAX; + }; + with_host_runtime(|runtime| usize::try_from(table.size(&runtime.store)).unwrap_or(usize::MAX)) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_table_get( + handle: *mut c_void, + index: usize, + out_bits: *mut u64, + out_is_null: *mut i32, + out_external: *mut *mut c_void, +) -> i32 { + if out_bits.is_null() || out_is_null.is_null() { + return 0; + } + let Some(table) = table_from_handle(handle) else { + return 0; + }; + with_host_runtime(|runtime| { + let Some(value) = table.get(&runtime.store, index as u64) else { + return 0; + }; + let (bits, is_null, external) = match value { + Val::ExternRef(Ref::Null) | Val::FuncRef(Ref::Null) => (0, 1, std::ptr::null_mut()), + Val::ExternRef(Ref::Val(value)) => { + let Some(bits) = value.data(&runtime.store).downcast_ref::() else { + return 0; + }; + (*bits, 0, std::ptr::null_mut()) + } + Val::FuncRef(Ref::Val(function)) => (0, 0, extern_handle(function.into())), + _ => return 0, + }; + unsafe { + *out_bits = bits; + *out_is_null = is_null; + if !out_external.is_null() { + *out_external = external; + } + } + 1 + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_table_set( + handle: *mut c_void, + index: usize, + bits: u64, + is_null: i32, + external: *mut c_void, +) -> i32 { + let Some(table) = table_from_handle(handle) else { + return 0; + }; + with_host_runtime(|runtime| { + let Some(value) = table_value_for_store(&mut runtime.store, table, bits, is_null, external) + else { + return 0; + }; + table.set(&mut runtime.store, index as u64, value).is_ok() as i32 + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_table_grow( + handle: *mut c_void, + delta: usize, + bits: u64, + is_null: i32, + external: *mut c_void, + out_old_len: *mut usize, +) -> i32 { + if out_old_len.is_null() { + return 0; + } + let Some(table) = table_from_handle(handle) else { + return 0; + }; + with_host_runtime(|runtime| { + let Some(value) = table_value_for_store(&mut runtime.store, table, bits, is_null, external) + else { + return 0; + }; + let Ok(old_len) = table.grow(&mut runtime.store, delta as u64, value) else { + return 0; + }; + let Ok(old_len) = usize::try_from(old_len) else { + return 0; + }; + unsafe { *out_old_len = old_len }; + 1 + }) +} + +#[no_mangle] +pub extern "C" fn perry_wasm_host_func_arity(handle: *mut c_void) -> usize { + let Some(Extern::Func(function)) = extern_from_handle(handle) else { + return usize::MAX; + }; + with_host_runtime(|runtime| function.ty(&runtime.store).params().len()) +} + +/// Invoke a function obtained from a funcref table. This is the generic +/// counterpart to the instance-export fast path in `lib.rs`. +#[no_mangle] +#[allow(clippy::too_many_arguments)] +pub extern "C" fn perry_wasm_host_func_call( + handle: *mut c_void, + arg_kinds: *const u8, + arg_bits: *const u64, + arg_count: usize, + out_kinds: *mut u8, + out_bits: *mut u64, + out_capacity: usize, + out_count: *mut usize, + out_err: *mut *mut c_char, +) -> i32 { + if out_count.is_null() + || (arg_count != 0 && (arg_kinds.is_null() || arg_bits.is_null())) + || (out_capacity != 0 && (out_kinds.is_null() || out_bits.is_null())) + { + capture_err(out_err, WasmHostError::Runtime("null arg".into())); + return 0; + } + let Some(Extern::Func(function)) = extern_from_handle(handle) else { + capture_err( + out_err, + WasmHostError::Runtime("invalid function handle".into()), + ); + return 0; + }; + let call_result = with_host_runtime(|runtime| -> Result, WasmHostError> { + let ty = function.ty(&runtime.store); + if ty.params().len() != arg_count { + return Err(WasmHostError::Runtime(format!( + "table function: arity mismatch (expects {}, got {arg_count})", + ty.params().len() + ))); + } + let kinds = unsafe { slice::from_raw_parts(arg_kinds, arg_count) }; + let bits = unsafe { slice::from_raw_parts(arg_bits, arg_count) }; + let mut args = Vec::with_capacity(arg_count); + for ((kind, bits), expected) in kinds + .iter() + .copied() + .zip(bits.iter().copied()) + .zip(ty.params().iter().copied()) + { + let value = match kind { + WASM_VAL_KIND_I32 => WasmVal::I32(bits as i32), + WASM_VAL_KIND_I64 => WasmVal::I64(bits as i64), + WASM_VAL_KIND_F32 => WasmVal::F32(f32::from_bits(bits as u32)), + WASM_VAL_KIND_F64 => WasmVal::F64(f64::from_bits(bits)), + other => { + return Err(WasmHostError::UnsupportedSignature(format!( + "arg kind {other}" + ))) + } + }; + let Some(value) = coerce_numeric_value(value, expected) else { + return Err(WasmHostError::UnsupportedSignature(format!( + "table function: unsupported parameter type {expected:?}" + ))); + }; + args.push(value); + } + if ty.results().len() > out_capacity { + return Err(WasmHostError::UnsupportedSignature(format!( + "table function: {} results exceed host capacity {out_capacity}", + ty.results().len() + ))); + } + let mut results: Vec = ty.results().iter().copied().map(Val::default).collect(); + function + .call(&mut runtime.store, &args, &mut results) + .map_err(|error| WasmHostError::Runtime(error.to_string()))?; + results + .iter() + .map(|value| { + val_kind_bits(value).ok_or_else(|| { + WasmHostError::UnsupportedSignature(format!( + "table function: unsupported result type {:?}", + value.ty() + )) + }) + }) + .collect() + }); + let values = match call_result { + Ok(values) => values, + Err(error) => { + capture_err(out_err, error); + return 0; + } + }; + for (index, (kind, bits)) in values.iter().copied().enumerate() { + unsafe { + *out_kinds.add(index) = kind; + *out_bits.add(index) = bits; + } + } + unsafe { *out_count = values.len() }; + 1 +} diff --git a/crates/perry-wasm-host/src/lib.rs b/crates/perry-wasm-host/src/lib.rs index 8fcf40c593..642be0a1b2 100644 --- a/crates/perry-wasm-host/src/lib.rs +++ b/crates/perry-wasm-host/src/lib.rs @@ -11,15 +11,21 @@ //! That keeps the wasmi version surface small and lets us swap engines //! (wasmtime, etc.) behind the same shape later. -use std::cell::RefCell; +use std::cell::{RefCell, UnsafeCell}; use std::collections::HashMap; -use std::sync::Arc; - -use wasmi::{Engine, ExternRef, ExternType, Func, Linker, Module, Ref, Store, Table, Val, ValType}; - -/// Numeric WebAssembly value. MVP supports only the four core numeric types; -/// `externref` / `funcref` / `v128` are out of scope (see issue #76, "Open -/// questions"). +use std::sync::{ + atomic::{AtomicI32, AtomicU64, Ordering}, + Arc, +}; + +use wasmi::{ + Engine, Extern, ExternRef, ExternType, Func, Global, Linker, Memory, MemoryType, Module, + Mutability, Ref, Store, Table, TableType, Val, ValType, +}; + +/// Numeric WebAssembly value used by the public Rust call API. JavaScript +/// import callbacks additionally marshal `externref`, while `funcref` values +/// cross the C boundary as opaque external handles. #[derive(Clone, Copy, Debug, PartialEq)] pub enum WasmVal { I32(i32), @@ -47,16 +53,76 @@ pub struct WasmModuleHandle(Arc); struct ModuleInner { engine: Engine, module: Module, + diagnostic_id: u64, + byte_len: usize, +} + +static NEXT_MODULE_ID: AtomicU64 = AtomicU64::new(1); + +fn diagnostics_enabled() -> bool { + std::env::var_os("PERRY_WASM_TRACE").is_some() + || std::env::var_os("PERRY_WASM_DIAGNOSTICS").is_some() +} + +fn trace_module(module: &ModuleInner, event: &str) { + if !diagnostics_enabled() { + return; + } + let imports = module + .module + .imports() + .map(|import| format!("{}.{}", import.module(), import.name())) + .collect::>() + .join(","); + let exports = module + .module + .exports() + .map(|export| export.name()) + .collect::>() + .join(","); + eprintln!( + "[perry-wasm] module#{} {event} bytes={} imports=[{imports}] exports=[{exports}]", + module.diagnostic_id, module.byte_len + ); +} + +/// All WebAssembly objects in one JavaScript agent share an engine and store. +/// +/// Emscripten side modules import the main module's memory, table, functions, +/// and mutable globals. wasmi external handles can only be linked into the +/// store that owns them, so the former one-store-per-instance layout could +/// never represent that graph. JavaScript execution in Perry is thread-local; +/// mirroring that here gives each Worker an independent WebAssembly agent +/// while allowing every instance on a worker to exchange externals. +struct HostRuntime { + engine: Engine, + store: Store<()>, +} + +thread_local! { + static HOST_RUNTIME: UnsafeCell = UnsafeCell::new({ + let engine = Engine::default(); + let store = Store::new(&engine, ()); + HostRuntime { engine, store } + }); } -/// Opaque instance. Owns its own `Store` so each instance has independent -/// memory / globals — matches JS `WebAssembly.Instance` semantics. +fn with_host_runtime(f: impl FnOnce(&mut HostRuntime) -> R) -> R { + HOST_RUNTIME.with(|runtime| unsafe { f(&mut *runtime.get()) }) +} + +/// Opaque instance backed by its JavaScript agent's shared store. Wasm +/// instances still own their defined state, while imported externals retain +/// identity across an Emscripten main/side-module graph. pub struct WasmInstanceHandle { inner: Box, } struct InstanceInner { - store: Store, + /// Pointer to this thread's [`HOST_RUNTIME`] store. The thread-local + /// allocation lives for the worker lifetime and instance handles never + /// cross workers. + store: *mut Store<()>, instance: wasmi::Instance, /// Keep the module alive for the lifetime of the instance so `engine` / /// `module` references stay valid. @@ -74,6 +140,18 @@ struct InstanceInner { /// `get_memory` there would put a string lookup back on the hot path /// (#9611). memory: Option, + import_context: Arc, + exit_code: Arc, +} + +impl WasmInstanceHandle { + fn store(&self) -> &Store<()> { + unsafe { &*self.inner.store } + } + + fn store_mut(&mut self) -> &mut Store<()> { + unsafe { &mut *self.inner.store } + } } /// One resolved export: the `Func` handle, its signature, and the argument / @@ -87,16 +165,11 @@ struct CachedExport { outs: Vec, } -struct WasmHostState { - exit_code: Option, - import_callback: Option, - import_context: u64, -} - #[derive(Clone, Copy)] struct PendingTableValue { bits: u64, is_null: bool, + external: *mut c_void, } enum PendingTableOp { @@ -137,14 +210,10 @@ fn begin_instance_call(inst: &mut WasmInstanceHandle) { if table_type.element() != ValType::ExternRef { continue; } - let Some(table) = inst - .inner - .instance - .get_table(&inst.inner.store, export.name()) - else { + let Some(table) = inst.inner.instance.get_table(inst.store(), export.name()) else { continue; }; - if let Ok(len) = usize::try_from(table.size(&inst.inner.store)) { + if let Ok(len) = usize::try_from(table.size(inst.store())) { lengths.insert(export.name().to_string(), len); } } @@ -173,9 +242,17 @@ fn finish_instance_call(inst: &mut WasmInstanceHandle) -> Result<(), WasmHostErr "table export {name:?} disappeared during imported callback" ))); }; - let value = table_value(inst, value.bits, value.is_null as i32); + let Some(value) = table_value( + inst, + table, + value.bits, + value.is_null as i32, + value.external, + ) else { + return Err(WasmHostError::Runtime("invalid table value".into())); + }; table - .set(&mut inst.inner.store, index as u64, value) + .set(inst.store_mut(), index as u64, value) .map_err(|error| WasmHostError::Runtime(error.to_string()))?; } PendingTableOp::Grow { name, delta, value } => { @@ -184,9 +261,17 @@ fn finish_instance_call(inst: &mut WasmInstanceHandle) -> Result<(), WasmHostErr "table export {name:?} disappeared during imported callback" ))); }; - let value = table_value(inst, value.bits, value.is_null as i32); + let Some(value) = table_value( + inst, + table, + value.bits, + value.is_null as i32, + value.external, + ) else { + return Err(WasmHostError::Runtime("invalid table value".into())); + }; table - .grow(&mut inst.inner.store, delta as u64, value) + .grow(inst.store_mut(), delta as u64, value) .map_err(|error| WasmHostError::Runtime(error.to_string()))?; } } @@ -222,6 +307,24 @@ pub type WasmImportCallback = unsafe extern "C" fn( result_count: usize, ) -> i32; +/// Resolve a JavaScript import value to a host-owned WebAssembly external. +/// Returning null asks the host to use the ordinary JS function callback; +/// non-function imports must resolve to a matching external. +pub type WasmImportResolverCallback = unsafe extern "C" fn( + context: u64, + module: *const u8, + module_len: usize, + name: *const u8, + name_len: usize, + kind: u8, +) -> *mut c_void; + +/// Opaque external handle shared with the JS wrapper layer. The external is +/// copyable, and its backing entity remains owned by the thread-local store. +struct WasmExternHandle { + item: Extern, +} + #[derive(Debug)] pub enum WasmHostError { Compile(String), @@ -250,15 +353,24 @@ impl std::error::Error for WasmHostError {} /// Cheap byte-level magic check (`\0asm\01\0\0\0`). Mirrors `WebAssembly.validate` /// — for the MVP we delegate to wasmi's full module decode. pub fn validate(bytes: &[u8]) -> bool { - let engine = Engine::default(); - Module::new(&engine, bytes).is_ok() + with_host_runtime(|runtime| Module::new(&runtime.engine, bytes).is_ok()) } /// Compile bytes to a module. No imports resolved at this stage. pub fn compile(bytes: &[u8]) -> Result { - let engine = Engine::default(); - let module = Module::new(&engine, bytes).map_err(|e| WasmHostError::Compile(e.to_string()))?; - Ok(WasmModuleHandle(Arc::new(ModuleInner { engine, module }))) + with_host_runtime(|runtime| { + let engine = runtime.engine.clone(); + let module = + Module::new(&engine, bytes).map_err(|e| WasmHostError::Compile(e.to_string()))?; + let inner = Arc::new(ModuleInner { + engine, + module, + diagnostic_id: NEXT_MODULE_ID.fetch_add(1, Ordering::Relaxed), + byte_len: bytes.len(), + }); + trace_module(&inner, "compiled"); + Ok(WasmModuleHandle(inner)) + }) } /// Instantiate with the module's imported numeric functions routed through an @@ -266,35 +378,66 @@ pub fn compile(bytes: &[u8]) -> Result { /// zero-result fallback. `proc_exit` records its status so the JS WASI wrapper /// can return it after `_start` completes. pub fn instantiate(module: &WasmModuleHandle) -> Result { - instantiate_with_import_callback(module, None, 0) + instantiate_with_import_callbacks(module, None, None, 0) } +#[cfg_attr(not(test), allow(dead_code))] fn instantiate_with_import_callback( module: &WasmModuleHandle, import_callback: Option, import_context: u64, ) -> Result { - let mut store = Store::new( - &module.0.engine, - WasmHostState { - exit_code: None, - import_callback, - import_context, - }, - ); - let mut linker = >::new(&module.0.engine); + instantiate_with_import_callbacks(module, import_callback, None, import_context) +} + +fn instantiate_with_import_callbacks( + module: &WasmModuleHandle, + import_callback: Option, + import_resolver: Option, + import_context_value: u64, +) -> Result { + trace_module(&module.0, "instantiating"); + let store = with_host_runtime(|runtime| &mut runtime.store as *mut Store<()>); + let import_context = Arc::new(AtomicU64::new(import_context_value)); + // i32::MIN is not a valid WASI process status and acts as "not exited". + let exit_code = Arc::new(AtomicI32::new(i32::MIN)); + let mut linker = >::new(&module.0.engine); for import in module.0.module.imports() { + let module_name = import.module().to_owned(); + let import_name = import.name().to_owned(); + let import_kind = extern_type_kind(import.ty()); + let resolved = import_resolver + .map(|resolver| unsafe { + resolver( + import_context_value, + module_name.as_ptr(), + module_name.len(), + import_name.as_ptr(), + import_name.len(), + import_kind, + ) + }) + .unwrap_or(std::ptr::null_mut()); + if !resolved.is_null() { + let external = unsafe { &*(resolved as *const WasmExternHandle) }.item; + linker + .define(&module_name, &import_name, external) + .map_err(|e| WasmHostError::Link(e.to_string()))?; + continue; + } + let ExternType::Func(ty) = import.ty() else { return Err(WasmHostError::Link(format!( - "unsupported import {}.{}", + "WebAssembly import {}.{} requires a {} value", import.module(), - import.name() + import.name(), + extern_kind_label(import_kind), ))); }; - let module_name = import.module().to_owned(); - let import_name = import.name().to_owned(); let callback_module = module_name.clone(); let callback_name = import_name.clone(); + let callback_context = import_context.clone(); + let callback_exit_code = exit_code.clone(); linker .func_new( &module_name, @@ -307,10 +450,10 @@ fn instantiate_with_import_callback( ) && callback_name == "proc_exit" { if let Some(Val::I32(code)) = params.first() { - caller.data_mut().exit_code = Some(*code); + callback_exit_code.store(*code, Ordering::Relaxed); } } - if let Some(callback) = caller.data().import_callback { + if let Some(callback) = import_callback { let mut arg_kinds = Vec::with_capacity(params.len()); let mut arg_bits = Vec::with_capacity(params.len()); let mut numeric = true; @@ -318,9 +461,26 @@ fn instantiate_with_import_callback( if let Some((kind, bits)) = val_kind_bits(param) { arg_kinds.push(kind); arg_bits.push(bits); - } else { - numeric = false; - break; + continue; + } + match param { + Val::ExternRef(Ref::Null) => { + arg_kinds.push(WASM_VAL_KIND_EXTERNREF); + arg_bits.push(JS_NULL_BITS); + } + Val::ExternRef(Ref::Val(value)) => { + let Some(bits) = value.data(&caller).downcast_ref::() + else { + numeric = false; + break; + }; + arg_kinds.push(WASM_VAL_KIND_EXTERNREF); + arg_bits.push(*bits); + } + _ => { + numeric = false; + break; + } } } let mut result_kinds = Vec::with_capacity(results.len()); @@ -328,15 +488,20 @@ fn instantiate_with_import_callback( for result in results.iter() { if let Some((kind, _)) = val_kind_bits(result) { result_kinds.push(kind); - } else { - numeric = false; - break; + continue; + } + match result { + Val::ExternRef(_) => result_kinds.push(WASM_VAL_KIND_EXTERNREF), + _ => { + numeric = false; + break; + } } } if numeric { let called = unsafe { callback( - caller.data().import_context, + callback_context.load(Ordering::Relaxed), callback_module.as_ptr(), callback_module.len(), callback_name.as_ptr(), @@ -355,7 +520,15 @@ fn instantiate_with_import_callback( .zip(result_kinds.iter()) .zip(result_bits.iter()) { - *result = val_from_kind_bits(*kind, *bits); + *result = if *kind == WASM_VAL_KIND_EXTERNREF { + if *bits == JS_NULL_BITS { + Val::ExternRef(Ref::Null) + } else { + Val::from(ExternRef::new(&mut caller, *bits)) + } + } else { + val_from_kind_bits(*kind, *bits) + }; } return Ok(()); } @@ -370,9 +543,10 @@ fn instantiate_with_import_callback( .map_err(|e| WasmHostError::Link(e.to_string()))?; } let instance = linker - .instantiate_and_start(&mut store, &module.0.module) + .instantiate_and_start(unsafe { &mut *store }, &module.0.module) .map_err(|e| WasmHostError::Link(e.to_string()))?; - let memory = instance.get_memory(&store, "memory"); + let memory = instance.get_memory(unsafe { &*store }, "memory"); + trace_module(&module.0, "instantiated"); Ok(WasmInstanceHandle { inner: Box::new(InstanceInner { store, @@ -381,11 +555,18 @@ fn instantiate_with_import_callback( exports: Vec::new(), export_handles: HashMap::new(), memory, + import_context, + exit_code, }), }) } fn coerce_numeric_value(value: WasmVal, expected: ValType) -> Option { + match (value, expected) { + (WasmVal::I32(value), ValType::I32) => return Some(Val::I32(value)), + (WasmVal::I64(value), ValType::I64) => return Some(Val::I64(value)), + _ => {} + } let number = match value { WasmVal::I32(value) => value as f64, WasmVal::I64(value) => value as f64, @@ -408,8 +589,8 @@ fn resolve_export(inst: &mut WasmInstanceHandle, name: &str) -> Option { if let Some(&index) = inst.inner.export_handles.get(name) { return Some(index); } - let func = inst.inner.instance.get_func(&inst.inner.store, name)?; - let ty = func.ty(&inst.inner.store); + let func = inst.inner.instance.get_func(inst.store(), name)?; + let ty = func.ty(inst.store()); let index = inst.inner.exports.len(); inst.inner.exports.push(CachedExport { name: name.to_string(), @@ -465,11 +646,12 @@ fn call_resolved_export( // Split the borrow: the call needs the store mutably while reading the // cached argument buffer and filling the cached result buffer, and all // three are disjoint fields of the same `InstanceInner`. + let store = inst.inner.store; let inner = &mut *inst.inner; let CachedExport { func, args, outs, .. } = &mut inner.exports[index]; - func.call(&mut inner.store, args, outs) + func.call(unsafe { &mut *store }, args, outs) }; let table_result = finish_instance_call(inst); call_result.map_err(|e| WasmHostError::Runtime(e.to_string()))?; @@ -527,7 +709,7 @@ pub fn call_export( // `perry_wasm_host_string_free`). // ──────────────────────────────────────────────────────────────────────── -use std::ffi::{c_char, CString}; +use std::ffi::{c_char, c_void, CString}; use std::slice; fn capture_err(out_err: *mut *mut c_char, e: WasmHostError) { @@ -608,6 +790,16 @@ fn extern_type_kind(ty: &ExternType) -> u8 { } } +fn extern_kind_label(kind: u8) -> &'static str { + match kind { + WASM_EXTERN_KIND_FUNCTION => "function", + WASM_EXTERN_KIND_TABLE => "table", + WASM_EXTERN_KIND_MEMORY => "memory", + WASM_EXTERN_KIND_GLOBAL => "global", + _ => "external", + } +} + #[no_mangle] pub extern "C" fn perry_wasm_host_module_exports_len(module: *mut WasmModuleHandle) -> usize { if module.is_null() { @@ -790,6 +982,7 @@ pub extern "C" fn perry_wasm_host_module_custom_section_at( pub extern "C" fn perry_wasm_host_instance_new( module: *mut WasmModuleHandle, import_callback: Option, + import_resolver: Option, import_context: u64, out_err: *mut *mut c_char, ) -> *mut WasmInstanceHandle { @@ -798,7 +991,12 @@ pub extern "C" fn perry_wasm_host_instance_new( return std::ptr::null_mut(); } let module = unsafe { &*module }; - match instantiate_with_import_callback(module, import_callback, import_context) { + match instantiate_with_import_callbacks( + module, + import_callback, + import_resolver, + import_context, + ) { Ok(i) => Box::into_raw(Box::new(i)), Err(e) => { capture_err(out_err, e); @@ -813,7 +1011,9 @@ pub extern "C" fn perry_wasm_host_instance_set_import_context( import_context: u64, ) { if let Some(inst) = unsafe { inst.as_mut() } { - inst.inner.store.data_mut().import_context = import_context; + inst.inner + .import_context + .store(import_context, Ordering::Relaxed); } } @@ -824,6 +1024,16 @@ pub extern "C" fn perry_wasm_host_instance_drop(inst: *mut WasmInstanceHandle) { } } +pub(crate) fn extern_handle(item: Extern) -> *mut c_void { + Box::into_raw(Box::new(WasmExternHandle { item })) as *mut c_void +} + +pub(crate) fn extern_from_handle(handle: *mut c_void) -> Option { + (!handle.is_null()).then(|| unsafe { (*(handle as *const WasmExternHandle)).item }) +} + +mod externals; + /// Return the byte length of the exported `memory`, or zero when absent. #[no_mangle] pub extern "C" fn perry_wasm_host_instance_memory_len(inst: *mut WasmInstanceHandle) -> usize { @@ -832,8 +1042,8 @@ pub extern "C" fn perry_wasm_host_instance_memory_len(inst: *mut WasmInstanceHan }; inst.inner .instance - .get_memory(&inst.inner.store, "memory") - .map(|memory| memory.data_size(&inst.inner.store)) + .get_memory(inst.store(), "memory") + .map(|memory| memory.data_size(inst.store())) .unwrap_or(0) } @@ -869,9 +1079,9 @@ pub extern "C" fn perry_wasm_host_instance_memory_span( return std::ptr::null_mut(); }; if !out_len.is_null() { - unsafe { *out_len = memory.data_size(&inst.inner.store) }; + unsafe { *out_len = memory.data_size(inst.store()) }; } - memory.data_ptr(&inst.inner.store) + memory.data_ptr(inst.store()) } /// Copy the exported `memory` into caller-provided storage. @@ -887,10 +1097,10 @@ pub extern "C" fn perry_wasm_host_instance_memory_copy( let Some(inst) = (unsafe { inst.as_ref() }) else { return 0; }; - let Some(memory) = inst.inner.instance.get_memory(&inst.inner.store, "memory") else { + let Some(memory) = inst.inner.instance.get_memory(inst.store(), "memory") else { return 0; }; - let data = memory.data(&inst.inner.store); + let data = memory.data(inst.store()); let copied = data.len().min(len); unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), out, copied) }; copied @@ -911,10 +1121,10 @@ pub extern "C" fn perry_wasm_host_instance_memory_write( let Some(inst) = (unsafe { inst.as_mut() }) else { return 0; }; - let Some(memory) = inst.inner.instance.get_memory(&inst.inner.store, "memory") else { + let Some(memory) = inst.inner.instance.get_memory(inst.store(), "memory") else { return 0; }; - let target = memory.data_mut(&mut inst.inner.store); + let target = memory.data_mut(inst.store_mut()); let copied = target.len().min(len); unsafe { std::ptr::copy_nonoverlapping(data, target.as_mut_ptr(), copied) }; copied @@ -924,6 +1134,8 @@ mod tables; pub use tables::WASM_VAL_KIND_NONE; use tables::*; +const JS_NULL_BITS: u64 = 0x7FFC_0000_0000_0002; + /// Shared marshalling for both C call paths: decode the argument arrays, run /// the export, and encode the results into the caller's output arrays. #[allow(clippy::too_many_arguments)] @@ -1185,7 +1397,7 @@ mod tests { 0x01, 0x02, 0x03, ]; - fn memory_span(inst: &mut WasmInstanceHandle) -> (*mut u8, usize) { + pub(super) fn memory_span(inst: &mut WasmInstanceHandle) -> (*mut u8, usize) { let mut len = 0usize; let ptr = perry_wasm_host_instance_memory_span(inst, &mut len); (ptr, len) @@ -1287,8 +1499,8 @@ mod tests { let mut inst = instantiate_with_import_callback(&module, Some(peek_memory_span_import_callback), 0) .expect("instantiate"); - inst.inner.store.data_mut().import_context = - &mut inst as *mut WasmInstanceHandle as usize as u64; + let context = &mut inst as *mut WasmInstanceHandle as usize as u64; + inst.inner.import_context.store(context, Ordering::Relaxed); call_export(&mut inst, "store", &[WasmVal::I32(5), WasmVal::I32(65)]).expect("store"); assert_eq!( @@ -1495,6 +1707,7 @@ mod tests { 1, 0x1234, 0, + std::ptr::null_mut(), ), 1 ); @@ -1508,6 +1721,7 @@ mod tests { 1, &mut bits, &mut is_null, + std::ptr::null_mut(), ), 1 ); @@ -1522,6 +1736,7 @@ mod tests { 3, 0, 1, + std::ptr::null_mut(), &mut old_len, ), 1 @@ -1561,6 +1776,7 @@ mod tests { 2, 0, 1, + std::ptr::null_mut(), &mut old_len, ), 1 @@ -1574,6 +1790,7 @@ mod tests { 3, 0x5678, 0, + std::ptr::null_mut(), ), 1 ); @@ -1586,8 +1803,8 @@ mod tests { let mut inst = instantiate_with_import_callback(&module, Some(reentrant_table_import_callback), 0) .expect("instantiate"); - inst.inner.store.data_mut().import_context = - &mut inst as *mut WasmInstanceHandle as usize as u64; + let context = &mut inst as *mut WasmInstanceHandle as usize as u64; + inst.inner.import_context.store(context, Ordering::Relaxed); call_export(&mut inst, "start", &[]).expect("start"); let name = b"refs"; @@ -1609,6 +1826,7 @@ mod tests { 3, &mut bits, &mut is_null, + std::ptr::null_mut(), ), 1 ); @@ -1622,7 +1840,7 @@ mod tests { let module = compile(bytes).expect("compile"); let mut inst = instantiate(&module).expect("instantiate with proc_exit"); call_export(&mut inst, "_start", &[]).expect("call _start"); - assert_eq!(inst.inner.store.data_mut().exit_code.take(), Some(7)); + assert_eq!(inst.inner.exit_code.swap(i32::MIN, Ordering::Relaxed), 7); } #[test] @@ -1754,3 +1972,6 @@ mod tests { perry_wasm_host_module_drop(custom); } } + +#[cfg(test)] +mod shared_import_tests; diff --git a/crates/perry-wasm-host/src/shared_import_tests.rs b/crates/perry-wasm-host/src/shared_import_tests.rs new file mode 100644 index 0000000000..390acfbccd --- /dev/null +++ b/crates/perry-wasm-host/src/shared_import_tests.rs @@ -0,0 +1,92 @@ +use super::*; + +const MAIN_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, + 0x03, 0x02, 0x01, 0x00, 0x04, 0x05, 0x01, 0x70, 0x01, 0x02, 0x0a, 0x05, 0x03, 0x01, 0x00, 0x01, + 0x06, 0x06, 0x01, 0x7f, 0x01, 0x41, 0x07, 0x0b, 0x07, 0x1f, 0x04, 0x06, 0x6d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x02, 0x00, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x01, 0x00, 0x04, 0x62, 0x61, 0x73, + 0x65, 0x03, 0x00, 0x03, 0x69, 0x6e, 0x63, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, + 0x41, 0x01, 0x6a, 0x0b, +]; + +const SIDE_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x12, 0x04, 0x60, 0x01, 0x7f, 0x01, 0x7f, + 0x60, 0x00, 0x01, 0x7f, 0x60, 0x00, 0x00, 0x60, 0x02, 0x7f, 0x7f, 0x00, 0x02, 0x34, 0x04, 0x03, + 0x65, 0x6e, 0x76, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x01, 0x03, 0x65, 0x6e, + 0x76, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x01, 0x70, 0x01, 0x02, 0x0a, 0x03, 0x65, 0x6e, 0x76, + 0x04, 0x62, 0x61, 0x73, 0x65, 0x03, 0x7f, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x03, 0x69, 0x6e, 0x63, + 0x00, 0x00, 0x03, 0x06, 0x05, 0x01, 0x02, 0x00, 0x03, 0x00, 0x07, 0x30, 0x05, 0x08, 0x72, 0x65, + 0x61, 0x64, 0x42, 0x61, 0x73, 0x65, 0x00, 0x01, 0x08, 0x62, 0x75, 0x6d, 0x70, 0x42, 0x61, 0x73, + 0x65, 0x00, 0x02, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x6e, 0x63, 0x00, 0x03, 0x05, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x00, 0x04, 0x04, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x05, 0x0a, 0x29, 0x05, 0x04, + 0x00, 0x23, 0x00, 0x0b, 0x09, 0x00, 0x23, 0x00, 0x41, 0x01, 0x6a, 0x24, 0x00, 0x0b, 0x06, 0x00, + 0x20, 0x00, 0x10, 0x00, 0x0b, 0x09, 0x00, 0x20, 0x00, 0x20, 0x01, 0x3a, 0x00, 0x00, 0x0b, 0x07, + 0x00, 0x20, 0x00, 0x2d, 0x00, 0x00, 0x0b, 0x00, 0x0d, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x01, 0x06, + 0x01, 0x00, 0x03, 0x69, 0x6e, 0x63, +]; + +struct Imports { + memory: *mut c_void, + table: *mut c_void, + base: *mut c_void, + inc: *mut c_void, +} + +unsafe extern "C" fn resolve( + context: u64, + _module: *const u8, + _module_len: usize, + name: *const u8, + name_len: usize, + _kind: u8, +) -> *mut c_void { + let imports = &*(context as *const Imports); + match std::slice::from_raw_parts(name, name_len) { + b"memory" => imports.memory, + b"table" => imports.table, + b"base" => imports.base, + b"inc" => imports.inc, + _ => std::ptr::null_mut(), + } +} + +#[test] +fn instances_share_imported_memory_table_global_and_function() { + let main_module = compile(MAIN_WASM).expect("compile main"); + let mut main = instantiate(&main_module).expect("instantiate main"); + fn export(main: &mut WasmInstanceHandle, name: &[u8]) -> *mut c_void { + externals::perry_wasm_host_instance_export_extern(main, name.as_ptr().cast(), name.len()) + } + let imports = Imports { + memory: export(&mut main, b"memory"), + table: export(&mut main, b"table"), + base: export(&mut main, b"base"), + inc: export(&mut main, b"inc"), + }; + let side_module = compile(SIDE_WASM).expect("compile side"); + let mut side = instantiate_with_import_callbacks( + &side_module, + None, + Some(resolve), + &imports as *const Imports as u64, + ) + .expect("instantiate side with shared externals"); + + assert_eq!( + call_export(&mut side, "readBase", &[]).unwrap(), + [WasmVal::I32(7)] + ); + call_export(&mut side, "bumpBase", &[]).unwrap(); + assert_eq!( + call_export(&mut side, "readBase", &[]).unwrap(), + [WasmVal::I32(8)] + ); + assert_eq!( + call_export(&mut side, "callInc", &[WasmVal::I32(41)]).unwrap(), + [WasmVal::I32(42)] + ); + call_export(&mut side, "store", &[WasmVal::I32(3), WasmVal::I32(99)]).unwrap(); + let (memory, len) = super::tests::memory_span(&mut main); + assert_eq!(len, 65536); + assert_eq!(unsafe { *memory.add(3) }, 99); +} diff --git a/crates/perry-wasm-host/src/tables.rs b/crates/perry-wasm-host/src/tables.rs index 5c0d6abdde..3e277be3c6 100644 --- a/crates/perry-wasm-host/src/tables.rs +++ b/crates/perry-wasm-host/src/tables.rs @@ -4,7 +4,7 @@ use super::*; pub(crate) fn instance_table(inst: &WasmInstanceHandle, name: &str) -> Option
{ - inst.inner.instance.get_table(&inst.inner.store, name) + inst.inner.instance.get_table(inst.store(), name) } /// Return the current length of an exported table, or `usize::MAX` when the @@ -27,7 +27,7 @@ pub extern "C" fn perry_wasm_host_instance_table_len( return len.unwrap_or(usize::MAX); } instance_table(inst, name) - .and_then(|table| usize::try_from(table.size(&inst.inner.store)).ok()) + .and_then(|table| usize::try_from(table.size(inst.store())).ok()) .unwrap_or(usize::MAX) } @@ -41,6 +41,7 @@ pub extern "C" fn perry_wasm_host_instance_table_get( index: usize, out_bits: *mut u64, out_is_null: *mut i32, + out_external: *mut *mut c_void, ) -> i32 { if out_bits.is_null() || out_is_null.is_null() { return 0; @@ -63,38 +64,68 @@ pub extern "C" fn perry_wasm_host_instance_table_get( unsafe { *out_bits = value.bits; *out_is_null = value.is_null as i32; + if !out_external.is_null() { + *out_external = value.external; + } } return 1; } let Some(table) = instance_table(inst, name) else { return 0; }; - let Some(Val::ExternRef(value)) = table.get(&inst.inner.store, index as u64) else { + let Some(value) = table.get(inst.store(), index as u64) else { return 0; }; match value { - Ref::Null => unsafe { + Val::ExternRef(Ref::Null) | Val::FuncRef(Ref::Null) => unsafe { *out_bits = 0; *out_is_null = 1; + if !out_external.is_null() { + *out_external = std::ptr::null_mut(); + } }, - Ref::Val(value) => { - let Some(bits) = value.data(&inst.inner.store).downcast_ref::() else { + Val::ExternRef(Ref::Val(value)) => { + let Some(bits) = value.data(inst.store()).downcast_ref::() else { return 0; }; unsafe { *out_bits = *bits; *out_is_null = 0; + if !out_external.is_null() { + *out_external = std::ptr::null_mut(); + } } } + Val::FuncRef(Ref::Val(function)) => unsafe { + *out_bits = 0; + *out_is_null = 0; + if !out_external.is_null() { + *out_external = extern_handle(function.into()); + } + }, + _ => return 0, } 1 } -pub(crate) fn table_value(inst: &mut WasmInstanceHandle, bits: u64, is_null: i32) -> Val { +pub(crate) fn table_value( + inst: &mut WasmInstanceHandle, + table: Table, + bits: u64, + is_null: i32, + external: *mut c_void, +) -> Option { + let element = table.ty(inst.store()).element(); if is_null != 0 { - Val::ExternRef(Ref::Null) - } else { - Val::from(ExternRef::new(&mut inst.inner.store, bits)) + return Some(Val::default(element)); + } + match element { + ValType::ExternRef => Some(Val::from(ExternRef::new(inst.store_mut(), bits))), + ValType::FuncRef => match extern_from_handle(external) { + Some(Extern::Func(function)) => Some(Val::FuncRef(Ref::Val(function))), + _ => None, + }, + _ => None, } } @@ -106,6 +137,7 @@ pub extern "C" fn perry_wasm_host_instance_table_set( index: usize, bits: u64, is_null: i32, + external: *mut c_void, ) -> i32 { let Some(inst) = (unsafe { inst.as_mut() }) else { return 0; @@ -116,6 +148,7 @@ pub extern "C" fn perry_wasm_host_instance_table_set( let pending_value = PendingTableValue { bits, is_null: is_null != 0, + external, }; if let Some(queued) = with_active_instance_tables(inst as *mut _ as usize, |active| { let Some(len) = active.lengths.get(name).copied() else { @@ -139,13 +172,10 @@ pub extern "C" fn perry_wasm_host_instance_table_set( let Some(table) = instance_table(inst, name) else { return 0; }; - if table.ty(&inst.inner.store).element() != ValType::ExternRef { + let Some(value) = table_value(inst, table, bits, is_null, external) else { return 0; - } - let value = table_value(inst, bits, is_null); - table - .set(&mut inst.inner.store, index as u64, value) - .is_ok() as i32 + }; + table.set(inst.store_mut(), index as u64, value).is_ok() as i32 } #[no_mangle] @@ -156,6 +186,7 @@ pub extern "C" fn perry_wasm_host_instance_table_grow( delta: usize, bits: u64, is_null: i32, + external: *mut c_void, out_old_len: *mut usize, ) -> i32 { if out_old_len.is_null() { @@ -170,6 +201,7 @@ pub extern "C" fn perry_wasm_host_instance_table_grow( let pending_value = PendingTableValue { bits, is_null: is_null != 0, + external, }; if let Some(old_len) = with_active_instance_tables(inst as *mut _ as usize, |active| { let old_len = *active.lengths.get(name)?; @@ -196,11 +228,10 @@ pub extern "C" fn perry_wasm_host_instance_table_grow( let Some(table) = instance_table(inst, name) else { return 0; }; - if table.ty(&inst.inner.store).element() != ValType::ExternRef { + let Some(value) = table_value(inst, table, bits, is_null, external) else { return 0; - } - let value = table_value(inst, bits, is_null); - let Ok(old_len) = table.grow(&mut inst.inner.store, delta as u64, value) else { + }; + let Ok(old_len) = table.grow(inst.store_mut(), delta as u64, value) else { return 0; }; let Ok(old_len) = usize::try_from(old_len) else { @@ -222,9 +253,10 @@ pub extern "C" fn perry_wasm_host_instance_take_exit_code( let Some(inst) = (unsafe { inst.as_mut() }) else { return 0; }; - let Some(code) = inst.inner.store.data_mut().exit_code.take() else { + let code = inst.inner.exit_code.swap(i32::MIN, Ordering::Relaxed); + if code == i32::MIN { return 0; - }; + } unsafe { *out_code = code }; 1 } @@ -235,6 +267,7 @@ pub const WASM_VAL_KIND_I32: u8 = 0; pub const WASM_VAL_KIND_I64: u8 = 1; pub const WASM_VAL_KIND_F32: u8 = 2; pub const WASM_VAL_KIND_F64: u8 = 3; +pub const WASM_VAL_KIND_EXTERNREF: u8 = 4; pub const WASM_VAL_KIND_NONE: u8 = 0xFF; /// Call an export by name. Args are encoded as parallel arrays: diff --git a/crates/perry/src/commands/compile/collect_modules/tests.rs b/crates/perry/src/commands/compile/collect_modules/tests.rs index 5af8ee041e..6d03afcd5c 100644 --- a/crates/perry/src/commands/compile/collect_modules/tests.rs +++ b/crates/perry/src/commands/compile/collect_modules/tests.rs @@ -753,6 +753,67 @@ console.log(tone); assert!(ctx.native_modules.contains_key(&canonical_asset)); } +fn assert_dynamic_asset_import(source: &str, filename: &str, bytes: &[u8]) { + let dir = tempfile::tempdir().expect("tempdir"); + let root = dir.path(); + let asset = root.join(filename); + let entry = root.join("entry.ts"); + std::fs::write(&asset, bytes).expect("write asset"); + std::fs::write(&entry, source).expect("write entry"); + + let mut ctx = CompilationContext::new(root.to_path_buf()); + ctx.entry_canonical = Some(entry.canonicalize().unwrap()); + let mut visited = HashSet::new(); + let mut next_class_id: perry_hir::ClassId = 1; + let progress = VerboseProgress::new(OutputFormat::Text, 0); + collect_modules( + &entry, + &mut ctx, + &mut visited, + OutputFormat::Text, + None, + &mut next_class_id, + false, + &progress, + None, + ) + .expect("collect modules"); + + let canonical_asset = asset.canonicalize().unwrap(); + assert!(ctx.file_loader_asset_paths.contains(&canonical_asset)); + assert!(ctx + .embedded_assets + .iter() + .any(|(name, path)| path == &canonical_asset && name.ends_with(filename))); + assert!(ctx.native_modules.contains_key(&canonical_asset)); +} + +#[test] +fn dynamic_wasm_import_attribute_returns_embedded_path() { + assert_dynamic_asset_import( + r#" +export async function load() { + return import("./tree-sitter.wasm", { with: { type: "wasm" } }); +} +"#, + "tree-sitter.wasm", + &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], + ); +} + +#[test] +fn dynamic_file_import_attribute_handles_tree_sitter_queries() { + assert_dynamic_asset_import( + r#" +export async function load() { + return import("./highlights.scm", { with: { type: "file" } }); +} +"#, + "highlights.scm", + b"(comment) @comment\n", + ); +} + #[test] fn missing_generated_module_fails_with_preparation_remediation() { let dir = tempfile::tempdir().expect("tempdir"); diff --git a/crates/perry/src/commands/compile/collect_modules_helpers.rs b/crates/perry/src/commands/compile/collect_modules_helpers.rs index 07b538cd4f..494223c2df 100644 --- a/crates/perry/src/commands/compile/collect_modules_helpers.rs +++ b/crates/perry/src/commands/compile/collect_modules_helpers.rs @@ -7,12 +7,72 @@ use std::collections::HashSet; use std::path::Path; -/// Return imports that request Bun's file loader: -/// `import path from "./asset.bin" with { type: "file" }`. +fn object_string_property<'a>( + object: &'a swc_ecma_ast::ObjectLit, + name: &str, +) -> Option<&'a swc_ecma_ast::Str> { + use swc_ecma_ast::{Expr, Lit, Prop, PropName, PropOrSpread}; + + object.props.iter().find_map(|prop| { + let PropOrSpread::Prop(prop) = prop else { + return None; + }; + let Prop::KeyValue(property) = prop.as_ref() else { + return None; + }; + let matches_name = match &property.key { + PropName::Ident(key) => key.sym == *name, + PropName::Str(key) => key.value == *name, + _ => false, + }; + if !matches_name { + return None; + } + match property.value.as_ref() { + Expr::Lit(Lit::Str(value)) => Some(value), + _ => None, + } + }) +} + +fn object_property<'a>( + object: &'a swc_ecma_ast::ObjectLit, + name: &str, +) -> Option<&'a swc_ecma_ast::Expr> { + use swc_ecma_ast::{Prop, PropName, PropOrSpread}; + + object.props.iter().find_map(|prop| { + let PropOrSpread::Prop(prop) = prop else { + return None; + }; + let Prop::KeyValue(property) = prop.as_ref() else { + return None; + }; + let matches_name = match &property.key { + PropName::Ident(key) => key.sym == *name, + PropName::Str(key) => key.value == *name, + _ => false, + }; + matches_name.then_some(property.value.as_ref()) + }) +} + +fn requests_asset_path(attributes: &swc_ecma_ast::ObjectLit) -> bool { + object_string_property(attributes, "type") + .and_then(|value| value.value.as_str()) + .is_some_and(|kind| matches!(kind, "file" | "wasm")) +} + +/// Return imports that request an embedded asset path. This includes static +/// Bun file-loader imports and dynamic import options used by OpenCode: +/// +/// `import path from "./asset.bin" with { type: "file" }` +/// `import("./tree-sitter.wasm", { with: { type: "wasm" } })` pub(super) fn file_loader_import_sources(module: &swc_ecma_ast::Module) -> HashSet { - use swc_ecma_ast::{Expr, Lit, ModuleDecl, ModuleItem, Prop, PropName, PropOrSpread}; + use swc_ecma_ast::{Callee, Expr, Lit, ModuleDecl, ModuleItem}; + use swc_ecma_visit::{Visit, VisitWith}; - module + let mut sources: HashSet = module .body .iter() .filter_map(|item| { @@ -20,27 +80,43 @@ pub(super) fn file_loader_import_sources(module: &swc_ecma_ast::Module) -> HashS return None; }; let attributes = import.with.as_deref()?; - let uses_file_loader = attributes.props.iter().any(|prop| { - let PropOrSpread::Prop(prop) = prop else { - return false; - }; - let Prop::KeyValue(property) = prop.as_ref() else { - return false; - }; - let is_type = match &property.key { - PropName::Ident(name) => name.sym == *"type", - PropName::Str(name) => name.value == *"type", - _ => false, - }; - is_type - && matches!( - property.value.as_ref(), - Expr::Lit(Lit::Str(value)) if value.value == *"file" - ) - }); - uses_file_loader.then(|| import.src.value.as_str().unwrap_or("").to_string()) + requests_asset_path(attributes) + .then(|| import.src.value.as_str().unwrap_or("").to_string()) }) - .collect() + .collect(); + + struct DynamicAssetImports<'a>(&'a mut HashSet); + impl Visit for DynamicAssetImports<'_> { + fn visit_call_expr(&mut self, call: &swc_ecma_ast::CallExpr) { + if matches!(call.callee, Callee::Import(_)) { + let source = call.args.first().and_then(|arg| match arg.expr.as_ref() { + Expr::Lit(Lit::Str(source)) => source.value.as_str(), + _ => None, + }); + let options = call.args.get(1).and_then(|arg| match arg.expr.as_ref() { + Expr::Object(options) => Some(options), + _ => None, + }); + let attributes = options.and_then(|options| { + object_property(options, "with") + .or_else(|| object_property(options, "assert")) + .and_then(|value| match value { + Expr::Object(attributes) => Some(attributes), + _ => None, + }) + .or(Some(options)) + }); + if let (Some(source), Some(attributes)) = (source, attributes) { + if requests_asset_path(attributes) { + self.0.insert(source.to_string()); + } + } + } + call.visit_children_with(self); + } + } + module.visit_with(&mut DynamicAssetImports(&mut sources)); + sources } /// Produce a stable virtual asset name without leaking an absolute source path. diff --git a/crates/perry/tests/issue_5234_wasm_esm_import.rs b/crates/perry/tests/issue_5234_wasm_esm_import.rs index ce02ed95c5..a8f620488c 100644 --- a/crates/perry/tests/issue_5234_wasm_esm_import.rs +++ b/crates/perry/tests/issue_5234_wasm_esm_import.rs @@ -163,6 +163,43 @@ const EXTERNREF_TABLE_CYCLE_WASM: &[u8] = &[ 0x01, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x10, 0x00, 0x0b, ]; +/// Main module for #10102's Emscripten-style shared-resource graph. Its +/// memory, funcref table, mutable global and function are all imported by +/// `SHARED_SIDE_WASM` through a Proxy-backed namespace. +const SHARED_MAIN_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, + 0x03, 0x02, 0x01, 0x00, 0x04, 0x05, 0x01, 0x70, 0x01, 0x02, 0x0a, 0x05, 0x03, 0x01, 0x00, 0x01, + 0x06, 0x06, 0x01, 0x7f, 0x01, 0x41, 0x07, 0x0b, 0x07, 0x1f, 0x04, 0x06, 0x6d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x02, 0x00, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x01, 0x00, 0x04, 0x62, 0x61, 0x73, + 0x65, 0x03, 0x00, 0x03, 0x69, 0x6e, 0x63, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, + 0x41, 0x01, 0x6a, 0x0b, +]; + +const SHARED_SIDE_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x12, 0x04, 0x60, 0x01, 0x7f, 0x01, 0x7f, + 0x60, 0x00, 0x01, 0x7f, 0x60, 0x00, 0x00, 0x60, 0x02, 0x7f, 0x7f, 0x00, 0x02, 0x34, 0x04, 0x03, + 0x65, 0x6e, 0x76, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x01, 0x03, 0x65, 0x6e, + 0x76, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x01, 0x70, 0x01, 0x02, 0x0a, 0x03, 0x65, 0x6e, 0x76, + 0x04, 0x62, 0x61, 0x73, 0x65, 0x03, 0x7f, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x03, 0x69, 0x6e, 0x63, + 0x00, 0x00, 0x03, 0x06, 0x05, 0x01, 0x02, 0x00, 0x03, 0x00, 0x07, 0x30, 0x05, 0x08, 0x72, 0x65, + 0x61, 0x64, 0x42, 0x61, 0x73, 0x65, 0x00, 0x01, 0x08, 0x62, 0x75, 0x6d, 0x70, 0x42, 0x61, 0x73, + 0x65, 0x00, 0x02, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x6e, 0x63, 0x00, 0x03, 0x05, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x00, 0x04, 0x04, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x05, 0x0a, 0x29, 0x05, 0x04, + 0x00, 0x23, 0x00, 0x0b, 0x09, 0x00, 0x23, 0x00, 0x41, 0x01, 0x6a, 0x24, 0x00, 0x0b, 0x06, 0x00, + 0x20, 0x00, 0x10, 0x00, 0x0b, 0x09, 0x00, 0x20, 0x00, 0x20, 0x01, 0x3a, 0x00, 0x00, 0x0b, 0x07, + 0x00, 0x20, 0x00, 0x2d, 0x00, 0x00, 0x0b, +]; + +/// Tree-sitter query exports use more than four parameters. Keep the wasm +/// wrapper aligned with Perry's existing dynamic closure ABI (0..=16 args). +const SUM11_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x10, 0x01, 0x60, 0x0b, 0x7f, 0x7f, 0x7f, + 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x09, + 0x01, 0x05, 0x73, 0x75, 0x6d, 0x31, 0x31, 0x00, 0x00, 0x0a, 0x24, 0x01, 0x22, 0x00, 0x20, 0x00, + 0x20, 0x01, 0x6a, 0x20, 0x02, 0x6a, 0x20, 0x03, 0x6a, 0x20, 0x04, 0x6a, 0x20, 0x05, 0x6a, 0x20, + 0x06, 0x6a, 0x20, 0x07, 0x6a, 0x20, 0x08, 0x6a, 0x20, 0x09, 0x6a, 0x20, 0x0a, 0x6a, 0x0b, +]; + const MAIN_FIXTURE: &str = r#" import wasmDefault, { add } from "./add.wasm"; import * as wasmNamespace from "./add.wasm"; @@ -181,6 +218,9 @@ import { growThenPeek, load as peekLoad, store as peekStore } from "./grow-then- import { runTableCycle } from "./table-glue"; import addWasmPath from "./file-add.wasm" with { type: "file" }; import importedWasmPath from "./file-imported.wasm" with { type: "file" }; +import sharedMainPath from "./shared-main.wasm" with { type: "file" }; +import sharedSidePath from "./shared-side.wasm" with { type: "file" }; +import sum11Path from "./sum11.wasm" with { type: "file" }; import { readFileSync } from "node:fs"; console.log("namespace=" + wasmNamespace.add(2, 3)); @@ -218,6 +258,52 @@ const importedInstance = new WebAssembly.Instance(importedModule, { console.log("fileImported=" + importedInstance.exports.call(20)); console.log("instanceLength=" + WebAssembly.Instance.length); +// #10102: Emscripten side modules import the main module's live externals via +// a Proxy. These must remain the exact same wasmi memory/table/global/function, +// not JS snapshots or placeholder resources from a second store. +const sharedMain = new WebAssembly.Instance( + new WebAssembly.Module(readFileSync(sharedMainPath)), +); +const sharedEnv = new Proxy({}, { + get(_target: object, name: string) { + return sharedMain.exports[name]; + }, +}); +const sharedSide = new WebAssembly.Instance( + new WebAssembly.Module(readFileSync(sharedSidePath)), + { env: sharedEnv }, +); +console.log("sharedGlobal=" + sharedSide.exports.readBase()); +sharedSide.exports.bumpBase(); +console.log("sharedGlobalAfter=" + sharedMain.exports.base.value); +console.log("sharedFunction=" + sharedSide.exports.callInc(41)); +sharedSide.exports.store(3, 99); +console.log("sharedMemory=" + new Uint8Array(sharedMain.exports.memory.buffer)[3]); +console.log("sharedTable=" + sharedMain.exports.table.length); +WebAssembly.instantiate(readFileSync(sharedSidePath), { env: sharedEnv }).then(({ instance }) => { + console.log("sharedThen=" + instance.exports.callInc(8)); +}); +const sum11 = new WebAssembly.Instance(new WebAssembly.Module(readFileSync(sum11Path))); +console.log("sum11=" + sum11.exports.sum11(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)); + +// Emscripten creates its table in JS and stores wasm-wrapped callbacks in it. +const jsTable = new WebAssembly.Table({ initial: 2, maximum: 4, element: "anyfunc" }); +jsTable.set(0, fileInstance.exports.add); +console.log("tableFunction=" + jsTable.get(0)(18, 24)); +console.log("tableGrow=" + jsTable.grow(1) + ":" + jsTable.length); + +const jsGlobal = new WebAssembly.Global({ value: "i32", mutable: true }, 7); +jsGlobal.value = 9; +console.log("globalCtor=" + jsGlobal.value); +const jsMemory = new WebAssembly.Memory({ initial: 1, maximum: 2 }); +new Uint8Array(jsMemory.buffer)[0] = 17; +console.log("memoryCtor=" + jsMemory.buffer.byteLength + ":" + new Uint8Array(jsMemory.buffer)[0]); +try { + jsMemory.grow(2); +} catch (error) { + console.log("memoryMaximum=" + error.name); +} + // #9611: `memory.buffer` exposes the engine's linear memory itself rather than // a copy synchronised around each call, so a view built BEFORE a call observes // what that call wrote, and a write through that view is what the next call @@ -320,6 +406,11 @@ fn write_fixture(root: &std::path::Path) { EXTERNREF_TABLE_CYCLE_WASM, ) .expect("write externref table cycle wasm"); + std::fs::write(root.join("shared-main.wasm"), SHARED_MAIN_WASM) + .expect("write shared main wasm"); + std::fs::write(root.join("shared-side.wasm"), SHARED_SIDE_WASM) + .expect("write shared side wasm"); + std::fs::write(root.join("sum11.wasm"), SUM11_WASM).expect("write sum11 wasm"); std::fs::write(root.join("glue.ts"), GLUE_FIXTURE).expect("write glue.ts"); std::fs::write(root.join("table-glue.ts"), TABLE_GLUE_FIXTURE).expect("write table glue ts"); std::fs::write(root.join("main.ts"), MAIN_FIXTURE).expect("write main.ts"); @@ -383,6 +474,21 @@ fn wasm_esm_import_instantiates_and_exposes_exports() { assert!(stdout.contains("fileInstance=21"), "stdout:\n{stdout}"); assert!(stdout.contains("fileImported=21"), "stdout:\n{stdout}"); assert!(stdout.contains("instanceLength=1"), "stdout:\n{stdout}"); + assert!(stdout.contains("sharedGlobal=7"), "stdout:\n{stdout}"); + assert!(stdout.contains("sharedGlobalAfter=8"), "stdout:\n{stdout}"); + assert!(stdout.contains("sharedFunction=42"), "stdout:\n{stdout}"); + assert!(stdout.contains("sharedMemory=99"), "stdout:\n{stdout}"); + assert!(stdout.contains("sharedTable=2"), "stdout:\n{stdout}"); + assert!(stdout.contains("sharedThen=9"), "stdout:\n{stdout}"); + assert!(stdout.contains("sum11=66"), "stdout:\n{stdout}"); + assert!(stdout.contains("tableFunction=42"), "stdout:\n{stdout}"); + assert!(stdout.contains("tableGrow=2:3"), "stdout:\n{stdout}"); + assert!(stdout.contains("globalCtor=9"), "stdout:\n{stdout}"); + assert!(stdout.contains("memoryCtor=65536:17"), "stdout:\n{stdout}"); + assert!( + stdout.contains("memoryMaximum=RangeError"), + "stdout:\n{stdout}" + ); // #9611 — zero-copy linear memory. assert!(stdout.contains("viewToWasm=21"), "stdout:\n{stdout}"); assert!(stdout.contains("wasmToView=77"), "stdout:\n{stdout}"); diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index 175df711d1..720219cd8f 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -944 +943 diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index b299d25523..eb2e7ecd34 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -155,4 +155,4 @@ 1 crates/perry-runtime/src/value/to_string.rs 19 crates/perry-runtime/src/wasi.rs 4 crates/perry-runtime/src/weakref.rs -20 crates/perry-runtime/src/webassembly.rs +19 crates/perry-runtime/src/webassembly.rs