Skip to main content

gasm_host/
threads.rs

1//! Real threads for guests (wasi-threads): a module built for `wasm32-wasip1-threads`
2//! imports a shared memory (`env.memory`) and `wasi.thread-spawn(start_arg) -> tid`, and
3//! exports `wasi_thread_start(tid, start_arg)`. Each spawned thread is an OS thread with
4//! its own store and its own instance of the same module, sharing the memory; its
5//! `_initialize` doesn't run (the memory is already set up).
6//!
7//! Worker threads compute and read: the WASI subset, `gasm.time_ms`, `gasm.log`,
8//! `gasm.has` and the assets (a copy taken when the thread starts) work there; the other
9//! gasm imports (video, audio, input, GPU, network, storage) belong to the main thread
10//! and do nothing on a worker. A trap or `proc_exit`
11//! on any thread ends the whole game (wasi-threads), seen by the runner after the frame.
12//! Threads aren't deterministic: headless runs allow none unless `--threads` says so
13//! (`thread-spawn` then fails, as when the limit is reached), so hashes stay comparable.
14//! See design/threads.md, part B.
15
16use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
17use std::sync::{Arc, Mutex};
18
19use wasmtime::{Caller, Engine, Linker, Module, SharedMemory};
20
21use crate::host::{GuestMemory, Host};
22
23/// How a thread ended the game, for the main thread to report.
24#[derive(Clone, Debug)]
25pub enum ThreadEnd {
26    Exit(i32),
27    Trap(String),
28}
29
30/// What spawning needs, shared by every thread of a game.
31pub struct Threads {
32    engine: Engine,
33    module: Module,
34    /// the game's linker (set once it's complete, before the main instance runs)
35    linker: Mutex<Option<Linker<Host>>>,
36    memory: SharedMemory,
37    /// most worker threads alive at once (0: spawning fails)
38    limit: usize,
39    live: AtomicUsize,
40    next_tid: AtomicI32,
41    end: Mutex<Option<ThreadEnd>>,
42    /// the main thread saw `end` (else the watchdog ends the process: it may be blocked
43    /// waiting for the thread that died, and no timeout interrupts an atomic wait)
44    acknowledged: std::sync::atomic::AtomicBool,
45    /// copied into each thread's host: the clock's origin
46    start: std::time::Instant,
47}
48
49impl Threads {
50    pub fn new(engine: &Engine, module: &Module, memory: SharedMemory, limit: usize, start: std::time::Instant) -> Arc<Threads> {
51        Arc::new(Threads {
52            engine: engine.clone(),
53            module: module.clone(),
54            linker: Mutex::new(None),
55            memory,
56            limit,
57            live: AtomicUsize::new(0),
58            next_tid: AtomicI32::new(1),
59            end: Mutex::new(None),
60            acknowledged: std::sync::atomic::AtomicBool::new(false),
61            start,
62        })
63    }
64
65    /// The finished linker, for the threads' instances.
66    pub fn set_linker(&self, linker: Linker<Host>) {
67        *self.linker.lock().unwrap() = Some(linker);
68    }
69
70    pub fn memory(&self) -> &SharedMemory {
71        &self.memory
72    }
73
74    /// A thread ended the game (the first end wins); the caller reports it.
75    pub fn ended(&self) -> Option<ThreadEnd> {
76        let e = self.end.lock().unwrap().clone();
77        if e.is_some() {
78            self.acknowledged.store(true, Ordering::Release);
79        }
80        e
81    }
82
83    /// Most worker threads at once (gasm.max_threads).
84    pub fn limit(&self) -> usize {
85        self.limit
86    }
87
88    pub fn live(&self) -> usize {
89        self.live.load(Ordering::Acquire)
90    }
91
92    /// `wasi.thread-spawn`: a thread id > 0, or a negative number if it can't start.
93    /// `assets`: the spawning thread's, copied (threaded resource loading reads files).
94    fn spawn(self: &Arc<Self>, start_arg: i32, assets: crate::assets::Assets) -> i32 {
95        if self.live.fetch_add(1, Ordering::AcqRel) >= self.limit {
96            self.live.fetch_sub(1, Ordering::AcqRel);
97            return -1;
98        }
99        let tid = self.next_tid.fetch_add(1, Ordering::AcqRel);
100        if tid >= 0x1fff_ffff {
101            // wasi-threads ids are 29 bits
102            self.live.fetch_sub(1, Ordering::AcqRel);
103            return -1;
104        }
105        let me = self.clone();
106        let started = std::thread::Builder::new().name(format!("gasm-thread-{tid}")).spawn(move || {
107            let r = me.run(tid, start_arg, assets);
108            if let Err(end) = r {
109                let first = {
110                    let mut e = me.end.lock().unwrap();
111                    let first = e.is_none();
112                    if first {
113                        *e = Some(end.clone());
114                    }
115                    first
116                };
117                // wasi-threads: a trap or exit on any thread ends the program. The main
118                // thread reports it after its frame; if it doesn't within 2 s (blocked
119                // joining this thread, say), end the process here.
120                if first {
121                    let me = me.clone();
122                    std::thread::spawn(move || {
123                        std::thread::sleep(std::time::Duration::from_secs(2));
124                        if !me.acknowledged.load(Ordering::Acquire) {
125                            match end {
126                                ThreadEnd::Exit(c) => {
127                                    eprintln!("[gasm] guest exited with code {c} (on a worker thread)");
128                                    std::process::exit(c);
129                                }
130                                ThreadEnd::Trap(t) => {
131                                    eprintln!("error: {t}");
132                                    std::process::exit(1);
133                                }
134                            }
135                        }
136                    });
137                }
138            }
139            me.live.fetch_sub(1, Ordering::AcqRel);
140        });
141        match started {
142            Ok(_) => tid,
143            Err(e) => {
144                eprintln!("[gasm] threads: can't start a thread: {e}");
145                self.live.fetch_sub(1, Ordering::AcqRel);
146                -1
147            }
148        }
149    }
150
151    fn run(self: &Arc<Self>, tid: i32, start_arg: i32, assets: crate::assets::Assets) -> Result<(), ThreadEnd> {
152        let linker = self.linker.lock().unwrap().clone().ok_or_else(|| ThreadEnd::Trap("threads: spawned before the game was linked".into()))?;
153        let mut host = Host::worker(self.clone(), self.start, assets);
154        host.set_memory(GuestMemory::Shared(self.memory.clone()));
155        let mut store = wasmtime::Store::new(&self.engine, host);
156        // a worker may run as long as it likes (the main thread's calls have the limit)
157        store.epoch_deadline_trap();
158        store.set_epoch_deadline(u64::MAX / 2);
159        let instance = linker.instantiate(&mut store, &self.module).map_err(|e| ThreadEnd::Trap(format!("thread {tid}: {e:#}")))?;
160        let start = instance
161            .get_typed_func::<(i32, i32), ()>(&mut store, "wasi_thread_start")
162            .map_err(|_| ThreadEnd::Trap("the module imports wasi.thread-spawn but doesn't export wasi_thread_start".into()))?;
163        match start.call(&mut store, (tid, start_arg)) {
164            Ok(()) => Ok(()),
165            Err(e) => match e.downcast_ref::<crate::wasi::Exit>() {
166                Some(x) => Err(ThreadEnd::Exit(x.0)),
167                None => Err(ThreadEnd::Trap(format!("thread {tid}: {e:#}"))),
168            },
169        }
170    }
171}
172
173/// `wasi.thread-spawn`, for modules that import it.
174pub fn add_to_linker(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
175    linker.func_wrap("wasi", "thread-spawn", |caller: Caller<'_, Host>, start_arg: i32| -> i32 {
176        match caller.data().threads() {
177            Some(t) => t.clone().spawn(start_arg, caller.data().assets.for_thread()),
178            None => -1,
179        }
180    })?;
181    Ok(())
182}
183
184/// The module's shared memory import (`env.memory`), if it has one: its type.
185pub fn shared_memory_import(module: &Module) -> Option<wasmtime::MemoryType> {
186    module.imports().find_map(|i| match i.ty() {
187        wasmtime::ExternType::Memory(m) if m.is_shared() => Some(m),
188        _ => None,
189    })
190}