Skip to main content

gasm/
lib.rs

1//! Rust bindings for the gasm game ABI v0 — see `spec/ABI.md`.
2//!
3//! ```ignore
4//! struct MyGame { t: u32 }
5//! impl gasm::Game for MyGame {
6//!     fn init() -> Result<Self, String> { gasm::set_frame_rate(60.0); Ok(MyGame { t: 0 }) }
7//!     fn frame(&mut self) { self.t += 1; /* simulate, draw */ }
8//! }
9//! gasm::game!(MyGame);
10//! ```
11//!
12//! Build with `--target wasm32-unknown-unknown` and `crate-type = ["cdylib"]`.
13//! On native targets the same code links against an in-process stub host
14//! ([`native`]) for tests and benchmarks.
15
16pub mod sys;
17
18#[cfg(not(target_arch = "wasm32"))]
19pub mod native;
20
21use std::cell::UnsafeCell;
22
23pub const ABI_VERSION: i32 = 0;
24
25// ---- core --------------------------------------------------------------------
26
27pub fn log(msg: &str) {
28    unsafe { sys::log(msg.as_ptr(), msg.len() as u32) }
29}
30
31/// `format!`-style logging: `gasm::log!("x = {x}")`.
32#[macro_export]
33macro_rules! log {
34    ($($t:tt)*) => { $crate::log(&::std::format!($($t)*)) };
35}
36
37/// Monotonic milliseconds (virtual, frame-derived time in headless runs).
38pub fn time_ms() -> f64 {
39    unsafe { sys::time_ms() }
40}
41
42/// Rate at which the runner calls `frame()`. Default 60.
43pub fn set_frame_rate(hz: f64) {
44    unsafe { sys::set_frame_rate(hz) }
45}
46
47/// Launch parameter (`--param k=v` natively, `?k=v` in the browser).
48pub fn param(name: &str) -> Option<String> {
49    let len = unsafe { sys::param(name.as_ptr(), name.len() as u32, std::ptr::null_mut(), 0) };
50    if len < 0 {
51        return None;
52    }
53    let mut buf = vec![0u8; len as usize];
54    unsafe { sys::param(name.as_ptr(), name.len() as u32, buf.as_mut_ptr(), len as u32) };
55    Some(String::from_utf8_lossy(&buf).into_owned())
56}
57
58/// End the game (WASI `proc_exit`). Runners treat code 0 as a clean exit.
59pub fn exit(code: i32) -> ! {
60    unsafe { sys::proc_exit(code) }
61}
62
63// ---- video / audio / input / assets --------------------------------------------
64
65/// Present an RGBA8 frame (`stride` bytes per row).
66pub fn present(rgba: &[u8], width: u32, height: u32, stride: u32) {
67    assert!(rgba.len() >= (stride * (height - 1) + width * 4) as usize, "present: buffer too small");
68    unsafe { sys::video_present(rgba.as_ptr(), width, height, stride) }
69}
70
71pub mod audio {
72    use std::sync::atomic::{AtomicU32, Ordering};
73    static CHANNELS: AtomicU32 = AtomicU32::new(2);
74
75    /// Format for [`push`]: sample rate (8–192 kHz) and 1 or 2 channels.
76    pub fn config(sample_rate: u32, channels: u32) {
77        CHANNELS.store(channels, Ordering::Relaxed);
78        unsafe { crate::sys::audio_config(sample_rate, channels) }
79    }
80
81    /// Queue interleaved f32 samples in [-1, 1].
82    pub fn push(samples: &[f32]) {
83        let ch = CHANNELS.load(Ordering::Relaxed).max(1);
84        unsafe { crate::sys::audio_push(samples.as_ptr(), samples.len() as u32 / ch) }
85    }
86}
87
88/// Virtual gamepad buttons (bit positions per spec/ABI.md).
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90pub struct Buttons(pub u32);
91
92impl Buttons {
93    pub const A: u32 = 1 << 0;
94    pub const B: u32 = 1 << 1;
95    pub const X: u32 = 1 << 2;
96    pub const Y: u32 = 1 << 3;
97    pub const L: u32 = 1 << 4;
98    pub const R: u32 = 1 << 5;
99    pub const SELECT: u32 = 1 << 6;
100    pub const START: u32 = 1 << 7;
101    pub const UP: u32 = 1 << 8;
102    pub const DOWN: u32 = 1 << 9;
103    pub const LEFT: u32 = 1 << 10;
104    pub const RIGHT: u32 = 1 << 11;
105
106    pub fn held(self, mask: u32) -> bool {
107        self.0 & mask != 0
108    }
109}
110
111/// Buttons held on virtual pad `player` (0..=3) this frame.
112pub fn pad(player: u32) -> Buttons {
113    Buttons(unsafe { sys::input_pad(player) })
114}
115
116/// Text typed since the previous frame (`'\u{8}'` = backspace, `'\n'` = enter),
117/// or `None` if the runner has no keyboard.
118pub fn text_input() -> Option<String> {
119    let len = unsafe { sys::text_input(std::ptr::null_mut(), 0) };
120    if len < 0 {
121        return None;
122    }
123    let mut buf = vec![0u8; len as usize];
124    if len > 0 {
125        unsafe { sys::text_input(buf.as_mut_ptr(), len as u32) };
126    }
127    Some(String::from_utf8_lossy(&buf).into_owned())
128}
129
130/// All asset names, sorted (by UTF-8 bytes). Folder entries keep their on-disk case.
131pub fn asset_names() -> Vec<String> {
132    let n = unsafe { sys::asset_count() };
133    (0..n)
134        .filter_map(|i| {
135            let len = unsafe { sys::asset_name(i, std::ptr::null_mut(), 0) };
136            if len < 0 {
137                return None;
138            }
139            let mut buf = vec![0u8; len as usize];
140            if len > 0 {
141                unsafe { sys::asset_name(i, buf.as_mut_ptr(), len as u32) };
142            }
143            Some(String::from_utf8_lossy(&buf).into_owned())
144        })
145        .collect()
146}
147
148/// Read a whole asset.
149pub fn asset(name: &str) -> Option<Vec<u8>> {
150    let n = unsafe { sys::asset_size(name.as_ptr(), name.len() as u32) };
151    if n < 0 {
152        return None;
153    }
154    let mut buf = vec![0u8; n as usize];
155    let got = unsafe { sys::asset_read(name.as_ptr(), name.len() as u32, buf.as_mut_ptr(), n as u32) };
156    buf.truncate(got.max(0) as usize);
157    Some(buf)
158}
159
160/// Read up to `buf.len()` bytes of an asset starting at `offset` (for streaming
161/// large assets). Returns bytes read (0 at the end), or `None` if missing.
162pub fn asset_read_at(name: &str, offset: u32, buf: &mut [u8]) -> Option<usize> {
163    let n = unsafe {
164        sys::asset_read_at(name.as_ptr(), name.len() as u32, offset, buf.as_mut_ptr(), buf.len() as u32)
165    };
166    (n >= 0).then_some(n as usize)
167}
168
169// ---- storage ------------------------------------------------------------------------
170
171/// Persistent per-game key/value store (`gasm:storage`): saves, settings, scores.
172///
173/// Keys: 1–128 bytes of `[A-Za-z0-9._-]`. Values up to 1 MiB, 16 MiB per game.
174/// The runner picks the namespace; headless runs start with an empty store.
175pub mod storage {
176    use crate::sys;
177
178    pub fn get(key: &str) -> Option<Vec<u8>> {
179        let n = unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, std::ptr::null_mut(), 0) };
180        if n < 0 {
181            return None;
182        }
183        let mut buf = vec![0u8; n as usize];
184        unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, buf.as_mut_ptr(), n as u32) };
185        Some(buf)
186    }
187
188    /// Store a value. False on invalid key, size/quota limits or I/O error.
189    pub fn set(key: &str, value: &[u8]) -> bool {
190        unsafe { sys::storage_set(key.as_ptr(), key.len() as u32, value.as_ptr(), value.len() as u32) == 0 }
191    }
192
193    /// Delete a key. False if it didn't exist.
194    pub fn delete(key: &str) -> bool {
195        unsafe { sys::storage_delete(key.as_ptr(), key.len() as u32) == 0 }
196    }
197
198    /// All keys of this game, sorted.
199    pub fn keys() -> Vec<String> {
200        let n = unsafe { sys::storage_count() };
201        (0..n)
202            .filter_map(|i| {
203                let len = unsafe { sys::storage_key(i, std::ptr::null_mut(), 0) };
204                if len < 0 {
205                    return None;
206                }
207                let mut buf = vec![0u8; len as usize];
208                unsafe { sys::storage_key(i, buf.as_mut_ptr(), len as u32) };
209                Some(String::from_utf8_lossy(&buf).into_owned())
210            })
211            .collect()
212    }
213}
214
215// ---- raw input ---------------------------------------------------------------------
216
217/// Raw keyboard, pointer and gamepads (next to the virtual [`pad`]s).
218///
219/// ```ignore
220/// use gasm::{input, keys};
221/// input::set_mode(input::KEYS_RAW);              // read the keyboard directly, no keymap pads
222/// let k = input::keys().unwrap_or_default();
223/// if k.held(keys::SHIFT_LEFT) && k.held(keys::ARROW_LEFT) { … }
224/// if let Some(p) = input::pointer() && p.pressed & input::MOUSE_LEFT != 0 { click(p.frame_x, p.frame_y) }
225/// ```
226pub mod input {
227    use crate::sys;
228
229    /// The runner stops mapping the keyboard to pads (gamepads still map).
230    pub const KEYS_RAW: u32 = 1 << 0;
231    /// Hide the system cursor over the game.
232    pub const POINTER_HIDDEN: u32 = 1 << 1;
233    /// Capture the pointer for relative motion (best effort; the browser needs a click).
234    pub const POINTER_LOCKED: u32 = 1 << 2;
235
236    pub const MOUSE_LEFT: u32 = 1 << 0;
237    pub const MOUSE_RIGHT: u32 = 1 << 1;
238    pub const MOUSE_MIDDLE: u32 = 1 << 2;
239    pub const MOUSE_BACK: u32 = 1 << 3;
240    pub const MOUSE_FORWARD: u32 = 1 << 4;
241
242    /// `KEYS_RAW | POINTER_HIDDEN | POINTER_LOCKED`, any combination.
243    pub fn set_mode(flags: u32) {
244        unsafe { sys::input_mode(flags) }
245    }
246
247    /// Keys held this frame; index with [`crate::keys`] constants.
248    #[derive(Clone, Copy, Default, PartialEq, Eq)]
249    pub struct Keys(pub [u8; 32]);
250
251    impl Keys {
252        pub fn held(&self, code: u32) -> bool {
253            (code as usize) < 256 && self.0[code as usize / 8] & (1 << (code % 8)) != 0
254        }
255    }
256
257    /// `None` if the runner has no keyboard.
258    pub fn keys() -> Option<Keys> {
259        let mut k = Keys::default();
260        (unsafe { sys::key_state(k.0.as_mut_ptr(), 32) } >= 0).then_some(k)
261    }
262
263    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
264    pub struct KeyEvent {
265        pub code: u32,
266        pub down: bool,
267    }
268
269    /// Presses and releases since the previous frame, in order (`None`: no keyboard).
270    pub fn key_events() -> Option<Vec<KeyEvent>> {
271        let n = unsafe { sys::key_events(std::ptr::null_mut(), 0) };
272        if n < 0 {
273            return None;
274        }
275        let mut b = vec![0u8; n as usize];
276        if n > 0 {
277            unsafe { sys::key_events(b.as_mut_ptr(), n as u32) };
278        }
279        Some(b.chunks_exact(4).map(|e| KeyEvent { code: u16::from_le_bytes([e[0], e[1]]) as u32, down: e[2] != 0 }).collect())
280    }
281
282    #[derive(Clone, Copy, Debug, Default, PartialEq)]
283    pub struct Pointer {
284        /// position in drawable pixels (gfx width/height space)
285        pub x: f32,
286        pub y: f32,
287        /// position in the last presented frame's pixels (2D games)
288        pub frame_x: f32,
289        pub frame_y: f32,
290        /// relative motion since the previous frame (also while locked)
291        pub dx: f32,
292        pub dy: f32,
293        /// about 1 per wheel notch; y > 0 = down
294        pub wheel_x: f32,
295        pub wheel_y: f32,
296        pub buttons: u32,
297        /// went down / up since the previous frame (a quick click shows in both)
298        pub pressed: u32,
299        pub released: u32,
300        pub flags: u32,
301    }
302
303    impl Pointer {
304        pub fn inside(&self) -> bool {
305            self.flags & 1 != 0
306        }
307        pub fn locked(&self) -> bool {
308            self.flags & 4 != 0
309        }
310    }
311
312    /// `None` if the runner has no pointer.
313    pub fn pointer() -> Option<Pointer> {
314        let mut b = [0u8; 48];
315        if unsafe { sys::pointer(b.as_mut_ptr(), 48) } < 0 {
316            return None;
317        }
318        let f = |i: usize| f32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
319        let u = |i: usize| u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
320        Some(Pointer {
321            x: f(0), y: f(4), frame_x: f(8), frame_y: f(12), dx: f(16), dy: f(20), wheel_x: f(24), wheel_y: f(28),
322            buttons: u(32), pressed: u(36), released: u(40), flags: u(44),
323        })
324    }
325
326    /// Raw gamepad or joystick. With `standard`, W3C order: buttons 0 south, 1 east,
327    /// 2 west, 3 north, 4/5 shoulders, 6/7 triggers, 8 select, 9 start, 10/11 sticks,
328    /// 12-15 d-pad, 16 home; axes 0/1 left stick, 2/3 right stick (y > 0 = down).
329    #[derive(Clone, Debug, Default, PartialEq)]
330    pub struct Gamepad {
331        pub connected: bool,
332        pub standard: bool,
333        pub buttons: Vec<f32>,
334        pub axes: Vec<f32>,
335    }
336
337    /// Slot 0-3 (connection order). `None` if the runner has no gamepad support.
338    pub fn gamepad(slot: u32) -> Option<Gamepad> {
339        let mut b = [0u8; 204];
340        if unsafe { sys::gamepad(slot, b.as_mut_ptr(), 204) } < 0 {
341            return None;
342        }
343        let u = |i: usize| u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
344        let f = |i: usize| f32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
345        let (flags, nb, na) = (u(0), (u(4) as usize).min(32), (u(8) as usize).min(16));
346        Some(Gamepad {
347            connected: flags & 1 != 0,
348            standard: flags & 2 != 0,
349            buttons: (0..nb).map(|i| f(12 + i * 4)).collect(),
350            axes: (0..na).map(|i| f(140 + i * 4)).collect(),
351        })
352    }
353
354    /// Device name, if a gamepad is connected in `slot`.
355    pub fn gamepad_name(slot: u32) -> Option<String> {
356        let n = unsafe { sys::gamepad_name(slot, std::ptr::null_mut(), 0) };
357        if n < 0 {
358            return None;
359        }
360        let mut b = vec![0u8; n as usize];
361        if n > 0 {
362            unsafe { sys::gamepad_name(slot, b.as_mut_ptr(), n as u32) };
363        }
364        Some(String::from_utf8_lossy(&b).into_owned())
365    }
366}
367
368/// Raw key codes (`GASM_KEY_*`, W3C `KeyboardEvent.code` names in [`keys::NAMES`]).
369pub use sys::keys;
370
371// ---- gfx ---------------------------------------------------------------------------
372
373/// GPU rendering through `gasm:gfx` (a WebGPU subset; descriptors are JSON).
374pub mod gfx {
375    use crate::sys;
376
377    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
378    pub struct Shader(pub u32);
379    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
380    pub struct Buffer(pub u32);
381    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
382    pub struct Pipeline(pub u32);
383    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
384    pub struct BindGroup(pub u32);
385    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
386    pub struct BindGroupLayout(pub u32);
387    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
388    pub struct Texture(pub u32);
389    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
390    pub struct Sampler(pub u32);
391
392    pub const COPY_DST: u32 = 0x08;
393    pub const INDEX: u32 = 0x10;
394    pub const VERTEX: u32 = 0x20;
395    pub const UNIFORM: u32 = 0x40;
396    pub const STORAGE: u32 = 0x80;
397
398    /// Bind group layout entry visibility.
399    pub const STAGE_VERTEX: u32 = 0x1;
400    pub const STAGE_FRAGMENT: u32 = 0x2;
401    /// Dynamic offsets must be multiples of this.
402    pub const OFFSET_ALIGNMENT: u32 = 256;
403
404    #[derive(Clone, Copy)]
405    pub enum IndexFormat {
406        U16 = 0,
407        U32 = 1,
408    }
409
410    /// Plain-old-data that can be uploaded byte for byte.
411    ///
412    /// # Safety
413    /// Implementors must have no padding and no pointers.
414    pub unsafe trait Pod: Copy {}
415    unsafe impl Pod for u8 {}
416    unsafe impl Pod for u16 {}
417    unsafe impl Pod for u32 {}
418    unsafe impl Pod for f32 {}
419    unsafe impl<T: Pod, const N: usize> Pod for [T; N] {}
420
421    pub fn as_bytes<T: Pod>(data: &[T]) -> &[u8] {
422        unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
423    }
424
425    /// Drawable size in pixels.
426    pub fn size() -> (u32, u32) {
427        unsafe { (sys::gfx_width(), sys::gfx_height()) }
428    }
429    pub fn create_shader(wgsl: &str) -> Shader {
430        Shader(unsafe { sys::gfx_create_shader(wgsl.as_ptr(), wgsl.len() as u32) })
431    }
432    pub fn create_buffer(size: u32, usage: u32) -> Buffer {
433        Buffer(unsafe { sys::gfx_create_buffer(size, usage) })
434    }
435    /// WebGPU `GPURenderPipelineDescriptor` as JSON (handles as numbers,
436    /// color format `"surface"`, depth format `"depth24plus"`).
437    pub fn create_pipeline(json: &str) -> Pipeline {
438        Pipeline(unsafe { sys::gfx_create_pipeline(json.as_ptr(), json.len() as u32) })
439    }
440    /// `{"layout":L,"entries":[...]}` or `{"pipeline":P,"group":0,"entries":[...]}`; entries
441    /// `{"binding":0,"buffer":B,"offset":0,"size":N}`, `{"binding":1,"texture":T}`, `{"binding":2,"sampler":S}`.
442    pub fn create_bind_group(json: &str) -> BindGroup {
443        BindGroup(unsafe { sys::gfx_create_bind_group(json.as_ptr(), json.len() as u32) })
444    }
445    /// `{"entries":[{"binding":0,"visibility":STAGE_VERTEX,"buffer":{"type":"uniform","hasDynamicOffset":true}}, ...]}`.
446    /// Bind groups made from it work with every pipeline whose `"layout":[...]` lists it.
447    pub fn create_bind_group_layout(json: &str) -> BindGroupLayout {
448        BindGroupLayout(unsafe { sys::gfx_create_bind_group_layout(json.as_ptr(), json.len() as u32) })
449    }
450    /// `{"size":[w,h],"format":"rgba8unorm","mipLevelCount":n}`
451    pub fn create_texture(json: &str) -> Texture {
452        Texture(unsafe { sys::gfx_create_texture(json.as_ptr(), json.len() as u32) })
453    }
454    /// Upload a tightly packed RGBA8 region (`rgba.len() == w * h * 4`) of mip level `mip`.
455    pub fn write_texture(t: Texture, mip: u32, x: u32, y: u32, w: u32, h: u32, rgba: &[u8]) {
456        unsafe { sys::gfx_write_texture(t.0, mip, x, y, w, h, rgba.as_ptr(), rgba.len() as u32) }
457    }
458    /// `{"addressModeU":"repeat","magFilter":"linear","minFilter":"linear","mipmapFilter":"linear"}`
459    pub fn create_sampler(json: &str) -> Sampler {
460        Sampler(unsafe { sys::gfx_create_sampler(json.as_ptr(), json.len() as u32) })
461    }
462    /// Offset and byte length must be multiples of 4.
463    pub fn write_buffer<T: Pod>(buf: Buffer, offset: u32, data: &[T]) {
464        let b = as_bytes(data);
465        unsafe { sys::gfx_write_buffer(buf.0, offset, b.as_ptr(), b.len() as u32) }
466    }
467    /// Returns false if the runner will discard this frame (you may skip drawing).
468    pub fn begin_frame(clear: [f32; 4]) -> bool {
469        unsafe { sys::gfx_begin_frame(clear[0], clear[1], clear[2], clear[3]) != 0 }
470    }
471    pub fn set_pipeline(p: Pipeline) {
472        unsafe { sys::gfx_set_pipeline(p.0) }
473    }
474    pub fn set_bind_group(index: u32, bg: BindGroup) {
475        unsafe { sys::gfx_set_bind_group(index, bg.0) }
476    }
477    /// One offset (multiple of [`OFFSET_ALIGNMENT`]) per dynamic-offset entry, in binding order.
478    pub fn set_bind_group_offsets(index: u32, bg: BindGroup, offsets: &[u32]) {
479        unsafe { sys::gfx_set_bind_group_offsets(index, bg.0, offsets.as_ptr(), offsets.len() as u32) }
480    }
481    /// Viewport in drawable pixels (clamped to the drawable), depth range 0..=1.
482    pub fn set_viewport(x: f32, y: f32, w: f32, h: f32, min_depth: f32, max_depth: f32) {
483        unsafe { sys::gfx_set_viewport(x, y, w, h, min_depth, max_depth) }
484    }
485    /// Scissor rectangle in drawable pixels (clamped to the drawable).
486    pub fn set_scissor_rect(x: u32, y: u32, w: u32, h: u32) {
487        unsafe { sys::gfx_set_scissor_rect(x, y, w, h) }
488    }
489    pub fn set_vertex_buffer(slot: u32, buf: Buffer, offset: u32) {
490        unsafe { sys::gfx_set_vertex_buffer(slot, buf.0, offset) }
491    }
492    pub fn set_index_buffer(buf: Buffer, format: IndexFormat, offset: u32) {
493        unsafe { sys::gfx_set_index_buffer(buf.0, format as u32, offset) }
494    }
495    pub fn draw(vertices: u32, instances: u32, first_vertex: u32, first_instance: u32) {
496        unsafe { sys::gfx_draw(vertices, instances, first_vertex, first_instance) }
497    }
498    pub fn draw_indexed(indices: u32, instances: u32, first_index: u32, base_vertex: i32, first_instance: u32) {
499        unsafe { sys::gfx_draw_indexed(indices, instances, first_index, base_vertex, first_instance) }
500    }
501    pub fn end_frame() {
502        unsafe { sys::gfx_end_frame() }
503    }
504}
505
506// ---- net ---------------------------------------------------------------------------
507
508/// Message connections through `gasm:net` (WebSocket semantics, non-blocking).
509pub mod net {
510    use crate::sys;
511
512    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
513    pub enum State {
514        Connecting,
515        Open,
516        Closed,
517        Error,
518    }
519
520    pub enum Recv {
521        Message(Vec<u8>),
522        Empty,
523        Closed,
524    }
525
526    #[derive(Debug)]
527    pub struct Conn(i32);
528
529    impl Conn {
530        /// Open `ws://` / `wss://`. `None` if the runner denies it.
531        pub fn open(url: &str) -> Option<Conn> {
532            let h = unsafe { sys::net_open(url.as_ptr(), url.len() as u32) };
533            (h > 0).then_some(Conn(h))
534        }
535        pub fn state(&self) -> State {
536            match unsafe { sys::net_state(self.0) } {
537                0 => State::Connecting,
538                1 => State::Open,
539                2 => State::Closed,
540                _ => State::Error,
541            }
542        }
543        /// Send one message. False if the connection is not open.
544        pub fn send(&self, msg: &[u8]) -> bool {
545            !msg.is_empty() && unsafe { sys::net_send(self.0, msg.as_ptr(), msg.len() as u32) } == 0
546        }
547        /// Next queued message, if any.
548        pub fn recv(&self) -> Recv {
549            let mut buf = [0u8; 1024];
550            let n = unsafe { sys::net_recv(self.0, buf.as_mut_ptr(), buf.len() as u32) };
551            match n {
552                0 => Recv::Empty,
553                n if n < 0 => Recv::Closed,
554                n if n as usize <= buf.len() => Recv::Message(buf[..n as usize].to_vec()),
555                n => {
556                    let mut big = vec![0u8; n as usize];
557                    unsafe { sys::net_recv(self.0, big.as_mut_ptr(), n as u32) };
558                    Recv::Message(big)
559                }
560            }
561        }
562    }
563
564    impl Drop for Conn {
565        fn drop(&mut self) {
566            unsafe { sys::net_close(self.0) }
567        }
568    }
569}
570
571// ---- game entry points ----------------------------------------------------------------
572
573/// A gasm game. `init` runs once; `frame` runs at the configured frame rate.
574pub trait Game: Sized + 'static {
575    fn init() -> Result<Self, String>;
576    fn frame(&mut self);
577    /// Called (best effort) when the player closes the game: flush saves here.
578    /// Not called after `gasm::exit`, a trap, or a crash, so also save periodically.
579    fn exit(&mut self) {}
580}
581
582#[doc(hidden)]
583pub struct GameCell<T>(pub UnsafeCell<Option<T>>);
584// Wasm guests are single-threaded; the runner never calls exports concurrently.
585unsafe impl<T> Sync for GameCell<T> {}
586
587#[doc(hidden)]
588pub fn __init<G: Game>(cell: &GameCell<G>) -> i32 {
589    std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))));
590    match G::init() {
591        Ok(g) => {
592            unsafe { *cell.0.get() = Some(g) };
593            0
594        }
595        Err(e) => {
596            log(&format!("init failed: {e}"));
597            1
598        }
599    }
600}
601
602#[doc(hidden)]
603pub fn __frame<G: Game>(cell: &GameCell<G>) {
604    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
605        g.frame();
606    }
607}
608
609#[doc(hidden)]
610pub fn __exit<G: Game>(cell: &GameCell<G>) {
611    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
612        g.exit();
613    }
614}
615
616/// Export the gasm entry points for a [`Game`] type.
617#[macro_export]
618macro_rules! game {
619    ($t:ty) => {
620        static __GASM_GAME: $crate::GameCell<$t> = $crate::GameCell(::std::cell::UnsafeCell::new(None));
621
622        #[unsafe(no_mangle)]
623        pub extern "C" fn gasm_abi_version() -> i32 {
624            $crate::ABI_VERSION
625        }
626        #[unsafe(no_mangle)]
627        pub extern "C" fn gasm_init() -> i32 {
628            $crate::__init::<$t>(&__GASM_GAME)
629        }
630        #[unsafe(no_mangle)]
631        pub extern "C" fn gasm_frame() {
632            $crate::__frame::<$t>(&__GASM_GAME)
633        }
634        #[unsafe(no_mangle)]
635        pub extern "C" fn gasm_exit() {
636            $crate::__exit::<$t>(&__GASM_GAME)
637        }
638    };
639}