use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering};
use std::thread;
use wasmtime::{
Caller, ExternType, InstancePre, Linker, Module, Result, SharedMemory, Store, format_err,
};
const WASI_ENTRY_POINT: &str = "wasi_thread_start";
pub struct WasiThreadsCtx<T> {
instance_pre: Arc<InstancePre<T>>,
tid: AtomicI32,
use_async: bool,
}
impl<T: Clone + Send + 'static> WasiThreadsCtx<T> {
pub fn new(module: Module, linker: Arc<Linker<T>>, use_async: bool) -> Result<Self> {
let instance_pre = Arc::new(linker.instantiate_pre(&module)?);
let tid = AtomicI32::new(0);
Ok(Self {
instance_pre,
tid,
use_async,
})
}
pub fn spawn(&self, host: T, thread_start_arg: i32) -> Result<i32> {
let instance_pre = self.instance_pre.clone();
if !has_entry_point(instance_pre.module()) {
log::error!(
"failed to find a wasi-threads entry point function; expected an export with name: {WASI_ENTRY_POINT}"
);
return Ok(-1);
}
if !has_correct_signature(instance_pre.module()) {
log::error!(
"the exported entry point function has an incorrect signature: expected `(i32, i32) -> ()`"
);
return Ok(-1);
}
let wasi_thread_id = self.next_thread_id();
if wasi_thread_id.is_none() {
log::error!("ran out of valid thread IDs");
return Ok(-1);
}
let wasi_thread_id = wasi_thread_id.unwrap();
let builder = thread::Builder::new().name(format!("wasi-thread-{wasi_thread_id}"));
let use_async = self.use_async;
builder.spawn(move || {
let result = catch_unwind(AssertUnwindSafe(|| {
let mut store = Store::new(&instance_pre.module().engine(), host);
let instance = if use_async {
wasmtime_wasi::runtime::in_tokio(instance_pre.instantiate_async(&mut store))
} else {
instance_pre.instantiate(&mut store)
}
.unwrap();
let thread_entry_point = instance
.get_typed_func::<(i32, i32), ()>(&mut store, WASI_ENTRY_POINT)
.unwrap();
log::trace!(
"spawned thread id = {wasi_thread_id}; calling start function `{WASI_ENTRY_POINT}` with: {thread_start_arg}"
);
let res = if use_async {
wasmtime_wasi::runtime::in_tokio(
thread_entry_point
.call_async(&mut store, (wasi_thread_id, thread_start_arg)),
)
} else {
thread_entry_point.call(&mut store, (wasi_thread_id, thread_start_arg))
};
match res {
Ok(_) => log::trace!("exiting thread id = {wasi_thread_id} normally"),
Err(e) => {
log::trace!("exiting thread id = {wasi_thread_id} due to error");
let e = wasi_common::maybe_exit_on_error(e);
eprintln!("Error: {e:?}");
std::process::exit(1);
}
}
}));
if let Err(e) = result {
eprintln!("wasi-thread-{wasi_thread_id} panicked: {e:?}");
std::process::exit(1);
}
})?;
Ok(wasi_thread_id)
}
fn next_thread_id(&self) -> Option<i32> {
match self
.tid
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| match v {
..=0x1ffffffe => Some(v + 1),
_ => None,
}) {
Ok(v) => Some(v + 1),
Err(_) => None,
}
}
}
pub fn add_to_linker<T: Clone + Send + 'static>(
linker: &mut wasmtime::Linker<T>,
store: &wasmtime::Store<T>,
module: &Module,
get_cx: impl Fn(&mut T) -> &WasiThreadsCtx<T> + Send + Sync + Copy + 'static,
) -> wasmtime::Result<()> {
linker.func_wrap(
"wasi",
"thread-spawn",
move |mut caller: Caller<'_, T>, start_arg: i32| -> i32 {
log::trace!("new thread requested via `wasi::thread_spawn` call");
let host = caller.data().clone();
let ctx = get_cx(caller.data_mut());
match ctx.spawn(host, start_arg) {
Ok(thread_id) => {
assert!(thread_id >= 0, "thread_id = {thread_id}");
thread_id
}
Err(e) => {
log::error!("failed to spawn thread: {e}");
-1
}
}
},
)?;
for import in module.imports() {
if let Some(m) = import.ty().memory() {
if m.is_shared() {
let mem = SharedMemory::new(module.engine(), m.clone())?;
linker.define(store, import.module(), import.name(), mem.clone())?;
} else {
return Err(format_err!(
"memory was not shared; a `wasi-threads` must import \
a shared memory as \"memory\""
));
}
}
}
Ok(())
}
fn has_entry_point(module: &Module) -> bool {
module.get_export(WASI_ENTRY_POINT).is_some()
}
fn has_correct_signature(module: &Module) -> bool {
match module.get_export(WASI_ENTRY_POINT) {
Some(ExternType::Func(ty)) => {
ty.params().len() == 2
&& ty.params().nth(0).unwrap().is_i32()
&& ty.params().nth(1).unwrap().is_i32()
&& ty.results().len() == 0
}
_ => false,
}
}