//! 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. //! //! `compileStreaming()` and `instantiateStreaming()` intentionally buffer the //! response body through `Response.arrayBuffer()` before handing bytes to //! `we-wasm`. The streaming decoder optimization can be layered on later; this //! keeps the first integration aligned with the existing Fetch/Streams promise //! and cancellation semantics. 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)); init_streaming_preamble(vm); } fn init_streaming_preamble(vm: &mut Vm) { let ast = match crate::parser::Parser::parse(WASM_STREAMING_PREAMBLE) { Ok(ast) => ast, Err(e) => { eprintln!("wasm streaming preamble parse error: {e}"); return; } }; let func = match crate::compiler::compile(&ast) { Ok(func) => func, Err(e) => { eprintln!("wasm streaming preamble compile error: {e}"); return; } }; if let Err(e) = vm.execute(&func) { eprintln!("wasm streaming preamble runtime error: {e}"); } } const WASM_STREAMING_PREAMBLE: &str = r#" function __we_wasm_compile_error(reason) { if (reason !== null && reason !== undefined && reason.name === "WebAssembly.CompileError") { return reason; } return WebAssembly.CompileError(String(reason)); } function __we_wasm_reject_compile_error(reason) { return Promise.reject(__we_wasm_compile_error(reason)); } function __we_wasm_header_value(headers, name) { if (headers === null || headers === undefined) return null; if (typeof headers.get === "function") return headers.get(name); var lower = String(name).toLowerCase(); return headers[lower] || headers[name] || null; } function __we_wasm_is_application_wasm(value) { if (value === null || value === undefined) return false; var mime = String(value).toLowerCase(); var semi = mime.indexOf(";"); if (semi >= 0) mime = mime.slice(0, semi); return mime.trim() === "application/wasm"; } function __we_wasm_streaming_validation_error(response) { if (response === null || response === undefined || typeof response.arrayBuffer !== "function") { return WebAssembly.CompileError("WebAssembly streaming source must be a Response"); } var contentType = __we_wasm_header_value(response.headers, "Content-Type"); if (!__we_wasm_is_application_wasm(contentType)) { return WebAssembly.CompileError("WebAssembly response has unsupported Content-Type"); } return null; } function __we_wasm_compile_streaming_response(response) { var err = __we_wasm_streaming_validation_error(response); if (err !== null) return Promise.reject(err); return response.arrayBuffer().then(function(buffer) { return WebAssembly.compile(buffer); }); } WebAssembly.compileStreaming = function(source) { return Promise.resolve(source) .then(function(response) { return __we_wasm_compile_streaming_response(response); }) .then(function(module) { return module; }, __we_wasm_reject_compile_error); }; WebAssembly.instantiateStreaming = function(source, importObject) { return WebAssembly.compileStreaming(source).then(function(module) { return WebAssembly.instantiate(module, importObject).then(function(instance) { return { module: module, instance: instance }; }); }); }; "#; 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 resolve_typed_array_index_get( realm: &WasmRealm, gc: &Gc, shapes: &ShapeTable, obj: GcRef, key: &str, ) -> Option { let index = key.parse::().ok()?; let (memory_addr, byte_offset, byte_len) = memory_target_for_indexed_object(gc, shapes, obj)?; if index >= byte_len as u64 { return Some(Value::Undefined); } let addr = runtime::MemoryAddr::from_raw(memory_addr); realm .store .memory(addr) .load_u8(byte_offset as u64 + index) .ok() .map(|b| Value::Number(b as f64)) } pub fn set_typed_array_index( realm: &mut WasmRealm, gc: &Gc, shapes: &ShapeTable, obj: GcRef, key: &str, value: &Value, ) -> bool { let Ok(index) = key.parse::() else { return false; }; let Some((memory_addr, byte_offset, byte_len)) = memory_target_for_indexed_object(gc, shapes, obj) else { return false; }; if index >= byte_len as u64 { return true; } let addr = runtime::MemoryAddr::from_raw(memory_addr); realm .store .memory_mut(addr) .store_u8(byte_offset as u64 + index, value.to_number() as u8) .is_ok() } fn memory_target_for_indexed_object( gc: &Gc, shapes: &ShapeTable, obj: GcRef, ) -> Option<(u32, u32, u32)> { let is_uint8_array = hidden_string(gc, shapes, obj, PROP_TYPED_ARRAY_KIND).as_deref() == Some("Uint8Array"); let is_memory_bytes_object = get_u32_property(gc, shapes, obj, "length").is_some() && hidden_u32(gc, shapes, obj, PROP_WASM_MEMORY_ADDR).is_some(); if !is_uint8_array && !is_memory_bytes_object { return None; } let memory_addr = hidden_u32(gc, shapes, obj, PROP_WASM_MEMORY_ADDR).or_else(|| { let buffer = match get_own_value(gc, shapes, obj, "buffer") { Value::Object(r) => r, _ => return None, }; hidden_u32(gc, shapes, buffer, PROP_WASM_MEMORY_ADDR) })?; let byte_offset = get_u32_property(gc, shapes, obj, "byteOffset").unwrap_or(0); let byte_len = get_u32_property(gc, shapes, obj, "byteLength") .or_else(|| get_u32_property(gc, shapes, obj, "length"))?; Some((memory_addr, byte_offset, byte_len)) } 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) = 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 uint8_array_subarray(args: &[Value], ctx: &mut NativeContext) -> Result { let target_ref = match ctx.this { Value::Object(r) => r, _ => { return Err(RuntimeError::type_error( "Uint8Array.subarray requires a Uint8Array", )) } }; if hidden_string(ctx.gc, ctx.shapes, target_ref, PROP_TYPED_ARRAY_KIND).as_deref() != Some("Uint8Array") { return Err(RuntimeError::type_error( "Uint8Array.subarray requires a Uint8Array", )); } let bytes = collect_memory_backed_view_bytes(ctx.wasm, ctx.gc, ctx.shapes, target_ref) .unwrap_or_else(|| collect_array_like_bytes(ctx.gc, ctx.shapes, target_ref)); let (start, end) = subarray_bounds(args, bytes.len()); Ok(make_uint8_array_from_bytes( ctx.wasm, ctx.gc, ctx.shapes, bytes[start..end].to_vec(), )) } fn subarray_bounds(args: &[Value], len: usize) -> (usize, usize) { let start = relative_index(args.first(), len, 0); let end = match args.get(1) { Some(Value::Undefined) | None => len, Some(value) => relative_index(Some(value), len, len), }; if end < start { (start, start) } else { (start, end) } } fn relative_index(value: Option<&Value>, len: usize, default: usize) -> usize { let Some(value) = value else { return default; }; let number = value.to_number(); if number.is_nan() { return 0; } let index = number.trunc() as isize; if index < 0 { len.saturating_sub(index.unsigned_abs()) } else { (index as usize).min(len) } } fn memory_addr_for_buffer_like(ctx: &NativeContext, obj: GcRef) -> Option { hidden_u32(ctx.gc, ctx.shapes, obj, PROP_WASM_MEMORY_ADDR) .or_else(|| current_memory_addr_for_buffer_like(ctx, obj)) .or_else(|| match get_own_value(ctx.gc, ctx.shapes, obj, "_bytes") { Value::Object(bytes) => hidden_u32(ctx.gc, ctx.shapes, bytes, PROP_WASM_MEMORY_ADDR), _ => None, }) } 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 { private_fields: std::collections::HashMap::new(), 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(); let subarray = make_native(gc, "subarray", uint8_array_subarray); 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, ); obj.insert_property( "subarray".to_string(), Property::builtin(Value::Function(subarray)), shapes, ); if memory_addr.is_none() { 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") { if let Some(bytes) = collect_memory_backed_view_bytes(ctx.wasm, ctx.gc, ctx.shapes, *obj) { return Ok(bytes); } return Ok(collect_array_like_bytes(ctx.gc, ctx.shapes, *obj)); } if let Some(bytes) = collect_memory_backed_bytes(ctx.wasm, ctx.gc, ctx.shapes, *obj) { return Ok(bytes); } 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() { if hidden_u32(gc, shapes, view_ref, PROP_WASM_MEMORY_ADDR).is_some() { continue; } let Some(memory_addr) = realm.last_memory_buffer else { 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 { 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, ); } } 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), getter: None, setter: None, 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_memory_backed_view_bytes( realm: &WasmRealm, gc: &Gc, shapes: &ShapeTable, obj: GcRef, ) -> Option> { let (memory_addr, byte_offset, byte_len) = memory_target_for_indexed_object(gc, shapes, obj)?; collect_memory_range_bytes(realm, memory_addr, byte_offset, byte_len) } fn collect_memory_backed_bytes( realm: &WasmRealm, gc: &Gc, shapes: &ShapeTable, obj: GcRef, ) -> Option> { if let Some(bytes) = collect_memory_backed_view_bytes(realm, gc, shapes, obj) { return Some(bytes); } let memory_addr = hidden_u32(gc, shapes, obj, PROP_WASM_MEMORY_ADDR)?; let byte_len = get_u32_property(gc, shapes, obj, "byteLength")?; collect_memory_range_bytes(realm, memory_addr, 0, byte_len) } fn collect_memory_range_bytes( realm: &WasmRealm, memory_addr: u32, byte_offset: u32, byte_len: u32, ) -> Option> { let start = byte_offset as usize; let end = start.checked_add(byte_len as usize)?; let addr = runtime::MemoryAddr::from_raw(memory_addr); let memory = realm.store.memory(addr).bytes(); if end > memory.len() { return None; } Some(memory[start..end].to_vec()) } 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(); get_own_value(gc, shapes, obj, &key) } 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 { match get_own_value(gc, shapes, obj, key) { 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) } fn eval_with_fetch_helpers(source: &str) -> (Vm, Result) { crate::fetch::reset_fetch_state(); let program = Parser::parse(source).expect("parse"); let func = compiler::compile(&program).expect("compile"); let mut vm = Vm::new(); crate::fetch::init_fetch_api(&mut vm); let result = vm.execute(&func); (vm, result) } fn add_wasm_response_setup() -> String { let mut src = bytes_setup("bytes", ADD_WASM); src.push_str( r#" var response = { headers: { get: function(name) { return "application/wasm"; } }, arrayBuffer: function() { return Promise.resolve(bytes); } }; "#, ); src } fn holder_prop(vm: &Vm, value: Value, key: &str) -> Value { match value { Value::Object(r) => get_own_value(&vm.gc, &vm.shapes, r, key), v => panic!("expected holder object, got {v:?}"), } } #[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 uint8_array_subarray_copies_requested_range() { let (vm, result) = eval_with_vm( "var a = new Uint8Array([4,5,6,7]); \ var b = a.subarray(1, 3); \ typeof a.subarray + ':' + b.length + ':' + b[0] + ',' + b[1]", ); assert_eq!(result.unwrap().to_js_string(&vm.gc), "function:2:5,6"); } #[test] fn uint8_array_subarray_clamps_negative_bounds() { let (vm, result) = eval_with_vm( "var a = new Uint8Array([1,2,3,4,5]); \ var b = a.subarray(-4, -1); \ b.length + ':' + b[0] + ',' + b[1] + ',' + b[2]", ); assert_eq!(result.unwrap().to_js_string(&vm.gc), "3:2,3,4"); } /// `Object.{keys,values,entries}` on a TypedArray must enumerate the /// integer element indices and their byte values, not the internal slots /// (`buffer`, `byteLength`, `__ta_kind__`, …). Regression test for the /// `Object.values(u)` path returning `BYTES_PER_ELEMENT` (isu issue 300). fn eval_to_string(src: &str) -> String { let (vm, result) = eval_with_vm(src); result.unwrap().to_js_string(&vm.gc) } #[test] fn object_keys_values_entries_on_typed_array() { let setup = bytes_setup("u", &[0, 97, 115, 109]); assert_eq!( eval_to_string(&format!("{setup} JSON.stringify(Object.keys(u));")), r#"["0","1","2","3"]"# ); assert_eq!( eval_to_string(&format!("{setup} JSON.stringify(Object.values(u));")), "[0,97,115,109]" ); assert_eq!( eval_to_string(&format!("{setup} JSON.stringify(Object.entries(u));")), r#"[["0",0],["1",97],["2",115],["3",109]]"# ); } #[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_buffer_large_index_writes_keep_aliasing_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); for (var j = 0; j < 4096; j = j + 1) {{ view[j] = j; }} view[4095];", bytes_setup("bytes", MEMORY_WASM) ); let (_, result) = eval_with_vm(&src); match result.unwrap() { Value::Number(n) => assert_eq!(n, 255.0), v => panic!("expected 255, got {v:?}"), } } #[test] fn memory_buffer_out_of_range_index_does_not_create_property() { 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[65536] = 7; view[65536];", bytes_setup("bytes", MEMORY_WASM) ); let (_, result) = eval_with_vm(&src); match result.unwrap() { Value::Undefined => {} v => panic!("expected undefined, got {v:?}"), } } #[test] fn memory_buffer_array_buffer_is_not_indexed() { let src = format!( "{} var m = new WebAssembly.Module(bytes); var i = new WebAssembly.Instance(m); var buf = i.exports.memory.buffer; buf[0] = 7; buf[0];", bytes_setup("bytes", MEMORY_WASM) ); let (_, result) = eval_with_vm(&src); match result.unwrap() { Value::Number(n) => assert_eq!(n, 7.0), v => panic!("expected ordinary buffer property, 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:?}"), } } #[test] fn compile_streaming_resolves_response_array_buffer() { let src = format!( r#" var holder = {{ out: 0 }}; {} WebAssembly.compileStreaming(response) .then(function(module) {{ return WebAssembly.instantiate(module); }}) .then(function(instance) {{ holder.out = instance.exports.add(8, 9); }}, function(err) {{ holder.out = err.name; }}); holder; "#, add_wasm_response_setup() ); let (vm, result) = eval_with_vm(&src); match holder_prop(&vm, result.unwrap(), "out") { Value::Number(n) => assert_eq!(n, 17.0), v => panic!("expected 17, got {v:?}"), } } #[test] fn instantiate_streaming_resolves_module_and_instance() { let src = format!( r#" var holder = {{ out: 0, hasModule: false }}; {} WebAssembly.instantiateStreaming(response) .then(function(result) {{ holder.out = result.instance.exports.add(10, 4); holder.hasModule = typeof result.module === "object"; }}); holder; "#, add_wasm_response_setup() ); let (vm, result) = eval_with_vm(&src); let holder = result.unwrap(); match holder_prop(&vm, holder.clone(), "out") { Value::Number(n) => assert_eq!(n, 14.0), v => panic!("expected 14, got {v:?}"), } match holder_prop(&vm, holder, "hasModule") { Value::Boolean(b) => assert!(b), v => panic!("expected hasModule=true, got {v:?}"), } } #[test] fn compile_streaming_rejects_mismatched_content_type_before_body_read() { let src = r#" var holder = { name: "", called: 0 }; var response = { headers: { get: function(name) { return "text/plain"; } }, arrayBuffer: function() { holder.called = 1; return Promise.resolve(new Uint8Array([0])); } }; WebAssembly.compileStreaming(response).then( function(_) { holder.name = "resolved"; }, function(err) { holder.name = err.name; } ); holder; "#; let (vm, result) = eval_with_vm(src); let holder = result.unwrap(); match holder_prop(&vm, holder.clone(), "name") { Value::String(s) => assert_eq!(s, "WebAssembly.CompileError"), v => panic!("expected CompileError, got {v:?}"), } match holder_prop(&vm, holder, "called") { Value::Number(n) => assert_eq!(n, 0.0), v => panic!("expected arrayBuffer not to be called, got {v:?}"), } } #[test] fn compile_streaming_maps_body_stream_error_to_compile_error() { let src = r#" var holder = { name: "" }; var response = { headers: { get: function(name) { return "application/wasm"; } }, bodyUsed: false, body: new ReadableStream({ start: function(controller) { controller.enqueue(new Uint8Array([0, 97, 115, 109])); controller.error("network broke"); } }), arrayBuffer: function() { return __we_fetch_consume_body_array_buffer(this); } }; WebAssembly.compileStreaming(response).then( function(_) { holder.name = "resolved"; }, function(err) { holder.name = err.name; } ); holder; "#; let (vm, result) = eval_with_fetch_helpers(src); match holder_prop(&vm, result.unwrap(), "name") { Value::String(s) => assert_eq!(s, "WebAssembly.CompileError"), v => panic!("expected CompileError, got {v:?}"), } } #[test] fn compile_streaming_maps_abort_signal_cancellation_to_compile_error() { let src = r#" var holder = { name: "" }; var ac = new AbortController(); var response = { headers: { get: function(name) { return "application/wasm"; } }, bodyUsed: false, body: new ReadableStream({ start: function(controller) { ac.signal.addEventListener("abort", function() { controller.error(ac.signal.reason); }); } }), arrayBuffer: function() { return __we_fetch_consume_body_array_buffer(this); } }; WebAssembly.compileStreaming(response).then( function(_) { holder.name = "resolved"; }, function(err) { holder.name = err.name; } ); ac.abort("cancelled"); holder; "#; let (vm, result) = eval_with_fetch_helpers(src); match holder_prop(&vm, result.unwrap(), "name") { Value::String(s) => assert_eq!(s, "WebAssembly.CompileError"), v => panic!("expected CompileError, got {v:?}"), } } }