Skip to main content

pi/
pi_wasm.rs

1//! PiWasm: WebAssembly polyfill for QuickJS runtime.
2//!
3//! Provides `globalThis.WebAssembly` inside QuickJS, backed by wasmtime.
4//! Enables JS extensions to use WebAssembly modules (e.g., Emscripten-compiled
5//! code) even though QuickJS lacks native WebAssembly support.
6//!
7//! Architecture:
8//! - Native Rust functions (`__pi_wasm_*`) handle compile/instantiate/call
9//! - A JS polyfill wraps them into the standard `WebAssembly` namespace
10//! - Optional staged virtual files and a small host import surface support
11//!   Emscripten-style modules such as the DOOM overlay fixture
12
13use std::cell::RefCell;
14use std::collections::HashMap;
15use std::rc::Rc;
16use std::time::Instant;
17
18use anyhow::anyhow;
19use rquickjs::function::Func;
20use rquickjs::{ArrayBuffer, Ctx, Value};
21use serde::Serialize;
22use tracing::debug;
23use wasmtime::{
24    Caller, Engine, ExternType, Instance as WasmInstance, Linker, Module as WasmModule, Store, Val,
25    ValType,
26};
27
28// ---------------------------------------------------------------------------
29// Bridge state
30// ---------------------------------------------------------------------------
31
32/// Host data stored in each wasmtime `Store`.
33struct WasmHostData {
34    /// Maximum memory pages allowed (enforced on grow).
35    max_memory_pages: u64,
36    /// Files staged from JS for modules that expect a host filesystem.
37    staged_files: HashMap<String, std::sync::Arc<Vec<u8>>>,
38    /// Open virtual file descriptors for staged files.
39    open_files: HashMap<u32, VirtualFileHandle>,
40    /// Next synthetic file descriptor.
41    next_fd: u32,
42    /// Monotonic start time for `emscripten_get_now`.
43    started_at: Instant,
44}
45
46/// Per-instance state: the wasmtime `Store` owns all WASM objects.
47struct InstanceState {
48    store: Store<WasmHostData>,
49    instance: WasmInstance,
50}
51
52#[derive(Clone)]
53struct VirtualFileHandle {
54    path: String,
55    position: usize,
56    readable: bool,
57    writable: bool,
58    append: bool,
59}
60
61#[derive(Serialize)]
62struct WasmExportEntry {
63    name: String,
64    kind: &'static str,
65}
66
67/// Per-JS-runtime WASM bridge state, shared via `Rc<RefCell<>>`.
68pub(crate) struct WasmBridgeState {
69    engine: Engine,
70    modules: HashMap<u32, WasmModule>,
71    instances: HashMap<u32, InstanceState>,
72    staged_files: HashMap<String, std::sync::Arc<Vec<u8>>>,
73    next_id: u32,
74    max_modules: usize,
75    max_instances: usize,
76}
77
78impl WasmBridgeState {
79    pub fn new() -> Self {
80        let engine = Engine::default();
81        Self {
82            engine,
83            modules: HashMap::new(),
84            instances: HashMap::new(),
85            staged_files: HashMap::new(),
86            next_id: 1,
87            max_modules: DEFAULT_MAX_MODULES,
88            max_instances: DEFAULT_MAX_INSTANCES,
89        }
90    }
91
92    fn alloc_id(&mut self) -> Result<u32, String> {
93        let start = match self.next_id {
94            0 => 1,
95            id if id > MAX_JS_WASM_ID => 1,
96            id => id,
97        };
98        let mut candidate = start;
99
100        loop {
101            if !self.modules.contains_key(&candidate) && !self.instances.contains_key(&candidate) {
102                self.next_id = candidate.wrapping_add(1);
103                if self.next_id == 0 || self.next_id > MAX_JS_WASM_ID {
104                    self.next_id = 1;
105                }
106                return Ok(candidate);
107            }
108
109            candidate = candidate.wrapping_add(1);
110            if candidate == 0 || candidate > MAX_JS_WASM_ID {
111                candidate = 1;
112            }
113            if candidate == start {
114                return Err("WASM instance/module id space exhausted".to_string());
115            }
116        }
117    }
118
119    #[cfg(test)]
120    fn set_limits_for_test(&mut self, max_modules: usize, max_instances: usize) {
121        self.max_modules = max_modules.max(1);
122        self.max_instances = max_instances.max(1);
123    }
124}
125
126const ERRNO_BADF: i32 = 8;
127const ERRNO_EXIST: i32 = 20;
128const ERRNO_FBIG: i32 = 27;
129const ERRNO_INVAL: i32 = 28;
130const ERRNO_NOENT: i32 = 44;
131
132const O_ACCMODE: i32 = 0o3;
133const O_WRONLY: i32 = 0o1;
134const O_RDWR: i32 = 0o2;
135const O_CREAT: i32 = 0o100;
136const O_EXCL: i32 = 0o200;
137const O_TRUNC: i32 = 0o1000;
138const O_APPEND: i32 = 0o2000;
139
140/// Hard cap on per-instance virtual file growth to avoid unbounded host allocation.
141const MAX_VIRTUAL_FILE_BYTES: usize = 64 * 1024 * 1024;
142
143const fn descriptor_access(flags: i32) -> Option<(bool, bool)> {
144    match flags & O_ACCMODE {
145        0 => Some((true, false)),
146        O_WRONLY => Some((false, true)),
147        O_RDWR => Some((true, true)),
148        _ => None,
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Error helpers
154// ---------------------------------------------------------------------------
155
156fn throw_wasm(ctx: &Ctx<'_>, class: &str, msg: &str) -> rquickjs::Error {
157    let text = format!("{class}: {msg}");
158    if let Ok(js_text) = rquickjs::String::from_str(ctx.clone(), &text) {
159        let _ = ctx.throw(js_text.into_value());
160    }
161    rquickjs::Error::Exception
162}
163
164// ---------------------------------------------------------------------------
165// Value conversion: JS ↔ WASM
166// ---------------------------------------------------------------------------
167
168fn extract_bytes(ctx: &Ctx<'_>, value: &Value<'_>) -> rquickjs::Result<Vec<u8>> {
169    // Try ArrayBuffer
170    if let Some(obj) = value.as_object() {
171        if let Some(ab) = obj.as_array_buffer() {
172            return ab
173                .as_bytes()
174                .map(<[u8]>::to_vec)
175                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Detached ArrayBuffer"));
176        }
177        if let Some(typed) = obj.as_typed_array::<u8>() {
178            return typed
179                .as_bytes()
180                .map(<[u8]>::to_vec)
181                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Detached TypedArray"));
182        }
183    }
184    // Try array of numbers
185    if let Some(arr) = value.as_array() {
186        let mut bytes = Vec::with_capacity(arr.len().min(1024 * 1024));
187        for i in 0..arr.len() {
188            let v: i32 = arr.get(i)?;
189            bytes.push(
190                u8::try_from(v)
191                    .map_err(|_| throw_wasm(ctx, "TypeError", "Byte value out of range"))?,
192            );
193        }
194        return Ok(bytes);
195    }
196    Err(throw_wasm(
197        ctx,
198        "TypeError",
199        "Expected ArrayBuffer or byte array",
200    ))
201}
202
203/// Convert a WASM `Val` to an f64 for returning to JS.
204/// Note: i64 is intentionally excluded to avoid silent precision loss.
205#[allow(clippy::cast_precision_loss)]
206fn val_to_f64(ctx: &Ctx<'_>, val: &Val) -> rquickjs::Result<f64> {
207    match val {
208        Val::I32(v) => Ok(f64::from(*v)),
209        Val::F32(bits) => Ok(f64::from(f32::from_bits(*bits))),
210        Val::F64(bits) => Ok(f64::from_bits(*bits)),
211        _ => Err(throw_wasm(
212            ctx,
213            "RuntimeError",
214            "Unsupported WASM return value type for PiJS bridge",
215        )),
216    }
217}
218
219/// Emulate JavaScript `ToInt32` semantics for number -> i32 coercion.
220#[allow(clippy::cast_possible_truncation)]
221fn js_to_i32(value: f64) -> i32 {
222    if !value.is_finite() || value == 0.0 {
223        return 0;
224    }
225
226    let mut wrapped = value.trunc() % TWO_POW_32;
227    if wrapped < 0.0 {
228        wrapped += TWO_POW_32;
229    }
230
231    if wrapped >= TWO_POW_31 {
232        (wrapped - TWO_POW_32) as i32
233    } else {
234        wrapped as i32
235    }
236}
237
238#[allow(clippy::cast_possible_truncation)]
239fn js_to_val(ctx: &Ctx<'_>, value: &Value<'_>, ty: &ValType) -> rquickjs::Result<Val> {
240    match ty {
241        ValType::I32 => {
242            let v: f64 = value
243                .as_number()
244                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for i32"))?;
245            Ok(Val::I32(js_to_i32(v)))
246        }
247        ValType::I64 => Err(throw_wasm(
248            ctx,
249            "TypeError",
250            "i64 parameters are not supported by PiJS WebAssembly bridge",
251        )),
252        ValType::F32 => {
253            let v: f64 = value
254                .as_number()
255                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f32"))?;
256            #[expect(clippy::cast_possible_truncation)]
257            Ok(Val::F32((v as f32).to_bits()))
258        }
259        ValType::F64 => {
260            let v: f64 = value
261                .as_number()
262                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f64"))?;
263            Ok(Val::F64(v.to_bits()))
264        }
265        _ => Err(throw_wasm(ctx, "TypeError", "Unsupported WASM value type")),
266    }
267}
268
269fn validate_call_result_types(ctx: &Ctx<'_>, result_types: &[ValType]) -> rquickjs::Result<()> {
270    if result_types.len() > 1 {
271        return Err(throw_wasm(
272            ctx,
273            "RuntimeError",
274            "Multi-value WASM results are not supported by PiJS WebAssembly bridge",
275        ));
276    }
277
278    if let Some(ty) = result_types.first() {
279        return match ty {
280            ValType::I32 | ValType::F32 | ValType::F64 => Ok(()),
281            ValType::I64 => Err(throw_wasm(
282                ctx,
283                "RuntimeError",
284                "i64 results are not supported by PiJS WebAssembly bridge",
285            )),
286            _ => Err(throw_wasm(
287                ctx,
288                "RuntimeError",
289                "Unsupported WASM return type for PiJS WebAssembly bridge",
290            )),
291        };
292    }
293
294    Ok(())
295}
296
297// ---------------------------------------------------------------------------
298// Import helpers
299// ---------------------------------------------------------------------------
300
301fn instance_memory(inst: &mut InstanceState, mem_name: &str) -> anyhow::Result<wasmtime::Memory> {
302    inst.instance
303        .get_memory(&mut inst.store, mem_name)
304        .ok_or_else(|| anyhow!("Memory '{mem_name}' not found"))
305}
306
307fn caller_memory(caller: &mut Caller<'_, WasmHostData>) -> anyhow::Result<wasmtime::Memory> {
308    caller
309        .get_export("memory")
310        .and_then(wasmtime::Extern::into_memory)
311        .ok_or_else(|| anyhow!("Exported memory 'memory' not found"))
312}
313
314fn checked_memory_range(
315    offset: usize,
316    len: usize,
317    memory_len: usize,
318) -> anyhow::Result<std::ops::Range<usize>> {
319    let end = offset
320        .checked_add(len)
321        .ok_or_else(|| anyhow!("Memory access overflow"))?;
322    if end > memory_len {
323        return Err(anyhow!("Memory access out of bounds"));
324    }
325    Ok(offset..end)
326}
327
328fn caller_read_bytes(
329    caller: &mut Caller<'_, WasmHostData>,
330    offset: usize,
331    len: usize,
332) -> anyhow::Result<Vec<u8>> {
333    let memory = caller_memory(caller)?;
334    let _ = checked_memory_range(offset, len, memory.data_size(&mut *caller))?;
335    let mut bytes = vec![0_u8; len];
336    memory
337        .read(&mut *caller, offset, &mut bytes)
338        .map_err(anyhow::Error::from)?;
339    Ok(bytes)
340}
341
342fn caller_write_bytes(
343    caller: &mut Caller<'_, WasmHostData>,
344    offset: usize,
345    bytes: &[u8],
346) -> anyhow::Result<()> {
347    let memory = caller_memory(caller)?;
348    let _ = checked_memory_range(offset, bytes.len(), memory.data_size(&mut *caller))?;
349    memory
350        .write(&mut *caller, offset, bytes)
351        .map_err(anyhow::Error::from)
352}
353
354fn caller_read_u32(caller: &mut Caller<'_, WasmHostData>, offset: usize) -> anyhow::Result<u32> {
355    let bytes = caller_read_bytes(caller, offset, 4)?;
356    Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
357}
358
359fn caller_write_u32(
360    caller: &mut Caller<'_, WasmHostData>,
361    offset: usize,
362    value: u32,
363) -> anyhow::Result<()> {
364    caller_write_bytes(caller, offset, &value.to_le_bytes())
365}
366
367fn caller_write_u64(
368    caller: &mut Caller<'_, WasmHostData>,
369    offset: usize,
370    value: u64,
371) -> anyhow::Result<()> {
372    caller_write_bytes(caller, offset, &value.to_le_bytes())
373}
374
375fn caller_read_c_string(
376    caller: &mut Caller<'_, WasmHostData>,
377    offset: usize,
378) -> anyhow::Result<String> {
379    let memory = caller_memory(caller)?;
380    let bytes = memory.data(&mut *caller);
381    let mut end = offset;
382    while end < bytes.len() && bytes[end] != 0 {
383        end += 1;
384    }
385    if end >= bytes.len() {
386        return Err(anyhow!("Unterminated string in WASM memory"));
387    }
388    Ok(String::from_utf8_lossy(&bytes[offset..end]).into_owned())
389}
390
391fn val_i32(params: &[Val], idx: usize, label: &str) -> anyhow::Result<i32> {
392    match params.get(idx) {
393        Some(Val::I32(value)) => Ok(*value),
394        _ => Err(anyhow!("Expected i32 parameter '{label}' at index {idx}")),
395    }
396}
397
398fn val_i64(params: &[Val], idx: usize, label: &str) -> anyhow::Result<i64> {
399    match params.get(idx) {
400        Some(Val::I64(value)) => Ok(*value),
401        Some(Val::I32(value)) => Ok(i64::from(*value)),
402        _ => Err(anyhow!("Expected i64 parameter '{label}' at index {idx}")),
403    }
404}
405
406const fn set_i32_result(results: &mut [Val], value: i32) {
407    if !results.is_empty() {
408        results[0] = Val::I32(value);
409    }
410}
411
412const fn set_f64_result(results: &mut [Val], value: f64) {
413    if !results.is_empty() {
414        results[0] = Val::F64(value.to_bits());
415    }
416}
417
418fn stub_import(
419    linker: &mut Linker<WasmHostData>,
420    mod_name: &str,
421    imp_name: &str,
422    func_ty: &wasmtime::FuncType,
423) -> Result<(), String> {
424    let result_types: Vec<ValType> = func_ty.results().collect();
425    linker
426        .func_new(
427            mod_name,
428            imp_name,
429            func_ty.clone(),
430            move |_caller: Caller<'_, WasmHostData>, _params: &[Val], results: &mut [Val]| {
431                for (i, ty) in result_types.iter().enumerate() {
432                    results[i] = Val::default_for_ty(ty).unwrap_or(Val::I32(0));
433                }
434                Ok(())
435            },
436        )
437        .map_err(|e| format!("Failed to stub import {mod_name}.{imp_name}: {e}"))?;
438    Ok(())
439}
440
441/// Register the small host import surface PiWasm currently supports.
442/// Any unsupported imports fall back to no-op/default stubs.
443#[allow(clippy::too_many_lines)]
444fn register_host_imports(
445    linker: &mut Linker<WasmHostData>,
446    module: &WasmModule,
447) -> Result<(), String> {
448    for import in module.imports() {
449        let mod_name = import.module();
450        let imp_name = import.name();
451        if let ExternType::Func(func_ty) = import.ty() {
452            match imp_name {
453                "__syscall_openat" => {
454                    linker
455                        .func_new(
456                            mod_name,
457                            imp_name,
458                            func_ty.clone(),
459                            move |mut caller, params, results| {
460                                let path_ptr = usize::try_from(val_i32(params, 1, "path")?)
461                                    .map_err(|_| anyhow!("Negative path pointer"))?;
462                                let flags = val_i32(params, 2, "flags")?;
463                                let path = caller_read_c_string(&mut caller, path_ptr)?;
464                                let Some((readable, writable)) = descriptor_access(flags) else {
465                                    set_i32_result(results, -ERRNO_INVAL);
466                                    return Ok(());
467                                };
468                                if flags & O_TRUNC != 0 && !writable {
469                                    set_i32_result(results, -ERRNO_INVAL);
470                                    return Ok(());
471                                }
472
473                                let append = flags & O_APPEND != 0;
474                                let (fd, bytes_len) = {
475                                    let host = caller.data_mut();
476                                    let path_exists = host.staged_files.contains_key(&path);
477                                    if !path_exists {
478                                        if flags & O_CREAT == 0 {
479                                            set_i32_result(results, -ERRNO_NOENT);
480                                            return Ok(());
481                                        }
482                                        host.staged_files
483                                            .insert(path.clone(), std::sync::Arc::new(Vec::new()));
484                                    } else if flags & O_CREAT != 0 && flags & O_EXCL != 0 {
485                                        set_i32_result(results, -ERRNO_EXIST);
486                                        return Ok(());
487                                    }
488
489                                    let (position, bytes_len) = {
490                                        let file_arc =
491                                            host.staged_files.get_mut(&path).ok_or_else(|| {
492                                                anyhow!("Virtual file disappeared during open")
493                                            })?;
494                                        if flags & O_TRUNC != 0 {
495                                            std::sync::Arc::make_mut(file_arc).clear();
496                                        }
497                                        let bytes_len = file_arc.len();
498                                        let position = if append { bytes_len } else { 0 };
499                                        (position, bytes_len)
500                                    };
501
502                                    if host.next_fd == u32::MAX {
503                                        return Err(anyhow!("Synthetic fd space exhausted"));
504                                    }
505                                    let fd = host.next_fd;
506                                    host.next_fd = host.next_fd.saturating_add(1);
507                                    host.open_files.insert(
508                                        fd,
509                                        VirtualFileHandle {
510                                            path: path.clone(),
511                                            position,
512                                            readable,
513                                            writable,
514                                            append,
515                                        },
516                                    );
517                                    (fd, bytes_len)
518                                };
519                                debug!(
520                                    path,
521                                    bytes = bytes_len,
522                                    fd,
523                                    readable,
524                                    writable,
525                                    append,
526                                    "wasm: staged file open"
527                                );
528                                set_i32_result(results, i32::try_from(fd).unwrap_or(i32::MAX));
529                                Ok(())
530                            },
531                        )
532                        .map_err(|e| {
533                            format!("Failed to register import {mod_name}.{imp_name}: {e}")
534                        })?;
535                }
536                "fd_read" => {
537                    linker
538                        .func_new(
539                            mod_name,
540                            imp_name,
541                            func_ty.clone(),
542                            move |mut caller, params, results| {
543                                let fd = u32::try_from(val_i32(params, 0, "fd")?)
544                                    .map_err(|_| anyhow!("Negative fd"))?;
545                                let iov = usize::try_from(val_i32(params, 1, "iov")?)
546                                    .map_err(|_| anyhow!("Negative iov pointer"))?;
547                                let iovcnt = usize::try_from(val_i32(params, 2, "iovcnt")?)
548                                    .map_err(|_| anyhow!("Negative iov count"))?;
549                                let pnum = usize::try_from(val_i32(params, 3, "pnum")?)
550                                    .map_err(|_| anyhow!("Negative pnum pointer"))?;
551
552                                let (path, mut position) =
553                                    if let Some(handle) = caller.data().open_files.get(&fd) {
554                                        if !handle.readable {
555                                            set_i32_result(results, ERRNO_BADF);
556                                            return Ok(());
557                                        }
558                                        (handle.path.clone(), handle.position)
559                                    } else {
560                                        set_i32_result(results, ERRNO_BADF);
561                                        return Ok(());
562                                    };
563
564                                let mut total = 0_usize;
565                                for index in 0..iovcnt {
566                                    let base = iov
567                                        .checked_add(index.saturating_mul(8))
568                                        .ok_or_else(|| anyhow!("iov overflow"))?;
569                                    let ptr = usize::try_from(caller_read_u32(&mut caller, base)?)
570                                        .map_err(|_| anyhow!("iov ptr overflow"))?;
571                                    let len =
572                                        usize::try_from(caller_read_u32(&mut caller, base + 4)?)
573                                            .map_err(|_| anyhow!("iov len overflow"))?;
574                                    let chunk = {
575                                        let host = caller.data();
576                                        let Some(file) = host.staged_files.get(&path) else {
577                                            set_i32_result(results, ERRNO_NOENT);
578                                            return Ok(());
579                                        };
580                                        if position >= file.len() || len == 0 {
581                                            Vec::new()
582                                        } else {
583                                            let available = file.len().saturating_sub(position);
584                                            let to_copy = available.min(len);
585                                            file[position..position + to_copy].to_vec()
586                                        }
587                                    };
588                                    if chunk.is_empty() {
589                                        break;
590                                    }
591                                    caller_write_bytes(&mut caller, ptr, &chunk)?;
592                                    position += chunk.len();
593                                    total += chunk.len();
594                                    if chunk.len() < len {
595                                        break;
596                                    }
597                                }
598
599                                caller_write_u32(
600                                    &mut caller,
601                                    pnum,
602                                    u32::try_from(total).unwrap_or(u32::MAX),
603                                )?;
604                                if let Some(handle) = caller.data_mut().open_files.get_mut(&fd) {
605                                    handle.position = position;
606                                } else {
607                                    set_i32_result(results, ERRNO_BADF);
608                                    return Ok(());
609                                }
610                                set_i32_result(results, 0);
611                                Ok(())
612                            },
613                        )
614                        .map_err(|e| {
615                            format!("Failed to register import {mod_name}.{imp_name}: {e}")
616                        })?;
617                }
618                "fd_seek" => {
619                    linker
620                        .func_new(
621                            mod_name,
622                            imp_name,
623                            func_ty.clone(),
624                            move |mut caller, params, results| {
625                                let fd = u32::try_from(val_i32(params, 0, "fd")?)
626                                    .map_err(|_| anyhow!("Negative fd"))?;
627                                let offset = val_i64(params, 1, "offset")?;
628                                let whence = val_i32(params, 2, "whence")?;
629                                let new_offset_ptr =
630                                    usize::try_from(val_i32(params, 3, "newOffset")?)
631                                        .map_err(|_| anyhow!("Negative newOffset pointer"))?;
632
633                                let (path, current_position) =
634                                    if let Some(handle) = caller.data().open_files.get(&fd) {
635                                        (handle.path.clone(), handle.position)
636                                    } else {
637                                        set_i32_result(results, ERRNO_BADF);
638                                        return Ok(());
639                                    };
640                                let Some(file_len) =
641                                    caller.data().staged_files.get(&path).map(|v| v.len())
642                                else {
643                                    set_i32_result(results, ERRNO_NOENT);
644                                    return Ok(());
645                                };
646                                let base = match whence {
647                                    0 => 0_i64,
648                                    1 => i64::try_from(current_position).unwrap_or(i64::MAX),
649                                    2 => i64::try_from(file_len).unwrap_or(i64::MAX),
650                                    _ => {
651                                        set_i32_result(results, ERRNO_INVAL);
652                                        return Ok(());
653                                    }
654                                };
655                                let next = base
656                                    .checked_add(offset)
657                                    .ok_or_else(|| anyhow!("Seek overflow"))?;
658                                if next < 0 {
659                                    set_i32_result(results, ERRNO_INVAL);
660                                    return Ok(());
661                                }
662                                let next =
663                                    usize::try_from(next).map_err(|_| anyhow!("Seek overflow"))?;
664                                if let Some(handle) = caller.data_mut().open_files.get_mut(&fd) {
665                                    handle.position = next;
666                                } else {
667                                    set_i32_result(results, ERRNO_BADF);
668                                    return Ok(());
669                                }
670                                caller_write_u64(
671                                    &mut caller,
672                                    new_offset_ptr,
673                                    u64::try_from(next).unwrap_or(u64::MAX),
674                                )?;
675                                set_i32_result(results, 0);
676                                Ok(())
677                            },
678                        )
679                        .map_err(|e| {
680                            format!("Failed to register import {mod_name}.{imp_name}: {e}")
681                        })?;
682                }
683                "fd_close" => {
684                    linker
685                        .func_new(
686                            mod_name,
687                            imp_name,
688                            func_ty.clone(),
689                            move |mut caller, params, results| {
690                                let fd = u32::try_from(val_i32(params, 0, "fd")?)
691                                    .map_err(|_| anyhow!("Negative fd"))?;
692                                let result = if caller.data_mut().open_files.remove(&fd).is_some() {
693                                    0
694                                } else {
695                                    ERRNO_BADF
696                                };
697                                set_i32_result(results, result);
698                                Ok(())
699                            },
700                        )
701                        .map_err(|e| {
702                            format!("Failed to register import {mod_name}.{imp_name}: {e}")
703                        })?;
704                }
705                "fd_write" => {
706                    linker
707                        .func_new(
708                            mod_name,
709                            imp_name,
710                            func_ty.clone(),
711                            move |mut caller, params, results| {
712                                let fd = u32::try_from(val_i32(params, 0, "fd")?)
713                                    .map_err(|_| anyhow!("Negative fd"))?;
714                                let iov = usize::try_from(val_i32(params, 1, "iov")?)
715                                    .map_err(|_| anyhow!("Negative iov pointer"))?;
716                                let iovcnt = usize::try_from(val_i32(params, 2, "iovcnt")?)
717                                    .map_err(|_| anyhow!("Negative iov count"))?;
718                                let pnum = usize::try_from(val_i32(params, 3, "pnum")?)
719                                    .map_err(|_| anyhow!("Negative pnum pointer"))?;
720                                let (path, mut position, append, file_len) = {
721                                    let host = caller.data();
722                                    if let Some(handle) = host.open_files.get(&fd) {
723                                        if !handle.writable {
724                                            set_i32_result(results, ERRNO_BADF);
725                                            return Ok(());
726                                        }
727                                        let Some(file_len) =
728                                            host.staged_files.get(&handle.path).map(|v| v.len())
729                                        else {
730                                            set_i32_result(results, ERRNO_NOENT);
731                                            return Ok(());
732                                        };
733                                        (
734                                            handle.path.clone(),
735                                            handle.position,
736                                            handle.append,
737                                            file_len,
738                                        )
739                                    } else {
740                                        set_i32_result(results, ERRNO_BADF);
741                                        return Ok(());
742                                    }
743                                };
744                                let base_position = if append { file_len } else { position };
745                                let mut total = 0_usize;
746                                let mut chunks = Vec::new();
747                                for index in 0..iovcnt {
748                                    let base = iov
749                                        .checked_add(index.saturating_mul(8))
750                                        .ok_or_else(|| anyhow!("iov overflow"))?;
751                                    let ptr = usize::try_from(caller_read_u32(&mut caller, base)?)
752                                        .map_err(|_| anyhow!("iov ptr overflow"))?;
753                                    let len =
754                                        usize::try_from(caller_read_u32(&mut caller, base + 4)?)
755                                            .map_err(|_| anyhow!("iov len overflow"))?;
756                                    if len == 0 {
757                                        continue;
758                                    }
759                                    let next_total = total
760                                        .checked_add(len)
761                                        .ok_or_else(|| anyhow!("fd_write byte count overflow"))?;
762                                    if base_position
763                                        .checked_add(next_total)
764                                        .ok_or_else(|| anyhow!("fd_write overflow"))?
765                                        > MAX_VIRTUAL_FILE_BYTES
766                                    {
767                                        set_i32_result(results, ERRNO_FBIG);
768                                        return Ok(());
769                                    }
770
771                                    let bytes = caller_read_bytes(&mut caller, ptr, len)?;
772                                    total = next_total;
773                                    chunks.push(bytes);
774                                }
775                                if total == 0 {
776                                    caller_write_u32(&mut caller, pnum, 0)?;
777                                    if let Some(handle) = caller.data_mut().open_files.get_mut(&fd)
778                                    {
779                                        handle.position = position;
780                                    } else {
781                                        set_i32_result(results, ERRNO_BADF);
782                                        return Ok(());
783                                    }
784                                    set_i32_result(results, 0);
785                                    return Ok(());
786                                }
787                                {
788                                    let host = caller.data_mut();
789                                    let Some(file_arc) = host.staged_files.get_mut(&path) else {
790                                        set_i32_result(results, ERRNO_NOENT);
791                                        return Ok(());
792                                    };
793                                    let file = std::sync::Arc::make_mut(file_arc);
794                                    if append {
795                                        position = base_position;
796                                    }
797                                    let end = position
798                                        .checked_add(total)
799                                        .ok_or_else(|| anyhow!("fd_write overflow"))?;
800                                    if end > MAX_VIRTUAL_FILE_BYTES {
801                                        set_i32_result(results, ERRNO_FBIG);
802                                        return Ok(());
803                                    }
804                                    if position > file.len() {
805                                        file.resize(position, 0);
806                                    }
807                                    if end > file.len() {
808                                        file.resize(end, 0);
809                                    }
810
811                                    // Stage guest buffers before mutating the virtual file so
812                                    // size-limit failures do not commit a partial multi-iov write.
813                                    let mut write_position = position;
814                                    for bytes in &chunks {
815                                        let chunk_end = write_position
816                                            .checked_add(bytes.len())
817                                            .ok_or_else(|| anyhow!("fd_write overflow"))?;
818                                        file[write_position..chunk_end].copy_from_slice(bytes);
819                                        write_position = chunk_end;
820                                    }
821                                    position = write_position;
822                                }
823                                caller_write_u32(
824                                    &mut caller,
825                                    pnum,
826                                    u32::try_from(total).unwrap_or(u32::MAX),
827                                )?;
828                                if let Some(handle) = caller.data_mut().open_files.get_mut(&fd) {
829                                    handle.position = position;
830                                } else {
831                                    set_i32_result(results, ERRNO_BADF);
832                                    return Ok(());
833                                }
834                                set_i32_result(results, 0);
835                                Ok(())
836                            },
837                        )
838                        .map_err(|e| {
839                            format!("Failed to register import {mod_name}.{imp_name}: {e}")
840                        })?;
841                }
842                "emscripten_get_now" => {
843                    linker
844                        .func_new(
845                            mod_name,
846                            imp_name,
847                            func_ty.clone(),
848                            move |caller, _params, results| {
849                                let elapsed_ms =
850                                    caller.data().started_at.elapsed().as_secs_f64() * 1000.0;
851                                set_f64_result(results, elapsed_ms);
852                                Ok(())
853                            },
854                        )
855                        .map_err(|e| {
856                            format!("Failed to register import {mod_name}.{imp_name}: {e}")
857                        })?;
858                }
859                "emscripten_resize_heap" => {
860                    linker
861                        .func_new(
862                            mod_name,
863                            imp_name,
864                            func_ty.clone(),
865                            move |mut caller, params, results| {
866                                let requested_size =
867                                    usize::try_from(val_i32(params, 0, "requestedSize")?)
868                                        .map_err(|_| anyhow!("Negative heap size"))?;
869                                let memory = caller_memory(&mut caller)?;
870                                let current_size = memory.data_size(&mut caller);
871                                if requested_size <= current_size {
872                                    set_i32_result(results, 1);
873                                    return Ok(());
874                                }
875                                let page_size =
876                                    usize::try_from(memory.page_size(&caller)).unwrap_or(65_536);
877                                let needed_bytes = requested_size.saturating_sub(current_size);
878                                let needed_pages =
879                                    (needed_bytes.saturating_add(page_size - 1)) / page_size;
880                                let current_pages = memory.size(&caller);
881                                let requested_pages = current_pages.saturating_add(
882                                    u64::try_from(needed_pages).unwrap_or(u64::MAX),
883                                );
884                                if requested_pages > caller.data().max_memory_pages {
885                                    set_i32_result(results, 0);
886                                    return Ok(());
887                                }
888                                let grown = memory
889                                    .grow(
890                                        &mut caller,
891                                        u64::try_from(needed_pages).unwrap_or(u64::MAX),
892                                    )
893                                    .is_ok();
894                                set_i32_result(results, i32::from(grown));
895                                Ok(())
896                            },
897                        )
898                        .map_err(|e| {
899                            format!("Failed to register import {mod_name}.{imp_name}: {e}")
900                        })?;
901                }
902                "__syscall_fcntl64" | "__syscall_ioctl" | "__syscall_mkdirat"
903                | "__syscall_renameat" | "__syscall_rmdir" | "__syscall_unlinkat"
904                | "_emscripten_system" | "exit" => {
905                    stub_import(linker, mod_name, imp_name, &func_ty)?;
906                }
907                _ => stub_import(linker, mod_name, imp_name, &func_ty)?,
908            }
909        } else {
910            // Non-function imports are currently skipped for MVP.
911        }
912    }
913    Ok(())
914}
915
916// ---------------------------------------------------------------------------
917// Public API: inject globalThis.WebAssembly
918// ---------------------------------------------------------------------------
919
920/// Maximum default memory pages (64 KiB per page → 64 MB).
921const DEFAULT_MAX_MEMORY_PAGES: u64 = 1024;
922/// Hard limit on compiled modules kept alive in one JS runtime.
923const DEFAULT_MAX_MODULES: usize = 256;
924/// Hard limit on instantiated modules kept alive in one JS runtime.
925const DEFAULT_MAX_INSTANCES: usize = 256;
926/// Keep IDs within QuickJS signed-int range for stable JS↔Rust roundtrips.
927const MAX_JS_WASM_ID: u32 = i32::MAX as u32;
928/// JS numeric coercion helpers.
929const TWO_POW_32: f64 = 4_294_967_296.0;
930const TWO_POW_31: f64 = 2_147_483_648.0;
931
932/// Inject `globalThis.WebAssembly` polyfill into the QuickJS context.
933#[allow(clippy::too_many_lines)]
934pub(crate) fn inject_wasm_globals(
935    ctx: &Ctx<'_>,
936    state: &Rc<RefCell<WasmBridgeState>>,
937) -> rquickjs::Result<()> {
938    let global = ctx.globals();
939
940    // ---- __pi_wasm_compile_native(bytes) → module_id ----
941    {
942        let st = Rc::clone(state);
943        global.set(
944            "__pi_wasm_compile_native",
945            Func::from(
946                move |ctx: Ctx<'_>, bytes_val: Value<'_>| -> rquickjs::Result<u32> {
947                    let bytes = extract_bytes(&ctx, &bytes_val)?;
948                    if bytes.len() > MAX_VIRTUAL_FILE_BYTES {
949                        return Err(throw_wasm(
950                            &ctx,
951                            "CompileError",
952                            &format!(
953                                "Module exceeds PiWasm limit ({} > {} bytes)",
954                                bytes.len(),
955                                MAX_VIRTUAL_FILE_BYTES
956                            ),
957                        ));
958                    }
959                    let mut bridge = st.borrow_mut();
960                    if bridge.modules.len() >= bridge.max_modules {
961                        return Err(throw_wasm(
962                            &ctx,
963                            "CompileError",
964                            &format!("Module limit reached ({})", bridge.max_modules),
965                        ));
966                    }
967                    let module = WasmModule::from_binary(&bridge.engine, &bytes)
968                        .map_err(|e| throw_wasm(&ctx, "CompileError", &e.to_string()))?;
969                    let id = bridge
970                        .alloc_id()
971                        .map_err(|e| throw_wasm(&ctx, "CompileError", &e))?;
972                    debug!(module_id = id, bytes_len = bytes.len(), "wasm: compiled");
973                    bridge.modules.insert(id, module);
974                    Ok(id)
975                },
976            ),
977        )?;
978    }
979
980    // ---- __pi_wasm_validate_native(bytes) → bool ----
981    {
982        let st = Rc::clone(state);
983        global.set(
984            "__pi_wasm_validate_native",
985            Func::from(
986                move |ctx: Ctx<'_>, bytes_val: Value<'_>| -> rquickjs::Result<bool> {
987                    let bytes = extract_bytes(&ctx, &bytes_val)?;
988                    if bytes.len() > MAX_VIRTUAL_FILE_BYTES {
989                        return Ok(false);
990                    }
991                    let bridge = st.borrow();
992                    Ok(WasmModule::from_binary(&bridge.engine, &bytes).is_ok())
993                },
994            ),
995        )?;
996    }
997
998    // ---- __pi_wasm_stage_file_native(path, bytes) → byte_length ----
999    {
1000        let st = Rc::clone(state);
1001        global.set(
1002            "__pi_wasm_stage_file_native",
1003            Func::from(
1004                move |ctx: Ctx<'_>, path: String, bytes_val: Value<'_>| -> rquickjs::Result<u32> {
1005                    let bytes = extract_bytes(&ctx, &bytes_val)?;
1006                    if bytes.len() > MAX_VIRTUAL_FILE_BYTES {
1007                        return Err(throw_wasm(
1008                            &ctx,
1009                            "RangeError",
1010                            &format!(
1011                                "Virtual file exceeds PiWasm limit ({} > {} bytes)",
1012                                bytes.len(),
1013                                MAX_VIRTUAL_FILE_BYTES
1014                            ),
1015                        ));
1016                    }
1017                    let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
1018                    debug!(path = %path, len_bytes = bytes.len(), "wasm: staged file");
1019                    st.borrow_mut()
1020                        .staged_files
1021                        .insert(path, std::sync::Arc::new(bytes));
1022                    Ok(len)
1023                },
1024            ),
1025        )?;
1026    }
1027
1028    // ---- __pi_wasm_instantiate_native(module_id) → instance_id ----
1029    {
1030        let st = Rc::clone(state);
1031        global.set(
1032            "__pi_wasm_instantiate_native",
1033            Func::from(
1034                move |ctx: Ctx<'_>, module_id: u32| -> rquickjs::Result<u32> {
1035                    let mut bridge = st.borrow_mut();
1036                    if bridge.instances.len() >= bridge.max_instances {
1037                        return Err(throw_wasm(
1038                            &ctx,
1039                            "RuntimeError",
1040                            &format!("Instance limit reached ({})", bridge.max_instances),
1041                        ));
1042                    }
1043                    let module = bridge
1044                        .modules
1045                        .get(&module_id)
1046                        .ok_or_else(|| throw_wasm(&ctx, "LinkError", "Module not found"))?
1047                        .clone();
1048
1049                    let mut linker = Linker::new(&bridge.engine);
1050                    register_host_imports(&mut linker, &module)
1051                        .map_err(|e| throw_wasm(&ctx, "LinkError", &e))?;
1052
1053                    let mut store = Store::new(
1054                        &bridge.engine,
1055                        WasmHostData {
1056                            max_memory_pages: DEFAULT_MAX_MEMORY_PAGES,
1057                            staged_files: bridge.staged_files.clone(),
1058                            open_files: HashMap::new(),
1059                            next_fd: 3,
1060                            started_at: Instant::now(),
1061                        },
1062                    );
1063                    let instance = linker
1064                        .instantiate(&mut store, &module)
1065                        .map_err(|e| throw_wasm(&ctx, "LinkError", &e.to_string()))?;
1066
1067                    let id = bridge
1068                        .alloc_id()
1069                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e))?;
1070                    debug!(instance_id = id, module_id, "wasm: instantiated");
1071                    bridge
1072                        .instances
1073                        .insert(id, InstanceState { store, instance });
1074                    Ok(id)
1075                },
1076            ),
1077        )?;
1078    }
1079
1080    // ---- __pi_wasm_get_exports_native(instance_id) → JSON string [{name, kind}] ----
1081    {
1082        let st = Rc::clone(state);
1083        global.set(
1084            "__pi_wasm_get_exports_native",
1085            Func::from(
1086                move |ctx: Ctx<'_>, instance_id: u32| -> rquickjs::Result<String> {
1087                    let mut bridge = st.borrow_mut();
1088                    let inst = bridge
1089                        .instances
1090                        .get_mut(&instance_id)
1091                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1092
1093                    let mut entries: Vec<WasmExportEntry> = Vec::new();
1094                    for export in inst.instance.exports(&mut inst.store) {
1095                        let name = export.name().to_string();
1096                        let kind = match export.into_extern() {
1097                            wasmtime::Extern::Func(_) => "func",
1098                            wasmtime::Extern::Memory(_) => "memory",
1099                            wasmtime::Extern::Table(_) => "table",
1100                            wasmtime::Extern::Global(_) => "global",
1101                            wasmtime::Extern::SharedMemory(_) => "shared-memory",
1102                            wasmtime::Extern::Tag(_) => "tag",
1103                        };
1104                        entries.push(WasmExportEntry { name, kind });
1105                    }
1106                    serde_json::to_string(&entries)
1107                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))
1108                },
1109            ),
1110        )?;
1111    }
1112
1113    // ---- __pi_wasm_call_export_native(instance_id, name, args_array) → f64 result ----
1114    {
1115        let st = Rc::clone(state);
1116        global.set(
1117            "__pi_wasm_call_export_native",
1118            Func::from(
1119                move |ctx: Ctx<'_>,
1120                      instance_id: u32,
1121                      name: String,
1122                      args_val: Value<'_>|
1123                      -> rquickjs::Result<f64> {
1124                    let mut bridge = st.borrow_mut();
1125                    let inst = bridge
1126                        .instances
1127                        .get_mut(&instance_id)
1128                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1129
1130                    let started = Instant::now();
1131                    let func = inst
1132                        .instance
1133                        .get_func(&mut inst.store, &name)
1134                        .ok_or_else(|| {
1135                            throw_wasm(&ctx, "RuntimeError", &format!("Export '{name}' not found"))
1136                        })?;
1137
1138                    let func_ty = func.ty(&inst.store);
1139                    let param_types: Vec<ValType> = func_ty.params().collect();
1140                    if param_types.iter().any(|ty| matches!(ty, ValType::I64)) {
1141                        return Err(throw_wasm(
1142                            &ctx,
1143                            "TypeError",
1144                            "i64 parameters are not supported by PiJS WebAssembly bridge",
1145                        ));
1146                    }
1147
1148                    // Convert JS args to WASM vals
1149                    let args_arr = args_val
1150                        .as_array()
1151                        .ok_or_else(|| throw_wasm(&ctx, "TypeError", "args must be an array"))?;
1152                    let mut params = Vec::with_capacity(param_types.len());
1153                    for (i, ty) in param_types.iter().enumerate() {
1154                        let js_val: Value<'_> = args_arr.get(i)?;
1155                        params.push(js_to_val(&ctx, &js_val, ty)?);
1156                    }
1157
1158                    // Allocate results
1159                    let result_types: Vec<ValType> = func_ty.results().collect();
1160                    validate_call_result_types(&ctx, &result_types)?;
1161                    let mut results: Vec<Val> = result_types
1162                        .iter()
1163                        .map(|ty| Val::default_for_ty(ty).unwrap_or(Val::I32(0)))
1164                        .collect();
1165
1166                    debug!(
1167                        instance_id,
1168                        export = %name,
1169                        argc = params.len(),
1170                        "wasm: call export start"
1171                    );
1172                    func.call(&mut inst.store, &params, &mut results)
1173                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1174                    debug!(
1175                        instance_id,
1176                        export = %name,
1177                        argc = params.len(),
1178                        elapsed_ms = started.elapsed().as_millis(),
1179                        "wasm: call export"
1180                    );
1181
1182                    // Return first result as f64 (supports i32/f32/f64 only).
1183                    results.first().map_or(Ok(0.0), |val| val_to_f64(&ctx, val))
1184                },
1185            ),
1186        )?;
1187    }
1188
1189    // ---- __pi_wasm_memory_read_native(instance_id, mem_name, offset, len) → byte array ----
1190    {
1191        let st = Rc::clone(state);
1192        global.set(
1193            "__pi_wasm_memory_read_native",
1194            Func::from(
1195                move |ctx: Ctx<'_>,
1196                      instance_id: u32,
1197                      mem_name: String,
1198                      offset: u32,
1199                      len: u32|
1200                      -> rquickjs::Result<Vec<u8>> {
1201                    let mut bridge = st.borrow_mut();
1202                    let inst = bridge
1203                        .instances
1204                        .get_mut(&instance_id)
1205                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1206                    let memory = instance_memory(inst, &mem_name)
1207                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1208                    let start = usize::try_from(offset)
1209                        .map_err(|_| throw_wasm(&ctx, "RuntimeError", "Offset overflow"))?;
1210                    let len = usize::try_from(len)
1211                        .map_err(|_| throw_wasm(&ctx, "RuntimeError", "Length overflow"))?;
1212                    let data = memory.data(&inst.store);
1213                    let range = checked_memory_range(start, len, data.len())
1214                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1215                    Ok(data[range].to_vec())
1216                },
1217            ),
1218        )?;
1219    }
1220
1221    // ---- __pi_wasm_memory_write_native(instance_id, mem_name, offset, bytes) → byte_length ----
1222    {
1223        let st = Rc::clone(state);
1224        global.set(
1225            "__pi_wasm_memory_write_native",
1226            Func::from(
1227                move |ctx: Ctx<'_>,
1228                      instance_id: u32,
1229                      mem_name: String,
1230                      offset: u32,
1231                      bytes_val: Value<'_>|
1232                      -> rquickjs::Result<u32> {
1233                    let bytes = extract_bytes(&ctx, &bytes_val)?;
1234                    let mut bridge = st.borrow_mut();
1235                    let inst = bridge
1236                        .instances
1237                        .get_mut(&instance_id)
1238                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1239                    let memory = instance_memory(inst, &mem_name)
1240                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1241                    let start = usize::try_from(offset)
1242                        .map_err(|_| throw_wasm(&ctx, "RuntimeError", "Offset overflow"))?;
1243                    let _ =
1244                        checked_memory_range(start, bytes.len(), memory.data_size(&mut inst.store))
1245                            .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1246                    memory
1247                        .write(&mut inst.store, start, &bytes)
1248                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1249                    Ok(u32::try_from(bytes.len()).unwrap_or(u32::MAX))
1250                },
1251            ),
1252        )?;
1253    }
1254
1255    // ---- __pi_wasm_get_buffer_native(instance_id, mem_name) → stores ArrayBuffer in global ----
1256    {
1257        let st = Rc::clone(state);
1258        global.set(
1259            "__pi_wasm_get_buffer_native",
1260            Func::from(
1261                move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<i32> {
1262                    let mut bridge = st.borrow_mut();
1263                    let inst = bridge
1264                        .instances
1265                        .get_mut(&instance_id)
1266                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1267                    let started = Instant::now();
1268                    let memory = instance_memory(inst, &mem_name)
1269                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
1270                    let data = memory.data(&inst.store);
1271                    let len = i32::try_from(data.len()).unwrap_or(i32::MAX);
1272                    let buffer = ArrayBuffer::new_copy(ctx.clone(), data)?;
1273                    ctx.globals().set("__pi_wasm_tmp_buf", buffer)?;
1274                    debug!(
1275                        instance_id,
1276                        memory = %mem_name,
1277                        len_bytes = data.len(),
1278                        elapsed_ms = started.elapsed().as_millis(),
1279                        "wasm: get memory buffer"
1280                    );
1281                    Ok(len)
1282                },
1283            ),
1284        )?;
1285    }
1286
1287    // ---- __pi_wasm_memory_grow_native(instance_id, mem_name, delta) → prev_pages ----
1288    {
1289        let st = Rc::clone(state);
1290        global.set(
1291            "__pi_wasm_memory_grow_native",
1292            Func::from(
1293                move |ctx: Ctx<'_>,
1294                      instance_id: u32,
1295                      mem_name: String,
1296                      delta: u32|
1297                      -> rquickjs::Result<i32> {
1298                    let mut bridge = st.borrow_mut();
1299                    let inst = bridge
1300                        .instances
1301                        .get_mut(&instance_id)
1302                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1303
1304                    // Enforce policy limit
1305                    let memory = inst
1306                        .instance
1307                        .get_memory(&mut inst.store, &mem_name)
1308                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
1309                    let current = memory.size(&inst.store);
1310                    let requested = current.saturating_add(u64::from(delta));
1311                    if requested > inst.store.data().max_memory_pages {
1312                        return Ok(-1); // growth denied by policy
1313                    }
1314
1315                    Ok(memory
1316                        .grow(&mut inst.store, u64::from(delta))
1317                        .map_or(-1, |prev| i32::try_from(prev).unwrap_or(-1)))
1318                },
1319            ),
1320        )?;
1321    }
1322
1323    // ---- __pi_wasm_memory_size_native(instance_id, mem_name) → pages ----
1324    {
1325        let st = Rc::clone(state);
1326        global.set(
1327            "__pi_wasm_memory_size_native",
1328            Func::from(
1329                move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<u32> {
1330                    let mut bridge = st.borrow_mut();
1331                    let inst = bridge
1332                        .instances
1333                        .get_mut(&instance_id)
1334                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
1335                    let memory = inst
1336                        .instance
1337                        .get_memory(&mut inst.store, &mem_name)
1338                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
1339                    Ok(u32::try_from(memory.size(&inst.store)).unwrap_or(u32::MAX))
1340                },
1341            ),
1342        )?;
1343    }
1344
1345    // ---- Inject the JS polyfill layer ----
1346    ctx.eval::<(), _>(WASM_POLYFILL_JS)?;
1347
1348    debug!("wasm: globalThis.WebAssembly polyfill injected");
1349    Ok(())
1350}
1351
1352// ---------------------------------------------------------------------------
1353// JS polyfill that wraps the native functions
1354// ---------------------------------------------------------------------------
1355
1356const WASM_POLYFILL_JS: &str = r#"
1357(function() {
1358  "use strict";
1359
1360  class CompileError extends Error {
1361    constructor(msg) { super(msg); this.name = "CompileError"; }
1362  }
1363  class LinkError extends Error {
1364    constructor(msg) { super(msg); this.name = "LinkError"; }
1365  }
1366  class RuntimeError extends Error {
1367    constructor(msg) { super(msg); this.name = "RuntimeError"; }
1368  }
1369
1370  // Synchronous thenable: behaves like syncResolve() but executes
1371  // .then() callbacks immediately. QuickJS doesn't auto-flush microtasks.
1372  function syncResolve(value) {
1373    return {
1374      then: function(resolve, _reject) {
1375        try {
1376          var r = resolve(value);
1377          return syncResolve(r);
1378        } catch(e) { return syncReject(e); }
1379      },
1380      "catch": function() { return syncResolve(value); }
1381    };
1382  }
1383  function syncReject(err) {
1384    return {
1385      then: function(_resolve, reject) {
1386        if (reject) { reject(err); return syncResolve(undefined); }
1387        return syncReject(err);
1388      },
1389      "catch": function(fn) { fn(err); return syncResolve(undefined); }
1390    };
1391  }
1392
1393  function normalizeBytes(source) {
1394    if (source instanceof ArrayBuffer) {
1395      return new Uint8Array(source);
1396    }
1397    if (ArrayBuffer.isView && ArrayBuffer.isView(source)) {
1398      return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
1399    }
1400    if (Array.isArray(source)) {
1401      return new Uint8Array(source);
1402    }
1403    throw new CompileError("Invalid source: expected ArrayBuffer, TypedArray, or byte array");
1404  }
1405
1406  function resolveStreamingBytes(source) {
1407    if (source && typeof source.arrayBuffer === "function") {
1408      return source.arrayBuffer();
1409    }
1410    if (source && typeof source.then === "function") {
1411      return source.then(function(resp) {
1412        if (resp && typeof resp.arrayBuffer === "function") {
1413          return resp.arrayBuffer();
1414        }
1415        return resp;
1416      });
1417    }
1418    return source;
1419  }
1420
1421  function buildExports(instanceId) {
1422    var info = JSON.parse(__pi_wasm_get_exports_native(instanceId));
1423    var exports = {};
1424    for (var i = 0; i < info.length; i++) {
1425      var exp = info[i];
1426      if (exp.kind === "func") {
1427        (function(name) {
1428          exports[name] = function() {
1429            var args = [];
1430            for (var j = 0; j < arguments.length; j++) args.push(arguments[j]);
1431            return __pi_wasm_call_export_native(instanceId, name, args);
1432          };
1433        })(exp.name);
1434      } else if (exp.kind === "memory") {
1435        (function(name) {
1436          var memObj = Object.create(WebAssembly.Memory.prototype);
1437          Object.defineProperty(memObj, "buffer", {
1438            get: function() {
1439              __pi_wasm_get_buffer_native(instanceId, name);
1440              return globalThis.__pi_wasm_tmp_buf;
1441            },
1442            configurable: true
1443          });
1444          memObj.grow = function(delta) {
1445            var prevPages = __pi_wasm_memory_grow_native(instanceId, name, delta);
1446            if (prevPages < 0) {
1447              throw new RangeError("WebAssembly.Memory.grow(): failed to grow memory");
1448            }
1449            return prevPages;
1450          };
1451          exports[name] = memObj;
1452        })(exp.name);
1453      }
1454    }
1455    return exports;
1456  }
1457
1458  globalThis.WebAssembly = {
1459    CompileError: CompileError,
1460    LinkError: LinkError,
1461    RuntimeError: RuntimeError,
1462
1463    compile: function(source) {
1464      try {
1465        var bytes = normalizeBytes(source);
1466        var arr = [];
1467        for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
1468        var moduleId = __pi_wasm_compile_native(arr);
1469        var wasmMod = { __wasm_module_id: moduleId };
1470        return syncResolve(wasmMod);
1471      } catch (e) {
1472        return syncReject(e);
1473      }
1474    },
1475
1476    instantiate: function(source, _imports) {
1477      try {
1478        var moduleId;
1479        if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
1480          moduleId = source.__wasm_module_id;
1481        } else {
1482          var bytes = normalizeBytes(source);
1483          var arr = [];
1484          for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
1485          moduleId = __pi_wasm_compile_native(arr);
1486        }
1487        var instanceId = __pi_wasm_instantiate_native(moduleId);
1488        var exports = buildExports(instanceId);
1489        var instance = { exports: exports };
1490        var wasmMod = { __wasm_module_id: moduleId };
1491        globalThis.__pi_wasm_last_instance_id = instanceId;
1492        instance.__pi_instance_id = instanceId;
1493        exports.__pi_instance_id = instanceId;
1494
1495        if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
1496          return syncResolve(instance);
1497        }
1498        return syncResolve({ module: wasmMod, instance: instance });
1499      } catch (e) {
1500        return syncReject(e);
1501      }
1502    },
1503
1504    validate: function(source) {
1505      var bytes = normalizeBytes(source);
1506      return __pi_wasm_validate_native(bytes);
1507    },
1508
1509    instantiateStreaming: function(source, imports) {
1510      try {
1511        var bytesOrPromise = resolveStreamingBytes(source);
1512        if (bytesOrPromise && typeof bytesOrPromise.then === "function") {
1513          return bytesOrPromise.then(
1514            function(bytes) { return WebAssembly.instantiate(bytes, imports); },
1515            function(err) { return syncReject(err); }
1516          );
1517        }
1518        return WebAssembly.instantiate(bytesOrPromise, imports);
1519      } catch (e) {
1520        return syncReject(e);
1521      }
1522    },
1523
1524    compileStreaming: function(source) {
1525      try {
1526        var bytesOrPromise = resolveStreamingBytes(source);
1527        if (bytesOrPromise && typeof bytesOrPromise.then === "function") {
1528          return bytesOrPromise.then(
1529            function(bytes) { return WebAssembly.compile(bytes); },
1530            function(err) { return syncReject(err); }
1531          );
1532        }
1533        return WebAssembly.compile(bytesOrPromise);
1534      } catch (e) {
1535        return syncReject(e);
1536      }
1537    },
1538
1539    Memory: function(descriptor) {
1540      if (!(this instanceof WebAssembly.Memory)) {
1541        throw new TypeError("WebAssembly.Memory must be called with new");
1542      }
1543      var initial = descriptor && descriptor.initial !== undefined ? descriptor.initial : 0;
1544      var maximum = descriptor && descriptor.maximum !== undefined ? descriptor.maximum : undefined;
1545      var initialInt = Number(initial);
1546      if (!Number.isFinite(initialInt) || initialInt < 0 || Math.floor(initialInt) !== initialInt) {
1547        throw new RangeError("WebAssembly.Memory: invalid initial size");
1548      }
1549      var maxInt = maximum === undefined ? undefined : Number(maximum);
1550      if (maxInt !== undefined) {
1551        if (!Number.isFinite(maxInt) || maxInt < 0 || Math.floor(maxInt) !== maxInt) {
1552          throw new RangeError("WebAssembly.Memory: invalid maximum size");
1553        }
1554        if (maxInt < initialInt) {
1555          throw new RangeError("WebAssembly.Memory: maximum size smaller than initial");
1556        }
1557      }
1558      this._pages = initialInt;
1559      this._maximum = maxInt;
1560      this._buffer = new ArrayBuffer(this._pages * 65536);
1561      Object.defineProperty(this, "buffer", {
1562        get: function() { return this._buffer; },
1563        configurable: true
1564      });
1565      this.grow = function(delta) {
1566        var deltaInt = Number(delta);
1567        if (!Number.isFinite(deltaInt) || deltaInt < 0 || Math.floor(deltaInt) !== deltaInt) {
1568          throw new RangeError("WebAssembly.Memory.grow(): invalid delta");
1569        }
1570        var old = this._pages;
1571        var next = old + deltaInt;
1572        if (this._maximum !== undefined && next > this._maximum) {
1573          throw new RangeError("WebAssembly.Memory.grow(): maximum size exceeded");
1574        }
1575        var nextBuffer = new ArrayBuffer(next * 65536);
1576        new Uint8Array(nextBuffer).set(new Uint8Array(this._buffer));
1577        this._buffer = nextBuffer;
1578        this._pages = next;
1579        return old;
1580      };
1581    },
1582
1583    Table: function(descriptor) {
1584      if (!(this instanceof WebAssembly.Table)) {
1585        throw new TypeError("WebAssembly.Table must be called with new");
1586      }
1587      var initial = descriptor && descriptor.initial !== undefined ? descriptor.initial : 0;
1588      var maximum = descriptor && descriptor.maximum !== undefined ? descriptor.maximum : undefined;
1589      var initialInt = Number(initial);
1590      if (!Number.isFinite(initialInt) || initialInt < 0 || Math.floor(initialInt) !== initialInt) {
1591        throw new RangeError("WebAssembly.Table: invalid initial size");
1592      }
1593      var maxInt = maximum === undefined ? undefined : Number(maximum);
1594      if (maxInt !== undefined) {
1595        if (!Number.isFinite(maxInt) || maxInt < 0 || Math.floor(maxInt) !== maxInt) {
1596          throw new RangeError("WebAssembly.Table: invalid maximum size");
1597        }
1598        if (maxInt < initialInt) {
1599          throw new RangeError("WebAssembly.Table: maximum size smaller than initial");
1600        }
1601      }
1602      var element = descriptor && descriptor.element ? String(descriptor.element) : "anyfunc";
1603      if (element !== "anyfunc" && element !== "funcref" && element !== "externref") {
1604        throw new TypeError("WebAssembly.Table: invalid element type");
1605      }
1606      this._element = element;
1607      this._maximum = maxInt;
1608      this._values = new Array(initialInt);
1609      for (var i = 0; i < initialInt; i++) this._values[i] = null;
1610      Object.defineProperty(this, "length", {
1611        get: function() { return this._values.length; },
1612        configurable: true
1613      });
1614      this.get = function(index) {
1615        var indexInt = Number(index);
1616        if (!Number.isFinite(indexInt) || indexInt < 0 || Math.floor(indexInt) !== indexInt) {
1617          throw new RangeError("WebAssembly.Table.get(): invalid index");
1618        }
1619        if (indexInt >= this._values.length) {
1620          throw new RangeError("WebAssembly.Table.get(): index out of bounds");
1621        }
1622        return this._values[indexInt];
1623      };
1624      this.set = function(index, value) {
1625        var indexInt = Number(index);
1626        if (!Number.isFinite(indexInt) || indexInt < 0 || Math.floor(indexInt) !== indexInt) {
1627          throw new RangeError("WebAssembly.Table.set(): invalid index");
1628        }
1629        if (indexInt >= this._values.length) {
1630          throw new RangeError("WebAssembly.Table.set(): index out of bounds");
1631        }
1632        this._values[indexInt] = value;
1633      };
1634      this.grow = function(delta, value) {
1635        var deltaInt = Number(delta);
1636        if (!Number.isFinite(deltaInt) || deltaInt < 0 || Math.floor(deltaInt) !== deltaInt) {
1637          throw new RangeError("WebAssembly.Table.grow(): invalid delta");
1638        }
1639        var old = this._values.length;
1640        var next = old + deltaInt;
1641        if (this._maximum !== undefined && next > this._maximum) {
1642          throw new RangeError("WebAssembly.Table.grow(): maximum size exceeded");
1643        }
1644        for (var i = old; i < next; i++) {
1645          this._values[i] = value === undefined ? null : value;
1646        }
1647        return old;
1648      };
1649    },
1650
1651    Global: function(descriptor, value) {
1652      if (!(this instanceof WebAssembly.Global)) {
1653        throw new TypeError("WebAssembly.Global must be called with new");
1654      }
1655      var valType = descriptor && descriptor.value ? String(descriptor.value) : "i32";
1656      var mutable = descriptor && descriptor.mutable ? true : false;
1657      this._type = valType;
1658      this._mutable = mutable;
1659      this._value = value;
1660      Object.defineProperty(this, "value", {
1661        get: function() { return this._value; },
1662        set: function(next) {
1663          if (!this._mutable) {
1664            throw new TypeError("WebAssembly.Global is immutable");
1665          }
1666          this._value = next;
1667        },
1668        configurable: true
1669      });
1670      this.valueOf = function() { return this._value; };
1671    }
1672  };
1673})();
1674"#;
1675
1676// ---------------------------------------------------------------------------
1677// Tests
1678// ---------------------------------------------------------------------------
1679
1680#[cfg(test)]
1681mod tests {
1682    use super::*;
1683
1684    /// Helper: create a QuickJS runtime, inject WASM globals, and run a test.
1685    fn run_wasm_test(f: impl FnOnce(&Ctx<'_>, Rc<RefCell<WasmBridgeState>>)) {
1686        let rt = rquickjs::Runtime::new().expect("create runtime");
1687        let ctx = rquickjs::Context::full(&rt).expect("create context");
1688        ctx.with(|ctx| {
1689            let state = Rc::new(RefCell::new(WasmBridgeState::new()));
1690            inject_wasm_globals(&ctx, &state).expect("inject globals");
1691            f(&ctx, state);
1692        });
1693    }
1694
1695    /// Get raw WASM binary bytes from WAT text.
1696    fn wat_to_wasm(wat: &str) -> Vec<u8> {
1697        wat::parse_str(wat).expect("parse WAT to WASM binary")
1698    }
1699
1700    #[test]
1701    fn js_to_i32_matches_javascript_wrapping_semantics() {
1702        assert_eq!(js_to_i32(2_147_483_648.0), -2_147_483_648);
1703        assert_eq!(js_to_i32(4_294_967_296.0), 0);
1704        assert_eq!(js_to_i32(-2_147_483_649.0), 2_147_483_647);
1705        assert_eq!(js_to_i32(-1.9), -1);
1706        assert_eq!(js_to_i32(1.9), 1);
1707        assert_eq!(js_to_i32(f64::NAN), 0);
1708        assert_eq!(js_to_i32(f64::INFINITY), 0);
1709        assert_eq!(js_to_i32(f64::NEG_INFINITY), 0);
1710    }
1711
1712    #[test]
1713    fn compile_and_instantiate_trivial_module() {
1714        let wasm_bytes = wat_to_wasm(
1715            r#"(module
1716              (func (export "add") (param i32 i32) (result i32)
1717                local.get 0 local.get 1 i32.add)
1718              (memory (export "memory") 1)
1719            )"#,
1720        );
1721        run_wasm_test(|ctx, _state| {
1722            // Store bytes as a JS array
1723            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1724            for (i, &b) in wasm_bytes.iter().enumerate() {
1725                arr.set(i, i32::from(b)).unwrap();
1726            }
1727            ctx.globals().set("__test_bytes", arr).unwrap();
1728
1729            // Compile
1730            let module_id: u32 = ctx
1731                .eval("__pi_wasm_compile_native(__test_bytes)")
1732                .expect("compile");
1733            assert!(module_id > 0);
1734
1735            // Instantiate
1736            let instance_id: u32 = ctx
1737                .eval(format!("__pi_wasm_instantiate_native({module_id})"))
1738                .expect("instantiate");
1739            assert!(instance_id > 0);
1740        });
1741    }
1742
1743    #[test]
1744    fn call_export_add() {
1745        let wasm_bytes = wat_to_wasm(
1746            r#"(module
1747              (func (export "add") (param i32 i32) (result i32)
1748                local.get 0 local.get 1 i32.add)
1749            )"#,
1750        );
1751        run_wasm_test(|ctx, _state| {
1752            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1753            for (i, &b) in wasm_bytes.iter().enumerate() {
1754                arr.set(i, i32::from(b)).unwrap();
1755            }
1756            ctx.globals().set("__test_bytes", arr).unwrap();
1757
1758            let result: i32 = ctx
1759                .eval(
1760                    r#"
1761                    var mid = __pi_wasm_compile_native(__test_bytes);
1762                    var iid = __pi_wasm_instantiate_native(mid);
1763                    __pi_wasm_call_export_native(iid, "add", [3, 4]);
1764                "#,
1765                )
1766                .expect("call add");
1767            assert_eq!(result, 7);
1768        });
1769    }
1770
1771    #[test]
1772    fn call_export_multiply() {
1773        let wasm_bytes = wat_to_wasm(
1774            r#"(module
1775              (func (export "mul") (param i32 i32) (result i32)
1776                local.get 0 local.get 1 i32.mul)
1777            )"#,
1778        );
1779        run_wasm_test(|ctx, _state| {
1780            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1781            for (i, &b) in wasm_bytes.iter().enumerate() {
1782                arr.set(i, i32::from(b)).unwrap();
1783            }
1784            ctx.globals().set("__test_bytes", arr).unwrap();
1785
1786            let result: i32 = ctx
1787                .eval(
1788                    r#"
1789                    var mid = __pi_wasm_compile_native(__test_bytes);
1790                    var iid = __pi_wasm_instantiate_native(mid);
1791                    __pi_wasm_call_export_native(iid, "mul", [6, 7]);
1792                "#,
1793                )
1794                .expect("call mul");
1795            assert_eq!(result, 42);
1796        });
1797    }
1798
1799    #[test]
1800    fn get_exports_lists_func_and_memory() {
1801        let wasm_bytes = wat_to_wasm(
1802            r#"(module
1803              (func (export "f1") (result i32) i32.const 1)
1804              (func (export "f2") (param i32) (result i32) local.get 0)
1805              (memory (export "mem") 2)
1806            )"#,
1807        );
1808        run_wasm_test(|ctx, _state| {
1809            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1810            for (i, &b) in wasm_bytes.iter().enumerate() {
1811                arr.set(i, i32::from(b)).unwrap();
1812            }
1813            ctx.globals().set("__test_bytes", arr).unwrap();
1814
1815            let count: i32 = ctx
1816                .eval(
1817                    r"
1818                    var mid = __pi_wasm_compile_native(__test_bytes);
1819                    var iid = __pi_wasm_instantiate_native(mid);
1820                    var exps = JSON.parse(__pi_wasm_get_exports_native(iid));
1821                    exps.length;
1822                ",
1823                )
1824                .expect("get exports count");
1825            assert_eq!(count, 3);
1826        });
1827    }
1828
1829    #[test]
1830    fn get_exports_json_handles_escaped_names() {
1831        let wasm_bytes = wat_to_wasm(
1832            r#"(module
1833              (func (export "name\"with_quote") (result i32) i32.const 1)
1834            )"#,
1835        );
1836        run_wasm_test(|ctx, _state| {
1837            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1838            for (i, &b) in wasm_bytes.iter().enumerate() {
1839                arr.set(i, i32::from(b)).unwrap();
1840            }
1841            ctx.globals().set("__test_bytes", arr).unwrap();
1842
1843            let name: String = ctx
1844                .eval(
1845                    r"
1846                    var mid = __pi_wasm_compile_native(__test_bytes);
1847                    var iid = __pi_wasm_instantiate_native(mid);
1848                    JSON.parse(__pi_wasm_get_exports_native(iid))[0].name;
1849                ",
1850                )
1851                .expect("parse export JSON");
1852            assert_eq!(name, "name\"with_quote");
1853        });
1854    }
1855
1856    #[test]
1857    fn memory_buffer_returns_arraybuffer() {
1858        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
1859        run_wasm_test(|ctx, _state| {
1860            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1861            for (i, &b) in wasm_bytes.iter().enumerate() {
1862                arr.set(i, i32::from(b)).unwrap();
1863            }
1864            ctx.globals().set("__test_bytes", arr).unwrap();
1865
1866            let size: i32 = ctx
1867                .eval(
1868                    r#"
1869                    var mid = __pi_wasm_compile_native(__test_bytes);
1870                    var iid = __pi_wasm_instantiate_native(mid);
1871                    var len = __pi_wasm_get_buffer_native(iid, "memory");
1872                    len;
1873                "#,
1874                )
1875                .expect("get buffer size");
1876            // 1 page = 64 KiB = 65536 bytes
1877            assert_eq!(size, 65536);
1878
1879            // Verify the ArrayBuffer was stored in the global
1880            let buf_size: i32 = ctx
1881                .eval("__pi_wasm_tmp_buf.byteLength")
1882                .expect("tmp buffer size");
1883            assert_eq!(buf_size, 65536);
1884        });
1885    }
1886
1887    #[test]
1888    fn memory_grow_succeeds() {
1889        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
1890        run_wasm_test(|ctx, _state| {
1891            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1892            for (i, &b) in wasm_bytes.iter().enumerate() {
1893                arr.set(i, i32::from(b)).unwrap();
1894            }
1895            ctx.globals().set("__test_bytes", arr).unwrap();
1896
1897            let prev: i32 = ctx
1898                .eval(
1899                    r#"
1900                    var mid = __pi_wasm_compile_native(__test_bytes);
1901                    var iid = __pi_wasm_instantiate_native(mid);
1902                    __pi_wasm_memory_grow_native(iid, "memory", 2);
1903                "#,
1904                )
1905                .expect("grow memory");
1906            // Previous size was 1 page
1907            assert_eq!(prev, 1);
1908
1909            let new_size: i32 = ctx
1910                .eval(r#"__pi_wasm_memory_size_native(iid, "memory")"#)
1911                .expect("memory size");
1912            assert_eq!(new_size, 3);
1913        });
1914    }
1915
1916    #[test]
1917    fn memory_grow_denied_by_policy() {
1918        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
1919        run_wasm_test(|ctx, state| {
1920            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1921            for (i, &b) in wasm_bytes.iter().enumerate() {
1922                arr.set(i, i32::from(b)).unwrap();
1923            }
1924            ctx.globals().set("__test_bytes", arr).unwrap();
1925
1926            let instance_id: u32 = ctx
1927                .eval(
1928                    r"
1929                    var mid = __pi_wasm_compile_native(__test_bytes);
1930                    __pi_wasm_instantiate_native(mid);
1931                ",
1932                )
1933                .expect("instantiate");
1934
1935            // Reduce max pages to 2 in the instance's store
1936            {
1937                let mut bridge = state.borrow_mut();
1938                let inst = bridge.instances.get_mut(&instance_id).unwrap();
1939                inst.store.data_mut().max_memory_pages = 2;
1940            }
1941
1942            // Try to grow by 5 pages → should be denied (1 + 5 > 2)
1943            let result: i32 = ctx
1944                .eval(format!(
1945                    "__pi_wasm_memory_grow_native({instance_id}, 'memory', 5)"
1946                ))
1947                .expect("grow denied");
1948            assert_eq!(result, -1);
1949        });
1950    }
1951
1952    #[test]
1953    fn compile_invalid_bytes_fails() {
1954        run_wasm_test(|ctx, _state| {
1955            let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native([0, 1, 2, 3])");
1956            assert!(result.is_err());
1957        });
1958    }
1959
1960    #[test]
1961    fn js_polyfill_webassembly_validate_accepts_valid_module() {
1962        let wasm_bytes = wat_to_wasm(r"(module)");
1963        run_wasm_test(|ctx, _state| {
1964            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1965            for (i, &b) in wasm_bytes.iter().enumerate() {
1966                arr.set(i, i32::from(b)).unwrap();
1967            }
1968            ctx.globals().set("__test_bytes", arr).unwrap();
1969
1970            let result: bool = ctx
1971                .eval("WebAssembly.validate(__test_bytes)")
1972                .expect("validate");
1973            assert!(result);
1974        });
1975    }
1976
1977    #[test]
1978    fn js_polyfill_webassembly_validate_rejects_invalid_module() {
1979        run_wasm_test(|ctx, _state| {
1980            let result: bool = ctx
1981                .eval("WebAssembly.validate([0, 1, 2, 3])")
1982                .expect("validate");
1983            assert!(!result);
1984        });
1985    }
1986
1987    #[test]
1988    fn js_polyfill_webassembly_validate_does_not_register_module() {
1989        let wasm_bytes = wat_to_wasm(r"(module)");
1990        run_wasm_test(|ctx, state| {
1991            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1992            for (i, &b) in wasm_bytes.iter().enumerate() {
1993                arr.set(i, i32::from(b)).unwrap();
1994            }
1995            ctx.globals().set("__test_bytes", arr).unwrap();
1996
1997            let before = state.borrow().modules.len();
1998            let result: bool = ctx
1999                .eval("WebAssembly.validate(__test_bytes)")
2000                .expect("validate");
2001            let after = state.borrow().modules.len();
2002
2003            assert!(result);
2004            assert_eq!(before, after);
2005        });
2006    }
2007
2008    #[test]
2009    fn js_polyfill_webassembly_table_basic() {
2010        run_wasm_test(|ctx, _state| {
2011            ctx.eval::<(), _>(
2012                r#"
2013                const table = new WebAssembly.Table({ element: "funcref", initial: 2, maximum: 3 });
2014                if (table.length !== 2) throw new Error("table length mismatch");
2015                table.set(0, function() { return 1; });
2016                if (typeof table.get(0) !== "function") throw new Error("table get failed");
2017                const old = table.grow(1);
2018                if (old !== 2 || table.length !== 3) throw new Error("table grow failed");
2019                let threw = false;
2020                try { table.grow(1); } catch (e) { threw = true; }
2021                if (!threw) throw new Error("table maximum not enforced");
2022                "#,
2023            )
2024            .expect("table polyfill");
2025        });
2026    }
2027
2028    #[test]
2029    fn js_polyfill_webassembly_table_validation() {
2030        run_wasm_test(|ctx, _state| {
2031            let result: bool = ctx
2032                .eval(
2033                    r#"
2034                    (() => {
2035                        const table = new WebAssembly.Table({ element: "funcref", initial: 1, maximum: 2 });
2036                        let ok = false;
2037                        try { table.get(0.5); } catch (e) { ok = e instanceof RangeError; }
2038                        if (!ok) return false;
2039                        ok = false;
2040                        try { table.grow(1.25); } catch (e) { ok = e instanceof RangeError; }
2041                        if (!ok) return false;
2042                        ok = false;
2043                        try { table.grow(2); } catch (e) { ok = e instanceof RangeError; }
2044                        return ok;
2045                    })()
2046                    "#,
2047                )
2048                .expect("table validation");
2049            assert!(result);
2050        });
2051    }
2052
2053    #[test]
2054    fn js_polyfill_webassembly_memory_validation() {
2055        run_wasm_test(|ctx, _state| {
2056            let result: bool = ctx
2057                .eval(
2058                    r"
2059                    (() => {
2060                        const mem = new WebAssembly.Memory({ initial: 1, maximum: 1 });
2061                        let ok = false;
2062                        try { mem.grow(0.5); } catch (e) { ok = e instanceof RangeError; }
2063                        if (!ok) return false;
2064                        ok = false;
2065                        try { mem.grow(1); } catch (e) { ok = e instanceof RangeError; }
2066                        return ok;
2067                    })()
2068                    ",
2069                )
2070                .expect("memory validation");
2071            assert!(result);
2072        });
2073    }
2074
2075    #[test]
2076    fn js_polyfill_webassembly_global_basic() {
2077        run_wasm_test(|ctx, _state| {
2078            ctx.eval::<(), _>(
2079                r#"
2080                const g = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
2081                if (g.value !== 1) throw new Error("global value mismatch");
2082                g.value = 2;
2083                if (g.value !== 2) throw new Error("global set failed");
2084                const imm = new WebAssembly.Global({ value: "i32" }, 7);
2085                let threw = false;
2086                try { imm.value = 9; } catch (e) { threw = true; }
2087                if (!threw) throw new Error("immutable global should throw");
2088                "#,
2089            )
2090            .expect("global polyfill");
2091        });
2092    }
2093
2094    #[test]
2095    fn instantiate_nonexistent_module_fails() {
2096        run_wasm_test(|ctx, _state| {
2097            let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_instantiate_native(99999)");
2098            assert!(result.is_err());
2099        });
2100    }
2101
2102    #[test]
2103    fn compile_rejects_when_module_limit_reached() {
2104        let wasm_bytes = wat_to_wasm(r"(module)");
2105        run_wasm_test(|ctx, state| {
2106            state.borrow_mut().set_limits_for_test(1, 8);
2107
2108            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2109            for (i, &b) in wasm_bytes.iter().enumerate() {
2110                arr.set(i, i32::from(b)).unwrap();
2111            }
2112            ctx.globals().set("__test_bytes", arr).unwrap();
2113
2114            let first: u32 = ctx
2115                .eval("__pi_wasm_compile_native(__test_bytes)")
2116                .expect("first compile");
2117            assert!(first > 0);
2118
2119            let second: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native(__test_bytes)");
2120            assert!(second.is_err());
2121        });
2122    }
2123
2124    #[test]
2125    fn instantiate_rejects_when_instance_limit_reached() {
2126        let wasm_bytes = wat_to_wasm(r"(module)");
2127        run_wasm_test(|ctx, state| {
2128            state.borrow_mut().set_limits_for_test(8, 1);
2129
2130            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2131            for (i, &b) in wasm_bytes.iter().enumerate() {
2132                arr.set(i, i32::from(b)).unwrap();
2133            }
2134            ctx.globals().set("__test_bytes", arr).unwrap();
2135
2136            let module_id: u32 = ctx
2137                .eval("__pi_wasm_compile_native(__test_bytes)")
2138                .expect("compile");
2139
2140            let first: u32 = ctx
2141                .eval(format!("__pi_wasm_instantiate_native({module_id})"))
2142                .expect("first instantiate");
2143            assert!(first > 0);
2144
2145            let second: rquickjs::Result<u32> =
2146                ctx.eval(format!("__pi_wasm_instantiate_native({module_id})"));
2147            assert!(second.is_err());
2148        });
2149    }
2150
2151    #[test]
2152    fn alloc_id_skips_zero_on_wrap() {
2153        let wasm_bytes = wat_to_wasm(r"(module)");
2154        run_wasm_test(|ctx, state| {
2155            {
2156                let mut bridge = state.borrow_mut();
2157                bridge.set_limits_for_test(8, 8);
2158                bridge.next_id = MAX_JS_WASM_ID;
2159            }
2160
2161            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2162            for (i, &b) in wasm_bytes.iter().enumerate() {
2163                arr.set(i, i32::from(b)).unwrap();
2164            }
2165            ctx.globals().set("__test_bytes", arr).unwrap();
2166
2167            let first: i32 = ctx
2168                .eval("__pi_wasm_compile_native(__test_bytes)")
2169                .expect("first compile");
2170            let second: i32 = ctx
2171                .eval("__pi_wasm_compile_native(__test_bytes)")
2172                .expect("second compile");
2173
2174            assert_eq!(first, i32::MAX);
2175            assert_eq!(second, 1);
2176        });
2177    }
2178
2179    #[test]
2180    fn call_nonexistent_export_fails() {
2181        let wasm_bytes = wat_to_wasm(r#"(module (func (export "f") (result i32) i32.const 1))"#);
2182        run_wasm_test(|ctx, _state| {
2183            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2184            for (i, &b) in wasm_bytes.iter().enumerate() {
2185                arr.set(i, i32::from(b)).unwrap();
2186            }
2187            ctx.globals().set("__test_bytes", arr).unwrap();
2188
2189            let result: rquickjs::Result<i32> = ctx.eval(
2190                r#"
2191                var mid = __pi_wasm_compile_native(__test_bytes);
2192                var iid = __pi_wasm_instantiate_native(mid);
2193                __pi_wasm_call_export_native(iid, "nonexistent", []);
2194            "#,
2195            );
2196            assert!(result.is_err());
2197        });
2198    }
2199
2200    #[test]
2201    fn call_export_i64_param_is_rejected() {
2202        let wasm_bytes = wat_to_wasm(
2203            r#"(module
2204              (func (export "id64") (param i64) (result i64)
2205                local.get 0)
2206            )"#,
2207        );
2208        run_wasm_test(|ctx, _state| {
2209            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2210            for (i, &b) in wasm_bytes.iter().enumerate() {
2211                arr.set(i, i32::from(b)).unwrap();
2212            }
2213            ctx.globals().set("__test_bytes", arr).unwrap();
2214
2215            let result: rquickjs::Result<i32> = ctx.eval(
2216                r#"
2217                var mid = __pi_wasm_compile_native(__test_bytes);
2218                var iid = __pi_wasm_instantiate_native(mid);
2219                __pi_wasm_call_export_native(iid, "id64", [1]);
2220            "#,
2221            );
2222            assert!(result.is_err());
2223        });
2224    }
2225
2226    #[test]
2227    fn call_export_i64_result_is_rejected() {
2228        let wasm_bytes = wat_to_wasm(
2229            r#"(module
2230              (func (export "ret64") (result i64)
2231                i64.const 42)
2232            )"#,
2233        );
2234        run_wasm_test(|ctx, _state| {
2235            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2236            for (i, &b) in wasm_bytes.iter().enumerate() {
2237                arr.set(i, i32::from(b)).unwrap();
2238            }
2239            ctx.globals().set("__test_bytes", arr).unwrap();
2240
2241            let result: rquickjs::Result<i32> = ctx.eval(
2242                r#"
2243                var mid = __pi_wasm_compile_native(__test_bytes);
2244                var iid = __pi_wasm_instantiate_native(mid);
2245                __pi_wasm_call_export_native(iid, "ret64", []);
2246            "#,
2247            );
2248            assert!(result.is_err());
2249        });
2250    }
2251
2252    #[test]
2253    fn call_export_multivalue_result_is_rejected() {
2254        let wasm_bytes = wat_to_wasm(
2255            r#"(module
2256              (func (export "pair") (result i32 i32)
2257                i32.const 1
2258                i32.const 2)
2259            )"#,
2260        );
2261        run_wasm_test(|ctx, _state| {
2262            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2263            for (i, &b) in wasm_bytes.iter().enumerate() {
2264                arr.set(i, i32::from(b)).unwrap();
2265            }
2266            ctx.globals().set("__test_bytes", arr).unwrap();
2267
2268            let result: rquickjs::Result<i32> = ctx.eval(
2269                r#"
2270                var mid = __pi_wasm_compile_native(__test_bytes);
2271                var iid = __pi_wasm_instantiate_native(mid);
2272                __pi_wasm_call_export_native(iid, "pair", []);
2273            "#,
2274            );
2275            assert!(result.is_err());
2276        });
2277    }
2278
2279    #[test]
2280    fn call_export_externref_result_is_rejected() {
2281        let wasm_bytes = wat_to_wasm(
2282            r#"(module
2283              (func (export "retref") (result externref)
2284                ref.null extern)
2285            )"#,
2286        );
2287        run_wasm_test(|ctx, _state| {
2288            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2289            for (i, &b) in wasm_bytes.iter().enumerate() {
2290                arr.set(i, i32::from(b)).unwrap();
2291            }
2292            ctx.globals().set("__test_bytes", arr).unwrap();
2293
2294            let result: rquickjs::Result<i32> = ctx.eval(
2295                r#"
2296                var mid = __pi_wasm_compile_native(__test_bytes);
2297                var iid = __pi_wasm_instantiate_native(mid);
2298                __pi_wasm_call_export_native(iid, "retref", []);
2299            "#,
2300            );
2301            assert!(result.is_err());
2302        });
2303    }
2304
2305    #[test]
2306    fn js_polyfill_webassembly_instantiate() {
2307        let wasm_bytes = wat_to_wasm(
2308            r#"(module
2309              (func (export "add") (param i32 i32) (result i32)
2310                local.get 0 local.get 1 i32.add)
2311            )"#,
2312        );
2313        run_wasm_test(|ctx, _state| {
2314            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2315            for (i, &b) in wasm_bytes.iter().enumerate() {
2316                arr.set(i, i32::from(b)).unwrap();
2317            }
2318            ctx.globals().set("__test_bytes", arr).unwrap();
2319
2320            // Use the full JS polyfill API (synchronous for QuickJS)
2321            let has_wa: bool = ctx
2322                .eval("typeof globalThis.WebAssembly !== 'undefined'")
2323                .expect("check WebAssembly");
2324            assert!(has_wa);
2325
2326            // WebAssembly.instantiate returns a Promise; in QuickJS we can
2327            // resolve it synchronously via .then()
2328            let result: i32 = ctx
2329                .eval(
2330                    r"
2331                    var __test_result = -1;
2332                    WebAssembly.instantiate(__test_bytes).then(function(r) {
2333                        __test_result = r.instance.exports.add(10, 20);
2334                    });
2335                    __test_result;
2336                ",
2337                )
2338                .expect("polyfill instantiate");
2339            assert_eq!(result, 30);
2340        });
2341    }
2342
2343    #[test]
2344    fn js_polyfill_webassembly_compile_streaming() {
2345        let wasm_bytes = wat_to_wasm(
2346            r#"(module
2347              (func (export "add") (param i32 i32) (result i32)
2348                local.get 0 local.get 1 i32.add)
2349            )"#,
2350        );
2351        run_wasm_test(|ctx, _state| {
2352            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2353            for (i, &b) in wasm_bytes.iter().enumerate() {
2354                arr.set(i, i32::from(b)).unwrap();
2355            }
2356            ctx.globals().set("__test_bytes", arr).unwrap();
2357
2358            let result: i32 = ctx
2359                .eval(
2360                    r"
2361                    var __test_result = -1;
2362                    var __resp = { arrayBuffer: function() { return new Uint8Array(__test_bytes).buffer; } };
2363                    WebAssembly.compileStreaming(__resp).then(function(mod) {
2364                        WebAssembly.instantiate(mod).then(function(r) {
2365                            __test_result = r.exports.add(2, 3);
2366                        });
2367                    });
2368                    __test_result;
2369                ",
2370                )
2371                .expect("polyfill compileStreaming");
2372            assert_eq!(result, 5);
2373        });
2374    }
2375
2376    #[test]
2377    fn js_polyfill_webassembly_instantiate_streaming() {
2378        let wasm_bytes = wat_to_wasm(
2379            r#"(module
2380              (func (export "add") (param i32 i32) (result i32)
2381                local.get 0 local.get 1 i32.add)
2382            )"#,
2383        );
2384        run_wasm_test(|ctx, _state| {
2385            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2386            for (i, &b) in wasm_bytes.iter().enumerate() {
2387                arr.set(i, i32::from(b)).unwrap();
2388            }
2389            ctx.globals().set("__test_bytes", arr).unwrap();
2390
2391            let result: i32 = ctx
2392                .eval(
2393                    r"
2394                    var __test_result = -1;
2395                    var __resp = { arrayBuffer: function() { return new Uint8Array(__test_bytes).buffer; } };
2396                    WebAssembly.instantiateStreaming(__resp).then(function(r) {
2397                        __test_result = r.instance.exports.add(6, 7);
2398                    });
2399                    __test_result;
2400                ",
2401                )
2402                .expect("polyfill instantiateStreaming");
2403            assert_eq!(result, 13);
2404        });
2405    }
2406
2407    #[test]
2408    fn js_polyfill_memory_buffer_getter() {
2409        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
2410        run_wasm_test(|ctx, _state| {
2411            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2412            for (i, &b) in wasm_bytes.iter().enumerate() {
2413                arr.set(i, i32::from(b)).unwrap();
2414            }
2415            ctx.globals().set("__test_bytes", arr).unwrap();
2416
2417            let size: i32 = ctx
2418                .eval(
2419                    r"
2420                    var __test_size = -1;
2421                    WebAssembly.instantiate(__test_bytes).then(function(r) {
2422                        __test_size = r.instance.exports.memory.buffer.byteLength;
2423                    });
2424                    __test_size;
2425                ",
2426                )
2427                .expect("polyfill memory buffer");
2428            assert_eq!(size, 65536);
2429        });
2430    }
2431
2432    #[test]
2433    fn js_polyfill_exported_memory_is_webassembly_memory() {
2434        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
2435        run_wasm_test(|ctx, _state| {
2436            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2437            for (i, &b) in wasm_bytes.iter().enumerate() {
2438                arr.set(i, i32::from(b)).unwrap();
2439            }
2440            ctx.globals().set("__test_bytes", arr).unwrap();
2441
2442            let is_memory: bool = ctx
2443                .eval(
2444                    r"
2445                    var __is_memory = false;
2446                    WebAssembly.instantiate(__test_bytes).then(function(r) {
2447                        __is_memory = r.instance.exports.memory instanceof WebAssembly.Memory;
2448                    });
2449                    __is_memory;
2450                ",
2451                )
2452                .expect("exported memory instanceof WebAssembly.Memory");
2453            assert!(is_memory);
2454        });
2455    }
2456
2457    #[test]
2458    fn js_polyfill_memory_grow_returns_previous_pages() {
2459        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
2460        run_wasm_test(|ctx, _state| {
2461            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2462            for (i, &b) in wasm_bytes.iter().enumerate() {
2463                arr.set(i, i32::from(b)).unwrap();
2464            }
2465            ctx.globals().set("__test_bytes", arr).unwrap();
2466
2467            let prev_pages: i32 = ctx
2468                .eval(
2469                    r"
2470                    var __test_prev = -1;
2471                    WebAssembly.instantiate(__test_bytes).then(function(r) {
2472                        __test_prev = r.instance.exports.memory.grow(2);
2473                    });
2474                    __test_prev;
2475                ",
2476                )
2477                .expect("polyfill memory grow");
2478            assert_eq!(prev_pages, 1);
2479
2480            let new_size: i32 = ctx
2481                .eval(
2482                    r"
2483                    var __test_size = -1;
2484                    WebAssembly.instantiate(__test_bytes).then(function(r) {
2485                        r.instance.exports.memory.grow(2);
2486                        __test_size = r.instance.exports.memory.buffer.byteLength;
2487                    });
2488                    __test_size;
2489                ",
2490                )
2491                .expect("polyfill memory size after grow");
2492            assert_eq!(new_size, 3 * 65536);
2493        });
2494    }
2495
2496    #[test]
2497    fn js_polyfill_memory_grow_failure_throws_range_error() {
2498        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 1))"#);
2499        run_wasm_test(|ctx, _state| {
2500            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2501            for (i, &b) in wasm_bytes.iter().enumerate() {
2502                arr.set(i, i32::from(b)).unwrap();
2503            }
2504            ctx.globals().set("__test_bytes", arr).unwrap();
2505
2506            let threw_range_error: bool = ctx
2507                .eval(
2508                    r"
2509                    var __threw_range_error = false;
2510                    WebAssembly.instantiate(__test_bytes).then(function(r) {
2511                        try {
2512                            r.instance.exports.memory.grow(1);
2513                        } catch (e) {
2514                            __threw_range_error = e instanceof RangeError;
2515                        }
2516                    });
2517                    __threw_range_error;
2518                ",
2519                )
2520                .expect("polyfill memory grow failure");
2521            assert!(threw_range_error);
2522        });
2523    }
2524
2525    #[test]
2526    fn js_memory_constructor_grow_preserves_existing_bytes() {
2527        run_wasm_test(|ctx, _state| {
2528            let summary: String = ctx
2529                .eval(
2530                    r#"
2531                    var mem = new WebAssembly.Memory({ initial: 1 });
2532                    var before = new Uint8Array(mem.buffer);
2533                    before[0] = 7;
2534                    before[65535] = 9;
2535                    var prev = mem.grow(1);
2536                    var after = new Uint8Array(mem.buffer);
2537                    [prev, after.byteLength, after[0], after[65535], after[65536]].join(",");
2538                "#,
2539                )
2540                .expect("memory constructor grow preserves bytes");
2541            assert_eq!(summary, "1,131072,7,9,0");
2542        });
2543    }
2544
2545    #[test]
2546    fn module_with_imports_instantiates_with_stubs() {
2547        let wasm_bytes = wat_to_wasm(
2548            r#"(module
2549              (import "env" "log" (func (param i32)))
2550              (func (export "run") (result i32)
2551                i32.const 42
2552                call 0
2553                i32.const 1)
2554            )"#,
2555        );
2556        run_wasm_test(|ctx, _state| {
2557            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2558            for (i, &b) in wasm_bytes.iter().enumerate() {
2559                arr.set(i, i32::from(b)).unwrap();
2560            }
2561            ctx.globals().set("__test_bytes", arr).unwrap();
2562
2563            let result: i32 = ctx
2564                .eval(
2565                    r#"
2566                    var mid = __pi_wasm_compile_native(__test_bytes);
2567                    var iid = __pi_wasm_instantiate_native(mid);
2568                    __pi_wasm_call_export_native(iid, "run", []);
2569                "#,
2570                )
2571                .expect("call with import stubs");
2572            assert_eq!(result, 1);
2573        });
2574    }
2575
2576    #[test]
2577    fn native_memory_helpers_round_trip_live_wasm_memory() {
2578        let wasm_bytes = wat_to_wasm(
2579            r#"(module
2580              (memory (export "memory") 1)
2581              (func (export "read32") (param i32) (result i32)
2582                local.get 0
2583                i32.load)
2584              (func (export "write32") (param i32 i32)
2585                local.get 0
2586                local.get 1
2587                i32.store)
2588            )"#,
2589        );
2590        run_wasm_test(|ctx, _state| {
2591            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2592            for (i, &b) in wasm_bytes.iter().enumerate() {
2593                arr.set(i, i32::from(b)).unwrap();
2594            }
2595            ctx.globals().set("__test_bytes", arr).unwrap();
2596
2597            let summary: String = ctx
2598                .eval(
2599                    r#"
2600                    var mid = __pi_wasm_compile_native(__test_bytes);
2601                    var iid = __pi_wasm_instantiate_native(mid);
2602                    __pi_wasm_memory_write_native(iid, "memory", 32, [1, 2, 3, 4]);
2603                    var readBack = __pi_wasm_call_export_native(iid, "read32", [32]);
2604                    __pi_wasm_call_export_native(iid, "write32", [40, 0x11223344]);
2605                    var bytes = __pi_wasm_memory_read_native(iid, "memory", 40, 4);
2606                    [readBack, bytes[0], bytes[1], bytes[2], bytes[3]].join(",");
2607                "#,
2608                )
2609                .expect("memory helpers round-trip");
2610            assert_eq!(summary, "67305985,68,51,34,17");
2611        });
2612    }
2613
2614    #[test]
2615    fn staged_file_host_imports_can_open_and_read_wad_bytes() {
2616        let wasm_bytes = wat_to_wasm(
2617            r#"(module
2618              (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2619              (import "env" "fd_read" (func $fd_read (param i32 i32 i32 i32) (result i32)))
2620              (import "env" "fd_close" (func $fd_close (param i32) (result i32)))
2621              (memory (export "memory") 1)
2622              (data (i32.const 64) "/doom/doom1.wad\00")
2623              (func (export "readfirst4") (result i32)
2624                (local $fd i32)
2625                i32.const 96
2626                i32.const 128
2627                i32.store
2628                i32.const 100
2629                i32.const 4
2630                i32.store
2631                i32.const -100
2632                i32.const 64
2633                i32.const 0
2634                i32.const 0
2635                call $openat
2636                local.tee $fd
2637                i32.const 96
2638                i32.const 1
2639                i32.const 104
2640                call $fd_read
2641                drop
2642                local.get $fd
2643                call $fd_close
2644                drop
2645                i32.const 128
2646                i32.load)
2647              (func (export "bytes_read") (result i32)
2648                i32.const 104
2649                i32.load)
2650            )"#,
2651        );
2652        run_wasm_test(|ctx, _state| {
2653            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2654            for (i, &b) in wasm_bytes.iter().enumerate() {
2655                arr.set(i, i32::from(b)).unwrap();
2656            }
2657            ctx.globals().set("__test_bytes", arr).unwrap();
2658
2659            let summary: String = ctx
2660                .eval(
2661                    r#"
2662                    __pi_wasm_stage_file_native("/doom/doom1.wad", [1, 2, 3, 4]);
2663                    var mid = __pi_wasm_compile_native(__test_bytes);
2664                    var iid = __pi_wasm_instantiate_native(mid);
2665                    var first = __pi_wasm_call_export_native(iid, "readfirst4", []);
2666                    var bytes = __pi_wasm_call_export_native(iid, "bytes_read", []);
2667                    [first, bytes].join(",");
2668                "#,
2669                )
2670                .expect("staged file read");
2671            assert_eq!(summary, "67305985,4");
2672        });
2673    }
2674
2675    #[test]
2676    fn staged_file_host_imports_can_create_and_write_virtual_files() {
2677        let wasm_bytes = wat_to_wasm(
2678            r#"(module
2679              (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2680              (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2681              (memory (export "memory") 1)
2682              (data (i32.const 64) "/tmp/out.bin\00")
2683              (data (i32.const 160) "\05\06\07")
2684              (func (export "write_new") (result i32)
2685                (local $fd i32)
2686                i32.const 128
2687                i32.const 160
2688                i32.store
2689                i32.const 132
2690                i32.const 3
2691                i32.store
2692                i32.const -100
2693                i32.const 64
2694                i32.const 577
2695                i32.const 0
2696                call $openat
2697                local.set $fd
2698                local.get $fd
2699                i32.const 128
2700                i32.const 1
2701                i32.const 136
2702                call $fd_write
2703                drop
2704                i32.const 136
2705                i32.load)
2706            )"#,
2707        );
2708        run_wasm_test(|ctx, state| {
2709            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2710            for (i, &b) in wasm_bytes.iter().enumerate() {
2711                arr.set(i, i32::from(b)).unwrap();
2712            }
2713            ctx.globals().set("__test_bytes", arr).unwrap();
2714
2715            let instance_id: u32 = ctx
2716                .eval(
2717                    r"
2718                    var mid = __pi_wasm_compile_native(__test_bytes);
2719                    __pi_wasm_instantiate_native(mid);
2720                ",
2721                )
2722                .expect("instantiate writable virtual file module");
2723
2724            let bytes_written: i32 = ctx
2725                .eval(format!(
2726                    r#"__pi_wasm_call_export_native({instance_id}, "write_new", [])"#
2727                ))
2728                .expect("write newly created virtual file");
2729            assert_eq!(bytes_written, 3);
2730
2731            let bridge = state.borrow();
2732            let contents = bridge
2733                .instances
2734                .get(&instance_id)
2735                .and_then(|inst| inst.store.data().staged_files.get("/tmp/out.bin"))
2736                .map(|arc| (**arc).clone());
2737            assert_eq!(contents, Some(vec![5, 6, 7]));
2738        });
2739    }
2740
2741    #[test]
2742    fn staged_file_host_imports_honor_truncate_flag_for_writes() {
2743        let wasm_bytes = wat_to_wasm(
2744            r#"(module
2745              (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2746              (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2747              (memory (export "memory") 1)
2748              (data (i32.const 64) "/tmp/existing.bin\00")
2749              (data (i32.const 160) "\09")
2750              (func (export "truncate_then_write") (result i32)
2751                (local $fd i32)
2752                i32.const 128
2753                i32.const 160
2754                i32.store
2755                i32.const 132
2756                i32.const 1
2757                i32.store
2758                i32.const -100
2759                i32.const 64
2760                i32.const 513
2761                i32.const 0
2762                call $openat
2763                local.set $fd
2764                local.get $fd
2765                i32.const 128
2766                i32.const 1
2767                i32.const 136
2768                call $fd_write
2769                drop
2770                i32.const 136
2771                i32.load)
2772            )"#,
2773        );
2774        run_wasm_test(|ctx, state| {
2775            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2776            for (i, &b) in wasm_bytes.iter().enumerate() {
2777                arr.set(i, i32::from(b)).unwrap();
2778            }
2779            ctx.globals().set("__test_bytes", arr).unwrap();
2780
2781            let instance_id: u32 = ctx
2782                .eval(
2783                    r#"
2784                    __pi_wasm_stage_file_native("/tmp/existing.bin", [1, 2, 3, 4]);
2785                    var mid = __pi_wasm_compile_native(__test_bytes);
2786                    __pi_wasm_instantiate_native(mid);
2787                "#,
2788                )
2789                .expect("instantiate truncate virtual file module");
2790
2791            let bytes_written: i32 = ctx
2792                .eval(format!(
2793                    r#"__pi_wasm_call_export_native({instance_id}, "truncate_then_write", [])"#
2794                ))
2795                .expect("truncate existing virtual file");
2796            assert_eq!(bytes_written, 1);
2797
2798            let bridge = state.borrow();
2799            let contents = bridge
2800                .instances
2801                .get(&instance_id)
2802                .and_then(|inst| inst.store.data().staged_files.get("/tmp/existing.bin"))
2803                .map(|arc| (**arc).clone());
2804            assert_eq!(contents, Some(vec![9]));
2805        });
2806    }
2807
2808    #[test]
2809    fn staged_file_host_imports_reject_write_on_read_only_descriptor() {
2810        let wasm_bytes = wat_to_wasm(
2811            r#"(module
2812              (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2813              (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2814              (memory (export "memory") 1)
2815              (data (i32.const 64) "/doom/doom1.wad\00")
2816              (data (i32.const 160) "\09\08")
2817              (func (export "write_read_only") (result i32)
2818                (local $fd i32)
2819                i32.const 128
2820                i32.const 160
2821                i32.store
2822                i32.const 132
2823                i32.const 2
2824                i32.store
2825                i32.const -100
2826                i32.const 64
2827                i32.const 0
2828                i32.const 0
2829                call $openat
2830                local.set $fd
2831                local.get $fd
2832                i32.const 128
2833                i32.const 1
2834                i32.const 136
2835                call $fd_write)
2836              (func (export "bytes_written") (result i32)
2837                i32.const 136
2838                i32.load)
2839            )"#,
2840        );
2841        run_wasm_test(|ctx, _state| {
2842            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2843            for (i, &b) in wasm_bytes.iter().enumerate() {
2844                arr.set(i, i32::from(b)).unwrap();
2845            }
2846            ctx.globals().set("__test_bytes", arr).unwrap();
2847
2848            let summary: String = ctx
2849                .eval(
2850                    r#"
2851                    __pi_wasm_stage_file_native("/doom/doom1.wad", [1, 2, 3, 4]);
2852                    var mid = __pi_wasm_compile_native(__test_bytes);
2853                    var iid = __pi_wasm_instantiate_native(mid);
2854                    var result = __pi_wasm_call_export_native(iid, "write_read_only", []);
2855                    var bytes = __pi_wasm_call_export_native(iid, "bytes_written", []);
2856                    [result, bytes].join(",");
2857                "#,
2858                )
2859                .expect("read-only descriptor write rejection");
2860            assert_eq!(summary, "8,0");
2861        });
2862    }
2863
2864    #[test]
2865    fn staged_file_host_imports_reject_write_past_virtual_file_limit() {
2866        let wasm_bytes = wat_to_wasm(&format!(
2867            r#"(module
2868              (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2869              (import "env" "fd_seek" (func $fd_seek (param i32 i64 i32 i32) (result i32)))
2870              (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2871              (memory (export "memory") 1)
2872              (data (i32.const 64) "/tmp/too-big.bin\00")
2873              (data (i32.const 160) "\07")
2874              (func (export "seek_then_write_too_large") (result i32)
2875                (local $fd i32)
2876                i32.const 128
2877                i32.const 160
2878                i32.store
2879                i32.const 132
2880                i32.const 1
2881                i32.store
2882                i32.const -100
2883                i32.const 64
2884                i32.const 577
2885                i32.const 0
2886                call $openat
2887                local.set $fd
2888                local.get $fd
2889                i64.const {MAX_VIRTUAL_FILE_BYTES}
2890                i32.const 0
2891                i32.const 144
2892                call $fd_seek
2893                drop
2894                local.get $fd
2895                i32.const 128
2896                i32.const 1
2897                i32.const 136
2898                call $fd_write)
2899              (func (export "bytes_written") (result i32)
2900                i32.const 136
2901                i32.load)
2902            )"#,
2903        ));
2904        run_wasm_test(|ctx, state| {
2905            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2906            for (i, &b) in wasm_bytes.iter().enumerate() {
2907                arr.set(i, i32::from(b)).unwrap();
2908            }
2909            ctx.globals().set("__test_bytes", arr).unwrap();
2910
2911            let instance_id: u32 = ctx
2912                .eval(
2913                    r"
2914                    var mid = __pi_wasm_compile_native(__test_bytes);
2915                    __pi_wasm_instantiate_native(mid);
2916                ",
2917                )
2918                .expect("instantiate large seek virtual file module");
2919
2920            let summary: String = ctx
2921                .eval(format!(
2922                    r#"
2923                    var result = __pi_wasm_call_export_native({instance_id}, "seek_then_write_too_large", []);
2924                    var bytes = __pi_wasm_call_export_native({instance_id}, "bytes_written", []);
2925                    [result, bytes].join(",");
2926                "#
2927                ))
2928                .expect("reject oversize virtual file write");
2929            assert_eq!(summary, "27,0");
2930
2931            let bridge = state.borrow();
2932            let len = bridge
2933                .instances
2934                .get(&instance_id)
2935                .and_then(|inst| inst.store.data().staged_files.get("/tmp/too-big.bin"))
2936                .map(|v| v.len());
2937            assert_eq!(len, Some(0));
2938        });
2939    }
2940
2941    #[test]
2942    fn staged_file_host_imports_reject_multi_iov_limit_overflow_atomically() {
2943        let near_limit = MAX_VIRTUAL_FILE_BYTES - 1;
2944        let wasm_bytes = wat_to_wasm(&format!(
2945            r#"(module
2946              (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
2947              (import "env" "fd_seek" (func $fd_seek (param i32 i64 i32 i32) (result i32)))
2948              (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
2949              (memory (export "memory") 1)
2950              (data (i32.const 64) "/tmp/too-big-split.bin\00")
2951              (data (i32.const 160) "\07\08")
2952              (func (export "split_write_too_large") (result i32)
2953                (local $fd i32)
2954                i32.const 128
2955                i32.const 160
2956                i32.store
2957                i32.const 132
2958                i32.const 1
2959                i32.store
2960                i32.const 136
2961                i32.const 161
2962                i32.store
2963                i32.const 140
2964                i32.const 1
2965                i32.store
2966                i32.const -100
2967                i32.const 64
2968                i32.const 577
2969                i32.const 0
2970                call $openat
2971                local.set $fd
2972                local.get $fd
2973                i64.const {near_limit}
2974                i32.const 0
2975                i32.const 152
2976                call $fd_seek
2977                drop
2978                local.get $fd
2979                i32.const 128
2980                i32.const 2
2981                i32.const 144
2982                call $fd_write)
2983              (func (export "bytes_written") (result i32)
2984                i32.const 144
2985                i32.load)
2986            )"#,
2987        ));
2988        run_wasm_test(|ctx, state| {
2989            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
2990            for (i, &b) in wasm_bytes.iter().enumerate() {
2991                arr.set(i, i32::from(b)).unwrap();
2992            }
2993            ctx.globals().set("__test_bytes", arr).unwrap();
2994
2995            let instance_id: u32 = ctx
2996                .eval(
2997                    r"
2998                    var mid = __pi_wasm_compile_native(__test_bytes);
2999                    __pi_wasm_instantiate_native(mid);
3000                ",
3001                )
3002                .expect("instantiate split oversize virtual file module");
3003
3004            let summary: String = ctx
3005                .eval(format!(
3006                    r#"
3007                    var result = __pi_wasm_call_export_native({instance_id}, "split_write_too_large", []);
3008                    var bytes = __pi_wasm_call_export_native({instance_id}, "bytes_written", []);
3009                    [result, bytes].join(",");
3010                "#
3011                ))
3012                .expect("reject split oversize virtual file write");
3013            assert_eq!(summary, "27,0");
3014
3015            let bridge = state.borrow();
3016            let len = bridge
3017                .instances
3018                .get(&instance_id)
3019                .and_then(|inst| inst.store.data().staged_files.get("/tmp/too-big-split.bin"))
3020                .map(|v| v.len());
3021            assert_eq!(len, Some(0));
3022        });
3023    }
3024
3025    #[test]
3026    fn staged_file_host_imports_allow_zero_length_write_past_virtual_file_limit() {
3027        let past_limit = MAX_VIRTUAL_FILE_BYTES + 1;
3028        let wasm_bytes = wat_to_wasm(&format!(
3029            r#"(module
3030              (import "env" "__syscall_openat" (func $openat (param i32 i32 i32 i32) (result i32)))
3031              (import "env" "fd_seek" (func $fd_seek (param i32 i64 i32 i32) (result i32)))
3032              (import "env" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
3033              (memory (export "memory") 1)
3034              (data (i32.const 64) "/tmp/too-big-zero.bin\00")
3035              (func (export "zero_write_after_large_seek") (result i32)
3036                (local $fd i32)
3037                i32.const 128
3038                i32.const 160
3039                i32.store
3040                i32.const 132
3041                i32.const 0
3042                i32.store
3043                i32.const -100
3044                i32.const 64
3045                i32.const 577
3046                i32.const 0
3047                call $openat
3048                local.set $fd
3049                local.get $fd
3050                i64.const {past_limit}
3051                i32.const 0
3052                i32.const 144
3053                call $fd_seek
3054                drop
3055                local.get $fd
3056                i32.const 128
3057                i32.const 1
3058                i32.const 136
3059                call $fd_write)
3060              (func (export "bytes_written") (result i32)
3061                i32.const 136
3062                i32.load)
3063            )"#,
3064        ));
3065        run_wasm_test(|ctx, state| {
3066            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
3067            for (i, &b) in wasm_bytes.iter().enumerate() {
3068                arr.set(i, i32::from(b)).unwrap();
3069            }
3070            ctx.globals().set("__test_bytes", arr).unwrap();
3071
3072            let instance_id: u32 = ctx
3073                .eval(
3074                    r"
3075                    var mid = __pi_wasm_compile_native(__test_bytes);
3076                    __pi_wasm_instantiate_native(mid);
3077                ",
3078                )
3079                .expect("instantiate zero-write virtual file module");
3080
3081            let summary: String = ctx
3082                .eval(format!(
3083                    r#"
3084                    var result = __pi_wasm_call_export_native({instance_id}, "zero_write_after_large_seek", []);
3085                    var bytes = __pi_wasm_call_export_native({instance_id}, "bytes_written", []);
3086                    [result, bytes].join(",");
3087                "#
3088                ))
3089                .expect("allow zero-length write after large seek");
3090            assert_eq!(summary, "0,0");
3091
3092            let bridge = state.borrow();
3093            let len = bridge
3094                .instances
3095                .get(&instance_id)
3096                .and_then(|inst| inst.store.data().staged_files.get("/tmp/too-big-zero.bin"))
3097                .map(|v| v.len());
3098            assert_eq!(len, Some(0));
3099        });
3100    }
3101}