use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use wasmtime::{Caller, Engine, Linker, Module, SharedMemory};
use crate::host::{GuestMemory, Host};
#[derive(Clone, Debug)]
pub enum ThreadEnd {
Exit(i32),
Trap(String),
}
pub struct Threads {
engine: Engine,
module: Module,
linker: Mutex<Option<Linker<Host>>>,
memory: SharedMemory,
limit: usize,
live: AtomicUsize,
next_tid: AtomicI32,
end: Mutex<Option<ThreadEnd>>,
acknowledged: std::sync::atomic::AtomicBool,
start: std::time::Instant,
}
impl Threads {
pub fn new(engine: &Engine, module: &Module, memory: SharedMemory, limit: usize, start: std::time::Instant) -> Arc<Threads> {
Arc::new(Threads {
engine: engine.clone(),
module: module.clone(),
linker: Mutex::new(None),
memory,
limit,
live: AtomicUsize::new(0),
next_tid: AtomicI32::new(1),
end: Mutex::new(None),
acknowledged: std::sync::atomic::AtomicBool::new(false),
start,
})
}
pub fn set_linker(&self, linker: Linker<Host>) {
*self.linker.lock().unwrap() = Some(linker);
}
pub fn memory(&self) -> &SharedMemory {
&self.memory
}
pub fn ended(&self) -> Option<ThreadEnd> {
let e = self.end.lock().unwrap().clone();
if e.is_some() {
self.acknowledged.store(true, Ordering::Release);
}
e
}
pub fn limit(&self) -> usize {
self.limit
}
pub fn live(&self) -> usize {
self.live.load(Ordering::Acquire)
}
fn spawn(self: &Arc<Self>, start_arg: i32, assets: crate::assets::Assets) -> i32 {
if self.live.fetch_add(1, Ordering::AcqRel) >= self.limit {
self.live.fetch_sub(1, Ordering::AcqRel);
return -1;
}
let tid = self.next_tid.fetch_add(1, Ordering::AcqRel);
if tid >= 0x1fff_ffff {
self.live.fetch_sub(1, Ordering::AcqRel);
return -1;
}
let me = self.clone();
let started = std::thread::Builder::new().name(format!("gasm-thread-{tid}")).spawn(move || {
let r = me.run(tid, start_arg, assets);
if let Err(end) = r {
let first = {
let mut e = me.end.lock().unwrap();
let first = e.is_none();
if first {
*e = Some(end.clone());
}
first
};
if first {
let me = me.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(2));
if !me.acknowledged.load(Ordering::Acquire) {
match end {
ThreadEnd::Exit(c) => {
eprintln!("[gasm] guest exited with code {c} (on a worker thread)");
std::process::exit(c);
}
ThreadEnd::Trap(t) => {
eprintln!("error: {t}");
std::process::exit(1);
}
}
}
});
}
}
me.live.fetch_sub(1, Ordering::AcqRel);
});
match started {
Ok(_) => tid,
Err(e) => {
eprintln!("[gasm] threads: can't start a thread: {e}");
self.live.fetch_sub(1, Ordering::AcqRel);
-1
}
}
}
fn run(self: &Arc<Self>, tid: i32, start_arg: i32, assets: crate::assets::Assets) -> Result<(), ThreadEnd> {
let linker = self.linker.lock().unwrap().clone().ok_or_else(|| ThreadEnd::Trap("threads: spawned before the game was linked".into()))?;
let mut host = Host::worker(self.clone(), self.start, assets);
host.set_memory(GuestMemory::Shared(self.memory.clone()));
let mut store = wasmtime::Store::new(&self.engine, host);
store.epoch_deadline_trap();
store.set_epoch_deadline(u64::MAX / 2);
let instance = linker.instantiate(&mut store, &self.module).map_err(|e| ThreadEnd::Trap(format!("thread {tid}: {e:#}")))?;
let start = instance
.get_typed_func::<(i32, i32), ()>(&mut store, "wasi_thread_start")
.map_err(|_| ThreadEnd::Trap("the module imports wasi.thread-spawn but doesn't export wasi_thread_start".into()))?;
match start.call(&mut store, (tid, start_arg)) {
Ok(()) => Ok(()),
Err(e) => match e.downcast_ref::<crate::wasi::Exit>() {
Some(x) => Err(ThreadEnd::Exit(x.0)),
None => Err(ThreadEnd::Trap(format!("thread {tid}: {e:#}"))),
},
}
}
}
pub fn add_to_linker(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
linker.func_wrap("wasi", "thread-spawn", |caller: Caller<'_, Host>, start_arg: i32| -> i32 {
match caller.data().threads() {
Some(t) => t.clone().spawn(start_arg, caller.data().assets.for_thread()),
None => -1,
}
})?;
Ok(())
}
pub fn shared_memory_import(module: &Module) -> Option<wasmtime::MemoryType> {
module.imports().find_map(|i| match i.ty() {
wasmtime::ExternType::Memory(m) if m.is_shared() => Some(m),
_ => None,
})
}