1use std::cell::RefCell;
13use std::collections::HashMap;
14use std::rc::Rc;
15
16use rquickjs::function::Func;
17use rquickjs::{ArrayBuffer, Ctx, Value};
18use serde::Serialize;
19use tracing::debug;
20use wasmtime::{
21 Caller, Engine, ExternType, Instance as WasmInstance, Linker, Module as WasmModule, Store, Val,
22 ValType,
23};
24
25struct WasmHostData {
31 max_memory_pages: u64,
33}
34
35struct InstanceState {
37 store: Store<WasmHostData>,
38 instance: WasmInstance,
39}
40
41#[derive(Serialize)]
42struct WasmExportEntry {
43 name: String,
44 kind: &'static str,
45}
46
47pub(crate) struct WasmBridgeState {
49 engine: Engine,
50 modules: HashMap<u32, WasmModule>,
51 instances: HashMap<u32, InstanceState>,
52 next_id: u32,
53 max_modules: usize,
54 max_instances: usize,
55}
56
57impl WasmBridgeState {
58 pub fn new() -> Self {
59 let engine = Engine::default();
60 Self {
61 engine,
62 modules: HashMap::new(),
63 instances: HashMap::new(),
64 next_id: 1,
65 max_modules: DEFAULT_MAX_MODULES,
66 max_instances: DEFAULT_MAX_INSTANCES,
67 }
68 }
69
70 fn alloc_id(&mut self) -> Result<u32, String> {
71 let start = match self.next_id {
72 0 => 1,
73 id if id > MAX_JS_WASM_ID => 1,
74 id => id,
75 };
76 let mut candidate = start;
77
78 loop {
79 if !self.modules.contains_key(&candidate) && !self.instances.contains_key(&candidate) {
80 self.next_id = candidate.wrapping_add(1);
81 if self.next_id == 0 || self.next_id > MAX_JS_WASM_ID {
82 self.next_id = 1;
83 }
84 return Ok(candidate);
85 }
86
87 candidate = candidate.wrapping_add(1);
88 if candidate == 0 || candidate > MAX_JS_WASM_ID {
89 candidate = 1;
90 }
91 if candidate == start {
92 return Err("WASM instance/module id space exhausted".to_string());
93 }
94 }
95 }
96
97 #[cfg(test)]
98 fn set_limits_for_test(&mut self, max_modules: usize, max_instances: usize) {
99 self.max_modules = max_modules.max(1);
100 self.max_instances = max_instances.max(1);
101 }
102}
103
104fn throw_wasm(ctx: &Ctx<'_>, class: &str, msg: &str) -> rquickjs::Error {
109 let text = format!("{class}: {msg}");
110 if let Ok(js_text) = rquickjs::String::from_str(ctx.clone(), &text) {
111 let _ = ctx.throw(js_text.into_value());
112 }
113 rquickjs::Error::Exception
114}
115
116fn extract_bytes(ctx: &Ctx<'_>, value: &Value<'_>) -> rquickjs::Result<Vec<u8>> {
121 if let Some(obj) = value.as_object() {
123 if let Some(ab) = obj.as_array_buffer() {
124 return ab
125 .as_bytes()
126 .map(<[u8]>::to_vec)
127 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Detached ArrayBuffer"));
128 }
129 }
130 if let Some(arr) = value.as_array() {
132 let mut bytes = Vec::with_capacity(arr.len());
133 for i in 0..arr.len() {
134 let v: i32 = arr.get(i)?;
135 bytes.push(
136 u8::try_from(v)
137 .map_err(|_| throw_wasm(ctx, "TypeError", "Byte value out of range"))?,
138 );
139 }
140 return Ok(bytes);
141 }
142 Err(throw_wasm(
143 ctx,
144 "TypeError",
145 "Expected ArrayBuffer or byte array",
146 ))
147}
148
149#[allow(clippy::cast_precision_loss)]
152fn val_to_f64(ctx: &Ctx<'_>, val: &Val) -> rquickjs::Result<f64> {
153 match val {
154 Val::I32(v) => Ok(f64::from(*v)),
155 Val::F32(bits) => Ok(f64::from(f32::from_bits(*bits))),
156 Val::F64(bits) => Ok(f64::from_bits(*bits)),
157 _ => Err(throw_wasm(
158 ctx,
159 "RuntimeError",
160 "Unsupported WASM return value type for PiJS bridge",
161 )),
162 }
163}
164
165#[allow(clippy::cast_possible_truncation)]
167fn js_to_i32(value: f64) -> i32 {
168 if !value.is_finite() || value == 0.0 {
169 return 0;
170 }
171
172 let mut wrapped = value.trunc() % TWO_POW_32;
173 if wrapped < 0.0 {
174 wrapped += TWO_POW_32;
175 }
176
177 if wrapped >= TWO_POW_31 {
178 (wrapped - TWO_POW_32) as i32
179 } else {
180 wrapped as i32
181 }
182}
183
184#[allow(clippy::cast_possible_truncation)]
185fn js_to_val(ctx: &Ctx<'_>, value: &Value<'_>, ty: &ValType) -> rquickjs::Result<Val> {
186 match ty {
187 ValType::I32 => {
188 let v: f64 = value
189 .as_number()
190 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for i32"))?;
191 Ok(Val::I32(js_to_i32(v)))
192 }
193 ValType::I64 => Err(throw_wasm(
194 ctx,
195 "TypeError",
196 "i64 parameters are not supported by PiJS WebAssembly bridge",
197 )),
198 ValType::F32 => {
199 let v: f64 = value
200 .as_number()
201 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f32"))?;
202 #[expect(clippy::cast_possible_truncation)]
203 Ok(Val::F32((v as f32).to_bits()))
204 }
205 ValType::F64 => {
206 let v: f64 = value
207 .as_number()
208 .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f64"))?;
209 Ok(Val::F64(v.to_bits()))
210 }
211 _ => Err(throw_wasm(ctx, "TypeError", "Unsupported WASM value type")),
212 }
213}
214
215fn validate_call_result_types(ctx: &Ctx<'_>, result_types: &[ValType]) -> rquickjs::Result<()> {
216 if result_types.len() > 1 {
217 return Err(throw_wasm(
218 ctx,
219 "RuntimeError",
220 "Multi-value WASM results are not supported by PiJS WebAssembly bridge",
221 ));
222 }
223
224 if let Some(ty) = result_types.first() {
225 return match ty {
226 ValType::I32 | ValType::F32 | ValType::F64 => Ok(()),
227 ValType::I64 => Err(throw_wasm(
228 ctx,
229 "RuntimeError",
230 "i64 results are not supported by PiJS WebAssembly bridge",
231 )),
232 _ => Err(throw_wasm(
233 ctx,
234 "RuntimeError",
235 "Unsupported WASM return type for PiJS WebAssembly bridge",
236 )),
237 };
238 }
239
240 Ok(())
241}
242
243fn register_stub_imports(
250 linker: &mut Linker<WasmHostData>,
251 module: &WasmModule,
252) -> Result<(), String> {
253 for import in module.imports() {
254 let mod_name = import.module();
255 let imp_name = import.name();
256 if let ExternType::Func(func_ty) = import.ty() {
257 let result_types: Vec<ValType> = func_ty.results().collect();
258 linker
259 .func_new(
260 mod_name,
261 imp_name,
262 func_ty.clone(),
263 move |_caller: Caller<'_, WasmHostData>,
264 _params: &[Val],
265 results: &mut [Val]| {
266 for (i, ty) in result_types.iter().enumerate() {
267 results[i] = Val::default_for_ty(ty).unwrap_or(Val::I32(0));
268 }
269 Ok(())
270 },
271 )
272 .map_err(|e| format!("Failed to stub import {mod_name}.{imp_name}: {e}"))?;
273 } else {
274 }
276 }
277 Ok(())
278}
279
280const DEFAULT_MAX_MEMORY_PAGES: u64 = 1024;
286const DEFAULT_MAX_MODULES: usize = 256;
288const DEFAULT_MAX_INSTANCES: usize = 256;
290const MAX_JS_WASM_ID: u32 = i32::MAX as u32;
292const TWO_POW_32: f64 = 4_294_967_296.0;
294const TWO_POW_31: f64 = 2_147_483_648.0;
295
296#[allow(clippy::too_many_lines)]
298pub(crate) fn inject_wasm_globals(
299 ctx: &Ctx<'_>,
300 state: &Rc<RefCell<WasmBridgeState>>,
301) -> rquickjs::Result<()> {
302 let global = ctx.globals();
303
304 {
306 let st = Rc::clone(state);
307 global.set(
308 "__pi_wasm_compile_native",
309 Func::from(
310 move |ctx: Ctx<'_>, bytes_val: Value<'_>| -> rquickjs::Result<u32> {
311 let bytes = extract_bytes(&ctx, &bytes_val)?;
312 let mut bridge = st.borrow_mut();
313 if bridge.modules.len() >= bridge.max_modules {
314 return Err(throw_wasm(
315 &ctx,
316 "CompileError",
317 &format!("Module limit reached ({})", bridge.max_modules),
318 ));
319 }
320 let module = WasmModule::from_binary(&bridge.engine, &bytes)
321 .map_err(|e| throw_wasm(&ctx, "CompileError", &e.to_string()))?;
322 let id = bridge
323 .alloc_id()
324 .map_err(|e| throw_wasm(&ctx, "CompileError", &e))?;
325 debug!(module_id = id, bytes_len = bytes.len(), "wasm: compiled");
326 bridge.modules.insert(id, module);
327 Ok(id)
328 },
329 ),
330 )?;
331 }
332
333 {
335 let st = Rc::clone(state);
336 global.set(
337 "__pi_wasm_instantiate_native",
338 Func::from(
339 move |ctx: Ctx<'_>, module_id: u32| -> rquickjs::Result<u32> {
340 let mut bridge = st.borrow_mut();
341 if bridge.instances.len() >= bridge.max_instances {
342 return Err(throw_wasm(
343 &ctx,
344 "RuntimeError",
345 &format!("Instance limit reached ({})", bridge.max_instances),
346 ));
347 }
348 let module = bridge
349 .modules
350 .get(&module_id)
351 .ok_or_else(|| throw_wasm(&ctx, "LinkError", "Module not found"))?
352 .clone();
353
354 let mut linker = Linker::new(&bridge.engine);
355 register_stub_imports(&mut linker, &module)
356 .map_err(|e| throw_wasm(&ctx, "LinkError", &e))?;
357
358 let mut store = Store::new(
359 &bridge.engine,
360 WasmHostData {
361 max_memory_pages: DEFAULT_MAX_MEMORY_PAGES,
362 },
363 );
364 let instance = linker
365 .instantiate(&mut store, &module)
366 .map_err(|e| throw_wasm(&ctx, "LinkError", &e.to_string()))?;
367
368 let id = bridge
369 .alloc_id()
370 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e))?;
371 debug!(instance_id = id, module_id, "wasm: instantiated");
372 bridge
373 .instances
374 .insert(id, InstanceState { store, instance });
375 Ok(id)
376 },
377 ),
378 )?;
379 }
380
381 {
383 let st = Rc::clone(state);
384 global.set(
385 "__pi_wasm_get_exports_native",
386 Func::from(
387 move |ctx: Ctx<'_>, instance_id: u32| -> rquickjs::Result<String> {
388 let mut bridge = st.borrow_mut();
389 let inst = bridge
390 .instances
391 .get_mut(&instance_id)
392 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
393
394 let mut entries: Vec<WasmExportEntry> = Vec::new();
395 for export in inst.instance.exports(&mut inst.store) {
396 let name = export.name().to_string();
397 let kind = match export.into_extern() {
398 wasmtime::Extern::Func(_) => "func",
399 wasmtime::Extern::Memory(_) => "memory",
400 wasmtime::Extern::Table(_) => "table",
401 wasmtime::Extern::Global(_) => "global",
402 wasmtime::Extern::SharedMemory(_) => "shared-memory",
403 wasmtime::Extern::Tag(_) => "tag",
404 };
405 entries.push(WasmExportEntry { name, kind });
406 }
407 serde_json::to_string(&entries)
408 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))
409 },
410 ),
411 )?;
412 }
413
414 {
416 let st = Rc::clone(state);
417 global.set(
418 "__pi_wasm_call_export_native",
419 Func::from(
420 move |ctx: Ctx<'_>,
421 instance_id: u32,
422 name: String,
423 args_val: Value<'_>|
424 -> rquickjs::Result<f64> {
425 let mut bridge = st.borrow_mut();
426 let inst = bridge
427 .instances
428 .get_mut(&instance_id)
429 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
430
431 let func = inst
432 .instance
433 .get_func(&mut inst.store, &name)
434 .ok_or_else(|| {
435 throw_wasm(&ctx, "RuntimeError", &format!("Export '{name}' not found"))
436 })?;
437
438 let func_ty = func.ty(&inst.store);
439 let param_types: Vec<ValType> = func_ty.params().collect();
440 if param_types.iter().any(|ty| matches!(ty, ValType::I64)) {
441 return Err(throw_wasm(
442 &ctx,
443 "TypeError",
444 "i64 parameters are not supported by PiJS WebAssembly bridge",
445 ));
446 }
447
448 let args_arr = args_val
450 .as_array()
451 .ok_or_else(|| throw_wasm(&ctx, "TypeError", "args must be an array"))?;
452 let mut params = Vec::with_capacity(param_types.len());
453 for (i, ty) in param_types.iter().enumerate() {
454 let js_val: Value<'_> = args_arr.get(i)?;
455 params.push(js_to_val(&ctx, &js_val, ty)?);
456 }
457
458 let result_types: Vec<ValType> = func_ty.results().collect();
460 validate_call_result_types(&ctx, &result_types)?;
461 let mut results: Vec<Val> = result_types
462 .iter()
463 .map(|ty| Val::default_for_ty(ty).unwrap_or(Val::I32(0)))
464 .collect();
465
466 func.call(&mut inst.store, ¶ms, &mut results)
467 .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
468
469 results.first().map_or(Ok(0.0), |val| val_to_f64(&ctx, val))
471 },
472 ),
473 )?;
474 }
475
476 {
478 let st = Rc::clone(state);
479 global.set(
480 "__pi_wasm_get_buffer_native",
481 Func::from(
482 move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<i32> {
483 let mut bridge = st.borrow_mut();
484 let inst = bridge
485 .instances
486 .get_mut(&instance_id)
487 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
488 let memory = inst
489 .instance
490 .get_memory(&mut inst.store, &mem_name)
491 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
492 let data = memory.data(&inst.store);
493 let len = i32::try_from(data.len()).unwrap_or(i32::MAX);
494 let buffer = ArrayBuffer::new_copy(ctx.clone(), data)?;
495 ctx.globals().set("__pi_wasm_tmp_buf", buffer)?;
496 Ok(len)
497 },
498 ),
499 )?;
500 }
501
502 {
504 let st = Rc::clone(state);
505 global.set(
506 "__pi_wasm_memory_grow_native",
507 Func::from(
508 move |ctx: Ctx<'_>,
509 instance_id: u32,
510 mem_name: String,
511 delta: u32|
512 -> rquickjs::Result<i32> {
513 let mut bridge = st.borrow_mut();
514 let inst = bridge
515 .instances
516 .get_mut(&instance_id)
517 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
518
519 let memory = inst
521 .instance
522 .get_memory(&mut inst.store, &mem_name)
523 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
524 let current = memory.size(&inst.store);
525 let requested = current.saturating_add(u64::from(delta));
526 if requested > inst.store.data().max_memory_pages {
527 return Ok(-1); }
529
530 Ok(memory
531 .grow(&mut inst.store, u64::from(delta))
532 .map_or(-1, |prev| i32::try_from(prev).unwrap_or(-1)))
533 },
534 ),
535 )?;
536 }
537
538 {
540 let st = Rc::clone(state);
541 global.set(
542 "__pi_wasm_memory_size_native",
543 Func::from(
544 move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<u32> {
545 let mut bridge = st.borrow_mut();
546 let inst = bridge
547 .instances
548 .get_mut(&instance_id)
549 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
550 let memory = inst
551 .instance
552 .get_memory(&mut inst.store, &mem_name)
553 .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
554 Ok(u32::try_from(memory.size(&inst.store)).unwrap_or(u32::MAX))
555 },
556 ),
557 )?;
558 }
559
560 ctx.eval::<(), _>(WASM_POLYFILL_JS)?;
562
563 debug!("wasm: globalThis.WebAssembly polyfill injected");
564 Ok(())
565}
566
567const WASM_POLYFILL_JS: &str = r#"
572(function() {
573 "use strict";
574
575 class CompileError extends Error {
576 constructor(msg) { super(msg); this.name = "CompileError"; }
577 }
578 class LinkError extends Error {
579 constructor(msg) { super(msg); this.name = "LinkError"; }
580 }
581 class RuntimeError extends Error {
582 constructor(msg) { super(msg); this.name = "RuntimeError"; }
583 }
584
585 // Synchronous thenable: behaves like syncResolve() but executes
586 // .then() callbacks immediately. QuickJS doesn't auto-flush microtasks.
587 function syncResolve(value) {
588 return {
589 then: function(resolve, _reject) {
590 try {
591 var r = resolve(value);
592 return syncResolve(r);
593 } catch(e) { return syncReject(e); }
594 },
595 "catch": function() { return syncResolve(value); }
596 };
597 }
598 function syncReject(err) {
599 return {
600 then: function(_resolve, reject) {
601 if (reject) { reject(err); return syncResolve(undefined); }
602 return syncReject(err);
603 },
604 "catch": function(fn) { fn(err); return syncResolve(undefined); }
605 };
606 }
607
608 function normalizeBytes(source) {
609 if (source instanceof ArrayBuffer) {
610 return new Uint8Array(source);
611 }
612 if (ArrayBuffer.isView && ArrayBuffer.isView(source)) {
613 return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
614 }
615 if (Array.isArray(source)) {
616 return new Uint8Array(source);
617 }
618 throw new CompileError("Invalid source: expected ArrayBuffer, TypedArray, or byte array");
619 }
620
621 function buildExports(instanceId) {
622 var info = JSON.parse(__pi_wasm_get_exports_native(instanceId));
623 var exports = {};
624 for (var i = 0; i < info.length; i++) {
625 var exp = info[i];
626 if (exp.kind === "func") {
627 (function(name) {
628 exports[name] = function() {
629 var args = [];
630 for (var j = 0; j < arguments.length; j++) args.push(arguments[j]);
631 return __pi_wasm_call_export_native(instanceId, name, args);
632 };
633 })(exp.name);
634 } else if (exp.kind === "memory") {
635 (function(name) {
636 var memObj = {};
637 Object.defineProperty(memObj, "buffer", {
638 get: function() {
639 __pi_wasm_get_buffer_native(instanceId, name);
640 return globalThis.__pi_wasm_tmp_buf;
641 },
642 configurable: true
643 });
644 memObj.grow = function(delta) {
645 var prevPages = __pi_wasm_memory_grow_native(instanceId, name, delta);
646 if (prevPages < 0) {
647 throw new RangeError("WebAssembly.Memory.grow(): failed to grow memory");
648 }
649 return prevPages;
650 };
651 exports[name] = memObj;
652 })(exp.name);
653 }
654 }
655 return exports;
656 }
657
658 globalThis.WebAssembly = {
659 CompileError: CompileError,
660 LinkError: LinkError,
661 RuntimeError: RuntimeError,
662
663 compile: function(source) {
664 try {
665 var bytes = normalizeBytes(source);
666 var arr = [];
667 for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
668 var moduleId = __pi_wasm_compile_native(arr);
669 var wasmMod = { __wasm_module_id: moduleId };
670 return syncResolve(wasmMod);
671 } catch (e) {
672 return syncReject(e);
673 }
674 },
675
676 instantiate: function(source, _imports) {
677 try {
678 var moduleId;
679 if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
680 moduleId = source.__wasm_module_id;
681 } else {
682 var bytes = normalizeBytes(source);
683 var arr = [];
684 for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
685 moduleId = __pi_wasm_compile_native(arr);
686 }
687 var instanceId = __pi_wasm_instantiate_native(moduleId);
688 var exports = buildExports(instanceId);
689 var instance = { exports: exports };
690 var wasmMod = { __wasm_module_id: moduleId };
691
692 if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
693 return syncResolve(instance);
694 }
695 return syncResolve({ module: wasmMod, instance: instance });
696 } catch (e) {
697 return syncReject(e);
698 }
699 },
700
701 validate: function(_bytes) {
702 throw new Error("WebAssembly.validate not yet supported in PiJS");
703 },
704
705 instantiateStreaming: function() {
706 throw new Error("WebAssembly.instantiateStreaming not supported in PiJS");
707 },
708
709 compileStreaming: function() {
710 throw new Error("WebAssembly.compileStreaming not supported in PiJS");
711 },
712
713 Memory: function(descriptor) {
714 if (!(this instanceof WebAssembly.Memory)) {
715 throw new TypeError("WebAssembly.Memory must be called with new");
716 }
717 var initial = descriptor && descriptor.initial ? descriptor.initial : 0;
718 this._pages = initial;
719 this._buffer = new ArrayBuffer(initial * 65536);
720 Object.defineProperty(this, "buffer", {
721 get: function() { return this._buffer; },
722 configurable: true
723 });
724 this.grow = function(delta) {
725 var old = this._pages;
726 this._pages += delta;
727 this._buffer = new ArrayBuffer(this._pages * 65536);
728 return old;
729 };
730 },
731
732 Table: function() {
733 throw new Error("WebAssembly.Table not yet supported in PiJS");
734 },
735
736 Global: function() {
737 throw new Error("WebAssembly.Global not yet supported in PiJS");
738 }
739 };
740})();
741"#;
742
743#[cfg(test)]
748mod tests {
749 use super::*;
750
751 fn run_wasm_test(f: impl FnOnce(&Ctx<'_>, Rc<RefCell<WasmBridgeState>>)) {
753 let rt = rquickjs::Runtime::new().expect("create runtime");
754 let ctx = rquickjs::Context::full(&rt).expect("create context");
755 ctx.with(|ctx| {
756 let state = Rc::new(RefCell::new(WasmBridgeState::new()));
757 inject_wasm_globals(&ctx, &state).expect("inject globals");
758 f(&ctx, state);
759 });
760 }
761
762 fn wat_to_wasm(wat: &str) -> Vec<u8> {
764 wat::parse_str(wat).expect("parse WAT to WASM binary")
765 }
766
767 #[test]
768 fn js_to_i32_matches_javascript_wrapping_semantics() {
769 assert_eq!(js_to_i32(2_147_483_648.0), -2_147_483_648);
770 assert_eq!(js_to_i32(4_294_967_296.0), 0);
771 assert_eq!(js_to_i32(-2_147_483_649.0), 2_147_483_647);
772 assert_eq!(js_to_i32(-1.9), -1);
773 assert_eq!(js_to_i32(1.9), 1);
774 assert_eq!(js_to_i32(f64::NAN), 0);
775 assert_eq!(js_to_i32(f64::INFINITY), 0);
776 assert_eq!(js_to_i32(f64::NEG_INFINITY), 0);
777 }
778
779 #[test]
780 fn compile_and_instantiate_trivial_module() {
781 let wasm_bytes = wat_to_wasm(
782 r#"(module
783 (func (export "add") (param i32 i32) (result i32)
784 local.get 0 local.get 1 i32.add)
785 (memory (export "memory") 1)
786 )"#,
787 );
788 run_wasm_test(|ctx, _state| {
789 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
791 for (i, &b) in wasm_bytes.iter().enumerate() {
792 arr.set(i, i32::from(b)).unwrap();
793 }
794 ctx.globals().set("__test_bytes", arr).unwrap();
795
796 let module_id: u32 = ctx
798 .eval("__pi_wasm_compile_native(__test_bytes)")
799 .expect("compile");
800 assert!(module_id > 0);
801
802 let instance_id: u32 = ctx
804 .eval(format!("__pi_wasm_instantiate_native({module_id})"))
805 .expect("instantiate");
806 assert!(instance_id > 0);
807 });
808 }
809
810 #[test]
811 fn call_export_add() {
812 let wasm_bytes = wat_to_wasm(
813 r#"(module
814 (func (export "add") (param i32 i32) (result i32)
815 local.get 0 local.get 1 i32.add)
816 )"#,
817 );
818 run_wasm_test(|ctx, _state| {
819 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
820 for (i, &b) in wasm_bytes.iter().enumerate() {
821 arr.set(i, i32::from(b)).unwrap();
822 }
823 ctx.globals().set("__test_bytes", arr).unwrap();
824
825 let result: i32 = ctx
826 .eval(
827 r#"
828 var mid = __pi_wasm_compile_native(__test_bytes);
829 var iid = __pi_wasm_instantiate_native(mid);
830 __pi_wasm_call_export_native(iid, "add", [3, 4]);
831 "#,
832 )
833 .expect("call add");
834 assert_eq!(result, 7);
835 });
836 }
837
838 #[test]
839 fn call_export_multiply() {
840 let wasm_bytes = wat_to_wasm(
841 r#"(module
842 (func (export "mul") (param i32 i32) (result i32)
843 local.get 0 local.get 1 i32.mul)
844 )"#,
845 );
846 run_wasm_test(|ctx, _state| {
847 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
848 for (i, &b) in wasm_bytes.iter().enumerate() {
849 arr.set(i, i32::from(b)).unwrap();
850 }
851 ctx.globals().set("__test_bytes", arr).unwrap();
852
853 let result: i32 = ctx
854 .eval(
855 r#"
856 var mid = __pi_wasm_compile_native(__test_bytes);
857 var iid = __pi_wasm_instantiate_native(mid);
858 __pi_wasm_call_export_native(iid, "mul", [6, 7]);
859 "#,
860 )
861 .expect("call mul");
862 assert_eq!(result, 42);
863 });
864 }
865
866 #[test]
867 fn get_exports_lists_func_and_memory() {
868 let wasm_bytes = wat_to_wasm(
869 r#"(module
870 (func (export "f1") (result i32) i32.const 1)
871 (func (export "f2") (param i32) (result i32) local.get 0)
872 (memory (export "mem") 2)
873 )"#,
874 );
875 run_wasm_test(|ctx, _state| {
876 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
877 for (i, &b) in wasm_bytes.iter().enumerate() {
878 arr.set(i, i32::from(b)).unwrap();
879 }
880 ctx.globals().set("__test_bytes", arr).unwrap();
881
882 let count: i32 = ctx
883 .eval(
884 r"
885 var mid = __pi_wasm_compile_native(__test_bytes);
886 var iid = __pi_wasm_instantiate_native(mid);
887 var exps = JSON.parse(__pi_wasm_get_exports_native(iid));
888 exps.length;
889 ",
890 )
891 .expect("get exports count");
892 assert_eq!(count, 3);
893 });
894 }
895
896 #[test]
897 fn get_exports_json_handles_escaped_names() {
898 let wasm_bytes = wat_to_wasm(
899 r#"(module
900 (func (export "name\"with_quote") (result i32) i32.const 1)
901 )"#,
902 );
903 run_wasm_test(|ctx, _state| {
904 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
905 for (i, &b) in wasm_bytes.iter().enumerate() {
906 arr.set(i, i32::from(b)).unwrap();
907 }
908 ctx.globals().set("__test_bytes", arr).unwrap();
909
910 let name: String = ctx
911 .eval(
912 r"
913 var mid = __pi_wasm_compile_native(__test_bytes);
914 var iid = __pi_wasm_instantiate_native(mid);
915 JSON.parse(__pi_wasm_get_exports_native(iid))[0].name;
916 ",
917 )
918 .expect("parse export JSON");
919 assert_eq!(name, "name\"with_quote");
920 });
921 }
922
923 #[test]
924 fn memory_buffer_returns_arraybuffer() {
925 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
926 run_wasm_test(|ctx, _state| {
927 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
928 for (i, &b) in wasm_bytes.iter().enumerate() {
929 arr.set(i, i32::from(b)).unwrap();
930 }
931 ctx.globals().set("__test_bytes", arr).unwrap();
932
933 let size: i32 = ctx
934 .eval(
935 r#"
936 var mid = __pi_wasm_compile_native(__test_bytes);
937 var iid = __pi_wasm_instantiate_native(mid);
938 var len = __pi_wasm_get_buffer_native(iid, "memory");
939 len;
940 "#,
941 )
942 .expect("get buffer size");
943 assert_eq!(size, 65536);
945
946 let buf_size: i32 = ctx
948 .eval("__pi_wasm_tmp_buf.byteLength")
949 .expect("tmp buffer size");
950 assert_eq!(buf_size, 65536);
951 });
952 }
953
954 #[test]
955 fn memory_grow_succeeds() {
956 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
957 run_wasm_test(|ctx, _state| {
958 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
959 for (i, &b) in wasm_bytes.iter().enumerate() {
960 arr.set(i, i32::from(b)).unwrap();
961 }
962 ctx.globals().set("__test_bytes", arr).unwrap();
963
964 let prev: i32 = ctx
965 .eval(
966 r#"
967 var mid = __pi_wasm_compile_native(__test_bytes);
968 var iid = __pi_wasm_instantiate_native(mid);
969 __pi_wasm_memory_grow_native(iid, "memory", 2);
970 "#,
971 )
972 .expect("grow memory");
973 assert_eq!(prev, 1);
975
976 let new_size: i32 = ctx
977 .eval(r#"__pi_wasm_memory_size_native(iid, "memory")"#)
978 .expect("memory size");
979 assert_eq!(new_size, 3);
980 });
981 }
982
983 #[test]
984 fn memory_grow_denied_by_policy() {
985 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
986 run_wasm_test(|ctx, state| {
987 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
988 for (i, &b) in wasm_bytes.iter().enumerate() {
989 arr.set(i, i32::from(b)).unwrap();
990 }
991 ctx.globals().set("__test_bytes", arr).unwrap();
992
993 let instance_id: u32 = ctx
994 .eval(
995 r"
996 var mid = __pi_wasm_compile_native(__test_bytes);
997 __pi_wasm_instantiate_native(mid);
998 ",
999 )
1000 .expect("instantiate");
1001
1002 {
1004 let mut bridge = state.borrow_mut();
1005 let inst = bridge.instances.get_mut(&instance_id).unwrap();
1006 inst.store.data_mut().max_memory_pages = 2;
1007 }
1008
1009 let result: i32 = ctx
1011 .eval(format!(
1012 "__pi_wasm_memory_grow_native({instance_id}, 'memory', 5)"
1013 ))
1014 .expect("grow denied");
1015 assert_eq!(result, -1);
1016 });
1017 }
1018
1019 #[test]
1020 fn compile_invalid_bytes_fails() {
1021 run_wasm_test(|ctx, _state| {
1022 let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native([0, 1, 2, 3])");
1023 assert!(result.is_err());
1024 });
1025 }
1026
1027 #[test]
1028 fn instantiate_nonexistent_module_fails() {
1029 run_wasm_test(|ctx, _state| {
1030 let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_instantiate_native(99999)");
1031 assert!(result.is_err());
1032 });
1033 }
1034
1035 #[test]
1036 fn compile_rejects_when_module_limit_reached() {
1037 let wasm_bytes = wat_to_wasm(r"(module)");
1038 run_wasm_test(|ctx, state| {
1039 state.borrow_mut().set_limits_for_test(1, 8);
1040
1041 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1042 for (i, &b) in wasm_bytes.iter().enumerate() {
1043 arr.set(i, i32::from(b)).unwrap();
1044 }
1045 ctx.globals().set("__test_bytes", arr).unwrap();
1046
1047 let first: u32 = ctx
1048 .eval("__pi_wasm_compile_native(__test_bytes)")
1049 .expect("first compile");
1050 assert!(first > 0);
1051
1052 let second: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native(__test_bytes)");
1053 assert!(second.is_err());
1054 });
1055 }
1056
1057 #[test]
1058 fn instantiate_rejects_when_instance_limit_reached() {
1059 let wasm_bytes = wat_to_wasm(r"(module)");
1060 run_wasm_test(|ctx, state| {
1061 state.borrow_mut().set_limits_for_test(8, 1);
1062
1063 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1064 for (i, &b) in wasm_bytes.iter().enumerate() {
1065 arr.set(i, i32::from(b)).unwrap();
1066 }
1067 ctx.globals().set("__test_bytes", arr).unwrap();
1068
1069 let module_id: u32 = ctx
1070 .eval("__pi_wasm_compile_native(__test_bytes)")
1071 .expect("compile");
1072
1073 let first: u32 = ctx
1074 .eval(format!("__pi_wasm_instantiate_native({module_id})"))
1075 .expect("first instantiate");
1076 assert!(first > 0);
1077
1078 let second: rquickjs::Result<u32> =
1079 ctx.eval(format!("__pi_wasm_instantiate_native({module_id})"));
1080 assert!(second.is_err());
1081 });
1082 }
1083
1084 #[test]
1085 fn alloc_id_skips_zero_on_wrap() {
1086 let wasm_bytes = wat_to_wasm(r"(module)");
1087 run_wasm_test(|ctx, state| {
1088 {
1089 let mut bridge = state.borrow_mut();
1090 bridge.set_limits_for_test(8, 8);
1091 bridge.next_id = MAX_JS_WASM_ID;
1092 }
1093
1094 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1095 for (i, &b) in wasm_bytes.iter().enumerate() {
1096 arr.set(i, i32::from(b)).unwrap();
1097 }
1098 ctx.globals().set("__test_bytes", arr).unwrap();
1099
1100 let first: i32 = ctx
1101 .eval("__pi_wasm_compile_native(__test_bytes)")
1102 .expect("first compile");
1103 let second: i32 = ctx
1104 .eval("__pi_wasm_compile_native(__test_bytes)")
1105 .expect("second compile");
1106
1107 assert_eq!(first, i32::MAX);
1108 assert_eq!(second, 1);
1109 });
1110 }
1111
1112 #[test]
1113 fn call_nonexistent_export_fails() {
1114 let wasm_bytes = wat_to_wasm(r#"(module (func (export "f") (result i32) i32.const 1))"#);
1115 run_wasm_test(|ctx, _state| {
1116 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1117 for (i, &b) in wasm_bytes.iter().enumerate() {
1118 arr.set(i, i32::from(b)).unwrap();
1119 }
1120 ctx.globals().set("__test_bytes", arr).unwrap();
1121
1122 let result: rquickjs::Result<i32> = ctx.eval(
1123 r#"
1124 var mid = __pi_wasm_compile_native(__test_bytes);
1125 var iid = __pi_wasm_instantiate_native(mid);
1126 __pi_wasm_call_export_native(iid, "nonexistent", []);
1127 "#,
1128 );
1129 assert!(result.is_err());
1130 });
1131 }
1132
1133 #[test]
1134 fn call_export_i64_param_is_rejected() {
1135 let wasm_bytes = wat_to_wasm(
1136 r#"(module
1137 (func (export "id64") (param i64) (result i64)
1138 local.get 0)
1139 )"#,
1140 );
1141 run_wasm_test(|ctx, _state| {
1142 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1143 for (i, &b) in wasm_bytes.iter().enumerate() {
1144 arr.set(i, i32::from(b)).unwrap();
1145 }
1146 ctx.globals().set("__test_bytes", arr).unwrap();
1147
1148 let result: rquickjs::Result<i32> = ctx.eval(
1149 r#"
1150 var mid = __pi_wasm_compile_native(__test_bytes);
1151 var iid = __pi_wasm_instantiate_native(mid);
1152 __pi_wasm_call_export_native(iid, "id64", [1]);
1153 "#,
1154 );
1155 assert!(result.is_err());
1156 });
1157 }
1158
1159 #[test]
1160 fn call_export_i64_result_is_rejected() {
1161 let wasm_bytes = wat_to_wasm(
1162 r#"(module
1163 (func (export "ret64") (result i64)
1164 i64.const 42)
1165 )"#,
1166 );
1167 run_wasm_test(|ctx, _state| {
1168 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1169 for (i, &b) in wasm_bytes.iter().enumerate() {
1170 arr.set(i, i32::from(b)).unwrap();
1171 }
1172 ctx.globals().set("__test_bytes", arr).unwrap();
1173
1174 let result: rquickjs::Result<i32> = ctx.eval(
1175 r#"
1176 var mid = __pi_wasm_compile_native(__test_bytes);
1177 var iid = __pi_wasm_instantiate_native(mid);
1178 __pi_wasm_call_export_native(iid, "ret64", []);
1179 "#,
1180 );
1181 assert!(result.is_err());
1182 });
1183 }
1184
1185 #[test]
1186 fn call_export_multivalue_result_is_rejected() {
1187 let wasm_bytes = wat_to_wasm(
1188 r#"(module
1189 (func (export "pair") (result i32 i32)
1190 i32.const 1
1191 i32.const 2)
1192 )"#,
1193 );
1194 run_wasm_test(|ctx, _state| {
1195 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1196 for (i, &b) in wasm_bytes.iter().enumerate() {
1197 arr.set(i, i32::from(b)).unwrap();
1198 }
1199 ctx.globals().set("__test_bytes", arr).unwrap();
1200
1201 let result: rquickjs::Result<i32> = ctx.eval(
1202 r#"
1203 var mid = __pi_wasm_compile_native(__test_bytes);
1204 var iid = __pi_wasm_instantiate_native(mid);
1205 __pi_wasm_call_export_native(iid, "pair", []);
1206 "#,
1207 );
1208 assert!(result.is_err());
1209 });
1210 }
1211
1212 #[test]
1213 fn call_export_externref_result_is_rejected() {
1214 let wasm_bytes = wat_to_wasm(
1215 r#"(module
1216 (func (export "retref") (result externref)
1217 ref.null extern)
1218 )"#,
1219 );
1220 run_wasm_test(|ctx, _state| {
1221 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1222 for (i, &b) in wasm_bytes.iter().enumerate() {
1223 arr.set(i, i32::from(b)).unwrap();
1224 }
1225 ctx.globals().set("__test_bytes", arr).unwrap();
1226
1227 let result: rquickjs::Result<i32> = ctx.eval(
1228 r#"
1229 var mid = __pi_wasm_compile_native(__test_bytes);
1230 var iid = __pi_wasm_instantiate_native(mid);
1231 __pi_wasm_call_export_native(iid, "retref", []);
1232 "#,
1233 );
1234 assert!(result.is_err());
1235 });
1236 }
1237
1238 #[test]
1239 fn js_polyfill_webassembly_instantiate() {
1240 let wasm_bytes = wat_to_wasm(
1241 r#"(module
1242 (func (export "add") (param i32 i32) (result i32)
1243 local.get 0 local.get 1 i32.add)
1244 )"#,
1245 );
1246 run_wasm_test(|ctx, _state| {
1247 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1248 for (i, &b) in wasm_bytes.iter().enumerate() {
1249 arr.set(i, i32::from(b)).unwrap();
1250 }
1251 ctx.globals().set("__test_bytes", arr).unwrap();
1252
1253 let has_wa: bool = ctx
1255 .eval("typeof globalThis.WebAssembly !== 'undefined'")
1256 .expect("check WebAssembly");
1257 assert!(has_wa);
1258
1259 let result: i32 = ctx
1262 .eval(
1263 r"
1264 var __test_result = -1;
1265 WebAssembly.instantiate(__test_bytes).then(function(r) {
1266 __test_result = r.instance.exports.add(10, 20);
1267 });
1268 __test_result;
1269 ",
1270 )
1271 .expect("polyfill instantiate");
1272 assert_eq!(result, 30);
1273 });
1274 }
1275
1276 #[test]
1277 fn js_polyfill_memory_buffer_getter() {
1278 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
1279 run_wasm_test(|ctx, _state| {
1280 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1281 for (i, &b) in wasm_bytes.iter().enumerate() {
1282 arr.set(i, i32::from(b)).unwrap();
1283 }
1284 ctx.globals().set("__test_bytes", arr).unwrap();
1285
1286 let size: i32 = ctx
1287 .eval(
1288 r"
1289 var __test_size = -1;
1290 WebAssembly.instantiate(__test_bytes).then(function(r) {
1291 __test_size = r.instance.exports.memory.buffer.byteLength;
1292 });
1293 __test_size;
1294 ",
1295 )
1296 .expect("polyfill memory buffer");
1297 assert_eq!(size, 65536);
1298 });
1299 }
1300
1301 #[test]
1302 fn js_polyfill_memory_grow_returns_previous_pages() {
1303 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
1304 run_wasm_test(|ctx, _state| {
1305 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1306 for (i, &b) in wasm_bytes.iter().enumerate() {
1307 arr.set(i, i32::from(b)).unwrap();
1308 }
1309 ctx.globals().set("__test_bytes", arr).unwrap();
1310
1311 let prev_pages: i32 = ctx
1312 .eval(
1313 r"
1314 var __test_prev = -1;
1315 WebAssembly.instantiate(__test_bytes).then(function(r) {
1316 __test_prev = r.instance.exports.memory.grow(2);
1317 });
1318 __test_prev;
1319 ",
1320 )
1321 .expect("polyfill memory grow");
1322 assert_eq!(prev_pages, 1);
1323
1324 let new_size: i32 = ctx
1325 .eval(
1326 r"
1327 var __test_size = -1;
1328 WebAssembly.instantiate(__test_bytes).then(function(r) {
1329 r.instance.exports.memory.grow(2);
1330 __test_size = r.instance.exports.memory.buffer.byteLength;
1331 });
1332 __test_size;
1333 ",
1334 )
1335 .expect("polyfill memory size after grow");
1336 assert_eq!(new_size, 3 * 65536);
1337 });
1338 }
1339
1340 #[test]
1341 fn js_polyfill_memory_grow_failure_throws_range_error() {
1342 let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 1))"#);
1343 run_wasm_test(|ctx, _state| {
1344 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1345 for (i, &b) in wasm_bytes.iter().enumerate() {
1346 arr.set(i, i32::from(b)).unwrap();
1347 }
1348 ctx.globals().set("__test_bytes", arr).unwrap();
1349
1350 let threw_range_error: bool = ctx
1351 .eval(
1352 r"
1353 var __threw_range_error = false;
1354 WebAssembly.instantiate(__test_bytes).then(function(r) {
1355 try {
1356 r.instance.exports.memory.grow(1);
1357 } catch (e) {
1358 __threw_range_error = e instanceof RangeError;
1359 }
1360 });
1361 __threw_range_error;
1362 ",
1363 )
1364 .expect("polyfill memory grow failure");
1365 assert!(threw_range_error);
1366 });
1367 }
1368
1369 #[test]
1370 fn module_with_imports_instantiates_with_stubs() {
1371 let wasm_bytes = wat_to_wasm(
1372 r#"(module
1373 (import "env" "log" (func (param i32)))
1374 (func (export "run") (result i32)
1375 i32.const 42
1376 call 0
1377 i32.const 1)
1378 )"#,
1379 );
1380 run_wasm_test(|ctx, _state| {
1381 let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1382 for (i, &b) in wasm_bytes.iter().enumerate() {
1383 arr.set(i, i32::from(b)).unwrap();
1384 }
1385 ctx.globals().set("__test_bytes", arr).unwrap();
1386
1387 let result: i32 = ctx
1388 .eval(
1389 r#"
1390 var mid = __pi_wasm_compile_native(__test_bytes);
1391 var iid = __pi_wasm_instantiate_native(mid);
1392 __pi_wasm_call_export_native(iid, "run", []);
1393 "#,
1394 )
1395 .expect("call with import stubs");
1396 assert_eq!(result, 1);
1397 });
1398 }
1399}