Repository: denoland/deno_core
Pull request #547 (2024-02-02 23:26:56)

refactor: Prepare for Wasm imports support
This commit prepares for support of WASM modules.

A new variant of "ModuleType::Wasm" was added.

Actually trying to load a WASM module will throw an error.


Patch:
diff --git a/core/modules/map.rs b/core/modules/map.rs
index b36f703..3f40cff 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -213,6 +213,13 @@ impl ModuleMap {
     self.data.borrow().get_name_by_id(id)
   }
 
+  pub(crate) fn get_type_by_module(
+    &self,
+    global: &v8::Global<v8::Module>,
+  ) -> Option<ModuleType> {
+    self.data.borrow().get_type_by_module(global)
+  }
+
   pub(crate) fn get_handle(
     &self,
     id: ModuleId,
@@ -306,6 +313,11 @@ impl ModuleMap {
           dynamic,
         )?
       }
+      ModuleType::Wasm => {
+        return Err(ModuleError::Other(generic_error(
+          "Importing Wasm modules is currently not supported.",
+        )));
+      }
       ModuleType::Json => {
         let code =
           ModuleSource::get_string_source(module_url_found.as_str(), code)
diff --git a/core/modules/mod.rs b/core/modules/mod.rs
index 6f3dd69..ebfe2f7 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -211,13 +211,14 @@ pub(crate) fn get_requested_module_type_from_attributes(
 
 /// A type of module to be executed.
 ///
-/// `deno_core` supports loading and executing JavaScript and JSON modules,
+/// `deno_core` supports loading and executing JavaScript, Wasm and JSON modules,
 /// by default, but embedders can customize it further by providing
 /// [`CustomModuleEvaluationCb`].
 #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
 #[repr(u32)]
 pub enum ModuleType {
   JavaScript,
+  Wasm,
   Json,
   Other(Cow<'static, str>),
 }
@@ -226,12 +227,45 @@ impl std::fmt::Display for ModuleType {
   fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
     match self {
       Self::JavaScript => write!(f, "JavaScript"),
+      Self::Wasm => write!(f, "Wasm"),
       Self::Json => write!(f, "JSON"),
       Self::Other(ty) => write!(f, "{}", ty),
     }
   }
 }
 
+impl ModuleType {
+  pub fn to_v8<'s>(
+    &self,
+    scope: &mut v8::HandleScope<'s>,
+  ) -> v8::Local<'s, v8::Value> {
+    match self {
+      ModuleType::JavaScript => v8::Integer::new(scope, 0).into(),
+      ModuleType::Wasm => v8::Integer::new(scope, 1).into(),
+      ModuleType::Json => v8::Integer::new(scope, 2).into(),
+      ModuleType::Other(ty) => v8::String::new(scope, ty).unwrap().into(),
+    }
+  }
+
+  pub fn try_from_v8(
+    scope: &mut v8::HandleScope,
+    value: v8::Local<v8::Value>,
+  ) -> Option<Self> {
+    Some(if let Some(int) = value.to_integer(scope) {
+      match int.int32_value(scope).unwrap_or_default() {
+        0 => ModuleType::JavaScript,
+        1 => ModuleType::Wasm,
+        2 => ModuleType::Json,
+        _ => return None,
+      }
+    } else if let Ok(str) = v8::Local::<v8::String>::try_from(value) {
+      ModuleType::Other(Cow::Owned(str.to_rust_string_lossy(scope)))
+    } else {
+      return None;
+    })
+  }
+}
+
 /// EsModule source code that will be loaded into V8.
 ///
 /// Users can implement `Into<ModuleInfo>` for different file types that
@@ -443,6 +477,7 @@ impl PartialEq<ModuleType> for RequestedModuleType {
   fn eq(&self, other: &ModuleType) -> bool {
     match other {
       ModuleType::JavaScript => self == &RequestedModuleType::None,
+      ModuleType::Wasm => self == &RequestedModuleType::None,
       ModuleType::Json => self == &RequestedModuleType::Json,
       ModuleType::Other(ty) => self == &RequestedModuleType::Other(ty.clone()),
     }
@@ -453,6 +488,7 @@ impl From<ModuleType> for RequestedModuleType {
   fn from(module_type: ModuleType) -> RequestedModuleType {
     match module_type {
       ModuleType::JavaScript => RequestedModuleType::None,
+      ModuleType::Wasm => RequestedModuleType::None,
       ModuleType::Json => RequestedModuleType::Json,
       ModuleType::Other(ty) => RequestedModuleType::Other(ty.clone()),
     }
@@ -486,7 +522,7 @@ pub(crate) struct ModuleInfo {
   pub main: bool,
   pub name: ModuleName,
   pub requests: Vec<ModuleRequest>,
-  pub module_type: RequestedModuleType,
+  pub module_type: ModuleType,
 }
 
 #[derive(Debug)]
diff --git a/core/modules/module_map_data.rs b/core/modules/module_map_data.rs
index 5a5dc67..b2e4dd8 100644
--- a/core/modules/module_map_data.rs
+++ b/core/modules/module_map_data.rs
@@ -166,16 +166,17 @@ impl ModuleMapData {
   ) -> ModuleId {
     let data = self;
     let id = data.handles.len();
-    let module_type = module_type.into();
     let (name1, name2) = name.into_cheap_copy();
     data.handles_inverted.insert(handle.clone(), id);
     data.handles.push(handle);
     if main {
       data.main_module_id = Some(id);
     }
+    // TODO(bartlomieju): verify if we can store `ModuleType` here instead
+    let requested_module_type = RequestedModuleType::from(module_type.clone());
     data
       .by_name
-      .insert(&module_type, name1, SymbolicModule::Mod(id));
+      .insert(&requested_module_type, name1, SymbolicModule::Mod(id));
     data.info.push(ModuleInfo {
       id,
       main,
@@ -267,6 +268,18 @@ impl ModuleMapData {
     }
   }
 
+  pub(crate) fn get_type_by_module(
+    &self,
+    global: &v8::Global<v8::Module>,
+  ) -> Option<ModuleType> {
+    if let Some(id) = self.handles_inverted.get(global) {
+      let info = self.info.get(*id).unwrap();
+      Some(info.module_type.clone())
+    } else {
+      None
+    }
+  }
+
   pub(crate) fn is_main_module(&self, global: &v8::Global<v8::Module>) -> bool {
     self
       .main_module_id
@@ -442,8 +455,7 @@ impl ModuleMapData {
         }
 
         let value = module_info_arr.get_index(scope, 3).unwrap();
-        let module_type =
-          RequestedModuleType::try_from_v8(scope, value).unwrap();
+        let module_type = ModuleType::try_from_v8(scope, value).unwrap();
 
         let main = self.main_module_id == Some(id);
         let module_info = ModuleInfo {
@@ -524,8 +536,10 @@ impl ModuleMapData {
     for info in modules {
       assert!(data.handles.get(info.id).is_some());
       assert_eq!(data.info.get(info.id).unwrap(), info);
+      let requested_module_type =
+        RequestedModuleType::from(info.module_type.clone());
       assert_eq!(
-        data.by_name.get(&info.module_type, &info.name),
+        data.by_name.get(&requested_module_type, &info.name),
         Some(&SymbolicModule::Mod(info.id))
       );
     }
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index d92661a..b56e556 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -23,6 +23,7 @@ use crate::runtime::InitMode;
 use crate::runtime::JsRealm;
 use crate::FastString;
 use crate::JsRuntime;
+use crate::ModuleType;
 
 pub(crate) fn create_external_references(
   ops: &[OpCtx],
@@ -171,6 +172,10 @@ pub mod v8_static_strings {
   pub static DIRNAME: v8::OneByteConst = onebyte_const!("dirname");
   pub static SET_UP_ASYNC_STUB: v8::OneByteConst =
     onebyte_const!("setUpAsyncStub");
+  pub static WEBASSEMBLY: v8::OneByteConst = onebyte_const!("WebAssembly");
+  pub static INSTANTIATE: v8::OneByteConst = onebyte_const!("instantiate");
+  pub static WASM_INSTANTIATE: v8::OneByteConst =
+    onebyte_const!("wasmInstantiate");
 }
 
 /// Create an object on the `globalThis` that looks like this:
@@ -452,11 +457,15 @@ pub extern "C" fn host_initialize_import_meta_object_callback(
   // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>`
   let scope = &mut unsafe { v8::CallbackScope::new(context) };
   let module_map = JsRealm::module_map_from(scope);
+  let state = JsRealm::state_from_scope(scope);
 
   let module_global = v8::Global::new(scope, module);
   let name = module_map
     .get_name_by_module(&module_global)
     .expect("Module not found");
+  let module_type = module_map
+    .get_type_by_module(&module_global)
+    .expect("Module not found");
 
   let url_key = v8_static_strings::new(scope, &v8_static_strings::URL);
   let url_val = v8::String::new(scope, &name).unwrap();
@@ -467,6 +476,19 @@ pub extern "C" fn host_initialize_import_meta_object_callback(
   let main_val = v8::Boolean::new(scope, main);
   meta.create_data_property(scope, main_key.into(), main_val.into());
 
+  // Add special method that allows WASM module to instantiate themselves.
+  if module_type == ModuleType::Wasm {
+    let wasm_instantiate_key =
+      v8_static_strings::new(scope, &v8_static_strings::WASM_INSTANTIATE);
+    let f = state.wasm_instantiate_fn.borrow().as_ref().unwrap().clone();
+    let wasm_instantiate_val = v8::Local::new(scope, &*f);
+    meta.create_data_property(
+      scope,
+      wasm_instantiate_key.into(),
+      wasm_instantiate_val.into(),
+    );
+  }
+
   let builder =
     v8::FunctionBuilder::new(import_meta_resolve).data(url_val.into());
   let val = v8::FunctionBuilder::<v8::Function>::build(builder, scope).unwrap();
diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs
index 5bb5264..5ca38b5 100644
--- a/core/runtime/jsrealm.rs
+++ b/core/runtime/jsrealm.rs
@@ -53,6 +53,7 @@ pub(crate) struct ContextState {
     RefCell<Option<Rc<v8::Global<v8::Function>>>>,
   pub(crate) js_wasm_streaming_cb:
     RefCell<Option<Rc<v8::Global<v8::Function>>>>,
+  pub(crate) wasm_instantiate_fn: RefCell<Option<Rc<v8::Global<v8::Function>>>>,
   pub(crate) unrefed_ops:
     RefCell<HashSet<i32, BuildHasherDefault<IdentityHasher>>>,
   pub(crate) pending_ops: Rc<OpDriverImpl>,
@@ -79,6 +80,7 @@ impl ContextState {
       has_next_tick_scheduled: Default::default(),
       js_event_loop_tick_cb: Default::default(),
       js_wasm_streaming_cb: Default::default(),
+      wasm_instantiate_fn: Default::default(),
       op_ctxs,
       pending_ops: op_driver,
       task_spawner_factory: Default::default(),
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 6dbf31a..196ed3a 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -837,7 +837,7 @@ impl JsRuntime {
         )?;
       }
 
-      js_runtime.store_js_callbacks(&realm);
+      js_runtime.store_js_callbacks(&realm, will_snapshot);
 
       js_runtime.init_extension_js(
         &realm,
@@ -1118,8 +1118,8 @@ impl JsRuntime {
 
   /// Grab and store JavaScript bindings to callbacks necessary for the
   /// JsRuntime to operate properly.
-  fn store_js_callbacks(&mut self, realm: &JsRealm) {
-    let (event_loop_tick_cb, build_custom_error_cb) = {
+  fn store_js_callbacks(&mut self, realm: &JsRealm, will_snapshot: bool) {
+    let (event_loop_tick_cb, build_custom_error_cb, wasm_instantiate_fn) = {
       let scope = &mut realm.handle_scope(self.v8_isolate());
       let context = realm.context();
       let context_local = v8::Local::new(scope, context);
@@ -1144,9 +1144,26 @@ impl JsRuntime {
         "Deno.core.buildCustomError",
       );
 
+      let mut wasm_instantiate_fn = None;
+      if !will_snapshot {
+        let web_assembly_object: v8::Local<v8::Object> = bindings::get(
+          scope,
+          global,
+          &v8_static_strings::WEBASSEMBLY,
+          "WebAssembly",
+        );
+        wasm_instantiate_fn = Some(bindings::get::<v8::Local<v8::Function>>(
+          scope,
+          web_assembly_object,
+          &v8_static_strings::INSTANTIATE,
+          "WebAssembly.instantiate",
+        ));
+      }
+
       (
         v8::Global::new(scope, event_loop_tick_cb),
         v8::Global::new(scope, build_custom_error_cb),
+        wasm_instantiate_fn.map(|f| v8::Global::new(scope, f)),
       )
     };
 
@@ -1161,6 +1178,12 @@ impl JsRuntime {
       .js_build_custom_error_cb
       .borrow_mut()
       .replace(Rc::new(build_custom_error_cb));
+    if let Some(wasm_instantiate_fn) = wasm_instantiate_fn {
+      state_rc
+        .wasm_instantiate_fn
+        .borrow_mut()
+        .replace(Rc::new(wasm_instantiate_fn));
+    }
   }
 
   /// Returns the runtime's op state, which can be used to maintain ops


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index b18a236..9429afc 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -916,7 +916,7 @@ export const foo = bytes;
           "foobar-synth".into()
         )
       }],
-      module_type: RequestedModuleType::Other("foobar".into()),
+      module_type: ModuleType::Other("foobar".into()),
     }
   );
   let info = data.info.get(mod_id - 1).unwrap();
@@ -927,7 +927,7 @@ export const foo = bytes;
       main: false,
       name: ascii_str!("file:///b.png"),
       requests: vec![],
-      module_type: RequestedModuleType::Other("foobar-synth".into()),
+      module_type: ModuleType::Other("foobar-synth".into()),
     }
   );
 }
diff --git a/core/runtime/tests/snapshot.rs b/core/runtime/tests/snapshot.rs
index 4ed0e62..5b16998 100644
--- a/core/runtime/tests/snapshot.rs
+++ b/core/runtime/tests/snapshot.rs
@@ -160,7 +160,7 @@ fn es_snapshot() {
         specifier: format!("file:///{prev}.js"),
         requested_module_type: RequestedModuleType::None,
       }],
-      module_type: RequestedModuleType::None,
+      module_type: ModuleType::JavaScript,
     }
   }
 
@@ -198,7 +198,7 @@ fn es_snapshot() {
     main: false,
     name: specifier.into(),
     requests: vec![],
-    module_type: RequestedModuleType::None,
+    module_type: ModuleType::JavaScript,
   });
 
   modules.extend((1..200).map(|i| create_module(&mut runtime, i, false)));