Skip to content

feat: Add support for source phase imports #1081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 100 additions & 5 deletions core/modules/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ impl ModuleMap {
self.data.borrow().get_handle(id)
}

pub(crate) fn get_source(
&self,
id: ModuleId,
) -> Option<v8::Global<v8::Object>> {
self.data.borrow().get_source(id)
}

pub(crate) fn serialize_for_snapshotting(
&self,
data_store: &mut SnapshotStoreDataStore,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<v8::Object> = wasm_module.into();
let wasm_module_value: v8::Local<v8::Value> = wasm_module.into();

let js_wasm_module_source =
Expand All @@ -770,15 +778,22 @@ 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,
name2,
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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<v8::Local<'s, v8::Object>> {
// SAFETY: `CallbackScope` can be safely constructed from `Local<Context>`
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)
Expand Down Expand Up @@ -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<String, String>,
) -> Option<v8::Local<'s, v8::Object>> {
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,
Expand Down
8 changes: 8 additions & 0 deletions core/modules/module_map_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub(crate) struct ModuleMapData {
pub(crate) handles_inverted: HashMap<v8::Global<v8::Module>, usize>,
/// The handles we have loaded so far, corresponding with the [`ModuleInfo`] in `info`.
pub(crate) handles: Vec<v8::Global<v8::Module>>,
pub(crate) sources: HashMap<ModuleId, v8::Global<v8::Object>>,
pub(crate) main_module_callbacks: Vec<v8::Global<v8::Function>>,
/// The modules we have loaded so far.
pub(crate) info: Vec<ModuleInfo>,
Expand Down Expand Up @@ -250,6 +251,13 @@ impl ModuleMapData {
self.handles.get(id).cloned()
}

pub(crate) fn get_source(
&self,
id: ModuleId,
) -> Option<v8::Global<v8::Object>> {
self.sources.get(&id).cloned()
}

pub(crate) fn get_name_by_module(
&self,
global: &v8::Global<v8::Module>,
Expand Down
21 changes: 20 additions & 1 deletion core/runtime/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<v8::Local<'s, v8::Promise>> {
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<v8::Local<'s, v8::Promise>> {
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)
Expand Down
4 changes: 4 additions & 0 deletions core/runtime/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
);
Expand Down
Binary file added testing/integration/source_phase_imports/add.wasm
Binary file not shown.
9 changes: 9 additions & 0 deletions testing/integration/source_phase_imports/add.wat
Original file line number Diff line number Diff line change
@@ -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))
)
20 changes: 20 additions & 0 deletions testing/integration/source_phase_imports/source_phase_imports.js
Original file line number Diff line number Diff line change
@@ -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));

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
9 changes: 9 additions & 0 deletions testing/integration/source_phase_imports_dynamic/add.wat
Original file line number Diff line number Diff line change
@@ -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))
)
Original file line number Diff line number Diff line change
@@ -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));

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions testing/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading