diff --git a/.isu/issues.json b/.isu/issues.json index dee429b..a8b6492 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -2710,7 +2710,7 @@ "labels": [], "assigned": [], "author": "pierrelf.com", - "state": "open", + "state": "closed", "created_at": "2026-05-17T17:01:12.874395+00:00" }, { diff --git a/Cargo.lock b/Cargo.lock index 84ab13f..937979f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,7 @@ dependencies = [ "we-style", "we-text", "we-url", + "we-wasm", ] [[package]] diff --git a/crates/js/Cargo.toml b/crates/js/Cargo.toml index ce3e00b..92c99c7 100644 --- a/crates/js/Cargo.toml +++ b/crates/js/Cargo.toml @@ -17,3 +17,4 @@ we-style = { path = "../style" } we-net = { path = "../net" } we-url = { path = "../url" } we-text = { path = "../text" } +we-wasm = { path = "../wasm" } diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index b429194..8096d84 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -277,6 +277,9 @@ pub fn init_builtins(vm: &mut Vm) { // Execute JS preamble for callback-based methods. init_js_preamble(vm); + // Register WebAssembly after Promise and typed-array basics exist. + crate::wasm::init_webassembly_api(vm); + // Register Streams API foundation (ReadableStream, WritableStream, // TransformStream, queuing strategies). Must run after the Promise // preamble because the streams implementation depends on `Promise`. @@ -1337,8 +1340,10 @@ fn array_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result Result, + wasm: &mut crate::wasm::WasmRealm, ) -> Result { match gc.get(func_ref) { Some(HeapObject::Function(fdata)) => match &fdata.kind { @@ -4855,8 +4862,10 @@ fn call_native_callback( gc, shapes, this: Value::Undefined, + callee: Some(func_ref), console_output, dom_bridge, + wasm, }; cb(args, &mut ctx) } @@ -5008,6 +5017,7 @@ fn set_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result { pub gc: &'a mut Gc, pub shapes: &'a mut ShapeTable, pub this: Value, + pub callee: Option, pub console_output: &'a dyn ConsoleOutput, pub dom_bridge: Option<&'a DomBridge>, + pub wasm: &'a mut crate::wasm::WasmRealm, } // ── JS Value ────────────────────────────────────────────────── @@ -755,6 +757,9 @@ pub enum ErrorKind { RangeError, SyntaxError, Error, + WasmCompileError, + WasmLinkError, + WasmRuntimeError, } impl fmt::Display for RuntimeError { @@ -765,6 +770,9 @@ impl fmt::Display for RuntimeError { ErrorKind::RangeError => "RangeError", ErrorKind::SyntaxError => "SyntaxError", ErrorKind::Error => "Error", + ErrorKind::WasmCompileError => "WebAssembly.CompileError", + ErrorKind::WasmLinkError => "WebAssembly.LinkError", + ErrorKind::WasmRuntimeError => "WebAssembly.RuntimeError", }; write!(f, "{name}: {}", self.message) } @@ -799,6 +807,27 @@ impl RuntimeError { } } + pub fn wasm_compile_error(msg: impl Into) -> Self { + Self { + kind: ErrorKind::WasmCompileError, + message: msg.into(), + } + } + + pub fn wasm_link_error(msg: impl Into) -> Self { + Self { + kind: ErrorKind::WasmLinkError, + message: msg.into(), + } + } + + pub fn wasm_runtime_error(msg: impl Into) -> Self { + Self { + kind: ErrorKind::WasmRuntimeError, + message: msg.into(), + } + } + /// Convert to a JS Value (an error object). Allocates through the GC. pub fn to_value(&self, gc: &mut Gc, shapes: &mut ShapeTable) -> Value { let mut obj = ObjectData::new(); @@ -813,6 +842,9 @@ impl RuntimeError { ErrorKind::RangeError => "RangeError", ErrorKind::SyntaxError => "SyntaxError", ErrorKind::Error => "Error", + ErrorKind::WasmCompileError => "WebAssembly.CompileError", + ErrorKind::WasmLinkError => "WebAssembly.LinkError", + ErrorKind::WasmRuntimeError => "WebAssembly.RuntimeError", }; obj.insert_property( "name".to_string(), @@ -1167,6 +1199,8 @@ pub struct Vm { console_output: Box, /// DOM bridge for JS-DOM interop (set via `attach_document`). pub(crate) dom_bridge: Option>, + /// WebAssembly runtime state for this JS realm. + pub wasm: crate::wasm::WasmRealm, /// JIT code buffer for compiled native code. jit_buffer: Option, /// Per-function call counts (keyed by GcRef of the function). @@ -1203,6 +1237,7 @@ impl Vm { promise_prototype: None, console_output: Box::new(StdConsoleOutput), dom_bridge: None, + wasm: crate::wasm::WasmRealm::default(), jit_buffer: None, jit_call_counts: HashMap::new(), jit_compiled: HashMap::new(), @@ -1490,10 +1525,15 @@ impl Vm { gc: &mut self.gc, shapes: &mut self.shapes, this, + callee: Some(func_ref), console_output: &*self.console_output, dom_bridge: dom_ref, + wasm: &mut self.wasm, }; let result = (native.callback)(args, &mut ctx)?; + if let Some(value) = crate::wasm::try_handle_wasm_marker(self, result.clone())? { + return Ok(value); + } // Check for generator resume marker. if let Value::Object(r) = &result { @@ -2963,6 +3003,9 @@ impl Vm { // BroadcastChannel JS objects must remain alive while open so their // message/messageerror handlers can fire when traffic arrives. roots.extend(crate::broadcast_channel::broadcast_gc_roots()); + // WebAssembly host imports and memory views may be retained by the + // wasm store even when ordinary JS objects no longer point at them. + roots.extend(self.wasm.gc_roots()); roots } @@ -3402,11 +3445,28 @@ impl Vm { gc: &mut self.gc, shapes: &mut self.shapes, this, + callee: Some(func_gc_ref), console_output: &*self.console_output, dom_bridge: dom_ref, + wasm: &mut self.wasm, }; match callback(&args, &mut ctx) { Ok(val) => { + match crate::wasm::try_handle_wasm_marker(self, val.clone()) { + Ok(Some(value)) => { + self.registers[base + dst as usize] = value; + continue; + } + Ok(None) => {} + Err(err) => { + let err_val = + err.to_value(&mut self.gc, &mut self.shapes); + if !self.handle_exception(err_val) { + return Err(err); + } + continue; + } + } // Check if this is a generator resume request. if let Value::Object(r) = &val { let is_resume = matches!( @@ -4007,6 +4067,17 @@ impl Vm { } _ => {} } + let written = self.registers[base + val_r as usize].clone(); + let key_string = + self.registers[base + key_r as usize].to_js_string(&self.gc); + crate::wasm::sync_typed_array_index_set( + &mut self.wasm, + &self.gc, + &self.shapes, + gc_ref, + &key_string, + &written, + ); } } } @@ -4959,11 +5030,18 @@ impl Vm { gc: &mut self.gc, shapes: &mut self.shapes, this, + callee: Some(func_gc_ref), console_output: &*self.console_output, dom_bridge: dom_ref, + wasm: &mut self.wasm, }; match callback(&args, &mut ctx) { Ok(val) => { + if let Some(value) = crate::wasm::try_handle_wasm_marker(self, val.clone())? + { + self.registers[base + dst as usize] = value; + return Ok(()); + } self.registers[base + dst as usize] = val; Ok(()) } diff --git a/crates/js/src/wasm.rs b/crates/js/src/wasm.rs new file mode 100644 index 0000000..31892d3 --- /dev/null +++ b/crates/js/src/wasm.rs @@ -0,0 +1,2061 @@ +//! WebAssembly JS API bindings. +//! +//! The `we-wasm` crate owns parsing, validation, instantiation, and execution. +//! This module exposes that engine through JS objects and keeps the bridge +//! intentionally small: JS wrappers carry typed runtime handles into a per-VM +//! wasm store, and exported wasm functions bounce back through the VM so wasm +//! imports can call ordinary JS functions. + +use std::collections::HashMap; + +use crate::builtins::{make_native, set_builtin_prop}; +use crate::gc::{Gc, GcRef}; +use crate::shape::ShapeTable; +use crate::vm::{ + FunctionData, FunctionKind, HeapObject, NativeContext, NativeFunc, ObjectData, Property, + RuntimeError, Value, Vm, +}; +use we_wasm::runtime::{ + self, Engine, ExternVal, FunctionInstance, HostContext, HostFuncTrampoline, ImportObject, Ref, + Store, Table, WasmValue, +}; +use we_wasm::{ + parse, validate, ExportDesc, GlobalType, ImportDesc, Limits, MemoryType, Module, Mutability, + RefType, TableType, ValType, +}; + +const WASM_OBJ: &str = "WebAssembly"; +const PROP_ARRAY_BUFFER_ID: &str = "__array_buffer_id"; +const PROP_TYPED_ARRAY_KIND: &str = "__typed_array_kind"; +const PROP_WASM_MODULE_ID: &str = "__wasm_module_id"; +const PROP_WASM_INSTANCE_ID: &str = "__wasm_instance_id"; +const PROP_WASM_FUNC_ADDR: &str = "__wasm_func_addr"; +const PROP_WASM_MEMORY_ADDR: &str = "__wasm_memory_addr"; +const PROP_WASM_TABLE_ADDR: &str = "__wasm_table_addr"; +const PROP_WASM_GLOBAL_ADDR: &str = "__wasm_global_addr"; +const PROP_WASM_INVOKE: &str = "__wasm_invoke"; +const PROP_WASM_ARGS: &str = "__wasm_args"; +const PROP_WASM_BUFFER_GENERATION: &str = "__wasm_buffer_generation"; +const PROP_DETACHED: &str = "detached"; + +#[derive(Default)] +pub struct WasmRealm { + pub store: Store, + modules: Vec, + instances: Vec, + array_buffers: Vec, + host_imports: Vec, + typed_array_roots: Vec, + memory_generations: HashMap, + last_memory_buffer: Option, +} + +struct WasmModuleRecord { + bytes: Vec, +} + +struct ArrayBufferRecord { + data: Vec, + detached: bool, +} + +#[derive(Clone)] +struct JsHostImport { + callback: GcRef, + params: Vec, + results: Vec, +} + +type WasmNativeMethod = ( + &'static str, + fn(&[Value], &mut NativeContext) -> Result, +); + +impl WasmRealm { + pub fn gc_roots(&self) -> Vec { + let mut roots = Vec::with_capacity(self.host_imports.len() + self.typed_array_roots.len()); + roots.extend(self.host_imports.iter().map(|i| i.callback)); + roots.extend(self.typed_array_roots.iter().copied()); + roots + } +} + +struct JsHostFunc { + import_id: u32, +} + +impl HostFuncTrampoline for JsHostFunc { + fn call(&self, _args: &[WasmValue]) -> Result, runtime::Trap> { + Err(runtime::Trap::Unreachable) + } + + fn call_with_context( + &self, + args: &[WasmValue], + context: Option<&mut dyn HostContext>, + ) -> Result, runtime::Trap> { + match context { + Some(ctx) => ctx.invoke_host_func(self.import_id, args), + None => Err(runtime::Trap::Unreachable), + } + } +} + +struct JsHostCallContext<'a> { + vm: &'a mut Vm, + last_error: Option, +} + +impl HostContext for JsHostCallContext<'_> { + fn invoke_host_func( + &mut self, + id: u32, + args: &[WasmValue], + ) -> Result, runtime::Trap> { + let import = match self.vm.wasm.host_imports.get(id as usize) { + Some(import) => import.clone(), + None => return Err(runtime::Trap::Unreachable), + }; + if args.len() != import.params.len() { + return Err(runtime::Trap::IndirectCallTypeMismatch); + } + + let js_args: Vec = args + .iter() + .copied() + .map(|v| wasm_to_js_value(&mut self.vm.wasm, &mut self.vm.gc, &mut self.vm.shapes, v)) + .collect(); + + let result = match self.vm.call_function(import.callback, &js_args) { + Ok(value) => value, + Err(err) => { + self.last_error = Some(err); + return Err(runtime::Trap::Unreachable); + } + }; + + coerce_js_results( + &mut self.vm.wasm, + &self.vm.gc, + &self.vm.shapes, + result, + &import.results, + ) + .map_err(|err| { + self.last_error = Some(err); + runtime::Trap::IndirectCallTypeMismatch + }) + } +} + +pub fn init_webassembly_api(vm: &mut Vm) { + init_array_buffer(vm); + init_uint8_array(vm); + + let mut wasm = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + wasm.prototype = Some(proto); + } + let wasm_ref = vm.gc.alloc(HeapObject::Object(wasm)); + + let methods: &[WasmNativeMethod] = &[ + ("validate", wasm_validate), + ("compile", wasm_compile), + ("instantiate", wasm_instantiate), + ("imports", wasm_module_imports), + ("exports", wasm_module_exports), + ("customSections", wasm_module_custom_sections), + ]; + for &(name, callback) in methods { + let f = make_native(&mut vm.gc, name, callback); + set_builtin_prop( + &mut vm.gc, + &mut vm.shapes, + wasm_ref, + name, + Value::Function(f), + ); + } + + let constructors: &[WasmNativeMethod] = &[ + ("Module", wasm_module_constructor), + ("Instance", wasm_instance_constructor), + ("Memory", wasm_memory_constructor), + ("Table", wasm_table_constructor), + ("Global", wasm_global_constructor), + ("CompileError", wasm_compile_error_constructor), + ("LinkError", wasm_link_error_constructor), + ("RuntimeError", wasm_runtime_error_constructor), + ]; + for &(name, callback) in constructors { + let f = make_native(&mut vm.gc, name, callback); + set_builtin_prop( + &mut vm.gc, + &mut vm.shapes, + wasm_ref, + name, + Value::Function(f), + ); + } + + vm.set_global(WASM_OBJ, Value::Object(wasm_ref)); +} + +pub fn try_handle_wasm_marker(vm: &mut Vm, value: Value) -> Result, RuntimeError> { + let marker = match value { + Value::Object(r) => r, + _ => return Ok(None), + }; + if !is_truthy_own(&vm.gc, &vm.shapes, marker, PROP_WASM_INVOKE) { + return Ok(None); + } + run_wasm_export(vm, marker).map(Some) +} + +pub fn sync_typed_array_index_set( + realm: &mut WasmRealm, + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, + key: &str, + value: &Value, +) { + let Ok(index) = key.parse::() else { + return; + }; + let Some((memory_addr, byte_offset)) = memory_target_for_indexed_object(gc, shapes, obj) else { + return; + }; + let addr = runtime::MemoryAddr::from_raw(memory_addr); + let _ = realm + .store + .memory_mut(addr) + .store_u8(byte_offset as u64 + index, value.to_number() as u8); +} + +fn memory_target_for_indexed_object( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, +) -> Option<(u32, u32)> { + if let Some(memory_addr) = hidden_u32(gc, shapes, obj, PROP_WASM_MEMORY_ADDR) { + return Some((memory_addr, 0)); + } + let buffer = match get_own_value(gc, shapes, obj, "buffer") { + Value::Object(r) => r, + _ => return None, + }; + let memory_addr = hidden_u32(gc, shapes, buffer, PROP_WASM_MEMORY_ADDR)?; + let byte_offset = get_u32_property(gc, shapes, obj, "byteOffset").unwrap_or(0); + Some((memory_addr, byte_offset)) +} + +fn init_array_buffer(vm: &mut Vm) { + let ctor = make_native(&mut vm.gc, "ArrayBuffer", array_buffer_constructor); + vm.set_global("ArrayBuffer", Value::Function(ctor)); +} + +fn init_uint8_array(vm: &mut Vm) { + let ctor = make_native(&mut vm.gc, "Uint8Array", uint8_array_constructor); + vm.set_global("Uint8Array", Value::Function(ctor)); +} + +fn array_buffer_constructor( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let byte_len = args.first().map(to_u32).unwrap_or(0) as usize; + Ok(make_array_buffer( + ctx.wasm, + ctx.gc, + ctx.shapes, + vec![0; byte_len], + )) +} + +fn uint8_array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let source = args.first().cloned().unwrap_or(Value::Number(0.0)); + match source { + Value::Number(n) => { + let byte_len = n.max(0.0) as usize; + if let Some(memory_addr) = ctx.wasm.last_memory_buffer { + let addr = runtime::MemoryAddr::from_raw(memory_addr); + if ctx.wasm.store.memory(addr).bytes().len() == byte_len { + let buffer = make_memory_buffer(ctx.wasm, ctx.gc, ctx.shapes, addr); + return Ok(make_uint8_array_for_memory( + ctx.wasm, + ctx.gc, + ctx.shapes, + memory_addr, + buffer, + )); + } + } + Ok(make_uint8_array_from_bytes( + ctx.wasm, + ctx.gc, + ctx.shapes, + vec![0; byte_len], + )) + } + Value::Object(obj) => { + if let Some(memory_addr) = hidden_u32(ctx.gc, ctx.shapes, obj, PROP_WASM_MEMORY_ADDR) { + return Ok(make_uint8_array_for_memory( + ctx.wasm, + ctx.gc, + ctx.shapes, + memory_addr, + obj, + )); + } + if let Some(memory_addr) = current_memory_addr_for_buffer_like(ctx, obj) { + return Ok(make_uint8_array_for_memory( + ctx.wasm, + ctx.gc, + ctx.shapes, + memory_addr, + obj, + )); + } + if let Some(id) = hidden_usize(ctx.gc, ctx.shapes, obj, PROP_ARRAY_BUFFER_ID) { + let data = ctx + .wasm + .array_buffers + .get(id) + .filter(|b| !b.detached) + .map(|b| b.data.clone()) + .unwrap_or_default(); + return Ok(make_uint8_array_view( + ctx.wasm, + ctx.gc, + ctx.shapes, + Some(id), + None, + data, + None, + )); + } + let bytes = collect_array_like_bytes(ctx.gc, ctx.shapes, obj); + Ok(make_uint8_array_from_bytes( + ctx.wasm, ctx.gc, ctx.shapes, bytes, + )) + } + _ => Ok(make_uint8_array_from_bytes( + ctx.wasm, + ctx.gc, + ctx.shapes, + Vec::new(), + )), + } +} + +fn current_memory_addr_for_buffer_like(ctx: &NativeContext, obj: GcRef) -> Option { + let memory_addr = ctx.wasm.last_memory_buffer?; + let byte_len = get_u32_property(ctx.gc, ctx.shapes, obj, "byteLength")? as usize; + if byte_len == 0 { + return None; + } + let addr = runtime::MemoryAddr::from_raw(memory_addr); + (ctx.wasm.store.memory(addr).bytes().len() == byte_len).then_some(memory_addr) +} + +fn wasm_validate(args: &[Value], ctx: &mut NativeContext) -> Result { + let bytes = match buffer_source_to_bytes(ctx, args.first()) { + Ok(bytes) => bytes, + Err(_) => return Ok(Value::Boolean(false)), + }; + let valid = parse(&bytes) + .map(|module| validate(&module).is_ok()) + .unwrap_or(false); + Ok(Value::Boolean(valid)) +} + +fn wasm_compile(args: &[Value], ctx: &mut NativeContext) -> Result { + let promise = crate::builtins::create_promise_object_pub(ctx.gc, ctx.shapes); + match compile_module_from_value(ctx, args.first()) { + Ok(module) => { + crate::builtins::resolve_promise_internal(ctx.gc, ctx.shapes, promise, module) + } + Err(err) => { + let reason = err.to_value(ctx.gc, ctx.shapes); + crate::builtins::reject_promise_internal(ctx.gc, ctx.shapes, promise, reason); + } + } + Ok(Value::Object(promise)) +} + +fn wasm_instantiate(args: &[Value], ctx: &mut NativeContext) -> Result { + let promise = crate::builtins::create_promise_object_pub(ctx.gc, ctx.shapes); + let first = args.first(); + let imports = args.get(1); + let result = if let Some(Value::Object(module_obj)) = first { + if hidden_usize(ctx.gc, ctx.shapes, *module_obj, PROP_WASM_MODULE_ID).is_some() { + instantiate_module_object(ctx, *module_obj, imports) + } else { + instantiate_buffer_source(ctx, first, imports) + } + } else { + instantiate_buffer_source(ctx, first, imports) + }; + + match result { + Ok(value) => crate::builtins::resolve_promise_internal(ctx.gc, ctx.shapes, promise, value), + Err(err) => { + let reason = err.to_value(ctx.gc, ctx.shapes); + crate::builtins::reject_promise_internal(ctx.gc, ctx.shapes, promise, reason); + } + } + Ok(Value::Object(promise)) +} + +fn wasm_module_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + compile_module_from_value(ctx, args.first()) +} + +fn wasm_instance_constructor( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let module_obj = match args.first() { + Some(Value::Object(r)) => *r, + _ => return Err(RuntimeError::wasm_link_error("Instance requires a Module")), + }; + instantiate_module_object(ctx, module_obj, args.get(1)) +} + +fn wasm_memory_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let desc = object_arg(args.first(), "Memory descriptor must be an object")?; + let initial = get_u32_property(ctx.gc, ctx.shapes, desc, "initial").unwrap_or(0); + let maximum = get_optional_u32_property(ctx.gc, ctx.shapes, desc, "maximum"); + let memory = runtime::Memory::new(MemoryType(Limits { + min: initial, + max: maximum, + })) + .map_err(|_| RuntimeError::range_error("invalid WebAssembly.Memory limits"))?; + let addr = ctx.wasm.store.alloc_memory(memory); + Ok(make_memory_object(ctx.wasm, ctx.gc, ctx.shapes, addr)) +} + +fn wasm_table_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let desc = object_arg(args.first(), "Table descriptor must be an object")?; + let initial = get_u32_property(ctx.gc, ctx.shapes, desc, "initial").unwrap_or(0); + let maximum = get_optional_u32_property(ctx.gc, ctx.shapes, desc, "maximum"); + let elem = match get_string_property(ctx.gc, ctx.shapes, desc, "element").as_deref() { + Some("externref") => RefType::ExternRef, + _ => RefType::FuncRef, + }; + let table = Table::new(TableType { + elem, + limits: Limits { + min: initial, + max: maximum, + }, + }) + .map_err(|_| RuntimeError::range_error("invalid WebAssembly.Table limits"))?; + let addr = ctx.wasm.store.alloc_table(table); + Ok(make_table_object(ctx.wasm, ctx.gc, ctx.shapes, addr)) +} + +fn wasm_global_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let desc = object_arg(args.first(), "Global descriptor must be an object")?; + let value_ty = global_value_type(ctx.gc, ctx.shapes, desc)?; + let mutable = get_bool_property(ctx.gc, ctx.shapes, desc, "mutable").unwrap_or(false); + let ty = GlobalType { + valtype: value_ty, + mutability: if mutable { + Mutability::Var + } else { + Mutability::Const + }, + }; + let initial = match args.get(1) { + Some(value) => coerce_js_to_wasm(ctx.wasm, ctx.gc, ctx.shapes, value.clone(), value_ty)?, + None => WasmValue::default_for(value_ty), + }; + let global = runtime::Global::new(ty, initial) + .map_err(|_| RuntimeError::type_error("invalid WebAssembly.Global value"))?; + let addr = ctx.wasm.store.alloc_global(global); + Ok(make_global_object(ctx.wasm, ctx.gc, ctx.shapes, addr)) +} + +fn wasm_module_imports(args: &[Value], ctx: &mut NativeContext) -> Result { + let module = parse_module_object(ctx, args.first())?; + let mut entries = Vec::new(); + for import in &module.imports { + let mut obj = ObjectData::new(); + obj.insert_property( + "module".to_string(), + Property::data(Value::String(import.module.clone())), + ctx.shapes, + ); + obj.insert_property( + "name".to_string(), + Property::data(Value::String(import.name.clone())), + ctx.shapes, + ); + obj.insert_property( + "kind".to_string(), + Property::data(Value::String(kind_for_import(&import.desc).to_string())), + ctx.shapes, + ); + entries.push(Value::Object(ctx.gc.alloc(HeapObject::Object(obj)))); + } + Ok(make_value_array(ctx.gc, ctx.shapes, &entries)) +} + +fn wasm_module_exports(args: &[Value], ctx: &mut NativeContext) -> Result { + let module = parse_module_object(ctx, args.first())?; + let mut entries = Vec::new(); + for export in &module.exports { + let mut obj = ObjectData::new(); + obj.insert_property( + "name".to_string(), + Property::data(Value::String(export.name.clone())), + ctx.shapes, + ); + obj.insert_property( + "kind".to_string(), + Property::data(Value::String(kind_for_export(&export.desc).to_string())), + ctx.shapes, + ); + entries.push(Value::Object(ctx.gc.alloc(HeapObject::Object(obj)))); + } + Ok(make_value_array(ctx.gc, ctx.shapes, &entries)) +} + +fn wasm_module_custom_sections( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let name = args + .get(1) + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let payloads: Vec> = { + let module = parse_module_object(ctx, args.first())?; + module + .custom_sections + .iter() + .filter(|section| section.name == name) + .map(|section| section.payload.to_vec()) + .collect() + }; + let mut sections = Vec::new(); + for payload in payloads { + sections.push(make_uint8_array_from_bytes( + ctx.wasm, ctx.gc, ctx.shapes, payload, + )); + } + Ok(make_value_array(ctx.gc, ctx.shapes, §ions)) +} + +fn wasm_compile_error_constructor( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + Ok(make_error_object( + ctx.gc, + ctx.shapes, + "WebAssembly.CompileError", + args.first(), + )) +} + +fn wasm_link_error_constructor( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + Ok(make_error_object( + ctx.gc, + ctx.shapes, + "WebAssembly.LinkError", + args.first(), + )) +} + +fn wasm_runtime_error_constructor( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + Ok(make_error_object( + ctx.gc, + ctx.shapes, + "WebAssembly.RuntimeError", + args.first(), + )) +} + +fn compile_module_from_value( + ctx: &mut NativeContext, + source: Option<&Value>, +) -> Result { + let bytes = buffer_source_to_bytes(ctx, source)?; + let parsed = parse(&bytes).map_err(|e| RuntimeError::wasm_compile_error(e.to_string()))?; + validate(&parsed).map_err(|e| RuntimeError::wasm_compile_error(e.to_string()))?; + let id = ctx.wasm.modules.len(); + ctx.wasm.modules.push(WasmModuleRecord { bytes }); + Ok(make_module_object(ctx.gc, ctx.shapes, id)) +} + +fn instantiate_buffer_source( + ctx: &mut NativeContext, + source: Option<&Value>, + imports: Option<&Value>, +) -> Result { + let module_obj = compile_module_from_value(ctx, source)?; + let module_ref = match module_obj { + Value::Object(r) => r, + _ => unreachable!("compile_module_from_value returns object"), + }; + let instance = instantiate_module_object(ctx, module_ref, imports)?; + let mut result = ObjectData::new(); + result.insert_property( + "module".to_string(), + Property::data(Value::Object(module_ref)), + ctx.shapes, + ); + result.insert_property("instance".to_string(), Property::data(instance), ctx.shapes); + Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(result)))) +} + +fn instantiate_module_object( + ctx: &mut NativeContext, + module_obj: GcRef, + imports_value: Option<&Value>, +) -> Result { + let module_id = hidden_usize(ctx.gc, ctx.shapes, module_obj, PROP_WASM_MODULE_ID) + .ok_or_else(|| RuntimeError::wasm_link_error("Instance requires a Module"))?; + let bytes = ctx + .wasm + .modules + .get(module_id) + .ok_or_else(|| RuntimeError::wasm_link_error("unknown Module"))? + .bytes + .clone(); + let parsed = parse(&bytes).map_err(|e| RuntimeError::wasm_compile_error(e.to_string()))?; + validate(&parsed).map_err(|e| RuntimeError::wasm_compile_error(e.to_string()))?; + let import_object = build_import_object(ctx, &parsed, imports_value)?; + sync_global_imports_from_wrappers(ctx); + sync_typed_array_views_to_store(ctx.wasm, ctx.gc, ctx.shapes); + + let instance = runtime::instantiate(&mut ctx.wasm.store, &parsed, &import_object) + .map_err(map_instantiation_error)?; + let instance_obj = make_instance_object(ctx, &instance)?; + ctx.wasm.instances.push(instance); + Ok(instance_obj) +} + +fn build_import_object( + ctx: &mut NativeContext, + module: &Module<'_>, + imports_value: Option<&Value>, +) -> Result { + let mut imports = ImportObject::new(); + let imports_obj = match imports_value { + Some(Value::Object(r)) => Some(*r), + Some(Value::Undefined) | None => None, + _ => return Err(RuntimeError::wasm_link_error("imports must be an object")), + }; + + for import in &module.imports { + let value = imports_obj + .and_then(|root| get_own_value(ctx.gc, ctx.shapes, root, &import.module).gc_ref()) + .map(|module_ref| get_own_value(ctx.gc, ctx.shapes, module_ref, &import.name)) + .unwrap_or(Value::Undefined); + match &import.desc { + ImportDesc::Func(type_index) => { + let callback = match value { + Value::Function(r) => r, + _ => continue, + }; + let ty = module + .types + .get(*type_index as usize) + .ok_or_else(|| RuntimeError::wasm_link_error("invalid function type index"))? + .clone(); + let import_id = ctx.wasm.host_imports.len() as u32; + ctx.wasm.host_imports.push(JsHostImport { + callback, + params: ty.params.clone(), + results: ty.results.clone(), + }); + let func = FunctionInstance::Host { + ty, + callback: Box::new(JsHostFunc { import_id }), + }; + let addr = ctx.wasm.store.alloc_func(func); + imports.define(&import.module, &import.name, ExternVal::Func(addr)); + } + ImportDesc::Memory(_) => { + if let Value::Object(r) = value { + if let Some(raw) = hidden_u32(ctx.gc, ctx.shapes, r, PROP_WASM_MEMORY_ADDR) { + imports.define( + &import.module, + &import.name, + ExternVal::Memory(runtime::MemoryAddr::from_raw(raw)), + ); + } + } + } + ImportDesc::Table(_) => { + if let Value::Object(r) = value { + if let Some(raw) = hidden_u32(ctx.gc, ctx.shapes, r, PROP_WASM_TABLE_ADDR) { + imports.define( + &import.module, + &import.name, + ExternVal::Table(runtime::TableAddr::from_raw(raw)), + ); + } + } + } + ImportDesc::Global(_) => { + if let Value::Object(r) = value { + if let Some(raw) = hidden_u32(ctx.gc, ctx.shapes, r, PROP_WASM_GLOBAL_ADDR) { + sync_global_wrapper(ctx, r); + imports.define( + &import.module, + &import.name, + ExternVal::Global(runtime::GlobalAddr::from_raw(raw)), + ); + } + } + } + } + } + + Ok(imports) +} + +fn make_instance_object( + ctx: &mut NativeContext, + instance: &runtime::Instance, +) -> Result { + let mut exports = ObjectData::new(); + for (name, value) in instance.exports().iter() { + let js_value = match value { + ExternVal::Func(addr) => make_wasm_function(ctx.gc, addr), + ExternVal::Memory(addr) => make_memory_object(ctx.wasm, ctx.gc, ctx.shapes, addr), + ExternVal::Table(addr) => make_table_object(ctx.wasm, ctx.gc, ctx.shapes, addr), + ExternVal::Global(addr) => make_global_object(ctx.wasm, ctx.gc, ctx.shapes, addr), + }; + exports.insert_property(name.to_string(), Property::data(js_value), ctx.shapes); + } + let exports_ref = ctx.gc.alloc(HeapObject::Object(exports)); + + let id = ctx.wasm.instances.len(); + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_WASM_INSTANCE_ID.to_string(), + Property::builtin(Value::Number(id as f64)), + ctx.shapes, + ); + obj.insert_property( + "exports".to_string(), + Property::data(Value::Object(exports_ref)), + ctx.shapes, + ); + Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj)))) +} + +fn make_wasm_function(gc: &mut Gc, addr: runtime::FuncAddr) -> Value { + let f = gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "wasm function".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: wasm_exported_function, + }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + if let Some(HeapObject::Function(data)) = gc.get_mut(f) { + data.properties.insert( + PROP_WASM_FUNC_ADDR.to_string(), + Property::builtin(Value::Number(addr.as_raw() as f64)), + ); + } + Value::Function(f) +} + +fn wasm_exported_function(args: &[Value], ctx: &mut NativeContext) -> Result { + let callee = ctx + .callee + .ok_or_else(|| RuntimeError::wasm_runtime_error("missing wasm callee"))?; + let addr = hidden_func_u32(ctx.gc, callee, PROP_WASM_FUNC_ADDR) + .ok_or_else(|| RuntimeError::wasm_runtime_error("missing wasm function address"))?; + let args_array = make_value_array(ctx.gc, ctx.shapes, args); + + let mut marker = ObjectData::new(); + marker.insert_property( + PROP_WASM_INVOKE.to_string(), + Property::builtin(Value::Boolean(true)), + ctx.shapes, + ); + marker.insert_property( + PROP_WASM_FUNC_ADDR.to_string(), + Property::builtin(Value::Number(addr as f64)), + ctx.shapes, + ); + marker.insert_property( + PROP_WASM_ARGS.to_string(), + Property::builtin(args_array), + ctx.shapes, + ); + Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(marker)))) +} + +fn run_wasm_export(vm: &mut Vm, marker: GcRef) -> Result { + let raw_addr = hidden_u32(&vm.gc, &vm.shapes, marker, PROP_WASM_FUNC_ADDR) + .ok_or_else(|| RuntimeError::wasm_runtime_error("invalid wasm function marker"))?; + let func_addr = runtime::FuncAddr::from_raw(raw_addr); + let args_array = match get_own_value(&vm.gc, &vm.shapes, marker, PROP_WASM_ARGS) { + Value::Object(r) => r, + _ => return Err(RuntimeError::wasm_runtime_error("invalid wasm arguments")), + }; + let ty = vm.wasm.store.func(func_addr).ty().clone(); + let args = read_js_args_for_wasm(&mut vm.wasm, &vm.gc, &vm.shapes, args_array, &ty.params)?; + sync_typed_array_views_to_store(&mut vm.wasm, &vm.gc, &vm.shapes); + + let mut store = std::mem::take(&mut vm.wasm.store); + let (invoke_result, host_error) = { + let mut host = JsHostCallContext { + vm, + last_error: None, + }; + let result = { + let mut engine = Engine::with_host_context(&mut store, &mut host); + engine.invoke(func_addr, args) + }; + (result, host.last_error) + }; + vm.wasm.store = store; + refresh_memory_views_from_store(&mut vm.wasm, &mut vm.gc, &mut vm.shapes); + + if let Some(err) = host_error { + return Err(err); + } + let results = + invoke_result.map_err(|trap| RuntimeError::wasm_runtime_error(format!("{trap:?}")))?; + Ok(wasm_results_to_js( + &mut vm.wasm, + &mut vm.gc, + &mut vm.shapes, + results, + )) +} + +fn make_memory_object( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + addr: runtime::MemoryAddr, +) -> Value { + let buffer = make_memory_buffer(realm, gc, shapes, addr); + let grow = make_native(gc, "grow", wasm_memory_grow); + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_WASM_MEMORY_ADDR.to_string(), + Property::builtin(Value::Number(addr.as_raw() as f64)), + shapes, + ); + obj.insert_property( + "buffer".to_string(), + Property::data(Value::Object(buffer)), + shapes, + ); + obj.insert_property( + "grow".to_string(), + Property::builtin(Value::Function(grow)), + shapes, + ); + Value::Object(gc.alloc(HeapObject::Object(obj))) +} + +fn make_memory_buffer( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + addr: runtime::MemoryAddr, +) -> GcRef { + let generation = realm + .memory_generations + .entry(addr.as_raw()) + .or_insert(0) + .to_owned(); + realm.last_memory_buffer = Some(addr.as_raw()); + let byte_len = realm.store.memory(addr).bytes().len(); + let bytes_ref = make_memory_bytes_object(realm, gc, shapes, addr); + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_WASM_MEMORY_ADDR.to_string(), + Property::builtin(Value::Number(addr.as_raw() as f64)), + shapes, + ); + obj.insert_property( + PROP_WASM_BUFFER_GENERATION.to_string(), + Property::builtin(Value::Number(generation as f64)), + shapes, + ); + obj.insert_property( + "byteLength".to_string(), + Property::data(Value::Number(byte_len as f64)), + shapes, + ); + obj.insert_property( + "_bytes".to_string(), + Property::builtin(Value::Object(bytes_ref)), + shapes, + ); + obj.insert_property( + PROP_DETACHED.to_string(), + Property::data(Value::Boolean(false)), + shapes, + ); + gc.alloc(HeapObject::Object(obj)) +} + +fn make_memory_bytes_object( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + addr: runtime::MemoryAddr, +) -> GcRef { + let byte_len = realm.store.memory(addr).bytes().len(); + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_WASM_MEMORY_ADDR.to_string(), + Property::builtin(Value::Number(addr.as_raw() as f64)), + shapes, + ); + obj.insert_property( + "byteLength".to_string(), + Property::data(Value::Number(byte_len as f64)), + shapes, + ); + obj.insert_property( + "length".to_string(), + Property::data(Value::Number(byte_len as f64)), + shapes, + ); + gc.alloc(HeapObject::Object(obj)) +} + +fn wasm_memory_grow(args: &[Value], ctx: &mut NativeContext) -> Result { + let memory_obj = match ctx.this { + Value::Object(r) => r, + _ => return Err(RuntimeError::type_error("Memory.grow called on non-memory")), + }; + let raw = hidden_u32(ctx.gc, ctx.shapes, memory_obj, PROP_WASM_MEMORY_ADDR) + .ok_or_else(|| RuntimeError::type_error("Memory.grow called on non-memory"))?; + let addr = runtime::MemoryAddr::from_raw(raw); + let delta = args.first().map(to_u32).unwrap_or(0); + let old = ctx + .wasm + .store + .memory_mut(addr) + .grow(delta) + .ok_or_else(|| RuntimeError::range_error("WebAssembly.Memory.grow exceeded limits"))?; + + if let Value::Object(old_buffer) = get_own_value(ctx.gc, ctx.shapes, memory_obj, "buffer") { + set_object_prop( + ctx.gc, + ctx.shapes, + old_buffer, + PROP_DETACHED, + Value::Boolean(true), + true, + ); + set_object_prop( + ctx.gc, + ctx.shapes, + old_buffer, + "byteLength", + Value::Number(0.0), + false, + ); + } + *ctx.wasm.memory_generations.entry(raw).or_insert(0) += 1; + let new_buffer = make_memory_buffer(ctx.wasm, ctx.gc, ctx.shapes, addr); + set_object_prop( + ctx.gc, + ctx.shapes, + memory_obj, + "buffer", + Value::Object(new_buffer), + false, + ); + refresh_memory_views_from_store(ctx.wasm, ctx.gc, ctx.shapes); + Ok(Value::Number(old as f64)) +} + +fn make_table_object( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + addr: runtime::TableAddr, +) -> Value { + let grow = make_native(gc, "grow", wasm_table_grow); + let get = make_native(gc, "get", wasm_table_get); + let set = make_native(gc, "set", wasm_table_set); + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_WASM_TABLE_ADDR.to_string(), + Property::builtin(Value::Number(addr.as_raw() as f64)), + shapes, + ); + obj.insert_property( + "length".to_string(), + Property::data(Value::Number(realm.store.table(addr).size() as f64)), + shapes, + ); + obj.insert_property( + "grow".to_string(), + Property::builtin(Value::Function(grow)), + shapes, + ); + obj.insert_property( + "get".to_string(), + Property::builtin(Value::Function(get)), + shapes, + ); + obj.insert_property( + "set".to_string(), + Property::builtin(Value::Function(set)), + shapes, + ); + Value::Object(gc.alloc(HeapObject::Object(obj))) +} + +fn wasm_table_grow(args: &[Value], ctx: &mut NativeContext) -> Result { + let table_obj = table_this(ctx)?; + let raw = hidden_u32(ctx.gc, ctx.shapes, table_obj, PROP_WASM_TABLE_ADDR).unwrap(); + let addr = runtime::TableAddr::from_raw(raw); + let delta = args.first().map(to_u32).unwrap_or(0); + let init = table_ref_from_js(ctx, addr, args.get(1).cloned().unwrap_or(Value::Null))?; + let old = ctx + .wasm + .store + .table_mut(addr) + .grow(delta, init) + .unwrap_or(u32::MAX); + set_object_prop( + ctx.gc, + ctx.shapes, + table_obj, + "length", + Value::Number(ctx.wasm.store.table(addr).size() as f64), + false, + ); + Ok(Value::Number(if old == u32::MAX { + -1.0 + } else { + old as f64 + })) +} + +fn wasm_table_get(args: &[Value], ctx: &mut NativeContext) -> Result { + let table_obj = table_this(ctx)?; + let raw = hidden_u32(ctx.gc, ctx.shapes, table_obj, PROP_WASM_TABLE_ADDR).unwrap(); + let addr = runtime::TableAddr::from_raw(raw); + let idx = args.first().map(to_u32).unwrap_or(0); + match ctx.wasm.store.table(addr).get(idx) { + Ok(Ref::Null(_)) => Ok(Value::Null), + Ok(Ref::Func(f)) => Ok(make_wasm_function(ctx.gc, f)), + Ok(Ref::Extern(_)) => Ok(Value::Undefined), + Err(_) => Err(RuntimeError::range_error("table index out of bounds")), + } +} + +fn wasm_table_set(args: &[Value], ctx: &mut NativeContext) -> Result { + let table_obj = table_this(ctx)?; + let raw = hidden_u32(ctx.gc, ctx.shapes, table_obj, PROP_WASM_TABLE_ADDR).unwrap(); + let addr = runtime::TableAddr::from_raw(raw); + let idx = args.first().map(to_u32).unwrap_or(0); + let value = table_ref_from_js(ctx, addr, args.get(1).cloned().unwrap_or(Value::Null))?; + ctx.wasm + .store + .table_mut(addr) + .set(idx, value) + .map_err(|_| RuntimeError::range_error("table index out of bounds"))?; + Ok(Value::Undefined) +} + +fn table_this(ctx: &NativeContext) -> Result { + match ctx.this { + Value::Object(r) => Ok(r), + _ => Err(RuntimeError::type_error("Table method called on non-table")), + } +} + +fn table_ref_from_js( + ctx: &NativeContext, + addr: runtime::TableAddr, + value: Value, +) -> Result { + let elem = ctx.wasm.store.table(addr).elem_type(); + match value { + Value::Null | Value::Undefined => Ok(Ref::Null(elem)), + Value::Function(r) if elem == RefType::FuncRef => { + let raw = hidden_func_u32(ctx.gc, r, PROP_WASM_FUNC_ADDR) + .ok_or_else(|| RuntimeError::type_error("expected WebAssembly function"))?; + Ok(Ref::Func(runtime::FuncAddr::from_raw(raw))) + } + _ => Err(RuntimeError::type_error("invalid table value")), + } +} + +fn make_global_object( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + addr: runtime::GlobalAddr, +) -> Value { + let value_of = make_native(gc, "valueOf", wasm_global_value_of); + let current = realm.store.global(addr).get(); + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_WASM_GLOBAL_ADDR.to_string(), + Property::builtin(Value::Number(addr.as_raw() as f64)), + shapes, + ); + obj.insert_property( + "value".to_string(), + Property::data(wasm_to_js_value(realm, gc, shapes, current)), + shapes, + ); + obj.insert_property( + "valueOf".to_string(), + Property::builtin(Value::Function(value_of)), + shapes, + ); + Value::Object(gc.alloc(HeapObject::Object(obj))) +} + +fn wasm_global_value_of(_args: &[Value], ctx: &mut NativeContext) -> Result { + let global_obj = match ctx.this { + Value::Object(r) => r, + _ => { + return Err(RuntimeError::type_error( + "Global.valueOf called on non-global", + )) + } + }; + sync_global_wrapper(ctx, global_obj); + Ok(get_own_value(ctx.gc, ctx.shapes, global_obj, "value")) +} + +fn make_module_object(gc: &mut Gc, shapes: &mut ShapeTable, id: usize) -> Value { + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_WASM_MODULE_ID.to_string(), + Property::builtin(Value::Number(id as f64)), + shapes, + ); + Value::Object(gc.alloc(HeapObject::Object(obj))) +} + +fn make_array_buffer( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + data: Vec, +) -> Value { + let id = realm.array_buffers.len(); + let len = data.len(); + realm.array_buffers.push(ArrayBufferRecord { + data, + detached: false, + }); + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_ARRAY_BUFFER_ID.to_string(), + Property::builtin(Value::Number(id as f64)), + shapes, + ); + obj.insert_property( + "byteLength".to_string(), + Property::data(Value::Number(len as f64)), + shapes, + ); + obj.insert_property( + PROP_DETACHED.to_string(), + Property::data(Value::Boolean(false)), + shapes, + ); + Value::Object(gc.alloc(HeapObject::Object(obj))) +} + +fn make_uint8_array_from_bytes( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + bytes: Vec, +) -> Value { + let buffer_id = realm.array_buffers.len(); + realm.array_buffers.push(ArrayBufferRecord { + data: bytes.clone(), + detached: false, + }); + make_uint8_array_view(realm, gc, shapes, Some(buffer_id), None, bytes, None) +} + +fn make_uint8_array_for_memory( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + memory_addr: u32, + buffer_ref: GcRef, +) -> Value { + let addr = runtime::MemoryAddr::from_raw(memory_addr); + let bytes = realm.store.memory(addr).bytes().to_vec(); + make_uint8_array_view( + realm, + gc, + shapes, + None, + Some(memory_addr), + bytes, + Some(buffer_ref), + ) +} + +fn make_uint8_array_view( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + buffer_id: Option, + memory_addr: Option, + bytes: Vec, + buffer_ref: Option, +) -> Value { + let mut obj = ObjectData::new(); + obj.insert_property( + PROP_TYPED_ARRAY_KIND.to_string(), + Property::builtin(Value::String("Uint8Array".to_string())), + shapes, + ); + if let Some(id) = buffer_id { + obj.insert_property( + PROP_ARRAY_BUFFER_ID.to_string(), + Property::builtin(Value::Number(id as f64)), + shapes, + ); + } + if let Some(addr) = memory_addr { + obj.insert_property( + PROP_WASM_MEMORY_ADDR.to_string(), + Property::builtin(Value::Number(addr as f64)), + shapes, + ); + } + if let Some(buf) = buffer_ref { + obj.insert_property( + "buffer".to_string(), + Property::data(Value::Object(buf)), + shapes, + ); + } else if let Some(id) = buffer_id { + let mut buffer = ObjectData::new(); + buffer.insert_property( + PROP_ARRAY_BUFFER_ID.to_string(), + Property::builtin(Value::Number(id as f64)), + shapes, + ); + buffer.insert_property( + "byteLength".to_string(), + Property::data(Value::Number(bytes.len() as f64)), + shapes, + ); + let buffer_ref = gc.alloc(HeapObject::Object(buffer)); + obj.insert_property( + "buffer".to_string(), + Property::data(Value::Object(buffer_ref)), + shapes, + ); + } + obj.insert_property( + "byteOffset".to_string(), + Property::data(Value::Number(0.0)), + shapes, + ); + obj.insert_property( + "byteLength".to_string(), + Property::data(Value::Number(bytes.len() as f64)), + shapes, + ); + obj.insert_property( + "length".to_string(), + Property::data(Value::Number(bytes.len() as f64)), + shapes, + ); + for (i, byte) in bytes.iter().enumerate() { + obj.insert_property( + i.to_string(), + Property::data(Value::Number(*byte as f64)), + shapes, + ); + } + let r = gc.alloc(HeapObject::Object(obj)); + realm.typed_array_roots.push(r); + Value::Object(r) +} + +fn buffer_source_to_bytes( + ctx: &NativeContext, + source: Option<&Value>, +) -> Result, RuntimeError> { + let source = source.ok_or_else(|| RuntimeError::type_error("BufferSource required"))?; + match source { + Value::Object(obj) => { + if hidden_string(ctx.gc, ctx.shapes, *obj, PROP_TYPED_ARRAY_KIND).as_deref() + == Some("Uint8Array") + { + return Ok(collect_array_like_bytes(ctx.gc, ctx.shapes, *obj)); + } + if let Some(id) = hidden_usize(ctx.gc, ctx.shapes, *obj, PROP_ARRAY_BUFFER_ID) { + return ctx + .wasm + .array_buffers + .get(id) + .filter(|b| !b.detached) + .map(|b| b.data.clone()) + .ok_or_else(|| RuntimeError::type_error("detached ArrayBuffer")); + } + Ok(collect_array_like_bytes(ctx.gc, ctx.shapes, *obj)) + } + _ => Err(RuntimeError::type_error("BufferSource required")), + } +} + +fn parse_module_object<'a>( + ctx: &'a NativeContext, + value: Option<&Value>, +) -> Result, RuntimeError> { + let obj = match value { + Some(Value::Object(r)) => *r, + _ => return Err(RuntimeError::type_error("Module required")), + }; + let id = hidden_usize(ctx.gc, ctx.shapes, obj, PROP_WASM_MODULE_ID) + .ok_or_else(|| RuntimeError::type_error("Module required"))?; + let bytes = &ctx + .wasm + .modules + .get(id) + .ok_or_else(|| RuntimeError::type_error("unknown Module"))? + .bytes; + parse(bytes).map_err(|e| RuntimeError::wasm_compile_error(e.to_string())) +} + +fn sync_typed_array_views_to_store( + realm: &mut WasmRealm, + gc: &Gc, + shapes: &ShapeTable, +) { + for view_ref in realm.typed_array_roots.iter().copied() { + let memory_addr = match hidden_u32(gc, shapes, view_ref, PROP_WASM_MEMORY_ADDR) { + Some(addr) => addr, + None => match realm.last_memory_buffer { + Some(addr) => addr, + None => continue, + }, + }; + let addr = runtime::MemoryAddr::from_raw(memory_addr); + let len = get_u32_property(gc, shapes, view_ref, "length").unwrap_or(0); + let memory_len = realm.store.memory(addr).bytes().len(); + if len as usize != memory_len + && hidden_u32(gc, shapes, view_ref, PROP_WASM_MEMORY_ADDR).is_none() + { + continue; + } + let limit = len.min(memory_len as u32); + for i in 0..limit { + let b = get_index_value(gc, shapes, view_ref, i as usize).to_number() as u8; + let _ = realm.store.memory_mut(addr).store_u8(i as u64, b); + } + } +} + +fn refresh_memory_views_from_store( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, +) { + let views: Vec = realm.typed_array_roots.clone(); + for view_ref in views { + let memory_addr = match hidden_u32(gc, shapes, view_ref, PROP_WASM_MEMORY_ADDR) { + Some(addr) => addr, + None => continue, + }; + let addr = runtime::MemoryAddr::from_raw(memory_addr); + let bytes = realm.store.memory(addr).bytes().to_vec(); + set_object_prop( + gc, + shapes, + view_ref, + "byteLength", + Value::Number(bytes.len() as f64), + false, + ); + set_object_prop( + gc, + shapes, + view_ref, + "length", + Value::Number(bytes.len() as f64), + false, + ); + for (i, b) in bytes.into_iter().enumerate() { + set_object_prop( + gc, + shapes, + view_ref, + &i.to_string(), + Value::Number(b as f64), + false, + ); + } + } +} + +fn sync_global_imports_from_wrappers(ctx: &mut NativeContext) { + let roots = ctx.wasm.gc_roots(); + for r in roots { + sync_global_wrapper(ctx, r); + } +} + +fn sync_global_wrapper(ctx: &mut NativeContext, obj: GcRef) { + let Some(raw) = hidden_u32(ctx.gc, ctx.shapes, obj, PROP_WASM_GLOBAL_ADDR) else { + return; + }; + let addr = runtime::GlobalAddr::from_raw(raw); + let ty = ctx.wasm.store.global(addr).val_type(); + let value = get_own_value(ctx.gc, ctx.shapes, obj, "value"); + if let Ok(wasm_value) = coerce_js_to_wasm(ctx.wasm, ctx.gc, ctx.shapes, value, ty) { + let _ = ctx.wasm.store.global_mut(addr).set_force(wasm_value); + } + let current = ctx.wasm.store.global(addr).get(); + let js_value = wasm_to_js_value(ctx.wasm, ctx.gc, ctx.shapes, current); + set_object_prop(ctx.gc, ctx.shapes, obj, "value", js_value, false); +} + +fn read_js_args_for_wasm( + realm: &mut WasmRealm, + gc: &Gc, + shapes: &ShapeTable, + args_array: GcRef, + params: &[ValType], +) -> Result, RuntimeError> { + let mut args = Vec::with_capacity(params.len()); + for (i, ty) in params.iter().copied().enumerate() { + let value = get_index_value(gc, shapes, args_array, i); + args.push(coerce_js_to_wasm(realm, gc, shapes, value, ty)?); + } + Ok(args) +} + +fn coerce_js_results( + realm: &mut WasmRealm, + gc: &Gc, + shapes: &ShapeTable, + value: Value, + results: &[ValType], +) -> Result, RuntimeError> { + if results.is_empty() { + return Ok(Vec::new()); + } + if results.len() == 1 { + return Ok(vec![coerce_js_to_wasm( + realm, gc, shapes, value, results[0], + )?]); + } + let obj = match value { + Value::Object(r) => r, + _ => { + return Err(RuntimeError::type_error( + "multi-value result must be array-like", + )) + } + }; + let mut out = Vec::with_capacity(results.len()); + for (i, ty) in results.iter().copied().enumerate() { + out.push(coerce_js_to_wasm( + realm, + gc, + shapes, + get_index_value(gc, shapes, obj, i), + ty, + )?); + } + Ok(out) +} + +fn coerce_js_to_wasm( + _realm: &mut WasmRealm, + gc: &Gc, + _shapes: &ShapeTable, + value: Value, + ty: ValType, +) -> Result { + match ty { + ValType::I32 => Ok(WasmValue::I32(value.to_number() as i32)), + ValType::I64 => Ok(WasmValue::I64(value.to_number() as i64)), + ValType::F32 => Ok(WasmValue::F32((value.to_number() as f32).to_bits())), + ValType::F64 => Ok(WasmValue::F64(value.to_number().to_bits())), + ValType::Ref(rt) => match (rt, value) { + (RefType::FuncRef, Value::Null | Value::Undefined) => { + Ok(WasmValue::Ref(Ref::Null(RefType::FuncRef))) + } + (RefType::FuncRef, Value::Function(r)) => { + let raw = hidden_func_u32(gc, r, PROP_WASM_FUNC_ADDR) + .ok_or_else(|| RuntimeError::type_error("expected WebAssembly function"))?; + Ok(WasmValue::Ref(Ref::Func(runtime::FuncAddr::from_raw(raw)))) + } + (RefType::FuncRef, _) => Err(RuntimeError::type_error("expected funcref")), + (RefType::ExternRef, Value::Null | Value::Undefined) => { + Ok(WasmValue::Ref(Ref::Null(RefType::ExternRef))) + } + (RefType::ExternRef, _) => Ok(WasmValue::Ref(Ref::Extern(0))), + }, + } +} + +fn wasm_to_js_value( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + value: WasmValue, +) -> Value { + match value { + WasmValue::I32(v) => Value::Number(v as f64), + WasmValue::I64(v) => Value::Number(v as f64), + WasmValue::F32(bits) => Value::Number(f32::from_bits(bits) as f64), + WasmValue::F64(bits) => Value::Number(f64::from_bits(bits)), + WasmValue::Ref(Ref::Null(_)) => Value::Null, + WasmValue::Ref(Ref::Func(addr)) => make_wasm_function(gc, addr), + WasmValue::Ref(Ref::Extern(_)) => { + let _ = (realm, shapes); + Value::Undefined + } + } +} + +fn wasm_results_to_js( + realm: &mut WasmRealm, + gc: &mut Gc, + shapes: &mut ShapeTable, + results: Vec, +) -> Value { + if results.is_empty() { + return Value::Undefined; + } + if results.len() == 1 { + return wasm_to_js_value(realm, gc, shapes, results[0]); + } + let values: Vec = results + .into_iter() + .map(|v| wasm_to_js_value(realm, gc, shapes, v)) + .collect(); + make_value_array(gc, shapes, &values) +} + +fn map_instantiation_error(err: runtime::InstantiationError) -> RuntimeError { + match err { + runtime::InstantiationError::Validation(e) => { + RuntimeError::wasm_compile_error(e.to_string()) + } + runtime::InstantiationError::Link(e) => RuntimeError::wasm_link_error(e.to_string()), + runtime::InstantiationError::Trap(t) => RuntimeError::wasm_runtime_error(format!("{t:?}")), + other => RuntimeError::wasm_link_error(other.to_string()), + } +} + +fn kind_for_import(desc: &ImportDesc) -> &'static str { + match desc { + ImportDesc::Func(_) => "function", + ImportDesc::Table(_) => "table", + ImportDesc::Memory(_) => "memory", + ImportDesc::Global(_) => "global", + } +} + +fn kind_for_export(desc: &ExportDesc) -> &'static str { + match desc { + ExportDesc::Func(_) => "function", + ExportDesc::Table(_) => "table", + ExportDesc::Memory(_) => "memory", + ExportDesc::Global(_) => "global", + } +} + +fn global_value_type( + gc: &Gc, + shapes: &ShapeTable, + desc: GcRef, +) -> Result { + match get_string_property(gc, shapes, desc, "value").as_deref() { + Some("i32") => Ok(ValType::I32), + Some("i64") => Ok(ValType::I64), + Some("f32") => Ok(ValType::F32), + Some("f64") => Ok(ValType::F64), + Some("externref") => Ok(ValType::EXTERNREF), + Some("anyfunc") | Some("funcref") => Ok(ValType::FUNCREF), + _ => Err(RuntimeError::type_error( + "unsupported WebAssembly.Global value type", + )), + } +} + +fn make_error_object( + gc: &mut Gc, + shapes: &mut ShapeTable, + name: &str, + message: Option<&Value>, +) -> Value { + let mut obj = ObjectData::new(); + obj.insert_property( + "name".to_string(), + Property::data(Value::String(name.to_string())), + shapes, + ); + obj.insert_property( + "message".to_string(), + Property::data(Value::String( + message.map(|v| v.to_js_string(gc)).unwrap_or_default(), + )), + shapes, + ); + Value::Object(gc.alloc(HeapObject::Object(obj))) +} + +fn object_arg(value: Option<&Value>, message: &str) -> Result { + match value { + Some(Value::Object(r)) => Ok(*r), + _ => Err(RuntimeError::type_error(message)), + } +} + +fn make_value_array(gc: &mut Gc, shapes: &mut ShapeTable, values: &[Value]) -> Value { + let mut obj = ObjectData::new(); + for (i, value) in values.iter().enumerate() { + obj.insert_property(i.to_string(), Property::data(value.clone()), shapes); + } + obj.insert_property( + "length".to_string(), + Property { + value: Value::Number(values.len() as f64), + writable: true, + enumerable: false, + configurable: false, + }, + shapes, + ); + Value::Object(gc.alloc(HeapObject::Object(obj))) +} + +fn collect_array_like_bytes(gc: &Gc, shapes: &ShapeTable, obj: GcRef) -> Vec { + let len = get_u32_property(gc, shapes, obj, "length").map(|n| n as usize); + if let Some(len) = len { + if len > 0 && has_own_property(gc, shapes, obj, "0") { + return collect_indexed_bytes(gc, shapes, obj, len); + } + } + if let Some(bytes) = collect_buffer_backed_bytes(gc, shapes, obj) { + return bytes; + } + if let Some(bytes) = collect_array_buffer_bytes(gc, shapes, obj) { + return bytes; + } + let len = len.unwrap_or(0); + collect_indexed_bytes(gc, shapes, obj, len) +} + +fn collect_indexed_bytes( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, + len: usize, +) -> Vec { + let mut bytes = Vec::with_capacity(len); + for i in 0..len { + bytes.push(get_index_value(gc, shapes, obj, i).to_number() as u8); + } + bytes +} + +fn collect_buffer_backed_bytes( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, +) -> Option> { + let buffer = match get_own_value(gc, shapes, obj, "buffer") { + Value::Object(r) => r, + _ => return None, + }; + let byte_offset = get_u32_property(gc, shapes, obj, "byteOffset").unwrap_or(0) as usize; + let byte_len = get_u32_property(gc, shapes, obj, "byteLength")? as usize; + collect_bytes_from_buffer_storage(gc, shapes, buffer, byte_offset, byte_len) +} + +fn collect_array_buffer_bytes( + gc: &Gc, + shapes: &ShapeTable, + buffer: GcRef, +) -> Option> { + let byte_len = get_u32_property(gc, shapes, buffer, "byteLength")? as usize; + collect_bytes_from_buffer_storage(gc, shapes, buffer, 0, byte_len) +} + +fn collect_bytes_from_buffer_storage( + gc: &Gc, + shapes: &ShapeTable, + buffer: GcRef, + byte_offset: usize, + byte_len: usize, +) -> Option> { + let bytes_obj = match get_own_value(gc, shapes, buffer, "_bytes") { + Value::Object(r) => r, + _ => return None, + }; + let mut bytes = Vec::with_capacity(byte_len); + for i in 0..byte_len { + bytes.push(get_index_value(gc, shapes, bytes_obj, byte_offset + i).to_number() as u8); + } + Some(bytes) +} + +fn get_index_value(gc: &Gc, shapes: &ShapeTable, obj: GcRef, idx: usize) -> Value { + let key = idx.to_string(); + match gc.get(obj) { + Some(HeapObject::Object(data)) => { + let from_entries = data + .property_entries(shapes) + .into_iter() + .filter(|(name, _)| name == &key) + .map(|(_, prop)| prop.value) + .next_back(); + from_entries.unwrap_or_else(|| get_own_value(gc, shapes, obj, &key)) + } + _ => Value::Undefined, + } +} + +fn get_own_value(gc: &Gc, shapes: &ShapeTable, obj: GcRef, key: &str) -> Value { + match gc.get(obj) { + Some(HeapObject::Object(data)) => data + .get_property(key, shapes) + .map(|p| p.value) + .unwrap_or(Value::Undefined), + Some(HeapObject::Function(data)) => data + .properties + .get(key) + .map(|p| p.value.clone()) + .unwrap_or(Value::Undefined), + _ => Value::Undefined, + } +} + +fn has_own_property(gc: &Gc, shapes: &ShapeTable, obj: GcRef, key: &str) -> bool { + match gc.get(obj) { + Some(HeapObject::Object(data)) => data.contains_key(key, shapes), + Some(HeapObject::Function(data)) => data.properties.contains_key(key), + _ => false, + } +} + +fn set_object_prop( + gc: &mut Gc, + shapes: &mut ShapeTable, + obj: GcRef, + key: &str, + value: Value, + builtin: bool, +) { + if let Some(HeapObject::Object(data)) = gc.get_mut(obj) { + let prop = if builtin { + Property::builtin(value) + } else { + Property::data(value) + }; + data.insert_property(key.to_string(), prop, shapes); + } +} + +fn hidden_usize(gc: &Gc, shapes: &ShapeTable, obj: GcRef, key: &str) -> Option { + hidden_u32(gc, shapes, obj, key).map(|n| n as usize) +} + +fn hidden_u32(gc: &Gc, shapes: &ShapeTable, obj: GcRef, key: &str) -> Option { + let value = match gc.get(obj) { + Some(HeapObject::Object(data)) => data + .property_entries(shapes) + .into_iter() + .filter(|(name, _)| name == key) + .map(|(_, prop)| prop.value) + .next_back() + .unwrap_or_else(|| get_own_value(gc, shapes, obj, key)), + _ => get_own_value(gc, shapes, obj, key), + }; + match value { + Value::Number(n) if n >= 0.0 => Some(n as u32), + _ => None, + } +} + +fn hidden_string( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, + key: &str, +) -> Option { + match get_own_value(gc, shapes, obj, key) { + Value::String(s) => Some(s), + _ => None, + } +} + +fn hidden_func_u32(gc: &Gc, func: GcRef, key: &str) -> Option { + match gc.get(func) { + Some(HeapObject::Function(data)) => match data.properties.get(key).map(|p| &p.value) { + Some(Value::Number(n)) if *n >= 0.0 => Some(*n as u32), + _ => None, + }, + _ => None, + } +} + +fn is_truthy_own(gc: &Gc, shapes: &ShapeTable, obj: GcRef, key: &str) -> bool { + matches!(get_own_value(gc, shapes, obj, key), Value::Boolean(true)) +} + +fn get_u32_property( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, + key: &str, +) -> Option { + match get_own_value(gc, shapes, obj, key) { + Value::Number(n) if n >= 0.0 => Some(n as u32), + _ => None, + } +} + +fn get_optional_u32_property( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, + key: &str, +) -> Option { + match get_own_value(gc, shapes, obj, key) { + Value::Undefined => None, + Value::Number(n) if n >= 0.0 => Some(n as u32), + _ => None, + } +} + +fn get_bool_property( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, + key: &str, +) -> Option { + match get_own_value(gc, shapes, obj, key) { + Value::Boolean(b) => Some(b), + _ => None, + } +} + +fn get_string_property( + gc: &Gc, + shapes: &ShapeTable, + obj: GcRef, + key: &str, +) -> Option { + match get_own_value(gc, shapes, obj, key) { + Value::String(s) => Some(s), + _ => None, + } +} + +fn to_u32(value: &Value) -> u32 { + value.to_number().max(0.0) as u32 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compiler; + use crate::parser::Parser; + + const ADD_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, + 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, + 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b, + ]; + + const IMPORT_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, + 0x7f, 0x02, 0x0b, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x03, 0x69, 0x6e, 0x63, 0x00, 0x00, 0x03, + 0x02, 0x01, 0x00, 0x07, 0x0b, 0x01, 0x07, 0x63, 0x61, 0x6c, 0x6c, 0x49, 0x6e, 0x63, 0x00, + 0x01, 0x0a, 0x08, 0x01, 0x06, 0x00, 0x20, 0x00, 0x10, 0x00, 0x0b, + ]; + + const MEMORY_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, + 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x12, 0x02, 0x06, 0x6d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x05, 0x6c, 0x6f, 0x61, 0x64, 0x30, 0x00, 0x00, 0x0a, + 0x09, 0x01, 0x07, 0x00, 0x41, 0x00, 0x2d, 0x00, 0x00, 0x0b, + ]; + + const NUMERIC_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x10, 0x03, 0x60, 0x01, 0x7e, 0x01, + 0x7e, 0x60, 0x01, 0x7d, 0x01, 0x7d, 0x60, 0x01, 0x7c, 0x01, 0x7c, 0x03, 0x04, 0x03, 0x00, + 0x01, 0x02, 0x07, 0x1c, 0x03, 0x06, 0x69, 0x64, 0x5f, 0x69, 0x36, 0x34, 0x00, 0x00, 0x06, + 0x69, 0x64, 0x5f, 0x66, 0x33, 0x32, 0x00, 0x01, 0x06, 0x69, 0x64, 0x5f, 0x66, 0x36, 0x34, + 0x00, 0x02, 0x0a, 0x10, 0x03, 0x04, 0x00, 0x20, 0x00, 0x0b, 0x04, 0x00, 0x20, 0x00, 0x0b, + 0x04, 0x00, 0x20, 0x00, 0x0b, + ]; + + const TRAP_WASM: &[u8] = &[ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, 0x03, + 0x02, 0x01, 0x00, 0x07, 0x08, 0x01, 0x04, 0x62, 0x6f, 0x6f, 0x6d, 0x00, 0x00, 0x0a, 0x05, + 0x01, 0x03, 0x00, 0x00, 0x0b, + ]; + + fn bytes_setup(name: &str, bytes: &[u8]) -> String { + let mut src = format!("var {name} = new Uint8Array({});", bytes.len()); + for (i, byte) in bytes.iter().enumerate() { + src.push_str(&format!("{name}[{i}] = {byte};")); + } + src + } + + fn eval_with_vm(source: &str) -> (Vm, Result) { + let program = Parser::parse(source).expect("parse"); + let func = compiler::compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.execute(&func); + (vm, result) + } + + #[test] + fn uint8_array_copies_array_literal_values() { + let src = format!( + "{} Object.values(u)[1];", + bytes_setup("u", &[0, 97, 115, 109]) + ); + let (_, result) = eval_with_vm(&src); + match result.unwrap() { + Value::Number(n) => assert_eq!(n, 97.0), + v => panic!("expected 97, got {v:?}"), + } + } + + #[test] + fn module_accepts_uint8array_backing_store_bytes() { + let bytes = ADD_WASM + .iter() + .map(|byte| byte.to_string()) + .collect::>() + .join(","); + let src = format!( + "var bytes = new Uint8Array([{}]); var m = new WebAssembly.Module(bytes); var i = new WebAssembly.Instance(m); i.exports.add(6, 7);", + bytes + ); + let (_, result) = eval_with_vm(&src); + match result.unwrap() { + Value::Number(n) => assert_eq!(n, 13.0), + v => panic!("expected 13, got {v:?}"), + } + } + + #[test] + fn module_instance_and_exported_function_work() { + let src = format!( + "{} var m = new WebAssembly.Module(bytes); var i = new WebAssembly.Instance(m); i.exports.add(2, 3);", + bytes_setup("bytes", ADD_WASM) + ); + let (_, result) = eval_with_vm(&src); + match result.unwrap() { + Value::Number(n) => assert_eq!(n, 5.0), + v => panic!("expected 5, got {v:?}"), + } + } + + #[test] + fn numeric_arguments_and_results_round_trip_as_numbers() { + let src = format!( + "{} var m = new WebAssembly.Module(bytes); var i = new WebAssembly.Instance(m); i.exports.id_i64(41) + i.exports.id_f32(0.5) + i.exports.id_f64(0.25);", + bytes_setup("bytes", NUMERIC_WASM) + ); + let (_, result) = eval_with_vm(&src); + match result.unwrap() { + Value::Number(n) => assert_eq!(n, 41.75), + v => panic!("expected 41.75, got {v:?}"), + } + } + + #[test] + fn instantiate_promise_resolves_buffer_source() { + let src = format!( + "var holder = {{ out: 0 }}; {} WebAssembly.instantiate(bytes).then(function(result) {{ holder.out = result.instance.exports.add(4, 5); }}); holder;", + bytes_setup("bytes", ADD_WASM) + ); + let (vm, result) = eval_with_vm(&src); + match result.unwrap() { + Value::Object(r) => match get_own_value(&vm.gc, &vm.shapes, r, "out") { + Value::Number(n) => assert_eq!(n, 9.0), + v => panic!("expected 9, got {v:?}"), + }, + v => panic!("expected holder object, got {v:?}"), + } + } + + #[test] + fn imported_js_function_is_callable_from_wasm() { + let src = format!( + "{} var m = new WebAssembly.Module(bytes); var i = new WebAssembly.Instance(m, {{ env: {{ inc: function(x) {{ return x + 1; }} }} }}); i.exports.callInc(41);", + bytes_setup("bytes", IMPORT_WASM) + ); + let (_, result) = eval_with_vm(&src); + match result.unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + } + + #[test] + fn memory_buffer_aliases_wasm_memory() { + let src = format!( + "{} var m = new WebAssembly.Module(bytes); var i = new WebAssembly.Instance(m); var buf = i.exports.memory.buffer; var view = new Uint8Array(buf); view[0] = 77; i.exports.load0();", + bytes_setup("bytes", MEMORY_WASM) + ); + let (_, result) = eval_with_vm(&src); + match result.unwrap() { + Value::Number(n) => assert_eq!(n, 77.0), + v => panic!("expected 77, got {v:?}"), + } + } + + #[test] + fn memory_grow_reissues_buffer_and_detaches_old_one() { + let src = "var mem = new WebAssembly.Memory({ initial: 1, maximum: 2 }); var old = mem.buffer; var before = old.byteLength; var grown = mem.grow(1); before + grown + old.byteLength + mem.buffer.byteLength;"; + let (_, result) = eval_with_vm(src); + match result.unwrap() { + Value::Number(n) => assert_eq!(n, 196609.0), + v => panic!("expected memory grow sum, got {v:?}"), + } + } + + #[test] + fn trap_maps_to_webassembly_runtime_error() { + let src = format!( + "try {{ {} var m = new WebAssembly.Module(bytes); var i = new WebAssembly.Instance(m); i.exports.boom(); 'no'; }} catch (e) {{ e.name; }}", + bytes_setup("bytes", TRAP_WASM) + ); + let (_, result) = eval_with_vm(&src); + match result.unwrap() { + Value::String(s) => assert_eq!(s, "WebAssembly.RuntimeError"), + v => panic!("expected WebAssembly.RuntimeError, got {v:?}"), + } + } +} diff --git a/crates/wasm/src/runtime/engine.rs b/crates/wasm/src/runtime/engine.rs index 45a508e..bbadfdf 100644 --- a/crates/wasm/src/runtime/engine.rs +++ b/crates/wasm/src/runtime/engine.rs @@ -19,7 +19,7 @@ use crate::instruction::Instruction; use crate::runtime::exec::{step_straight, StepOutcome}; -use crate::runtime::function::FunctionInstance; +use crate::runtime::function::{FunctionInstance, HostContext}; use crate::runtime::store::{FuncAddr, ModuleAddr, Store}; use crate::runtime::trap::Trap; use crate::runtime::value::{Ref, WasmValue}; @@ -34,6 +34,7 @@ pub const DEFAULT_MAX_CALL_DEPTH: usize = 1024; /// [`Trap::StackOverflow`] rather than blowing the host stack. pub struct Engine<'a> { store: &'a mut Store, + host_context: Option<&'a mut dyn HostContext>, max_depth: usize, depth: usize, } @@ -49,11 +50,23 @@ impl<'a> Engine<'a> { pub fn with_max_depth(store: &'a mut Store, max_depth: usize) -> Self { Self { store, + host_context: None, max_depth, depth: 0, } } + /// Build an engine with an embedder callback context for imported host + /// functions. + pub fn with_host_context(store: &'a mut Store, host_context: &'a mut dyn HostContext) -> Self { + Self { + store, + host_context: Some(host_context), + max_depth: DEFAULT_MAX_CALL_DEPTH, + depth: 0, + } + } + /// The shared store. pub fn store(&self) -> &Store { self.store @@ -151,7 +164,10 @@ impl<'a> Engine<'a> { FunctionInstance::Host { callback, .. } => callback, _ => unreachable!("plan/function mismatch"), }; - let out = callback.call(&args)?; + let out = match self.host_context.as_mut() { + Some(context) => callback.call_with_context(&args, Some(&mut **context))?, + None => callback.call_with_context(&args, None)?, + }; if out.len() != results.len() { return Err(Trap::IndirectCallTypeMismatch); } diff --git a/crates/wasm/src/runtime/function.rs b/crates/wasm/src/runtime/function.rs index 6f56957..ded5731 100644 --- a/crates/wasm/src/runtime/function.rs +++ b/crates/wasm/src/runtime/function.rs @@ -18,6 +18,13 @@ use crate::types::{FuncType, ValType}; /// keeping the bound looser lets hosts capture `Rc`/`RefCell` state. pub type HostFunc = Box; +/// Host-side context available to callbacks that need to cross back into an +/// embedder. Plain Rust host functions ignore this and use [`call`]. +pub trait HostContext { + /// Invoke an embedder-owned host function by id. + fn invoke_host_func(&mut self, id: u32, args: &[WasmValue]) -> Result, Trap>; +} + /// Trait object behind [`HostFunc`]. Boxed so multiple host functions can /// share a function table without leaking the concrete closure type. pub trait HostFuncTrampoline { @@ -26,6 +33,16 @@ pub trait HostFuncTrampoline { /// callee returns its result vector (which the interpreter then /// type-checks against the declared results). fn call(&self, args: &[WasmValue]) -> Result, Trap>; + + /// Context-aware entry point for embedders that need to bridge a wasm host + /// call into another runtime, such as JavaScript. + fn call_with_context( + &self, + args: &[WasmValue], + _context: Option<&mut dyn HostContext>, + ) -> Result, Trap> { + self.call(args) + } } impl HostFuncTrampoline for F diff --git a/crates/wasm/src/runtime/mod.rs b/crates/wasm/src/runtime/mod.rs index 6cf3960..983cb4d 100644 --- a/crates/wasm/src/runtime/mod.rs +++ b/crates/wasm/src/runtime/mod.rs @@ -31,7 +31,7 @@ pub use data::DataInstance; pub use element::ElementInstance; pub use engine::{Engine, DEFAULT_MAX_CALL_DEPTH}; pub use exec::{Interpreter, StepOutcome}; -pub use function::{FunctionInstance, HostFunc, HostFuncTrampoline}; +pub use function::{FunctionInstance, HostContext, HostFunc, HostFuncTrampoline}; pub use global::Global; pub use instance::{ExternVal, ModuleInstance}; pub use instantiate::{ diff --git a/tools/generate_icon/src/main.rs b/tools/generate_icon/src/main.rs index 699e98f..6e498c0 100644 --- a/tools/generate_icon/src/main.rs +++ b/tools/generate_icon/src/main.rs @@ -168,7 +168,7 @@ fn downsample_rgba( dst_width: u32, dst_height: u32, ) -> Result, String> { - if src_width % dst_width != 0 || src_height % dst_height != 0 { + if !src_width.is_multiple_of(dst_width) || !src_height.is_multiple_of(dst_height) { return Err("downsample target must divide source dimensions exactly".to_string()); } if src.len() != (src_width * src_height * 4) as usize {