diff --git a/core/modules/map.rs b/core/modules/map.rs index efb4e9a37..b1fd2a63c 100644 --- a/core/modules/map.rs +++ b/core/modules/map.rs @@ -282,6 +282,13 @@ impl ModuleMap { self.data.borrow().get_handle(id) } + pub(crate) fn get_source( + &self, + id: ModuleId, + ) -> Option> { + self.data.borrow().get_source(id) + } + pub(crate) fn serialize_for_snapshotting( &self, data_store: &mut SnapshotStoreDataStore, @@ -682,7 +689,7 @@ impl ModuleMap { .to_rust_string_lossy(tc_scope); let import_attributes = module_request.get_import_attributes(); - + // TODO(bartlomieju): this should handle import phase - ie. we don't handle source phase imports here let attributes = parse_import_attributes( tc_scope, import_attributes, @@ -756,6 +763,7 @@ impl ModuleMap { let Some(wasm_module) = v8::WasmModuleObject::compile(scope, bytes) else { return Err(ModuleConcreteError::WasmCompile(name.to_string()).into()); }; + let wasm_module_object: v8::Local = wasm_module.into(); let wasm_module_value: v8::Local = wasm_module.into(); let js_wasm_module_source = @@ -770,7 +778,7 @@ impl ModuleMap { let _synthetic_mod_id = self.new_synthetic_module(scope, name1, synthetic_module_type, exports); - self.new_module_from_js_source( + let mod_id = self.new_module_from_js_source( scope, false, ModuleType::Wasm, @@ -778,7 +786,14 @@ impl ModuleMap { js_wasm_module_source.into(), is_dynamic_import, None, - ) + )?; + self + .data + .borrow_mut() + .sources + .insert(mod_id, v8::Global::new(scope, wasm_module_object)); + + Ok(mod_id) } pub(crate) fn new_json_module( @@ -834,8 +849,11 @@ impl ModuleMap { } tc_scope.set_slot(self as *const _); - let instantiate_result = - module.instantiate_module(tc_scope, Self::module_resolve_callback); + let instantiate_result = module.instantiate_module2( + tc_scope, + Self::module_resolve_callback, + Self::module_source_callback, + ); tc_scope.remove_slot::<*const Self>(); if instantiate_result.is_none() { let exception = tc_scope.exception().unwrap(); @@ -894,6 +912,54 @@ impl ModuleMap { None } + fn module_source_callback<'s>( + context: v8::Local<'s, v8::Context>, + specifier: v8::Local<'s, v8::String>, + import_attributes: v8::Local<'s, v8::FixedArray>, + referrer: v8::Local<'s, v8::Module>, + ) -> Option> { + // SAFETY: `CallbackScope` can be safely constructed from `Local` + let scope = &mut unsafe { v8::CallbackScope::new(context) }; + + let module_map = + // SAFETY: We retrieve the pointer from the slot, having just set it a few stack frames up + unsafe { scope.get_slot::<*const Self>().unwrap().as_ref().unwrap() }; + + let referrer_global = v8::Global::new(scope, referrer); + + let referrer_name = module_map + .data + .borrow() + .get_name_by_module(&referrer_global) + .expect("ModuleInfo not found"); + + let specifier_str = specifier.to_rust_string_lossy(scope); + + let attributes = parse_import_attributes( + scope, + import_attributes, + ImportAttributesKind::StaticImport, + ); + let maybe_source = module_map.source_callback( + scope, + &specifier_str, + &referrer_name, + attributes, + ); + if let Some(source) = maybe_source { + return Some(source); + } + + crate::error::throw_js_error_class( + scope, + // TODO(bartlomieju): d8 uses SyntaxError here + &JsErrorBox::type_error(format!( + r#"Module source can not be imported for "{specifier_str}" from "{referrer_name}""# + )), + ); + None + } + /// Resolve provided module. This function calls out to `loader.resolve`, /// but applies some additional checks that disallow resolving/importing /// certain modules (eg. `ext:` or `node:` modules) @@ -955,6 +1021,35 @@ impl ModuleMap { None } + /// Called by `module_source_callback` during module instantiation. + fn source_callback<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + specifier: &str, + referrer: &str, + import_attributes: HashMap, + ) -> Option> { + let resolved_specifier = + match self.resolve(specifier, referrer, ResolutionKind::Import) { + Ok(s) => s, + Err(e) => { + crate::error::throw_js_error_class(scope, &e); + return None; + } + }; + + let module_type = + get_requested_module_type_from_attributes(&import_attributes); + + if let Some(id) = self.get_id(resolved_specifier.as_str(), module_type) { + if let Some(handle) = self.get_source(id) { + return Some(v8::Local::new(scope, handle)); + } + } + + None + } + pub(crate) fn get_requested_modules( &self, id: ModuleId, diff --git a/core/modules/module_map_data.rs b/core/modules/module_map_data.rs index 0625a3e9d..5d7036036 100644 --- a/core/modules/module_map_data.rs +++ b/core/modules/module_map_data.rs @@ -131,6 +131,7 @@ pub(crate) struct ModuleMapData { pub(crate) handles_inverted: HashMap, usize>, /// The handles we have loaded so far, corresponding with the [`ModuleInfo`] in `info`. pub(crate) handles: Vec>, + pub(crate) sources: HashMap>, pub(crate) main_module_callbacks: Vec>, /// The modules we have loaded so far. pub(crate) info: Vec, @@ -250,6 +251,13 @@ impl ModuleMapData { self.handles.get(id).cloned() } + pub(crate) fn get_source( + &self, + id: ModuleId, + ) -> Option> { + self.sources.get(&id).cloned() + } + pub(crate) fn get_name_by_module( &self, global: &v8::Global, diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs index 24a3db382..e21e49e18 100644 --- a/core/runtime/bindings.rs +++ b/core/runtime/bindings.rs @@ -640,14 +640,33 @@ pub extern "C" fn wasm_async_resolve_promise_callback( #[allow(clippy::unnecessary_wraps)] pub fn host_import_module_dynamically_callback<'s>( + scope: &mut v8::HandleScope<'s>, + host_defined_options: v8::Local<'s, v8::Data>, + resource_name: v8::Local<'s, v8::Value>, + specifier: v8::Local<'s, v8::String>, + import_attributes: v8::Local<'s, v8::FixedArray>, +) -> Option> { + host_import_module_with_phase_dynamically_callback( + scope, + host_defined_options, + resource_name, + specifier, + v8::ModuleImportPhase::kEvaluation, + import_attributes, + ) +} + +#[allow(clippy::unnecessary_wraps)] +pub fn host_import_module_with_phase_dynamically_callback<'s>( scope: &mut v8::HandleScope<'s>, _host_defined_options: v8::Local<'s, v8::Data>, resource_name: v8::Local<'s, v8::Value>, specifier: v8::Local<'s, v8::String>, + phase: v8::ModuleImportPhase, import_attributes: v8::Local<'s, v8::FixedArray>, ) -> Option> { + eprintln!("dynamic import phase {:?}", phase); let cped = scope.get_continuation_preserved_embedder_data(); - // NOTE(bartlomieju): will crash for non-UTF-8 specifier let specifier_str = specifier .to_string(scope) diff --git a/core/runtime/setup.rs b/core/runtime/setup.rs index e2ca26806..56d6fc15b 100644 --- a/core/runtime/setup.rs +++ b/core/runtime/setup.rs @@ -28,6 +28,7 @@ fn v8_init( " --turbo_fast_api_calls", " --harmony-temporal", " --js-float16array", + " --js-source-phase-imports", ); let snapshot_flags = "--predictable --random-seed=42"; let expose_natives_flags = "--expose_gc --allow_natives_syntax"; @@ -172,6 +173,9 @@ pub fn create_isolate( isolate.set_host_import_module_dynamically_callback( bindings::host_import_module_dynamically_callback, ); + isolate.set_host_import_module_with_phase_dynamically_callback( + bindings::host_import_module_with_phase_dynamically_callback, + ); isolate.set_wasm_async_resolve_promise_callback( bindings::wasm_async_resolve_promise_callback, ); diff --git a/testing/integration/source_phase_imports/add.wasm b/testing/integration/source_phase_imports/add.wasm new file mode 100644 index 000000000..357f72da7 Binary files /dev/null and b/testing/integration/source_phase_imports/add.wasm differ diff --git a/testing/integration/source_phase_imports/add.wat b/testing/integration/source_phase_imports/add.wat new file mode 100644 index 000000000..88e8ef27f --- /dev/null +++ b/testing/integration/source_phase_imports/add.wat @@ -0,0 +1,9 @@ +(module + (func $add (param $a i32) (param $b i32) (result i32) + local.get $a + local.get $b + i32.add + ) + + (export "add" (func $add)) +) \ No newline at end of file diff --git a/testing/integration/source_phase_imports/source_phase_imports.js b/testing/integration/source_phase_imports/source_phase_imports.js new file mode 100644 index 000000000..81eaacfb8 --- /dev/null +++ b/testing/integration/source_phase_imports/source_phase_imports.js @@ -0,0 +1,20 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import source mod from "./add.wasm"; + +// To regenerate Wasm file use: +// npx -p wabt wat2wasm ./testing/integration/source_phase_imports/add.wat -o ./testing/integration/source_phase_imports/add.wasm + +if (Object.getPrototypeOf(mod) !== WebAssembly.Module.prototype) { + throw new Error("Wrong prototype"); +} + +if (mod[Symbol.toStringTag] !== "WebAssembly.Module") { + throw new Error("Wrong Symbol.toStringTag"); +} + +console.log(mod[Symbol.toStringTag]); +console.log("exports", WebAssembly.Module.exports(mod)); +console.log("imports", WebAssembly.Module.imports(mod)); + +const instance = new WebAssembly.Instance(mod, {}); +console.log("result", instance.exports.add(1, 2)); diff --git a/testing/integration/source_phase_imports/source_phase_imports.out b/testing/integration/source_phase_imports/source_phase_imports.out new file mode 100644 index 000000000..1d3636565 --- /dev/null +++ b/testing/integration/source_phase_imports/source_phase_imports.out @@ -0,0 +1,9 @@ +WebAssembly.Module +exports [ + { + "name": "add", + "kind": "function" + } +] +imports [] +result 3 diff --git a/testing/integration/source_phase_imports_dynamic/add.wasm b/testing/integration/source_phase_imports_dynamic/add.wasm new file mode 100644 index 000000000..357f72da7 Binary files /dev/null and b/testing/integration/source_phase_imports_dynamic/add.wasm differ diff --git a/testing/integration/source_phase_imports_dynamic/add.wat b/testing/integration/source_phase_imports_dynamic/add.wat new file mode 100644 index 000000000..88e8ef27f --- /dev/null +++ b/testing/integration/source_phase_imports_dynamic/add.wat @@ -0,0 +1,9 @@ +(module + (func $add (param $a i32) (param $b i32) (result i32) + local.get $a + local.get $b + i32.add + ) + + (export "add" (func $add)) +) \ No newline at end of file diff --git a/testing/integration/source_phase_imports_dynamic/source_phase_imports_dynamic.js b/testing/integration/source_phase_imports_dynamic/source_phase_imports_dynamic.js new file mode 100644 index 000000000..2035702dd --- /dev/null +++ b/testing/integration/source_phase_imports_dynamic/source_phase_imports_dynamic.js @@ -0,0 +1,20 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +const mod = await import.source("./add.wasm"); + +// To regenerate Wasm file use: +// npx -p wabt wat2wasm ./testing/integration/source_phase_imports_dynamic/add.wat -o ./testing/integration/source_phase_imports_dynamic/add.wasm + +// if (Object.getPrototypeOf(mod) !== WebAssembly.Module.prototype) { +// throw new Error("Wrong prototype"); +// } + +// if (mod[Symbol.toStringTag] !== "WebAssembly.Module") { +// throw new Error("Wrong Symbol.toStringTag"); +// } + +console.log(mod[Symbol.toStringTag]); +// console.log("exports", WebAssembly.Module.exports(mod)); +// console.log("imports", WebAssembly.Module.imports(mod)); + +// const instance = new WebAssembly.Instance(mod, {}); +// console.log("result", instance.exports.add(1, 2)); diff --git a/testing/integration/source_phase_imports_dynamic/source_phase_imports_dynamic.out b/testing/integration/source_phase_imports_dynamic/source_phase_imports_dynamic.out new file mode 100644 index 000000000..1d3636565 --- /dev/null +++ b/testing/integration/source_phase_imports_dynamic/source_phase_imports_dynamic.out @@ -0,0 +1,9 @@ +WebAssembly.Module +exports [ + { + "name": "add", + "kind": "function" + } +] +imports [] +result 3 diff --git a/testing/lib.rs b/testing/lib.rs index 510b27a63..04b82da18 100644 --- a/testing/lib.rs +++ b/testing/lib.rs @@ -79,6 +79,8 @@ integration_test!( module_types, pending_unref_op_tla, smoke_test, + source_phase_imports, + source_phase_imports_dynamic, timer_ref, timer_ref_and_cancel, timer_many,