wasi-threads: an initial implementation (#5484)
This commit includes a set of changes that add initial support for `wasi-threads` to Wasmtime:
* feat: remove mutability from the WasiCtx Table
This patch adds interior mutability to the WasiCtx Table and the Table elements.
Major pain points:
* `File` only needs `RwLock<cap_std::fs::File>` to implement
`File::set_fdflags()` on Windows, because of [1]
* Because `File` needs a `RwLock` and `RwLock*Guard` cannot
be hold across an `.await`, The `async` from
`async fn num_ready_bytes(&self)` had to be removed
* Because `File` needs a `RwLock` and `RwLock*Guard` cannot
be dereferenced in `pollable`, the signature of
`fn pollable(&self) -> Option<rustix::fd::BorrowedFd>`
changed to `fn pollable(&self) -> Option<Arc<dyn AsFd + '_>>`
[1] da238e324e/src/fs/fd_flags.rs (L210-L217)
* wasi-threads: add an initial implementation
This change is a first step toward implementing `wasi-threads` in
Wasmtime. We may find that it has some missing pieces, but the core
functionality is there: when `wasi::thread_spawn` is called by a running
WebAssembly module, a function named `wasi_thread_start` is found in the
module's exports and called in a new instance. The shared memory of the
original instance is reused in the new instance.
This new WASI proposal is in its early stages and details are still
being hashed out in the [spec] and [wasi-libc] repositories. Due to its
experimental state, the `wasi-threads` functionality is hidden behind
both a compile-time and runtime flag: one must build with `--features
wasi-threads` but also run the Wasmtime CLI with `--wasm-features
threads` and `--wasi-modules experimental-wasi-threads`. One can
experiment with `wasi-threads` by running:
```console
$ cargo run --features wasi-threads -- \
--wasm-features threads --wasi-modules experimental-wasi-threads \
<a threads-enabled module>
```
Threads-enabled Wasm modules are not yet easy to build. Hopefully this
is resolved soon, but in the meantime see the use of
`THREAD_MODEL=posix` in the [wasi-libc] repository for some clues on
what is necessary. Wiggle complicates things by requiring the Wasm
memory to be exported with a certain name and `wasi-threads` also
expects that memory to be imported; this build-time obstacle can be
overcome with the `--import-memory --export-memory` flags only available
in the latest Clang tree. Due to all of this, the included tests are
written directly in WAT--run these with:
```console
$ cargo test --features wasi-threads -p wasmtime-cli -- cli_tests
```
[spec]: https://github.com/WebAssembly/wasi-threads
[wasi-libc]: https://github.com/WebAssembly/wasi-libc
This change does not protect the WASI implementations themselves from
concurrent access. This is already complete in previous commits or left
for future commits in certain cases (e.g., wasi-nn).
* wasi-threads: factor out process exit logic
As is being discussed [elsewhere], either calling `proc_exit` or
trapping in any thread should halt execution of all threads. The
Wasmtime CLI already has logic for adapting a WebAssembly error code to
a code expected in each OS. This change factors out this logic to a new
function, `maybe_exit_on_error`, for use within the `wasi-threads`
implementation.
This will work reasonably well for CLI users of Wasmtime +
`wasi-threads`, but embedders will want something better in the future:
when a `wasi-threads` threads fails, they may not want their application
to exit. Handling this is tricky, because it will require cancelling the
threads spawned by the `wasi-threads` implementation, something that is
not trivial to do in Rust. With this change, we defer that work until
later in order to provide a working implementation of `wasi-threads` for
experimentation.
[elsewhere]: https://github.com/WebAssembly/wasi-threads/pull/17
* review: work around `fd_fdstat_set_flags`
In order to make progress with wasi-threads, this change temporarily
works around limitations induced by `wasi-common`'s
`fd_fdstat_set_flags` to allow `&mut self` use in the implementation.
Eventual resolution is tracked in
https://github.com/bytecodealliance/wasmtime/issues/5643. This change
makes several related helper functions (e.g., `set_fdflags`) take `&mut
self` as well.
* test: use `wait`/`notify` to improve `threads.wat` test
Previously, the test simply executed in a loop for some hardcoded number
of iterations. This changes uses `wait` and `notify` and atomic
operations to keep track of when the spawned threads are done and join
on the main thread appropriately.
* various fixes and tweaks due to the PR review
---------
Signed-off-by: Harald Hoyer <harald@profian.com>
Co-authored-by: Harald Hoyer <harald@profian.com>
Co-authored-by: Alex Crichton <alex@alexcrichton.com>
This commit is contained in:
23
crates/wasi-threads/Cargo.toml
Normal file
23
crates/wasi-threads/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "wasmtime-wasi-threads"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
description = "Wasmtime implementation of the wasi-threads API"
|
||||
documentation = "https://docs.rs/wasmtime-wasi-nn"
|
||||
license = "Apache-2.0 WITH LLVM-exception"
|
||||
categories = ["wasm", "parallelism", "threads"]
|
||||
keywords = ["webassembly", "wasm", "neural-network"]
|
||||
repository = "https://github.com/bytecodealliance/wasmtime"
|
||||
readme = "README.md"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rand = "0.8"
|
||||
wasi-common = { workspace = true }
|
||||
wasmtime = { workspace = true }
|
||||
wasmtime-wasi = { workspace = true, features = ["exit"] }
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "experimental" }
|
||||
12
crates/wasi-threads/README.md
Normal file
12
crates/wasi-threads/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# wasmtime-wasi-threads
|
||||
|
||||
Implement the `wasi-threads` [specification] in Wasmtime.
|
||||
|
||||
[specification]: https://github.com/WebAssembly/wasi-threads
|
||||
|
||||
> Note: this crate is experimental and not yet suitable for use in multi-tenant
|
||||
> embeddings. As specified, a trap or WASI exit in one thread must end execution
|
||||
> for all threads. Due to the complexity of stopping threads, however, this
|
||||
> implementation currently exits the process entirely. This will work for some
|
||||
> use cases (e.g., CLI usage) but not for embedders. This warning can be removed
|
||||
> once a suitable mechanism is implemented that avoids exiting the process.
|
||||
159
crates/wasi-threads/src/lib.rs
Normal file
159
crates/wasi-threads/src/lib.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
//! Implement [`wasi-threads`].
|
||||
//!
|
||||
//! [`wasi-threads`]: https://github.com/WebAssembly/wasi-threads
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use rand::Rng;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use wasmtime::{Caller, Linker, Module, SharedMemory, Store, ValType};
|
||||
use wasmtime_wasi::maybe_exit_on_error;
|
||||
|
||||
// This name is a function export designated by the wasi-threads specification:
|
||||
// https://github.com/WebAssembly/wasi-threads/#detailed-design-discussion
|
||||
const WASI_ENTRY_POINT: &str = "wasi_thread_start";
|
||||
|
||||
pub struct WasiThreadsCtx<T> {
|
||||
module: Module,
|
||||
linker: Arc<Linker<T>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Send + 'static> WasiThreadsCtx<T> {
|
||||
pub fn new(module: Module, linker: Arc<Linker<T>>) -> Result<Self> {
|
||||
if !has_wasi_entry_point(&module) {
|
||||
bail!(
|
||||
"failed to find wasi-threads entry point function: {}",
|
||||
WASI_ENTRY_POINT
|
||||
);
|
||||
}
|
||||
Ok(Self { module, linker })
|
||||
}
|
||||
|
||||
pub fn spawn(&self, host: T, thread_start_arg: i32) -> Result<i32> {
|
||||
let module = self.module.clone();
|
||||
let linker = self.linker.clone();
|
||||
|
||||
// Start a Rust thread running a new instance of the current module.
|
||||
let wasi_thread_id = random_thread_id();
|
||||
let builder = thread::Builder::new().name(format!("wasi-thread-{}", wasi_thread_id));
|
||||
builder.spawn(move || {
|
||||
// Catch any panic failures in host code; e.g., if a WASI module
|
||||
// were to crash, we want all threads to exit, not just this one.
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
// Each new instance is created in its own store.
|
||||
let mut store = Store::new(&module.engine(), host);
|
||||
|
||||
// Ideally, we would have already checked much earlier (e.g.,
|
||||
// `new`) whether the module can be instantiated. Because
|
||||
// `Linker::instantiate_pre` requires a `Store` and that is only
|
||||
// available now. TODO:
|
||||
// https://github.com/bytecodealliance/wasmtime/issues/5675.
|
||||
let instance = linker.instantiate(&mut store, &module).expect(&format!(
|
||||
"wasi-thread-{} exited unsuccessfully: failed to instantiate",
|
||||
wasi_thread_id
|
||||
));
|
||||
let thread_entry_point = instance
|
||||
.get_typed_func::<(i32, i32), ()>(&mut store, WASI_ENTRY_POINT)
|
||||
.unwrap();
|
||||
|
||||
// Start the thread's entry point. Any traps or calls to
|
||||
// `proc_exit`, by specification, should end execution for all
|
||||
// threads. This code uses `process::exit` to do so, which is what
|
||||
// the user expects from the CLI but probably not in a Wasmtime
|
||||
// embedding.
|
||||
log::trace!(
|
||||
"spawned thread id = {}; calling start function `{}` with: {}",
|
||||
wasi_thread_id,
|
||||
WASI_ENTRY_POINT,
|
||||
thread_start_arg
|
||||
);
|
||||
match thread_entry_point.call(&mut store, (wasi_thread_id, thread_start_arg)) {
|
||||
Ok(_) => log::trace!("exiting thread id = {} normally", wasi_thread_id),
|
||||
Err(e) => {
|
||||
log::trace!("exiting thread id = {} due to error", wasi_thread_id);
|
||||
let e = maybe_exit_on_error(e);
|
||||
eprintln!("Error: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("wasi-thread-{} panicked: {:?}", wasi_thread_id, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(wasi_thread_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for generating valid WASI thread IDs (TID).
|
||||
///
|
||||
/// Callers of `wasi_thread_spawn` expect a TID >=0 to indicate a successful
|
||||
/// spawning of the thread whereas a negative return value indicates an
|
||||
/// failure to spawn.
|
||||
fn random_thread_id() -> i32 {
|
||||
let tid: u32 = rand::thread_rng().gen();
|
||||
(tid >> 1) as i32
|
||||
}
|
||||
|
||||
/// Manually add the WASI `thread_spawn` function to the linker.
|
||||
///
|
||||
/// It is unclear what namespace the `wasi-threads` proposal should live under:
|
||||
/// it is not clear if it should be included in any of the `preview*` releases
|
||||
/// so for the time being its module namespace is simply `"wasi"` (TODO).
|
||||
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,
|
||||
) -> anyhow::Result<SharedMemory> {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
// Find the shared memory import and satisfy it with a newly-created shared
|
||||
// memory import. This currently does not handle multiple memories (TODO).
|
||||
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())?;
|
||||
return Ok(mem);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"unable to link a shared memory import to the module; a `wasi-threads` \
|
||||
module should import a single shared memory as \"memory\""
|
||||
))
|
||||
}
|
||||
|
||||
fn has_wasi_entry_point(module: &Module) -> bool {
|
||||
module
|
||||
.get_export(WASI_ENTRY_POINT)
|
||||
.and_then(|t| t.func().cloned())
|
||||
.and_then(|t| {
|
||||
let params: Vec<ValType> = t.params().collect();
|
||||
Some(params == [ValType::I32, ValType::I32] && t.results().len() == 0)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
Reference in New Issue
Block a user