From 7b5094cda3b5827d2af7f35ba1a90b5ea20f4bfc Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 30 Jul 2026 23:49:41 -0700 Subject: [PATCH 1/5] fix(api): apply SDK JavaScript module fixes --- lib/api/src/backend/js/entities/module.rs | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/api/src/backend/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index 74926a736eaa..235df72b7ba0 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -40,6 +40,8 @@ pub struct ModuleTypeHints { pub struct Module { module: JsHandle, name: Option, + #[cfg(feature = "wasm-types-polyfill")] + info: ModuleInfo, // WebAssembly type hints type_hints: Option, #[cfg(feature = "js-serializable-module")] @@ -91,23 +93,24 @@ impl Module { // The module is now validated, so we can safely parse it's types #[cfg(feature = "wasm-types-polyfill")] - let (type_hints, name) = { - let info = crate::polyfill::translate_module(&binary[..]).unwrap(); + let (type_hints, name, module_info) = { + let translated = crate::polyfill::translate_module(&binary[..]).unwrap(); ( Some(ModuleTypeHints { - imports: info + imports: translated .info .imports() .map(|import| import.ty().clone()) .collect::>(), - exports: info + exports: translated .info .exports() .map(|export| export.ty().clone()) .collect::>(), }), - info.info.name, + translated.info.name.clone(), + translated.info, ) }; #[cfg(not(feature = "wasm-types-polyfill"))] @@ -117,6 +120,8 @@ impl Module { module: JsHandle::new(module), type_hints, name, + #[cfg(feature = "wasm-types-polyfill")] + info: module_info, #[cfg(feature = "js-serializable-module")] raw_bytes: Some(binary), } @@ -458,7 +463,14 @@ impl Module { } pub(crate) fn info(&self) -> &ModuleInfo { - unimplemented!() + #[cfg(feature = "wasm-types-polyfill")] + { + &self.info + } + #[cfg(not(feature = "wasm-types-polyfill"))] + { + unimplemented!("module info requires the wasm-types-polyfill feature") + } } } @@ -469,6 +481,8 @@ impl From for Module { module: JsHandle::new(module), name: None, type_hints: None, + #[cfg(feature = "wasm-types-polyfill")] + info: ModuleInfo::default(), #[cfg(feature = "js-serializable-module")] raw_bytes: None, } From f7637fe52d652412c9cd6d264b14d7bdc619ff11 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 11:26:40 -0700 Subject: [PATCH 2/5] Added Module new_async function --- Cargo.lock | 1 + lib/api/Cargo.toml | 3 +- lib/api/src/backend/js/entities/module.rs | 36 +++++++++++++++++++++++ lib/api/src/entities/module/inner.rs | 22 ++++++++++++++ lib/api/src/entities/module/mod.rs | 14 +++++++++ lib/api/tests/module.rs | 21 +++++++++++++ 6 files changed, 96 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 92f02ebc3a9a..d869543d694b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7188,6 +7188,7 @@ dependencies = [ "tracing", "ureq 3.3.0", "wasm-bindgen", + "wasm-bindgen-futures", "wasm-bindgen-test", "wasmer-compiler", "wasmer-compiler-cranelift", diff --git a/lib/api/Cargo.toml b/lib/api/Cargo.toml index fca8b4d4a51e..3c2f6b4be02a 100644 --- a/lib/api/Cargo.toml +++ b/lib/api/Cargo.toml @@ -86,6 +86,7 @@ wasmer-types = { path = "../types", version = "=7.2.1", default-features = false "std", ] } wasm-bindgen.workspace = true +wasm-bindgen-futures = { workspace = true, optional = true } js-sys.workspace = true wasmer-derive = { path = "../derive", version = "=7.2.1" } wasmer-compiler = { path = "../compiler", version = "=7.2.1" } @@ -162,7 +163,7 @@ v8-default = ["v8", "wat"] wasm-c-api = ["wasm-types-polyfill"] # Features for `js`. -js = ["wasm-bindgen", "js-sys"] +js = ["wasm-bindgen", "wasm-bindgen-futures", "js-sys"] js-default = ["js", "std", "wasm-types-polyfill"] wasm-types-polyfill = ["wasmparser"] diff --git a/lib/api/src/backend/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index 235df72b7ba0..0cf1c12ce16c 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -3,7 +3,11 @@ use std::path::Path; use bytes::Bytes; use js_sys::{Reflect, Uint8Array, WebAssembly}; use tracing::{debug, warn}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; use wasm_bindgen::{JsValue, prelude::*}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::JsFuture; use wasmer_types::{ CompileError, DeserializeError, ExportType, ExportsIterator, ExternType, FunctionType, GlobalType, ImportType, ImportsIterator, MemoryType, ModuleInfo, Mutability, Pages, @@ -62,6 +66,38 @@ impl From for JsValue { } impl Module { + #[cfg(target_arch = "wasm32")] + pub(crate) async fn new_async( + _engine: &impl AsEngineRef, + binary: &[u8], + ) -> Result { + // Copy the bytes into JavaScript-owned memory before awaiting. A view + // into Wasm linear memory could be invalidated if that memory grows. + let js_bytes = Uint8Array::from(binary); + let module = JsFuture::from(WebAssembly::compile(&js_bytes)) + .await + .map_err(|error| { + CompileError::Validate( + error + .as_string() + .or_else(|| { + Reflect::get(&error, &JsValue::from_str("message")) + .ok() + .and_then(|message| message.as_string()) + }) + .unwrap_or_else(|| "Unknown validation error".to_owned()), + ) + })? + .dyn_into::() + .map_err(|_| { + CompileError::Validate( + "WebAssembly.compile returned an unexpected value".to_owned(), + ) + })?; + + Ok(unsafe { Self::from_js_module(module, binary) }) + } + pub(crate) fn from_binary( _engine: &impl AsEngineRef, binary: &[u8], diff --git a/lib/api/src/entities/module/inner.rs b/lib/api/src/entities/module/inner.rs index ed24347ef04c..618b52208740 100644 --- a/lib/api/src/entities/module/inner.rs +++ b/lib/api/src/entities/module/inner.rs @@ -44,6 +44,28 @@ impl BackendModule { Self::from_binary(engine, bytes.as_ref()) } + #[inline] + pub async fn new_async( + engine: &impl AsEngineRef, + bytes: impl AsRef<[u8]>, + ) -> Result { + #[cfg(all(feature = "js", target_arch = "wasm32"))] + if matches!(engine.as_engine_ref().inner.be, crate::BackendEngine::Js(_)) { + #[cfg(feature = "wat")] + let bytes = wat::parse_bytes(bytes.as_ref()).map_err(|e| { + CompileError::Wasm(WasmError::Generic(format!( + "Error when converting wat: {e}", + ))) + })?; + + return crate::backend::js::entities::module::Module::new_async(engine, bytes.as_ref()) + .await + .map(Self::Js); + } + + Self::new(engine, bytes) + } + #[inline] pub fn new_with_progress( engine: &impl AsEngineRef, diff --git a/lib/api/src/entities/module/mod.rs b/lib/api/src/entities/module/mod.rs index ebafc5b2a366..5114c7fa6b4d 100644 --- a/lib/api/src/entities/module/mod.rs +++ b/lib/api/src/entities/module/mod.rs @@ -113,6 +113,20 @@ impl Module { BackendModule::new(engine, bytes).map(Self) } + /// Asynchronously creates a new WebAssembly module. + /// + /// This is equivalent to [`Module::new`] on backends whose compilation is + /// synchronous. The JavaScript backend uses the host's asynchronous + /// WebAssembly compilation API, which avoids blocking the browser's main + /// thread and supports modules that browsers reject for synchronous + /// compilation there. + pub async fn new_async( + engine: &impl AsEngineRef, + bytes: impl AsRef<[u8]>, + ) -> Result { + BackendModule::new_async(engine, bytes).await.map(Self) + } + /// Creates a new WebAssembly module from a file path. pub fn from_file( engine: &impl AsEngineRef, diff --git a/lib/api/tests/module.rs b/lib/api/tests/module.rs index 31969694d0c1..abaccd5f7811 100644 --- a/lib/api/tests/module.rs +++ b/lib/api/tests/module.rs @@ -9,6 +9,27 @@ use std::ffi::OsStr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; +async fn assert_module_new_async() -> Result<(), String> { + let store = Store::default(); + let module = Module::new_async(&store, "(module (func (export \"run\")))") + .await + .map_err(|error| format!("{error:?}"))?; + assert!(module.exports().any(|export| export.name() == "run")); + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn module_new_async() -> Result<(), String> { + futures::executor::block_on(assert_module_new_async()) +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn module_new_async() -> Result<(), JsValue> { + assert_module_new_async().await.map_err(JsValue::from_str) +} + #[engine_test] fn module_get_name() -> Result<(), String> { let store = Store::default(); From 9ef4b4915f15aeca9e7177723b6d1c57b64b6855 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 12:38:07 -0700 Subject: [PATCH 3/5] Improved polyfill support of table element types --- lib/api/src/backend/js/entities/module.rs | 101 ++++++++++++++++++- lib/api/src/backend/js/entities/table.rs | 3 +- lib/api/src/backend/js/vm/function.rs | 63 +++++++++++- lib/api/src/utils/polyfill.rs | 115 ++++++++++++++++++++-- 4 files changed, 268 insertions(+), 14 deletions(-) diff --git a/lib/api/src/backend/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index 0cf1c12ce16c..c1140e531513 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{collections::HashMap, path::Path}; use bytes::Bytes; use js_sys::{Reflect, Uint8Array, WebAssembly}; @@ -10,8 +10,8 @@ use wasm_bindgen::{JsValue, prelude::*}; use wasm_bindgen_futures::JsFuture; use wasmer_types::{ CompileError, DeserializeError, ExportType, ExportsIterator, ExternType, FunctionType, - GlobalType, ImportType, ImportsIterator, MemoryType, ModuleInfo, Mutability, Pages, - SerializeError, TableType, Type, + GlobalIndex, GlobalType, ImportIndex, ImportType, ImportsIterator, InitExpr, InitExprOp, + MemoryType, ModuleInfo, Mutability, Pages, SerializeError, TableIndex, TableType, Type, }; use crate::{ @@ -52,6 +52,39 @@ pub struct Module { raw_bytes: Option, } +#[cfg(feature = "wasm-types-polyfill")] +fn evaluate_i32_init_expr( + expression: &InitExpr, + globals: &HashMap, +) -> Option { + let mut stack = Vec::::new(); + for operation in expression.ops() { + match operation { + InitExprOp::GlobalGetI32(index) | InitExprOp::GlobalGetI64(index) => { + stack.push(*globals.get(index)?); + } + InitExprOp::I32Const(value) => stack.push(i64::from(*value)), + InitExprOp::I64Const(value) => stack.push(*value), + InitExprOp::I32Add | InitExprOp::I64Add => { + let rhs = stack.pop()?; + let lhs = stack.pop()?; + stack.push(lhs.checked_add(rhs)?); + } + InitExprOp::I32Sub | InitExprOp::I64Sub => { + let rhs = stack.pop()?; + let lhs = stack.pop()?; + stack.push(lhs.checked_sub(rhs)?); + } + InitExprOp::I32Mul | InitExprOp::I64Mul => { + let rhs = stack.pop()?; + let lhs = stack.pop()?; + stack.push(lhs.checked_mul(rhs)?); + } + } + } + u32::try_from(stack.pop()?).ok().filter(|_| stack.is_empty()) +} + // XXX // Do not rely on `Module` being `Send`: it will panic at runtime // if accessed from multiple threads thanks to [`JsHandle`]. @@ -246,8 +279,66 @@ impl Module { // in case the import is not found, the JS Wasm VM will handle // the error for us, so we don't need to handle it } - WebAssembly::Instance::new(&self.module, &imports_object) - .map_err(|e: JsValue| -> RuntimeError { e.into() }) + let instance = WebAssembly::Instance::new(&self.module, &imports_object) + .map_err(|e: JsValue| -> RuntimeError { e.into() })?; + #[cfg(feature = "wasm-types-polyfill")] + self.annotate_imported_table_functions(store, imports); + Ok(instance) + } + + #[cfg(feature = "wasm-types-polyfill")] + fn annotate_imported_table_functions( + &self, + store: &mut impl AsStoreMut, + imports: &Imports, + ) { + let mut tables = HashMap::::new(); + let mut globals = HashMap::::new(); + + for (key, import_index) in &self.info.imports { + let Some(extern_) = imports.get_export(&key.module, &key.field) else { + continue; + }; + match (import_index, extern_) { + (ImportIndex::Table(index), Extern::Table(table)) => { + tables.insert(*index, table); + } + (ImportIndex::Global(index), Extern::Global(global)) => { + let value = match global.get(store) { + crate::Value::I32(value) => i64::from(value), + crate::Value::I64(value) => value, + _ => continue, + }; + globals.insert(*index, value); + } + _ => {} + } + } + for initializer in &self.info.table_initializers { + let Some(table) = tables.get(&initializer.table_index) else { + continue; + }; + let Some(start) = evaluate_i32_init_expr(&initializer.offset_expr, &globals) else { + continue; + }; + for (offset, function_index) in initializer.elements.iter().enumerate() { + let Some(signature_index) = self.info.functions.get(*function_index) else { + continue; + }; + let Some(function_type) = self.info.signatures.get(*signature_index) else { + continue; + }; + let Ok(function) = table + .as_js() + .handle + .table + .get(start.saturating_add(offset as u32)) + else { + continue; + }; + crate::js::vm::VMFunction::annotate_type(&function, function_type); + } + } } pub fn name(&self) -> Option<&str> { diff --git a/lib/api/src/backend/js/entities/table.rs b/lib/api/src/backend/js/entities/table.rs index 40d224b3d2a7..91f8c562cc0f 100644 --- a/lib/api/src/backend/js/entities/table.rs +++ b/lib/api/src/backend/js/entities/table.rs @@ -72,7 +72,8 @@ impl Table { pub fn get(&self, store: &mut impl AsStoreMut, index: u32) -> Option { if let Ok(func) = self.handle.table.get(index) { - let ty = FunctionType::new(vec![], vec![]); + let ty = VMFunction::type_from_js(&func) + .unwrap_or_else(|| FunctionType::new(vec![], vec![])); let vm_function = VMFunction::new(func, ty); let function = crate::Function::from_vm_extern( store, diff --git a/lib/api/src/backend/js/vm/function.rs b/lib/api/src/backend/js/vm/function.rs index 6f50bfe28ce9..681319b53752 100644 --- a/lib/api/src/backend/js/vm/function.rs +++ b/lib/api/src/backend/js/vm/function.rs @@ -1,8 +1,45 @@ use std::any::Any; use crate::js::utils::js_handle::JsHandle; -use js_sys::Function as JsFunction; -use wasmer_types::{FunctionType, RawValue}; +use js_sys::{Array, Function as JsFunction, Reflect, Symbol}; +use wasm_bindgen::{JsCast, JsValue}; +use wasmer_types::{FunctionType, RawValue, Type}; + +fn type_key() -> Symbol { + Symbol::for_("wasmer.function-type") +} + +fn encode_types(types: &[Type]) -> Array { + Array::from_iter(types.iter().map(|ty| { + JsValue::from_f64(match ty { + Type::I32 => 0.0, + Type::I64 => 1.0, + Type::F32 => 2.0, + Type::F64 => 3.0, + Type::V128 => 4.0, + Type::ExternRef => 5.0, + Type::FuncRef => 6.0, + Type::ExceptionRef => 7.0, + }) + })) +} + +fn decode_types(types: &Array) -> Option> { + types + .iter() + .map(|value| match value.as_f64()? as u8 { + 0 => Some(Type::I32), + 1 => Some(Type::I64), + 2 => Some(Type::F32), + 3 => Some(Type::F64), + 4 => Some(Type::V128), + 5 => Some(Type::ExternRef), + 6 => Some(Type::FuncRef), + 7 => Some(Type::ExceptionRef), + _ => None, + }) + .collect() +} /// The VM Function type #[derive(Clone, Eq)] @@ -16,11 +53,33 @@ unsafe impl Sync for VMFunction {} impl VMFunction { pub(crate) fn new(function: JsFunction, ty: FunctionType) -> Self { + Self::annotate_type(&function, &ty); Self { function: JsHandle::new(function), ty, } } + + pub(crate) fn annotate_type(function: &JsFunction, ty: &FunctionType) { + let encoded = Array::of2( + &encode_types(ty.params()).into(), + &encode_types(ty.results()).into(), + ); + let _ = Reflect::set(&function, type_key().as_ref(), &encoded); + } + + pub(crate) fn type_from_js(function: &JsFunction) -> Option { + let encoded = Reflect::get(function, type_key().as_ref()) + .ok()? + .dyn_into::() + .ok()?; + let params = encoded.get(0).dyn_into::().ok()?; + let results = encoded.get(1).dyn_into::().ok()?; + Some(FunctionType::new( + decode_types(¶ms)?, + decode_types(&results)?, + )) + } } impl PartialEq for VMFunction { diff --git a/lib/api/src/utils/polyfill.rs b/lib/api/src/utils/polyfill.rs index 320c8316047b..a8acaad3535c 100644 --- a/lib/api/src/utils/polyfill.rs +++ b/lib/api/src/utils/polyfill.rs @@ -7,17 +7,19 @@ use core::convert::TryFrom; use std::vec::Vec; use wasmer_types::entity::EntityRef; +use wasmer_types::entity::packed_option::ReservedValue; use wasmer_types::{ ExportIndex, FunctionIndex, FunctionType, GlobalIndex, GlobalType, ImportIndex, MemoryIndex, - MemoryType, ModuleInfo, Pages, SignatureHash, SignatureIndex, TableIndex, TableType, TagIndex, - TagType, Type, + InitExpr, InitExprOp, MemoryType, ModuleInfo, Pages, SignatureHash, SignatureIndex, + TableIndex, TableInitializer, TableType, TagIndex, TagType, Type, }; use wasmparser::{ - self, BinaryReaderError, Export, ExportSectionReader, ExternalKind, FunctionSectionReader, - GlobalSectionReader, GlobalType as WPGlobalType, ImportSectionReader, Imports, - MemorySectionReader, MemoryType as WPMemoryType, NameSectionReader, Parser, Payload, - TableSectionReader, TagType as WPTagType, TypeRef, TypeSectionReader, + self, BinaryReaderError, ElementItems, ElementKind, ElementSectionReader, Export, + ExportSectionReader, ExternalKind, FunctionSectionReader, GlobalSectionReader, + GlobalType as WPGlobalType, ImportSectionReader, Imports, MemorySectionReader, + MemoryType as WPMemoryType, NameSectionReader, Operator, Parser, Payload, TableSectionReader, + TagType as WPTagType, TypeRef, TypeSectionReader, }; pub type WasmResult = Result; @@ -343,6 +345,10 @@ pub fn translate_module(data: &[u8]) -> WasmResult { parse_start_section(func, &mut module_info)?; } + Payload::ElementSection(elements) => { + parse_element_section(elements, &mut module_info)?; + } + Payload::TagSection(tags) => { parse_tag_section(tags, &mut module_info)?; } @@ -373,6 +379,103 @@ pub fn translate_module(data: &[u8]) -> WasmResult { Ok(module_info) } +fn parse_element_section( + elements: ElementSectionReader<'_>, + module: &mut ModuleInfoPolyfill, +) -> WasmResult<()> { + for element in elements { + let element = element.map_err(transform_err)?; + let ElementKind::Active { + table_index, + offset_expr, + } = element.kind + else { + continue; + }; + + let mut functions = Vec::new(); + match element.items { + ElementItems::Functions(items) => { + for item in items { + functions.push(FunctionIndex::from_u32(item.map_err(transform_err)?)); + } + } + ElementItems::Expressions(_, items) => { + for item in items { + let expression = item.map_err(transform_err)?; + let operator = expression + .get_operators_reader() + .read() + .map_err(transform_err)?; + match operator { + Operator::RefFunc { function_index } => { + functions.push(FunctionIndex::from_u32(function_index)); + } + Operator::RefNull { .. } => { + functions.push(FunctionIndex::reserved_value()); + } + other => { + return Err(format!( + "unsupported element expression in type polyfill: {other:?}" + )); + } + } + } + } + } + + module.info.table_initializers.push(TableInitializer { + table_index: TableIndex::from_u32(table_index.unwrap_or(0)), + offset_expr: parse_init_expr(&offset_expr, &module.info)?, + elements: functions.into_boxed_slice(), + }); + } + Ok(()) +} + +fn parse_init_expr( + expression: &wasmparser::ConstExpr<'_>, + module: &ModuleInfo, +) -> WasmResult { + let mut reader = expression.get_operators_reader(); + let mut operations = Vec::new(); + loop { + match reader.read().map_err(transform_err)? { + Operator::End => break, + Operator::I32Const { value } => operations.push(InitExprOp::I32Const(value)), + Operator::I64Const { value } => operations.push(InitExprOp::I64Const(value)), + Operator::GlobalGet { global_index } => { + let index = GlobalIndex::from_u32(global_index); + match module + .global_type(index) + .ok_or_else(|| format!("unknown global {global_index} in element offset"))? + .ty + { + Type::I32 => operations.push(InitExprOp::GlobalGetI32(index)), + Type::I64 => operations.push(InitExprOp::GlobalGetI64(index)), + other => { + return Err(format!( + "unsupported {other:?} global in element offset" + )); + } + } + } + Operator::I32Add => operations.push(InitExprOp::I32Add), + Operator::I32Sub => operations.push(InitExprOp::I32Sub), + Operator::I32Mul => operations.push(InitExprOp::I32Mul), + Operator::I64Add => operations.push(InitExprOp::I64Add), + Operator::I64Sub => operations.push(InitExprOp::I64Sub), + Operator::I64Mul => operations.push(InitExprOp::I64Mul), + other => { + return Err(format!( + "unsupported operator in element offset: {other:?}" + )); + } + } + } + Ok(InitExpr::new(operations.into_boxed_slice())) +} + fn validate_exported_types(module_info: &ModuleInfoPolyfill) -> WasmResult<()> { for (name, export) in module_info.info.exports.iter() { let uses_externref = match export { From d73dc073baff61ceff258f6f37f2795d0b4e64c0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 12:38:39 -0700 Subject: [PATCH 4/5] Added support for jspi --- lib/api/Cargo.toml | 1 + .../src/backend/js/entities/function/env.rs | 153 ++++++++- .../src/backend/js/entities/function/mod.rs | 309 ++++++++++++++++++ .../src/backend/js/entities/function/typed.rs | 57 ++++ lib/api/src/backend/js/jspi.rs | 81 +++++ lib/api/src/backend/js/mod.rs | 2 + lib/api/src/entities/engine/mod.rs | 4 +- lib/api/src/entities/function/env/inner.rs | 87 +++-- lib/api/src/entities/function/inner.rs | 22 +- lib/api/src/utils/native/typed_func.rs | 7 +- lib/api/tests/jspi_async.rs | 2 +- lib/api/tests/jspi_async_js.rs | 42 +++ lib/api/tests/module.rs | 47 ++- lib/api/tests/simple_greenthread.rs | 195 +++++++---- 14 files changed, 904 insertions(+), 105 deletions(-) create mode 100644 lib/api/src/backend/js/jspi.rs create mode 100644 lib/api/tests/jspi_async_js.rs diff --git a/lib/api/Cargo.toml b/lib/api/Cargo.toml index 3c2f6b4be02a..2bdc17ea0f87 100644 --- a/lib/api/Cargo.toml +++ b/lib/api/Cargo.toml @@ -101,6 +101,7 @@ target-lexicon.workspace = true [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wat.workspace = true anyhow.workspace = true +futures.workspace = true wasm-bindgen-test.workspace = true macro-wasmer-engine-test = { version = "7.2.1", path = "./macro-wasmer-engine-test" } diff --git a/lib/api/src/backend/js/entities/function/env.rs b/lib/api/src/backend/js/entities/function/env.rs index deb51a2a9236..367019cbf494 100644 --- a/lib/api/src/backend/js/entities/function/env.rs +++ b/lib/api/src/backend/js/entities/function/env.rs @@ -5,6 +5,12 @@ use crate::{ js::{store::StoreHandle, vm::VMFunctionEnvironment}, store::{AsStoreMut, AsStoreRef, StoreRef}, }; +#[cfg(feature = "experimental-async")] +use crate::{ + AsStoreAsync, StoreAsync, StoreAsyncReadLock, StoreAsyncWriteLock, +}; +#[cfg(feature = "experimental-async")] +use wasmer_types::StoreId; #[derive(Debug)] #[repr(transparent)] @@ -33,7 +39,7 @@ impl FunctionEnv { /// Get the data as reference pub fn as_ref<'a>(&self, store: &'a impl AsStoreRef) -> &'a T where - T: Any + Send + 'static + Sized, + T: Any + 'static + Sized, { self.handle .get(store.as_store_ref().objects().as_js()) @@ -52,7 +58,7 @@ impl FunctionEnv { /// Get the data as mutable pub fn as_mut<'a>(&self, store: &'a mut impl AsStoreMut) -> &'a mut T where - T: Any + Send + 'static + Sized, + T: Any + 'static + Sized, { self.handle .get_mut(store.objects_mut().as_js_mut()) @@ -64,7 +70,7 @@ impl FunctionEnv { /// Convert it into a `FunctionEnvMut` pub fn into_mut(self, store: &mut impl AsStoreMut) -> FunctionEnvMut<'_, T> where - T: Any + Send + 'static + Sized, + T: Any + 'static + Sized, { FunctionEnvMut { store_mut: store.as_store_mut(), @@ -146,6 +152,11 @@ impl FunctionEnvMut<'_, T> { let data = unsafe { &mut *data }; (data, self.store_mut.as_store_mut()) } + + #[cfg(feature = "experimental-async")] + pub fn as_store_async(&self) -> Option { + self.store_mut.as_store_async() + } } impl AsStoreRef for FunctionEnvMut<'_, T> { @@ -205,3 +216,139 @@ impl From> for crate::FunctionEnv { Self(crate::BackendFunctionEnv::Js(value)) } } + +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvMut { + pub(crate) store: StoreAsync, + pub(crate) func_env: FunctionEnv, +} + +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandle { + read_lock: StoreAsyncReadLock, + pub(crate) func_env: FunctionEnv, +} + +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandleMut { + write_lock: StoreAsyncWriteLock, + pub(crate) func_env: FunctionEnv, +} + +#[cfg(feature = "experimental-async")] +impl Clone for AsyncFunctionEnvMut { + fn clone(&self) -> Self { + Self { + store: StoreAsync { + id: self.store.id, + inner: self.store.inner.clone(), + }, + func_env: self.func_env.clone(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl Debug for AsyncFunctionEnvMut +where + T: Send + Debug + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.store.inner.try_read() { + Some(read_lock) => self.func_env.as_ref(&read_lock).fmt(f), + None => write!(f, "AsyncFunctionEnvMut {{ }}"), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvMut { + pub(crate) fn store_id(&self) -> StoreId { + self.store.id + } + + pub async fn read(&self) -> AsyncFunctionEnvHandle { + AsyncFunctionEnvHandle { + read_lock: self.store.read_lock().await, + func_env: self.func_env.clone(), + } + } + + pub async fn write(&self) -> AsyncFunctionEnvHandleMut { + AsyncFunctionEnvHandleMut { + write_lock: self.store.write_lock().await, + func_env: self.func_env.clone(), + } + } + + pub fn as_ref(&self) -> FunctionEnv { + self.func_env.clone() + } + + pub fn as_mut(&mut self) -> Self { + self.clone() + } + + pub fn as_store_async(&self) -> impl AsStoreAsync + 'static { + StoreAsync { + id: self.store.id, + inner: self.store.inner.clone(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandle { + pub fn data(&self) -> &T { + self.func_env.as_ref(&self.read_lock) + } + + pub fn data_and_store(&self) -> (&T, &impl AsStoreRef) { + (self.data(), &self.read_lock) + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for AsyncFunctionEnvHandle { + fn as_store_ref(&self) -> StoreRef<'_> { + self.read_lock.as_store_ref() + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandleMut { + pub fn data_mut(&mut self) -> &mut T { + self.func_env.as_mut(&mut self.write_lock) + } + + pub fn data_and_store_mut(&mut self) -> (&mut T, &mut impl AsStoreMut) { + let data = self.data_mut() as *mut T; + let data = unsafe { &mut *data }; + (data, &mut self.write_lock) + } + + pub fn as_function_env_mut(&mut self) -> FunctionEnvMut<'_, T> { + FunctionEnvMut { + store_mut: self.write_lock.as_store_mut(), + func_env: self.func_env.clone(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for AsyncFunctionEnvHandleMut { + fn as_store_ref(&self) -> StoreRef<'_> { + self.write_lock.as_store_ref() + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreMut for AsyncFunctionEnvHandleMut { + fn as_store_mut(&mut self) -> StoreMut<'_> { + self.write_lock.as_store_mut() + } + + fn objects_mut(&mut self) -> &mut crate::StoreObjects { + self.write_lock.objects_mut() + } +} diff --git a/lib/api/src/backend/js/entities/function/mod.rs b/lib/api/src/backend/js/entities/function/mod.rs index 4d442fae57ad..1dd70c27f003 100644 --- a/lib/api/src/backend/js/entities/function/mod.rs +++ b/lib/api/src/backend/js/entities/function/mod.rs @@ -1,11 +1,17 @@ pub(crate) mod env; pub(crate) mod typed; use std::marker::PhantomData; +#[cfg(feature = "experimental-async")] +use std::{future::Future, pin::Pin, rc::Rc, sync::Arc}; pub(crate) use typed::*; use js_sys::{Array, Function as JsFunction}; +#[cfg(feature = "experimental-async")] +use js_sys::{Promise, Reflect}; use wasm_bindgen::{JsCast, prelude::*}; +#[cfg(feature = "experimental-async")] +use wasm_bindgen_futures::{JsFuture, future_to_promise}; use wasmer_types::{FunctionType, RawValue}; use crate::{ @@ -19,6 +25,12 @@ use crate::{ }, vm::{VMExtern, VMExternFunction}, }; +#[cfg(feature = "experimental-async")] +use crate::{ + AsStoreAsync, AsyncFunctionEnvMut, BackendAsyncFunctionEnvMut, StoreAsync, StoreContext, + entities::function::async_host::{AsyncFunctionEnv, AsyncHostFunction}, + js::{function::env::AsyncFunctionEnvMut as JsAsyncFunctionEnvMut, jspi}, +}; use std::panic::{self, AssertUnwindSafe}; @@ -50,6 +62,210 @@ impl Function { )) } + #[cfg(feature = "experimental-async")] + pub(crate) fn new_async( + store: &mut impl AsStoreMut, + ty: FT, + func: F, + ) -> Self + where + FT: Into, + F: Fn(&[Value]) -> Fut + 'static, + Fut: Future, RuntimeError>> + 'static, + { + let env = FunctionEnv::new(store, ()); + Self::new_with_env_async(store, &env, ty, move |_env, values| func(values)) + } + + #[cfg(feature = "experimental-async")] + pub(crate) fn new_with_env_async( + store: &mut impl AsStoreMut, + env: &FunctionEnv, + ty: FT, + func: F, + ) -> Self + where + FT: Into, + F: Fn(AsyncFunctionEnvMut, &[Value]) -> Fut + 'static, + Fut: Future, RuntimeError>> + 'static, + T: 'static, + { + assert!( + jspi::is_supported(), + "the JavaScript host does not support WebAssembly JSPI" + ); + + let function_type = ty.into(); + let func_ty = function_type.clone(); + let store_id = store.objects_mut().id(); + let raw_env = env.as_js().clone(); + let func = Rc::new(func); + let wrapped_func = Closure::wrap(Box::new(move |args: &Array| -> Promise { + let Some(async_store) = jspi::active_store(store_id) else { + return Promise::reject(&JsValue::from_str( + "an async host function was called outside Function::call_async", + )); + }; + let callback_store = async_store.store(); + let js_env = JsAsyncFunctionEnvMut { + store: async_store, + func_env: raw_env.clone(), + }; + let env_mut = + AsyncFunctionEnvMut(BackendAsyncFunctionEnvMut::Js(js_env)); + let values = function_type + .params() + .iter() + .enumerate() + .map(|(index, ty)| js_value_to_wasmer(ty, &args.get(index as u32))) + .collect::>(); + let result_types = function_type.results().to_vec(); + let func = Rc::clone(&func); + + future_to_promise(async move { + let write_lock = callback_store.write_lock().await; + let store_context = StoreContext::install_async(write_lock.inner); + let future = func(env_mut, &values); + drop(store_context); + + let results = future.await.map_err(JsValue::from)?; + match result_types.len() { + 0 => Ok(JsValue::UNDEFINED), + 1 => Ok(wasmer_value_to_js(&results[0])), + _ => Ok(wasmer_array_to_js_array(&results).into()), + } + }) + }) as Box Promise>) + .into_js_value(); + + let variadic = + JsFunction::new_with_args("f", "return f(Array.prototype.slice.call(arguments, 1))"); + let function = variadic + .bind1(&JsValue::UNDEFINED, &wrapped_func) + .unchecked_into::(); + let function = match jspi::suspending(&function) { + Ok(function) => function, + Err(error) => wasm_bindgen::throw_val(error), + }; + let vm_function = VMFunction::new(function, func_ty); + Self::from_vm_extern( + &mut store.as_store_mut(), + VMExternFunction::Js(vm_function), + ) + } + + #[cfg(feature = "experimental-async")] + pub(crate) fn new_typed_async( + store: &mut impl AsStoreMut, + func: F, + ) -> Self + where + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, + F: AsyncHostFunction<(), Args, Rets, WithoutEnv> + 'static, + { + let env = FunctionEnv::new(store, ()); + let signature = FunctionType::new(Args::wasm_types(), Rets::wasm_types()); + let args_sig = Arc::new(signature.clone()); + let results_sig = Arc::new(signature.clone()); + let func = Arc::new(func); + Self::new_with_env_async( + store, + &env, + signature, + move |mut env_mut, values| -> Pin< + Box, RuntimeError>>>, + > { + let js_env = match env_mut.0 { + BackendAsyncFunctionEnvMut::Js(ref mut js_env) => js_env, + _ => panic!("Not a js backend"), + }; + let mut store_wrapper = unsafe { StoreContext::get_current(js_env.store_id()) }; + let mut store_mut = store_wrapper.as_mut(); + let args = match typed_args_from_values::( + &mut store_mut, + args_sig.as_ref(), + values, + ) { + Ok(args) => args, + Err(error) => return Box::pin(async { Err(error) }), + }; + drop(store_wrapper); + let func = Arc::clone(&func); + let results_sig = Arc::clone(&results_sig); + let future = func + .as_ref() + .call_async(AsyncFunctionEnv::new(), args); + Box::pin(async move { + let typed_result = future.await?; + let mut store_mut = env_mut.write().await; + typed_results_to_values::( + &mut store_mut.as_store_mut(), + results_sig.as_ref(), + typed_result, + ) + }) + }, + ) + } + + #[cfg(feature = "experimental-async")] + pub(crate) fn new_typed_with_env_async( + store: &mut impl AsStoreMut, + env: &FunctionEnv, + func: F, + ) -> Self + where + T: 'static, + F: AsyncHostFunction + 'static, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, + { + let signature = FunctionType::new(Args::wasm_types(), Rets::wasm_types()); + let args_sig = Arc::new(signature.clone()); + let results_sig = Arc::new(signature.clone()); + let func = Arc::new(func); + Self::new_with_env_async( + store, + env, + signature, + move |mut env_mut, values| -> Pin< + Box, RuntimeError>>>, + > { + let js_env = match env_mut.0 { + BackendAsyncFunctionEnvMut::Js(ref mut js_env) => js_env, + _ => panic!("Not a js backend"), + }; + let mut store_wrapper = unsafe { StoreContext::get_current(js_env.store_id()) }; + let mut store_mut = store_wrapper.as_mut(); + let args = match typed_args_from_values::( + &mut store_mut, + args_sig.as_ref(), + values, + ) { + Ok(args) => args, + Err(error) => return Box::pin(async { Err(error) }), + }; + drop(store_wrapper); + let env_mut_clone = env_mut.as_mut(); + let func = Arc::clone(&func); + let results_sig = Arc::clone(&results_sig); + let future = func + .as_ref() + .call_async(AsyncFunctionEnv::with_env(env_mut), args); + Box::pin(async move { + let typed_result = future.await?; + let mut store_mut = env_mut_clone.write().await; + typed_results_to_values::( + &mut store_mut.as_store_mut(), + results_sig.as_ref(), + typed_result, + ) + }) + }, + ) + } + #[allow(clippy::cast_ptr_alignment)] pub fn new_with_env( store: &mut impl AsStoreMut, @@ -262,6 +478,55 @@ impl Function { } } + #[cfg(feature = "experimental-async")] + #[allow(clippy::type_complexity)] + pub(crate) fn call_async( + &self, + store: &impl AsStoreAsync, + params: Vec, + ) -> Pin, RuntimeError>> + 'static>> { + let function = self.clone(); + let store = store.store(); + Box::pin(async move { + let _active_store = jspi::install_store(store.store()); + let function_type = function.handle.ty.clone(); + let write_lock = store.write_lock().await; + let arguments = Array::new_with_length(params.len() as u32); + for (index, param) in params.iter().enumerate() { + arguments.set(index as u32, param.as_jsvalue(&write_lock)); + } + + let store_context = StoreContext::install_async(write_lock.inner); + let promising = jspi::promising(&function.handle.function) + .map_err(RuntimeError::from)?; + let promise = Reflect::apply(&promising, &JsValue::NULL, &arguments) + .map_err(RuntimeError::from)? + .dyn_into::() + .map_err(RuntimeError::from)?; + drop(store_context); + + let result = JsFuture::from(promise).await.map_err(RuntimeError::from)?; + match function_type.results().len() { + 0 => Ok(Box::<[Value]>::default()), + 1 => Ok(vec![js_value_to_wasmer( + &function_type.results()[0], + &result, + )] + .into_boxed_slice()), + _ => { + let result: Array = result.into(); + Ok(function_type + .results() + .iter() + .enumerate() + .map(|(index, ty)| js_value_to_wasmer(ty, &result.get(index as u32))) + .collect::>() + .into_boxed_slice()) + } + } + }) + } + pub(crate) fn from_vm_extern(_store: &mut impl AsStoreMut, internal: VMExternFunction) -> Self { Self { handle: internal.unwrap_js(), @@ -292,6 +557,50 @@ impl Function { } } +#[cfg(feature = "experimental-async")] +fn typed_args_from_values( + store: &mut StoreMut, + function_type: &FunctionType, + values: &[Value], +) -> Result +where + Args: WasmTypeList, +{ + if values.len() != function_type.params().len() { + return Err(RuntimeError::new( + "typed host function received wrong number of parameters", + )); + } + let mut raw_array = Args::empty_array(); + for (slot, value) in raw_array.as_mut().iter_mut().zip(values) { + *slot = value.as_raw(store); + } + unsafe { Ok(Args::from_array(store, raw_array)) } +} + +#[cfg(feature = "experimental-async")] +fn typed_results_to_values( + store: &mut StoreMut, + function_type: &FunctionType, + results: Rets, +) -> Result, RuntimeError> +where + Rets: WasmTypeList, +{ + let mut raw_array = unsafe { results.into_array(store) }; + let mut values = Vec::with_capacity(function_type.results().len()); + for (raw, ty) in raw_array + .as_mut() + .iter() + .zip(function_type.results()) + { + unsafe { + values.push(Value::from_raw(store, *ty, *raw)); + } + } + Ok(values) +} + impl std::fmt::Debug for Function { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.debug_struct("Function").finish() diff --git a/lib/api/src/backend/js/entities/function/typed.rs b/lib/api/src/backend/js/entities/function/typed.rs index 229fab787dab..129702f132c3 100644 --- a/lib/api/src/backend/js/entities/function/typed.rs +++ b/lib/api/src/backend/js/entities/function/typed.rs @@ -12,7 +12,11 @@ use crate::{ TypedFunction, Value, WasmTypeList, js::utils::convert::{AsJs, js_value_to_wasmer}, }; +#[cfg(feature = "experimental-async")] +use crate::{AsStoreAsync, StoreAsync}; use js_sys::Array; +#[cfg(feature = "experimental-async")] +use std::future::Future; use std::iter::FromIterator; use wasm_bindgen::JsValue; use wasmer_types::RawValue; @@ -25,6 +29,35 @@ macro_rules! impl_native_traits { $( $x: FromToNativeWasmType, )* Rets: WasmTypeList, { + /// Call the typed func asynchronously through JSPI. + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "experimental-async")] + pub(crate) fn call_async_js( + func: crate::Function, + store: StoreAsync, + $( $x: $x, )* + ) -> impl Future> + 'static + where + $( $x: FromToNativeWasmType + 'static, )* + { + async move { + let mut write = store.write_lock().await; + let func_ty = func.ty(&mut write); + let mut params_raw = [ $( $x.to_native().into_raw(&mut write) ),* ]; + let mut params_values = Vec::with_capacity(params_raw.len()); + for (raw, ty) in params_raw.iter().zip(func_ty.params()) { + unsafe { + params_values.push(Value::from_raw(&mut write, *ty, *raw)); + } + } + drop(write); + + let results = func.call_async(&store, params_values).await?; + let mut write = store.write_lock().await; + convert_results::(&mut write, func_ty, &results) + } + } + /// Call the typed func and return results. #[allow(clippy::too_many_arguments)] pub fn call_js(&self, mut store: &mut impl AsStoreMut, $( $x: $x, )* ) -> Result where @@ -133,3 +166,27 @@ impl_native_traits!( impl_native_traits!( A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20 ); + +#[cfg(feature = "experimental-async")] +fn convert_results( + store: &mut impl AsStoreMut, + func_ty: wasmer_types::FunctionType, + results: &[Value], +) -> Result { + if results.len() != func_ty.results().len() { + return Err(RuntimeError::new("result arity mismatch")); + } + + let mut rets_list_array = Rets::empty_array(); + for ((slot, ty), value) in rets_list_array + .as_mut() + .iter_mut() + .zip(func_ty.results()) + .zip(results) + { + debug_assert_eq!(value.ty(), *ty); + *slot = value.as_raw(store); + } + + Ok(unsafe { Rets::from_array(store, rets_list_array) }) +} diff --git a/lib/api/src/backend/js/jspi.rs b/lib/api/src/backend/js/jspi.rs new file mode 100644 index 000000000000..e5fc665d3876 --- /dev/null +++ b/lib/api/src/backend/js/jspi.rs @@ -0,0 +1,81 @@ +use std::{cell::RefCell, collections::HashMap}; + +use crate::{AsStoreAsync, StoreAsync}; +use js_sys::{Function, Reflect}; +use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; +use wasmer_types::StoreId; + +struct ActiveStore { + store: StoreAsync, + users: usize, +} + +thread_local! { + static ACTIVE_STORES: RefCell> = + RefCell::new(HashMap::new()); +} + +pub(crate) struct ActiveStoreGuard { + id: StoreId, +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = WebAssembly, js_name = promising, catch)] + fn promising_raw(function: &Function) -> Result; + + #[wasm_bindgen(js_namespace = WebAssembly, js_name = Suspending)] + type Suspending; + + #[wasm_bindgen(constructor, js_namespace = WebAssembly, catch)] + fn new(function: &Function) -> Result; +} + +pub(crate) fn is_supported() -> bool { + let Ok(webassembly) = Reflect::get(&js_sys::global(), &JsValue::from_str("WebAssembly")) else { + return false; + }; + Reflect::get(&webassembly, &JsValue::from_str("promising")) + .is_ok_and(|value| value.is_function()) + && Reflect::get(&webassembly, &JsValue::from_str("Suspending")) + .is_ok_and(|value| value.is_function()) +} + +pub(crate) fn promising(function: &Function) -> Result { + promising_raw(function) +} + +pub(crate) fn suspending(function: &Function) -> Result { + Suspending::new(function).map(JsCast::unchecked_into) +} + +pub(crate) fn install_store(store: StoreAsync) -> ActiveStoreGuard { + let id = store.store_id(); + ACTIVE_STORES.with(|stores| { + let mut stores = stores.borrow_mut(); + stores + .entry(id) + .and_modify(|active| active.users += 1) + .or_insert(ActiveStore { store, users: 1 }); + }); + ActiveStoreGuard { id } +} + +pub(crate) fn active_store(id: StoreId) -> Option { + ACTIVE_STORES.with(|stores| stores.borrow().get(&id).map(|active| active.store.store())) +} + +impl Drop for ActiveStoreGuard { + fn drop(&mut self) { + ACTIVE_STORES.with(|stores| { + let mut stores = stores.borrow_mut(); + let active = stores + .get_mut(&self.id) + .expect("active JSPI store guard is unbalanced"); + active.users -= 1; + if active.users == 0 { + stores.remove(&self.id); + } + }); + } +} diff --git a/lib/api/src/backend/js/mod.rs b/lib/api/src/backend/js/mod.rs index 1bb4f6822280..09525ad6b3b7 100644 --- a/lib/api/src/backend/js/mod.rs +++ b/lib/api/src/backend/js/mod.rs @@ -2,6 +2,8 @@ pub(crate) mod entities; pub(crate) mod error; +#[cfg(feature = "experimental-async")] +pub(crate) mod jspi; pub(crate) mod utils; pub(crate) mod vm; diff --git a/lib/api/src/entities/engine/mod.rs b/lib/api/src/entities/engine/mod.rs index 8498ae55bb89..bb0f4021f196 100644 --- a/lib/api/src/entities/engine/mod.rs +++ b/lib/api/src/entities/engine/mod.rs @@ -231,7 +231,9 @@ impl Engine { pub fn supports_async(&self) -> bool { match self.be { #[cfg(feature = "sys")] - BackendEngine::Sys(ref e) => true, + BackendEngine::Sys(_) => true, + #[cfg(feature = "js")] + BackendEngine::Js(_) => crate::backend::js::jspi::is_supported(), _ => false, } } diff --git a/lib/api/src/entities/function/env/inner.rs b/lib/api/src/entities/function/env/inner.rs index 69f105449802..6f27d9b21cc3 100644 --- a/lib/api/src/entities/function/env/inner.rs +++ b/lib/api/src/entities/function/env/inner.rs @@ -155,14 +155,8 @@ impl BackendFunctionEnvMut<'_, T> { /// context is async. #[cfg(feature = "experimental-async")] pub fn as_store_async(&self) -> Option { - match self { - #[cfg(feature = "sys")] - Self::Sys(f) => f.as_store_async(), - #[cfg(feature = "sys")] - _ => unsupported_async_backend(), - #[cfg(not(feature = "sys"))] - _ => unsupported_async_backend::>(), - } + let id = self.as_store_ref().inner.objects.id(); + crate::StoreAsync::from_context(id) } } @@ -207,7 +201,10 @@ pub enum BackendAsyncFunctionEnvMut { #[cfg(feature = "sys")] /// The function environment for the `sys` runtime. Sys(crate::backend::sys::function::env::AsyncFunctionEnvMut), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + /// The function environment for the `js` runtime. + Js(crate::backend::js::function::env::AsyncFunctionEnvMut), + #[cfg(feature = "v8")] /// Placeholder for unsupported backends. Unsupported(PhantomData), } @@ -218,7 +215,10 @@ pub enum BackendAsyncFunctionEnvHandle { #[cfg(feature = "sys")] /// The function environment handle for the `sys` runtime. Sys(crate::backend::sys::function::env::AsyncFunctionEnvHandle), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + /// The function environment handle for the `js` runtime. + Js(crate::backend::js::function::env::AsyncFunctionEnvHandle), + #[cfg(feature = "v8")] /// Placeholder for unsupported backends. Unsupported(PhantomData), } @@ -229,7 +229,10 @@ pub enum BackendAsyncFunctionEnvHandleMut { #[cfg(feature = "sys")] /// The function environment handle for the `sys` runtime. Sys(crate::backend::sys::function::env::AsyncFunctionEnvHandleMut), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + /// The function environment handle for the `js` runtime. + Js(crate::backend::js::function::env::AsyncFunctionEnvHandleMut), + #[cfg(feature = "v8")] /// Placeholder for unsupported backends. Unsupported(PhantomData), } @@ -242,7 +245,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendAsyncFunctionEnvHandle::Sys(f.read().await), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendAsyncFunctionEnvHandle::Js(f.read().await), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -253,7 +258,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendAsyncFunctionEnvHandleMut::Sys(f.write().await), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendAsyncFunctionEnvHandleMut::Js(f.write().await), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -263,7 +270,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendFunctionEnv::Sys(f.as_ref()), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendFunctionEnv::Js(f.as_ref()), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -273,7 +282,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => Self::Sys(f.as_mut()), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => Self::Js(f.as_mut()), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -283,9 +294,11 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.as_store_async(), - #[cfg(all(feature = "sys", any(feature = "v8", feature = "js")))] + #[cfg(feature = "js")] + Self::Js(f) => f.as_store_async(), + #[cfg(all(feature = "sys", feature = "v8"))] _ => unsupported_async_backend(), - #[cfg(all(not(feature = "sys"), any(feature = "v8", feature = "js")))] + #[cfg(all(not(feature = "sys"), feature = "v8"))] _ => unsupported_async_backend::(), } } @@ -298,7 +311,9 @@ impl BackendAsyncFunctionEnvHandle { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data(), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => f.data(), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -308,9 +323,11 @@ impl BackendAsyncFunctionEnvHandle { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data_and_store(), - #[cfg(all(feature = "sys", any(feature = "v8", feature = "js")))] + #[cfg(feature = "js")] + Self::Js(f) => f.data_and_store(), + #[cfg(all(feature = "sys", feature = "v8"))] _ => unsupported_async_backend(), - #[cfg(all(not(feature = "sys"), any(feature = "v8", feature = "js")))] + #[cfg(all(not(feature = "sys"), feature = "v8"))] _ => unsupported_async_backend::<(&T, &StoreRef)>(), } } @@ -322,7 +339,9 @@ impl AsStoreRef for BackendAsyncFunctionEnvHandle { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreRef::as_store_ref(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreRef::as_store_ref(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -335,7 +354,9 @@ impl BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data_mut(), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => f.data_mut(), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -345,9 +366,11 @@ impl BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data_and_store_mut(), - #[cfg(all(feature = "sys", any(feature = "v8", feature = "js")))] + #[cfg(feature = "js")] + Self::Js(f) => f.data_and_store_mut(), + #[cfg(all(feature = "sys", feature = "v8"))] _ => unsupported_async_backend(), - #[cfg(all(not(feature = "sys"), any(feature = "v8", feature = "js")))] + #[cfg(all(not(feature = "sys"), feature = "v8"))] _ => unsupported_async_backend::<(&mut T, &mut crate::StoreMut)>(), } } @@ -358,7 +381,9 @@ impl BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendFunctionEnvMut::Sys(f.as_function_env_mut()), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendFunctionEnvMut::Js(f.as_function_env_mut()), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -370,7 +395,9 @@ impl AsStoreRef for BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreRef::as_store_ref(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreRef::as_store_ref(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -382,7 +409,9 @@ impl AsStoreMut for BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreMut::as_store_mut(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreMut::as_store_mut(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -391,7 +420,9 @@ impl AsStoreMut for BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreMut::objects_mut(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreMut::objects_mut(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } diff --git a/lib/api/src/entities/function/inner.rs b/lib/api/src/entities/function/inner.rs index 509241f1c583..7e72a89f58f0 100644 --- a/lib/api/src/entities/function/inner.rs +++ b/lib/api/src/entities/function/inner.rs @@ -229,7 +229,9 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_async(store, ty, func), + ), } } @@ -264,7 +266,11 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_with_env_async( + store, env, ty, func, + ), + ), } } @@ -288,7 +294,9 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_typed_async(store, func), + ), } } @@ -315,7 +323,11 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_typed_with_env_async( + store, env, func, + ), + ), } } @@ -456,7 +468,7 @@ impl BackendFunction { #[cfg(feature = "v8")] Self::V8(_) => unsupported_async_future(), #[cfg(feature = "js")] - Self::Js(_) => unsupported_async_future(), + Self::Js(f) => f.call_async(store, params), } } diff --git a/lib/api/src/utils/native/typed_func.rs b/lib/api/src/utils/native/typed_func.rs index 39f2b11d758a..aa62c42d03ba 100644 --- a/lib/api/src/utils/native/typed_func.rs +++ b/lib/api/src/utils/native/typed_func.rs @@ -103,7 +103,10 @@ macro_rules! impl_native_traits { #[cfg(feature = "v8")] BackendStore::V8(_) => async_backend_error(), #[cfg(feature = "js")] - BackendStore::Js(_) => async_backend_error(), + BackendStore::Js(_) => { + drop(read_lock); + Self::call_async_js(func, store, $([]),*).await + } } } } @@ -163,6 +166,6 @@ impl_native_traits!( fn async_backend_error() -> Result { Err(RuntimeError::new( - "async calls are only supported with the `sys` backend", + "async calls are not supported with the `v8` backend", )) } diff --git a/lib/api/tests/jspi_async.rs b/lib/api/tests/jspi_async.rs index e4b145424084..1491a4e69ffe 100644 --- a/lib/api/tests/jspi_async.rs +++ b/lib/api/tests/jspi_async.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "experimental-async")] +#![cfg(all(feature = "experimental-async", not(target_arch = "wasm32")))] use std::{cell::RefCell, sync::OnceLock}; diff --git a/lib/api/tests/jspi_async_js.rs b/lib/api/tests/jspi_async_js.rs new file mode 100644 index 000000000000..86dba7ca4ad9 --- /dev/null +++ b/lib/api/tests/jspi_async_js.rs @@ -0,0 +1,42 @@ +#![cfg(all(feature = "experimental-async", feature = "js", target_arch = "wasm32"))] + +use js_sys::Promise; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::JsFuture; +use wasm_bindgen_test::wasm_bindgen_test; +use wasmer::{Function, Instance, Module, Store, TypedFunction, imports}; + +#[wasm_bindgen_test] +async fn typed_async_host_and_guest_calls_use_jspi() { + let mut store = Store::default(); + let module = Module::new( + &store, + r#" + (module + (import "host" "increment" (func $increment (param i32) (result i32))) + (func (export "compute") (param i32) (result i32) + local.get 0 + call $increment)) + "#, + ) + .unwrap(); + let increment = Function::new_typed_async(&mut store, async move |value: i32| { + JsFuture::from(Promise::resolve(&JsValue::UNDEFINED)) + .await + .unwrap(); + value + 1 + }); + let imports = imports! { + "host" => { + "increment" => increment, + } + }; + let instance = Instance::new(&mut store, &module, &imports).unwrap(); + let compute: TypedFunction = instance + .exports + .get_typed_function(&store, "compute") + .unwrap(); + + let result = compute.call_async(&store.into_async(), 41).await.unwrap(); + assert_eq!(result, 42); +} diff --git a/lib/api/tests/module.rs b/lib/api/tests/module.rs index abaccd5f7811..b1449c7eeea6 100644 --- a/lib/api/tests/module.rs +++ b/lib/api/tests/module.rs @@ -1,4 +1,6 @@ use macro_wasmer_engine_test::engine_test; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; #[cfg(feature = "js")] use wasm_bindgen_test::*; @@ -27,7 +29,50 @@ fn module_new_async() -> Result<(), String> { #[cfg(target_arch = "wasm32")] #[wasm_bindgen_test] async fn module_new_async() -> Result<(), JsValue> { - assert_module_new_async().await.map_err(JsValue::from_str) + assert_module_new_async() + .await + .map_err(|error| JsValue::from_str(&error)) +} + +#[cfg(all(target_arch = "wasm32", feature = "js"))] +#[wasm_bindgen_test] +fn imported_table_elements_preserve_function_types() { + let mut store = Store::default(); + let module = Module::new( + &store, + r#" + (module + (import "env" "table" (table 8 funcref)) + (import "env" "table_base" (global $table_base i32)) + (type $trampoline_type (func (param i32 i32 i32) (result i32))) + (func $trampoline (type $trampoline_type) + local.get 0) + (elem (global.get $table_base) func $trampoline)) + "#, + ) + .unwrap(); + let table = Table::new( + &mut store, + TableType::new(Type::FuncRef, 8, None), + Value::FuncRef(None), + ) + .unwrap(); + let table_base = Global::new(&mut store, Value::I32(3)); + let imports = imports! { + "env" => { + "table" => table.clone(), + "table_base" => table_base, + } + }; + + Instance::new(&mut store, &module, &imports).unwrap(); + let Value::FuncRef(Some(function)) = table.get(&mut store, 3).unwrap() else { + panic!("expected the initialized table element to be a function"); + }; + assert_eq!( + function.ty(&store), + FunctionType::new(vec![Type::I32, Type::I32, Type::I32], vec![Type::I32]) + ); } #[engine_test] diff --git a/lib/api/tests/simple_greenthread.rs b/lib/api/tests/simple_greenthread.rs index 61f8e512be9b..e3a363ef5ec5 100644 --- a/lib/api/tests/simple_greenthread.rs +++ b/lib/api/tests/simple_greenthread.rs @@ -1,17 +1,67 @@ #![cfg(feature = "experimental-async")] use std::collections::BTreeMap; +use std::future::Future; use std::sync::atomic::AtomicU32; use std::sync::{Arc, RwLock}; use anyhow::Result; +use futures::channel::oneshot; +use futures::future::{AbortHandle, Abortable}; +#[cfg(not(target_arch = "wasm32"))] use futures::task::LocalSpawnExt; -use futures::{FutureExt, channel::oneshot}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::wasm_bindgen_test; use wasmer::{ - AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, - Module, RuntimeError, Store, Type, Value, imports, + AsStoreAsync, AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, + Instance, Memory, Module, RuntimeError, Store, Type, Value, imports, }; +const SWITCHING_WAT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/examples/simple-greenthread.wat" +)); +const SWITCHING_LOGS: &[&str] = &[ + "[gr1] main -> test1", + "[gr2] test1 -> test2", + "[gr1] test1 <- test2", + "[gr2] test1 -> test2", + "[gr1] test1 <- test2", + "[main] main <- test1", +]; +const REGRESSION_WAT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/examples/simple-greenthread2.wat" +)); +const REGRESSION_LOGS: &[&str] = &[ + "[main] switching to side", + "[side] switching to main", + "[main] switching to side", + "[side] switching to main", + "[main] returned", +]; + +#[derive(Clone)] +struct TestSpawner { + #[cfg(not(target_arch = "wasm32"))] + inner: futures::executor::LocalSpawner, +} + +impl TestSpawner { + fn spawn(&self, future: impl Future + 'static) { + #[cfg(not(target_arch = "wasm32"))] + self.inner.spawn_local(future).unwrap(); + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(future); + } +} + +struct SpawnedTask { + abort: AbortHandle, + done: oneshot::Receiver<()>, +} + struct GreenEnv { logs: Vec, memory: Option, @@ -19,7 +69,8 @@ struct GreenEnv { current_greenthread_id: Arc>, next_free_id: AtomicU32, entrypoint: Option, - spawner: Option, + spawner: Option, + spawned_tasks: Vec, } // Required for carrying the spawner around. Safe because we don't do threads. @@ -38,6 +89,7 @@ impl GreenEnv { next_free_id: AtomicU32::new(1), entrypoint: None, spawner: None, + spawned_tasks: Vec::new(), } } } @@ -84,15 +136,25 @@ async fn greenthread_new( .insert(new_greenthread_id, new_greenthread); let spawner = data.spawner.as_ref().expect("spawner set").clone(); - spawner - .spawn_local(async move { - receiver.await.unwrap(); - let resumer = function - .call_async(&async_store, vec![Value::I32(entrypoint_data as i32)]) - .await; - panic!("Greenthread function returned {:?}", resumer); - }) - .unwrap(); + let (abort, registration) = AbortHandle::new_pair(); + let (done_tx, done) = oneshot::channel(); + data.spawned_tasks.push(SpawnedTask { abort, done }); + spawner.spawn(async move { + let result = Abortable::new( + async move { + receiver.await.unwrap(); + function + .call_async(&async_store, vec![Value::I32(entrypoint_data as i32)]) + .await + }, + registration, + ) + .await; + if let Ok(result) = result { + panic!("Greenthread function returned {result:?}"); + } + let _ = done_tx.send(()); + }); Ok(new_greenthread_id) } @@ -137,12 +199,12 @@ async fn greenthread_switch(env: AsyncFunctionEnvMut, next_greenthread (receiver, current_id_arc, current_greenthread_id) }; - let _ = receiver.map(|_| ()).await; + let _ = receiver.await; *current_id_arc.write().unwrap() = current_greenthread_id; } -fn run_greenthread_test(wat: &[u8]) -> Result> { +async fn run_greenthread_test(wat: &[u8], spawner: TestSpawner) -> Result> { let mut store = Store::default(); let module = Module::new(&store.engine(), wat)?; @@ -203,73 +265,78 @@ fn run_greenthread_test(wat: &[u8]) -> Result> { .unwrap() .insert(0, main_greenthread); - let mut localpool = futures::executor::LocalPool::new(); - let local_spawner = localpool.spawner(); - env.as_mut(&mut store).spawner = Some(local_spawner); + env.as_mut(&mut store).spawner = Some(spawner); let store_async = store.into_async(); - localpool - .run_until(main_fn.call_async(&store_async, vec![])) - .unwrap(); + main_fn.call_async(&store_async, vec![]).await?; - // If there are no more clones of it, StoreAsync can also be - // turned back into a store. We need to drop the local pool - // first to drop the futures and their references to the store. - drop(localpool); + let spawned_tasks = { + let mut store = store_async.write_lock().await; + std::mem::take(&mut env.as_mut(&mut store).spawned_tasks) + }; + for task in &spawned_tasks { + task.abort.abort(); + } + for task in spawned_tasks { + let _ = task.done.await; + } - let store = store_async.into_store().ok().unwrap(); + let store = store_async.read_lock().await; Ok(env.as_ref(&store).logs.clone()) } +#[cfg(not(target_arch = "wasm32"))] +fn run_greenthread_test_native(wat: &[u8]) -> Result> { + let mut local_pool = futures::executor::LocalPool::new(); + let spawner = TestSpawner { + inner: local_pool.spawner(), + }; + local_pool.run_until(run_greenthread_test(wat, spawner)) +} + +fn assert_logs(logs: &[String], expected: &[&str]) { + assert_eq!(logs.len(), expected.len()); + for (index, expected) in expected.iter().enumerate() { + assert_eq!( + logs[index], *expected, + "Log entry mismatch at index {index}: {logs:?}" + ); + } +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn green_threads_switch_and_log_in_expected_order() -> Result<()> { - let logs = run_greenthread_test(include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../tests/examples/simple-greenthread.wat" - )))?; - - // Expected logs - let expected = [ - "[gr1] main -> test1", - "[gr2] test1 -> test2", - "[gr1] test1 <- test2", - "[gr2] test1 -> test2", - "[gr1] test1 <- test2", - "[main] main <- test1", - ]; - - assert_eq!(logs.len(), expected.len(),); - for (i, exp) in expected.iter().enumerate() { - assert_eq!(logs[i], *exp, "Log entry mismatch at index {i}: {:?}", logs); - } + let logs = run_greenthread_test_native(SWITCHING_WAT)?; + assert_logs(&logs, SWITCHING_LOGS); Ok(()) } +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn green_threads_switch_and_log_in_expected_order() { + let logs = run_greenthread_test(SWITCHING_WAT, TestSpawner {}) + .await + .unwrap(); + assert_logs(&logs, SWITCHING_LOGS); +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn green_threads_switch_main_crashed() -> Result<()> { - let logs = run_greenthread_test(include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../tests/examples/simple-greenthread2.wat" - )))?; - - // Expected logs - let expected = [ - "[main] switching to side", - "[side] switching to main", - "[main] switching to side", - "[side] switching to main", - "[main] returned", - ]; - - eprintln!("logs: {:?}", logs); - assert_eq!(logs.len(), expected.len()); - for (i, exp) in expected.iter().enumerate() { - assert_eq!(logs[i], *exp, "Log entry mismatch at index {i}: {:?}", logs); - } + let logs = run_greenthread_test_native(REGRESSION_WAT)?; + assert_logs(&logs, REGRESSION_LOGS); Ok(()) } + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn green_threads_switch_main_crashed() { + let logs = run_greenthread_test(REGRESSION_WAT, TestSpawner {}) + .await + .unwrap(); + assert_logs(&logs, REGRESSION_LOGS); +} From bb0df024cebafd176198ce0246e5eef543a35ede Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 30 Jul 2026 23:49:55 -0700 Subject: [PATCH 5/5] test(api): compile greenthread fixture before module creation --- lib/api/tests/simple_greenthread.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/api/tests/simple_greenthread.rs b/lib/api/tests/simple_greenthread.rs index e3a363ef5ec5..e691c2047d64 100644 --- a/lib/api/tests/simple_greenthread.rs +++ b/lib/api/tests/simple_greenthread.rs @@ -206,7 +206,8 @@ async fn greenthread_switch(env: AsyncFunctionEnvMut, next_greenthread async fn run_greenthread_test(wat: &[u8], spawner: TestSpawner) -> Result> { let mut store = Store::default(); - let module = Module::new(&store.engine(), wat)?; + let wasm = wat::parse_bytes(wat)?; + let module = Module::new(&store.engine(), wasm)?; let env = FunctionEnv::new(&mut store, GreenEnv::new());