Path: blob/main/crates/wasi-threads/src/lib.rs
3068 views
//! Implement [`wasi-threads`].1//!2//! [`wasi-threads`]: https://github.com/WebAssembly/wasi-threads34use std::panic::{AssertUnwindSafe, catch_unwind};5use std::sync::Arc;6use std::sync::atomic::{AtomicI32, Ordering};7use std::thread;8use wasmtime::{9Caller, ExternType, InstancePre, Linker, Module, Result, SharedMemory, Store, format_err,10};1112// This name is a function export designated by the wasi-threads specification:13// https://github.com/WebAssembly/wasi-threads/#detailed-design-discussion14const WASI_ENTRY_POINT: &str = "wasi_thread_start";1516pub struct WasiThreadsCtx<T> {17instance_pre: Arc<InstancePre<T>>,18tid: AtomicI32,19use_async: bool,20}2122impl<T: Clone + Send + 'static> WasiThreadsCtx<T> {23pub fn new(module: Module, linker: Arc<Linker<T>>, use_async: bool) -> Result<Self> {24let instance_pre = Arc::new(linker.instantiate_pre(&module)?);25let tid = AtomicI32::new(0);26Ok(Self {27instance_pre,28tid,29use_async,30})31}3233pub fn spawn(&self, host: T, thread_start_arg: i32) -> Result<i32> {34let instance_pre = self.instance_pre.clone();3536// Check that the thread entry point is present. Why here? If we check37// for this too early, then we cannot accept modules that do not have an38// entry point but never spawn a thread. As pointed out in39// https://github.com/bytecodealliance/wasmtime/issues/6153, checking40// the entry point here allows wasi-threads to be compatible with more41// modules.42//43// As defined in the wasi-threads specification, returning a negative44// result here indicates to the guest module that the spawn failed.45if !has_entry_point(instance_pre.module()) {46log::error!(47"failed to find a wasi-threads entry point function; expected an export with name: {WASI_ENTRY_POINT}"48);49return Ok(-1);50}51if !has_correct_signature(instance_pre.module()) {52log::error!(53"the exported entry point function has an incorrect signature: expected `(i32, i32) -> ()`"54);55return Ok(-1);56}5758let wasi_thread_id = self.next_thread_id();59if wasi_thread_id.is_none() {60log::error!("ran out of valid thread IDs");61return Ok(-1);62}63let wasi_thread_id = wasi_thread_id.unwrap();6465// Start a Rust thread running a new instance of the current module.66let builder = thread::Builder::new().name(format!("wasi-thread-{wasi_thread_id}"));67let use_async = self.use_async;68builder.spawn(move || {69// Catch any panic failures in host code; e.g., if a WASI module70// were to crash, we want all threads to exit, not just this one.71let result = catch_unwind(AssertUnwindSafe(|| {72// Each new instance is created in its own store.73let mut store = Store::new(&instance_pre.module().engine(), host);7475let instance = if use_async {76wasmtime_wasi::runtime::in_tokio(instance_pre.instantiate_async(&mut store))77} else {78instance_pre.instantiate(&mut store)79}80.unwrap();8182let thread_entry_point = instance83.get_typed_func::<(i32, i32), ()>(&mut store, WASI_ENTRY_POINT)84.unwrap();8586// Start the thread's entry point. Any traps or calls to87// `proc_exit`, by specification, should end execution for all88// threads. This code uses `process::exit` to do so, which is89// what the user expects from the CLI but probably not in a90// Wasmtime embedding.91log::trace!(92"spawned thread id = {wasi_thread_id}; calling start function `{WASI_ENTRY_POINT}` with: {thread_start_arg}"93);94let res = if use_async {95wasmtime_wasi::runtime::in_tokio(96thread_entry_point97.call_async(&mut store, (wasi_thread_id, thread_start_arg)),98)99} else {100thread_entry_point.call(&mut store, (wasi_thread_id, thread_start_arg))101};102match res {103Ok(_) => log::trace!("exiting thread id = {wasi_thread_id} normally"),104Err(e) => {105log::trace!("exiting thread id = {wasi_thread_id} due to error");106let e = wasi_common::maybe_exit_on_error(e);107eprintln!("Error: {e:?}");108std::process::exit(1);109}110}111}));112113if let Err(e) = result {114eprintln!("wasi-thread-{wasi_thread_id} panicked: {e:?}");115std::process::exit(1);116}117})?;118119Ok(wasi_thread_id)120}121122/// Helper for generating valid WASI thread IDs (TID).123///124/// Callers of `wasi_thread_spawn` expect a TID in range of 0 < TID <= 0x1FFFFFFF125/// to indicate a successful spawning of the thread whereas a negative126/// return value indicates an failure to spawn.127fn next_thread_id(&self) -> Option<i32> {128match self129.tid130.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| match v {131..=0x1ffffffe => Some(v + 1),132_ => None,133}) {134Ok(v) => Some(v + 1),135Err(_) => None,136}137}138}139140/// Manually add the WASI `thread_spawn` function to the linker.141///142/// It is unclear what namespace the `wasi-threads` proposal should live under:143/// it is not clear if it should be included in any of the `preview*` releases144/// so for the time being its module namespace is simply `"wasi"` (TODO).145pub fn add_to_linker<T: Clone + Send + 'static>(146linker: &mut wasmtime::Linker<T>,147store: &wasmtime::Store<T>,148module: &Module,149get_cx: impl Fn(&mut T) -> &WasiThreadsCtx<T> + Send + Sync + Copy + 'static,150) -> wasmtime::Result<()> {151linker.func_wrap(152"wasi",153"thread-spawn",154move |mut caller: Caller<'_, T>, start_arg: i32| -> i32 {155log::trace!("new thread requested via `wasi::thread_spawn` call");156let host = caller.data().clone();157let ctx = get_cx(caller.data_mut());158match ctx.spawn(host, start_arg) {159Ok(thread_id) => {160assert!(thread_id >= 0, "thread_id = {thread_id}");161thread_id162}163Err(e) => {164log::error!("failed to spawn thread: {e}");165-1166}167}168},169)?;170171// Find the shared memory import and satisfy it with a newly-created shared172// memory import.173for import in module.imports() {174if let Some(m) = import.ty().memory() {175if m.is_shared() {176let mem = SharedMemory::new(module.engine(), m.clone())?;177linker.define(store, import.module(), import.name(), mem.clone())?;178} else {179return Err(format_err!(180"memory was not shared; a `wasi-threads` must import \181a shared memory as \"memory\""182));183}184}185}186Ok(())187}188189/// Check if wasi-threads' `wasi_thread_start` export is present.190fn has_entry_point(module: &Module) -> bool {191module.get_export(WASI_ENTRY_POINT).is_some()192}193194/// Check if the entry function has the correct signature `(i32, i32) -> ()`.195fn has_correct_signature(module: &Module) -> bool {196match module.get_export(WASI_ENTRY_POINT) {197Some(ExternType::Func(ty)) => {198ty.params().len() == 2199&& ty.params().nth(0).unwrap().is_i32()200&& ty.params().nth(1).unwrap().is_i32()201&& ty.results().len() == 0202}203_ => false,204}205}206207208