Skip to main content

gasm_host/
host.rs

1//! The gasm ABI v0 host: instantiates a guest and implements the "gasm",
2//! "gasm:gfx" and "gasm:net" imports.
3
4use std::collections::HashMap;
5use std::time::Instant;
6
7use wasmtime::{Caller, Engine, Instance, Linker, Memory, Module, Store, TypedFunc, bail, format_err};
8use wasmtime_wasi::WasiCtx;
9use wasmtime_wasi::p1::{self, WasiP1Ctx};
10
11use crate::assets::Assets;
12use crate::audio::AudioSink;
13use crate::gfx::Gfx;
14use crate::net::Net;
15use crate::storage::Storage;
16
17pub const ABI_VERSION: i32 = 0;
18
19/// FNV-1a 32-bit; mirrored in runners/web/gasm-host.js for cross-runner checks.
20#[derive(Clone, Copy)]
21pub struct Fnv(pub u32);
22
23impl Fnv {
24    pub fn new() -> Self {
25        Fnv(0x811c_9dc5)
26    }
27    pub fn update(&mut self, bytes: &[u8]) {
28        let mut h = self.0;
29        for &b in bytes {
30            h ^= b as u32;
31            h = h.wrapping_mul(0x0100_0193);
32        }
33        self.0 = h;
34    }
35}
36
37pub struct Host {
38    wasi: WasiP1Ctx,
39    memory: Option<Memory>,
40    start: Instant,
41    /// Headless runs use frame-derived time so results are reproducible.
42    pub virtual_time_ms: Option<f64>,
43    pub frame_rate: f64,
44    pub pads: [u32; 4],
45    /// UTF-8 typed since the previous frame (set by the runner before each frame);
46    /// None: no keyboard, text_input returns -1
47    pub text: Option<String>,
48    pub assets: Assets,
49    /// sorted asset names, computed on first asset_count/asset_name
50    asset_names: Option<Vec<String>>,
51    pub params: HashMap<String, String>,
52    pub audio: Option<AudioSink>,
53    pub gfx: Gfx,
54    pub net: Net,
55    pub storage: Storage,
56    /// false during catch-up frames: gfx begin_frame returns 0
57    pub show_frame: bool,
58    audio_rate: u32,
59    audio_channels: u32,
60    /// Last presented frame as RGBA8, tightly packed.
61    pub rgba: Vec<u8>,
62    pub width: usize,
63    pub height: usize,
64    pub frames_presented: u64,
65    pub hashing: bool,
66    pub video_hash: Fnv,
67    pub audio_hash: Fnv,
68    pub audio_frames: u64,
69}
70
71impl Host {
72    pub fn new(
73        assets: impl Into<Assets>,
74        params: HashMap<String, String>,
75        audio: Option<AudioSink>,
76        gfx: Gfx,
77        net: Net,
78        storage: Storage,
79    ) -> Self {
80        let wasi = WasiCtx::builder().inherit_stdout().inherit_stderr().build_p1();
81        Host {
82            wasi,
83            memory: None,
84            start: Instant::now(),
85            virtual_time_ms: None,
86            frame_rate: 60.0,
87            pads: [0; 4],
88            text: None,
89            assets: assets.into(),
90            asset_names: None,
91            params,
92            audio,
93            gfx,
94            net,
95            storage,
96            show_frame: true,
97            audio_rate: 44100,
98            audio_channels: 2,
99            rgba: Vec::new(),
100            width: 0,
101            height: 0,
102            frames_presented: 0,
103            hashing: true,
104            video_hash: Fnv::new(),
105            audio_hash: Fnv::new(),
106            audio_frames: 0,
107        }
108    }
109}
110
111/// Borrow `len` bytes at guest offset `ptr`, trapping the guest if out of bounds.
112fn guest_slice<'a>(mem: &'a [u8], ptr: u32, len: u64) -> wasmtime::Result<&'a [u8]> {
113    let start = ptr as u64;
114    let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
115    if end > mem.len() as u64 {
116        bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
117    }
118    Ok(&mem[start as usize..end as usize])
119}
120
121fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<Memory> {
122    caller.data().memory.ok_or_else(|| format_err!("guest memory not yet available"))
123}
124
125fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
126    let mem = memory(caller)?;
127    let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
128    Ok(String::from_utf8_lossy(bytes).into_owned())
129}
130
131fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
132    linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| {
133        let msg = guest_str(&caller, ptr, len)?;
134        eprintln!("[guest] {msg}");
135        Ok(())
136    })?;
137
138    linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
139        let h = caller.data();
140        h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
141    })?;
142
143    linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
144        if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
145            caller.data_mut().frame_rate = hz;
146        }
147    })?;
148
149    linker.func_wrap(
150        "gasm",
151        "video_present",
152        |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
153            let (w, h, stride) = (w as usize, h as usize, stride as usize);
154            if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
155                bail!("video_present: bad geometry {w}x{h} stride {stride}");
156            }
157            let mem = memory(&caller)?;
158            let (data, host) = mem.data_and_store_mut(&mut caller);
159            let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
160            host.rgba.resize(w * h * 4, 0);
161            for y in 0..h {
162                let row = &src[y * stride..y * stride + w * 4];
163                host.rgba[y * w * 4..(y + 1) * w * 4].copy_from_slice(row);
164            }
165            if host.hashing {
166                host.video_hash.update(&host.rgba);
167            }
168            host.width = w;
169            host.height = h;
170            host.frames_presented += 1;
171            Ok(())
172        },
173    )?;
174
175    linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
176        let h = caller.data_mut();
177        if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
178            h.audio_rate = rate;
179            h.audio_channels = channels;
180            if let Some(a) = &mut h.audio {
181                a.configure(rate, channels);
182            }
183        }
184    })?;
185
186    linker.func_wrap(
187        "gasm",
188        "audio_push",
189        |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
190            let mem = memory(&caller)?;
191            let (data, host) = mem.data_and_store_mut(&mut caller);
192            let len = frames as u64 * host.audio_channels as u64 * 4;
193            let bytes = guest_slice(data, ptr, len)?;
194            if host.hashing {
195                host.audio_hash.update(bytes);
196            }
197            host.audio_frames += frames as u64;
198            if let Some(a) = &mut host.audio {
199                a.push_le_f32(bytes);
200            }
201            Ok(())
202        },
203    )?;
204
205    linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
206        caller.data().pads.get(player as usize).copied().unwrap_or(0)
207    })?;
208
209    linker.func_wrap(
210        "gasm",
211        "text_input",
212        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
213            let mem = memory(&caller)?;
214            let (data, host) = mem.data_and_store_mut(&mut caller);
215            let Some(t) = &host.text else { return Ok(-1) };
216            let t = t.as_bytes();
217            if t.len() <= cap as usize {
218                guest_slice(data, dst, t.len() as u64)?;
219                data[dst as usize..dst as usize + t.len()].copy_from_slice(t);
220            }
221            Ok(t.len() as i32)
222        },
223    )?;
224
225    linker.func_wrap(
226        "gasm",
227        "param",
228        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
229            let name = guest_str(&caller, ptr, len)?;
230            let mem = memory(&caller)?;
231            let (data, host) = mem.data_and_store_mut(&mut caller);
232            let Some(v) = host.params.get(&name) else { return Ok(-1) };
233            let v = v.as_bytes();
234            if v.len() <= cap as usize {
235                guest_slice(data, dst, v.len() as u64)?;
236                data[dst as usize..dst as usize + v.len()].copy_from_slice(v);
237            }
238            Ok(v.len() as i32)
239        },
240    )?;
241
242    linker.func_wrap(
243        "gasm",
244        "asset_read_at",
245        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
246            let name = guest_str(&caller, ptr, len)?;
247            let mem = memory(&caller)?;
248            let (data, host) = mem.data_and_store_mut(&mut caller);
249            let Some(size) = host.assets.size(&name) else { return Ok(-1) };
250            let n = size.saturating_sub(offset as u64).min(cap as u64);
251            guest_slice(data, dst, n)?; // bounds check before touching guest memory
252            let dst = &mut data[dst as usize..dst as usize + n as usize];
253            Ok(host.assets.read_at(&name, offset as u64, dst).map_or(-1, |k| k as i32))
254        },
255    )?;
256
257    linker.func_wrap("gasm", "asset_count", |mut caller: Caller<'_, Host>| -> u32 {
258        let h = caller.data_mut();
259        h.asset_names.get_or_insert_with(|| h.assets.names()).len() as u32
260    })?;
261
262    linker.func_wrap(
263        "gasm",
264        "asset_name",
265        |mut caller: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
266            let mem = memory(&caller)?;
267            let (data, host) = mem.data_and_store_mut(&mut caller);
268            let names = host.asset_names.get_or_insert_with(|| host.assets.names());
269            let Some(n) = names.get(index as usize) else { return Ok(-1) };
270            let n = n.as_bytes();
271            if n.len() <= cap as usize {
272                guest_slice(data, dst, n.len() as u64)?;
273                data[dst as usize..dst as usize + n.len()].copy_from_slice(n);
274            }
275            Ok(n.len() as i32)
276        },
277    )?;
278
279    linker.func_wrap(
280        "gasm",
281        "asset_size",
282        |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
283            let name = guest_str(&caller, ptr, len)?;
284            Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i32))
285        },
286    )?;
287
288    linker.func_wrap(
289        "gasm",
290        "asset_read",
291        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
292            let name = guest_str(&caller, ptr, len)?;
293            let mem = memory(&caller)?;
294            let (data, host) = mem.data_and_store_mut(&mut caller);
295            let Some(size) = host.assets.size(&name) else { return Ok(-1) };
296            let n = size.min(cap as u64);
297            guest_slice(data, dst, n)?;
298            let dst = &mut data[dst as usize..dst as usize + n as usize];
299            Ok(host.assets.read_at(&name, 0, dst).map_or(-1, |k| k as i32))
300        },
301    )?;
302    Ok(())
303}
304
305/// Ahead-of-time compile a guest to native code for this host (no JIT needed at load).
306pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
307    Engine::default().precompile_module(wasm)
308}
309
310fn trap(e: String) -> wasmtime::Error {
311    format_err!("{e}")
312}
313
314fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
315    const M: &str = "gasm:gfx";
316    linker.func_wrap(M, "width", |c: Caller<'_, Host>| c.data().gfx.size().0)?;
317    linker.func_wrap(M, "height", |c: Caller<'_, Host>| c.data().gfx.size().1)?;
318    linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
319        let src = guest_str(&c, ptr, len)?;
320        c.data_mut().gfx.create_shader(&src).map_err(trap)
321    })?;
322    linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
323        c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
324    })?;
325    linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
326        let json = guest_str(&c, ptr, len)?;
327        c.data_mut().gfx.create_pipeline(&json).map_err(trap)
328    })?;
329    linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
330        let json = guest_str(&c, ptr, len)?;
331        c.data_mut().gfx.create_bind_group(&json).map_err(trap)
332    })?;
333    linker.func_wrap(
334        M,
335        "write_buffer",
336        |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
337            let mem = memory(&c)?;
338            let (data, host) = mem.data_and_store_mut(&mut c);
339            let bytes = guest_slice(data, ptr, len as u64)?;
340            if host.hashing {
341                host.video_hash.update(bytes);
342            }
343            host.gfx.write_buffer(buf, offset, bytes).map_err(trap)
344        },
345    )?;
346    linker.func_wrap(M, "create_bind_group_layout", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
347        let json = guest_str(&c, ptr, len)?;
348        c.data_mut().gfx.create_bind_group_layout(&json).map_err(trap)
349    })?;
350    linker.func_wrap(M, "create_texture", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
351        let json = guest_str(&c, ptr, len)?;
352        c.data_mut().gfx.create_texture(&json).map_err(trap)
353    })?;
354    linker.func_wrap(M, "create_sampler", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
355        let json = guest_str(&c, ptr, len)?;
356        c.data_mut().gfx.create_sampler(&json).map_err(trap)
357    })?;
358    linker.func_wrap(
359        M,
360        "write_texture",
361        |mut c: Caller<'_, Host>, tex: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
362            let mem = memory(&c)?;
363            let (data, host) = mem.data_and_store_mut(&mut c);
364            let bytes = guest_slice(data, ptr, len as u64)?;
365            host.gfx.write_texture(tex, mip, x, y, w, h, bytes).map_err(trap)?;
366            if host.hashing {
367                // header (little-endian u32s) + payload, so the target region counts too
368                for v in [tex, mip, x, y, w, h] {
369                    host.video_hash.update(&v.to_le_bytes());
370                }
371                host.video_hash.update(bytes);
372            }
373            Ok(())
374        },
375    )?;
376    linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
377        let h = c.data_mut();
378        let show = h.show_frame;
379        h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
380    })?;
381    linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
382        c.data_mut().gfx.set_pipeline(p).map_err(trap)
383    })?;
384    linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
385        c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
386    })?;
387    linker.func_wrap(
388        M,
389        "set_bind_group_offsets",
390        |mut c: Caller<'_, Host>, i: u32, bg: u32, ptr: u32, count: u32| -> wasmtime::Result<()> {
391            let mem = memory(&c)?;
392            let (data, host) = mem.data_and_store_mut(&mut c);
393            let bytes = guest_slice(data, ptr, count as u64 * 4)?;
394            let offsets: Vec<u32> = bytes.chunks_exact(4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
395            host.gfx.set_bind_group_offsets(i, bg, &offsets).map_err(trap)
396        },
397    )?;
398    linker.func_wrap(
399        M,
400        "set_viewport",
401        |mut c: Caller<'_, Host>, x: f32, y: f32, w: f32, h: f32, min: f32, max: f32| -> wasmtime::Result<()> {
402            c.data_mut().gfx.set_viewport(x, y, w, h, min, max).map_err(trap)
403        },
404    )?;
405    linker.func_wrap(M, "set_scissor_rect", |mut c: Caller<'_, Host>, x: u32, y: u32, w: u32, h: u32| -> wasmtime::Result<()> {
406        c.data_mut().gfx.set_scissor_rect(x, y, w, h).map_err(trap)
407    })?;
408    linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
409        c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
410    })?;
411    linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
412        c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
413    })?;
414    linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| {
415        c.data_mut().gfx.draw(vc, ic, fv, fi)
416    })?;
417    linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| {
418        c.data_mut().gfx.draw_indexed(ic, n, first, base, fi)
419    })?;
420    linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
421        let h = c.data_mut();
422        h.frames_presented += 1;
423        h.gfx.end_frame().map_err(trap)
424    })?;
425    Ok(())
426}
427
428fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
429    const M: &str = "gasm:net";
430    linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
431        let url = guest_str(&c, ptr, len)?;
432        Ok(c.data_mut().net.open(&url))
433    })?;
434    linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| c.data().net.state(h))?;
435    linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
436        if len == 0 {
437            return Ok(-1);
438        }
439        let mem = memory(&c)?;
440        let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
441        Ok(c.data_mut().net.send(h, msg))
442    })?;
443    linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
444        let mem = memory(&c)?;
445        let (data, host) = mem.data_and_store_mut(&mut c);
446        let msg = match host.net.peek(h) {
447            Ok(m) => m,
448            Err(code) => return Ok(code),
449        };
450        let n = msg.len();
451        if n <= cap as usize {
452            guest_slice(data, dst, n as u64)?;
453            data[dst as usize..dst as usize + n].copy_from_slice(msg);
454            host.net.pop(h);
455        }
456        Ok(n as i32)
457    })?;
458    linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| c.data_mut().net.close(h))?;
459    Ok(())
460}
461
462fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
463    const M: &str = "gasm:storage";
464    linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
465        let key = guest_str(&c, kp, kl)?;
466        let mem = memory(&c)?;
467        let (data, host) = mem.data_and_store_mut(&mut c);
468        let Some(v) = host.storage.get(&key) else { return Ok(-1) };
469        if v.len() <= cap as usize {
470            guest_slice(data, dst, v.len() as u64)?;
471            data[dst as usize..dst as usize + v.len()].copy_from_slice(v);
472        }
473        Ok(v.len() as i32)
474    })?;
475    linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
476        let key = guest_str(&c, kp, kl)?;
477        let mem = memory(&c)?;
478        let (data, host) = mem.data_and_store_mut(&mut c);
479        let value = guest_slice(data, vp, vl as u64)?;
480        Ok(match host.storage.set(&key, value) {
481            Ok(()) => 0,
482            Err(e) => {
483                eprintln!("[gasm] storage: {e}");
484                -1
485            }
486        })
487    })?;
488    linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
489        let key = guest_str(&c, kp, kl)?;
490        Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
491    })?;
492    Ok(())
493}
494
495/// Why a guest call ended.
496#[derive(Debug)]
497pub enum Stop {
498    /// The guest called proc_exit(code).
499    Exit(i32),
500    Trap(String),
501}
502
503impl std::fmt::Display for Stop {
504    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505        match self {
506            Stop::Exit(code) => write!(f, "guest exited with code {code}"),
507            Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
508        }
509    }
510}
511
512impl std::error::Error for Stop {}
513
514fn classify(e: wasmtime::Error) -> Stop {
515    match e.downcast_ref::<wasmtime_wasi::I32Exit>() {
516        Some(exit) => Stop::Exit(exit.0),
517        None => Stop::Trap(format!("{e:?}")),
518    }
519}
520
521pub struct Game {
522    pub store: Store<Host>,
523    frame: TypedFunc<(), ()>,
524    exit: Option<TypedFunc<(), ()>>,
525}
526
527impl Game {
528    pub fn load(wasm: &[u8], host: Host) -> Result<Game, Stop> {
529        Self::load_inner(wasm, host).map_err(classify)
530    }
531
532    fn load_inner(wasm: &[u8], host: Host) -> wasmtime::Result<Game> {
533        let engine = Engine::default();
534        let module = if wasm.starts_with(b"\0asm") {
535            Module::new(&engine, wasm)?
536        } else {
537            // Precompiled artifact from `gasm-run --compile`. Only load files you
538            // produced yourself: native code is trusted, unlike .wasm.
539            unsafe { Module::deserialize(&engine, wasm)? }
540        };
541        let mut linker: Linker<Host> = Linker::new(&engine);
542        p1::add_to_linker_sync(&mut linker, |h: &mut Host| &mut h.wasi)?;
543        add_gasm_imports(&mut linker)?;
544        add_gfx_imports(&mut linker)?;
545        add_net_imports(&mut linker)?;
546        add_storage_imports(&mut linker)?;
547        // Imports this runner doesn't know (e.g. JS glue some Rust crates pull in)
548        // link as traps: harmless unless the guest actually calls them.
549        linker.define_unknown_imports_as_traps(&module)?;
550
551        let mut store = Store::new(&engine, host);
552        let instance: Instance = linker.instantiate(&mut store, &module)?;
553        let memory = instance
554            .get_memory(&mut store, "memory")
555            .ok_or_else(|| format_err!("guest does not export `memory`"))?;
556        store.data_mut().memory = Some(memory);
557
558        if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
559            init.call(&mut store, ())?;
560        }
561        let version = instance
562            .get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?
563            .call(&mut store, ())?;
564        if version != ABI_VERSION {
565            bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
566        }
567        let rc = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?.call(&mut store, ())?;
568        if rc != 0 {
569            bail!("gasm_init failed with code {rc}");
570        }
571        let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
572        let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
573        Ok(Game { store, frame, exit })
574    }
575
576    pub fn frame(&mut self) -> Result<(), Stop> {
577        self.frame.call(&mut self.store, ()).map_err(classify)
578    }
579
580    /// Best-effort "the player is quitting" notification (optional `gasm_exit` export).
581    pub fn exit(&mut self) {
582        if let Some(f) = self.exit.take() {
583            if let Err(e) = f.call(&mut self.store, ()) {
584                if let Stop::Trap(t) = classify(e) {
585                    eprintln!("[gasm] gasm_exit trapped: {t}");
586                }
587            }
588        }
589    }
590
591    pub fn host(&self) -> &Host {
592        self.store.data()
593    }
594
595    pub fn host_mut(&mut self) -> &mut Host {
596        self.store.data_mut()
597    }
598}