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