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>
501 lines
14 KiB
Rust
501 lines
14 KiB
Rust
use anyhow::{bail, Context, Result};
|
|
use std::fs::File;
|
|
use std::io::{Read, Write};
|
|
use std::path::Path;
|
|
use std::process::{Command, Output, Stdio};
|
|
use tempfile::{NamedTempFile, TempDir};
|
|
|
|
// Run the wasmtime CLI with the provided args and return the `Output`.
|
|
// If the `stdin` is `Some`, opens the file and redirects to the child's stdin.
|
|
fn run_wasmtime_for_output(args: &[&str], stdin: Option<&Path>) -> Result<Output> {
|
|
let runner = std::env::vars()
|
|
.filter(|(k, _v)| k.starts_with("CARGO_TARGET") && k.ends_with("RUNNER"))
|
|
.next();
|
|
let mut me = std::env::current_exe()?;
|
|
me.pop(); // chop off the file name
|
|
me.pop(); // chop off `deps`
|
|
me.push("wasmtime");
|
|
|
|
let stdin = stdin
|
|
.map(File::open)
|
|
.transpose()
|
|
.context("Cannot open a file to use as stdin")?;
|
|
|
|
// If we're running tests with a "runner" then we might be doing something
|
|
// like cross-emulation, so spin up the emulator rather than the tests
|
|
// itself, which may not be natively executable.
|
|
let mut cmd = if let Some((_, runner)) = runner {
|
|
let mut parts = runner.split_whitespace();
|
|
let mut cmd = Command::new(parts.next().unwrap());
|
|
for arg in parts {
|
|
cmd.arg(arg);
|
|
}
|
|
cmd.arg(&me);
|
|
cmd
|
|
} else {
|
|
Command::new(&me)
|
|
};
|
|
|
|
if let Some(mut f) = stdin {
|
|
let mut buf = Vec::new();
|
|
f.read_to_end(&mut buf)?;
|
|
|
|
let mut child = cmd
|
|
.stdout(Stdio::piped())
|
|
.stdin(Stdio::piped())
|
|
.args(args)
|
|
.spawn()?;
|
|
|
|
let mut stdin = child.stdin.take().unwrap();
|
|
std::thread::spawn(move || {
|
|
stdin
|
|
.write_all(&buf)
|
|
.expect("failed to write module to child stdin")
|
|
});
|
|
child.wait_with_output().map_err(Into::into)
|
|
} else {
|
|
cmd.args(args).output().map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
// Run the wasmtime CLI with the provided args and, if it succeeds, return
|
|
// the standard output in a `String`.
|
|
fn run_wasmtime(args: &[&str]) -> Result<String> {
|
|
let output = run_wasmtime_for_output(args, None)?;
|
|
if !output.status.success() {
|
|
bail!(
|
|
"Failed to execute wasmtime with: {:?}\n{}",
|
|
args,
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
Ok(String::from_utf8(output.stdout).unwrap())
|
|
}
|
|
|
|
fn build_wasm(wat_path: impl AsRef<Path>) -> Result<NamedTempFile> {
|
|
let mut wasm_file = NamedTempFile::new()?;
|
|
let wasm = wat::parse_file(wat_path)?;
|
|
wasm_file.write(&wasm)?;
|
|
Ok(wasm_file)
|
|
}
|
|
|
|
// Very basic use case: compile binary wasm file and run specific function with arguments.
|
|
#[test]
|
|
fn run_wasmtime_simple() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/simple.wat")?;
|
|
run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--invoke",
|
|
"simple",
|
|
"--disable-cache",
|
|
"4",
|
|
])?;
|
|
Ok(())
|
|
}
|
|
|
|
// Wasmtime shakk when not enough arguments were provided.
|
|
#[test]
|
|
fn run_wasmtime_simple_fail_no_args() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/simple.wat")?;
|
|
assert!(
|
|
run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--disable-cache",
|
|
"--invoke",
|
|
"simple",
|
|
])
|
|
.is_err(),
|
|
"shall fail"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// Running simple wat
|
|
#[test]
|
|
fn run_wasmtime_simple_wat() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/simple.wat")?;
|
|
run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--invoke",
|
|
"simple",
|
|
"--disable-cache",
|
|
"4",
|
|
])?;
|
|
assert_eq!(
|
|
run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--invoke",
|
|
"get_f32",
|
|
"--disable-cache",
|
|
])?,
|
|
"100\n"
|
|
);
|
|
assert_eq!(
|
|
run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--invoke",
|
|
"get_f64",
|
|
"--disable-cache",
|
|
])?,
|
|
"100\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// Running a wat that traps.
|
|
#[test]
|
|
fn run_wasmtime_unreachable_wat() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/unreachable.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
|
|
assert_ne!(output.stderr, b"");
|
|
assert_eq!(output.stdout, b"");
|
|
assert!(!output.status.success());
|
|
|
|
let code = output
|
|
.status
|
|
.code()
|
|
.expect("wasmtime process should exit normally");
|
|
|
|
// Test for the specific error code Wasmtime uses to indicate a trap return.
|
|
#[cfg(unix)]
|
|
assert_eq!(code, 128 + libc::SIGABRT);
|
|
#[cfg(windows)]
|
|
assert_eq!(code, 3);
|
|
Ok(())
|
|
}
|
|
|
|
// Run a simple WASI hello world, snapshot0 edition.
|
|
#[test]
|
|
fn hello_wasi_snapshot0() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot0.wat")?;
|
|
let stdout = run_wasmtime(&[wasm.path().to_str().unwrap(), "--disable-cache"])?;
|
|
assert_eq!(stdout, "Hello, world!\n");
|
|
Ok(())
|
|
}
|
|
|
|
// Run a simple WASI hello world, snapshot1 edition.
|
|
#[test]
|
|
fn hello_wasi_snapshot1() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot1.wat")?;
|
|
let stdout = run_wasmtime(&[wasm.path().to_str().unwrap(), "--disable-cache"])?;
|
|
assert_eq!(stdout, "Hello, world!\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn timeout_in_start() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/iloop-start.wat")?;
|
|
let output = run_wasmtime_for_output(
|
|
&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--wasm-timeout",
|
|
"1ms",
|
|
"--disable-cache",
|
|
],
|
|
None,
|
|
)?;
|
|
assert!(!output.status.success());
|
|
assert_eq!(output.stdout, b"");
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr.contains("wasm trap: interrupt"),
|
|
"bad stderr: {}",
|
|
stderr
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn timeout_in_invoke() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/iloop-invoke.wat")?;
|
|
let output = run_wasmtime_for_output(
|
|
&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--wasm-timeout",
|
|
"1ms",
|
|
"--disable-cache",
|
|
],
|
|
None,
|
|
)?;
|
|
assert!(!output.status.success());
|
|
assert_eq!(output.stdout, b"");
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
stderr.contains("wasm trap: interrupt"),
|
|
"bad stderr: {}",
|
|
stderr
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// Exit with a valid non-zero exit code, snapshot0 edition.
|
|
#[test]
|
|
fn exit2_wasi_snapshot0() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/exit2_wasi_snapshot0.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
assert_eq!(output.status.code().unwrap(), 2);
|
|
Ok(())
|
|
}
|
|
|
|
// Exit with a valid non-zero exit code, snapshot1 edition.
|
|
#[test]
|
|
fn exit2_wasi_snapshot1() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/exit2_wasi_snapshot1.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
assert_eq!(output.status.code().unwrap(), 2);
|
|
Ok(())
|
|
}
|
|
|
|
// Exit with a valid non-zero exit code, snapshot0 edition.
|
|
#[test]
|
|
fn exit125_wasi_snapshot0() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/exit125_wasi_snapshot0.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
if cfg!(windows) {
|
|
assert_eq!(output.status.code().unwrap(), 1);
|
|
} else {
|
|
assert_eq!(output.status.code().unwrap(), 125);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Exit with a valid non-zero exit code, snapshot1 edition.
|
|
#[test]
|
|
fn exit125_wasi_snapshot1() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/exit125_wasi_snapshot1.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
if cfg!(windows) {
|
|
assert_eq!(output.status.code().unwrap(), 1);
|
|
} else {
|
|
assert_eq!(output.status.code().unwrap(), 125);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Exit with an invalid non-zero exit code, snapshot0 edition.
|
|
#[test]
|
|
fn exit126_wasi_snapshot0() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/exit126_wasi_snapshot0.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
assert_eq!(output.status.code().unwrap(), 1);
|
|
assert!(output.stdout.is_empty());
|
|
assert!(String::from_utf8_lossy(&output.stderr).contains("invalid exit status"));
|
|
Ok(())
|
|
}
|
|
|
|
// Exit with an invalid non-zero exit code, snapshot1 edition.
|
|
#[test]
|
|
fn exit126_wasi_snapshot1() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/exit126_wasi_snapshot1.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
assert_eq!(output.status.code().unwrap(), 1);
|
|
assert!(output.stdout.is_empty());
|
|
assert!(String::from_utf8_lossy(&output.stderr).contains("invalid exit status"));
|
|
Ok(())
|
|
}
|
|
|
|
// Run a minimal command program.
|
|
#[test]
|
|
fn minimal_command() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/minimal-command.wat")?;
|
|
let stdout = run_wasmtime(&[wasm.path().to_str().unwrap(), "--disable-cache"])?;
|
|
assert_eq!(stdout, "");
|
|
Ok(())
|
|
}
|
|
|
|
// Run a minimal reactor program.
|
|
#[test]
|
|
fn minimal_reactor() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/minimal-reactor.wat")?;
|
|
let stdout = run_wasmtime(&[wasm.path().to_str().unwrap(), "--disable-cache"])?;
|
|
assert_eq!(stdout, "");
|
|
Ok(())
|
|
}
|
|
|
|
// Attempt to call invoke on a command.
|
|
#[test]
|
|
fn command_invoke() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/minimal-command.wat")?;
|
|
run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--invoke",
|
|
"_start",
|
|
"--disable-cache",
|
|
])?;
|
|
Ok(())
|
|
}
|
|
|
|
// Attempt to call invoke on a command.
|
|
#[test]
|
|
fn reactor_invoke() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/minimal-reactor.wat")?;
|
|
run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--invoke",
|
|
"_initialize",
|
|
"--disable-cache",
|
|
])?;
|
|
Ok(())
|
|
}
|
|
|
|
// Run the greeter test, which runs a preloaded reactor and a command.
|
|
#[test]
|
|
fn greeter() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/greeter_command.wat")?;
|
|
let stdout = run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--disable-cache",
|
|
"--preload",
|
|
"reactor=tests/all/cli_tests/greeter_reactor.wat",
|
|
])?;
|
|
assert_eq!(
|
|
stdout,
|
|
"Hello _initialize\nHello _start\nHello greet\nHello done\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// Run the greeter test, but this time preload a command.
|
|
#[test]
|
|
fn greeter_preload_command() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/greeter_reactor.wat")?;
|
|
let stdout = run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--disable-cache",
|
|
"--preload",
|
|
"reactor=tests/all/cli_tests/hello_wasi_snapshot1.wat",
|
|
])?;
|
|
assert_eq!(stdout, "Hello _initialize\n");
|
|
Ok(())
|
|
}
|
|
|
|
// Run the greeter test, which runs a preloaded reactor and a command.
|
|
#[test]
|
|
fn greeter_preload_callable_command() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/greeter_command.wat")?;
|
|
let stdout = run_wasmtime(&[
|
|
"run",
|
|
wasm.path().to_str().unwrap(),
|
|
"--disable-cache",
|
|
"--preload",
|
|
"reactor=tests/all/cli_tests/greeter_callable_command.wat",
|
|
])?;
|
|
assert_eq!(stdout, "Hello _start\nHello callable greet\nHello done\n");
|
|
Ok(())
|
|
}
|
|
|
|
// Ensure successful WASI exit call with FPR saving frames on stack for Windows x64
|
|
// See https://github.com/bytecodealliance/wasmtime/issues/1967
|
|
#[test]
|
|
fn exit_with_saved_fprs() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/exit_with_saved_fprs.wat")?;
|
|
let output =
|
|
run_wasmtime_for_output(&[wasm.path().to_str().unwrap(), "--disable-cache"], None)?;
|
|
assert_eq!(output.status.code().unwrap(), 0);
|
|
assert!(output.stdout.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn run_cwasm() -> Result<()> {
|
|
let td = TempDir::new()?;
|
|
let cwasm = td.path().join("foo.cwasm");
|
|
let stdout = run_wasmtime(&[
|
|
"compile",
|
|
"tests/all/cli_tests/simple.wat",
|
|
"-o",
|
|
cwasm.to_str().unwrap(),
|
|
])?;
|
|
assert_eq!(stdout, "");
|
|
let stdout = run_wasmtime(&["run", "--allow-precompiled", cwasm.to_str().unwrap()])?;
|
|
assert_eq!(stdout, "");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn hello_wasi_snapshot0_from_stdin() -> Result<()> {
|
|
// Run a simple WASI hello world, snapshot0 edition.
|
|
// The module is piped from standard input.
|
|
let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot0.wat")?;
|
|
let stdout = {
|
|
let path = wasm.path();
|
|
let args: &[&str] = &["-", "--disable-cache"];
|
|
let output = run_wasmtime_for_output(args, Some(path))?;
|
|
if !output.status.success() {
|
|
bail!(
|
|
"Failed to execute wasmtime with: {:?}\n{}",
|
|
args,
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
Ok::<_, anyhow::Error>(String::from_utf8(output.stdout).unwrap())
|
|
}?;
|
|
assert_eq!(stdout, "Hello, world!\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn run_cwasm_from_stdin() -> Result<()> {
|
|
let td = TempDir::new()?;
|
|
let cwasm = td.path().join("foo.cwasm");
|
|
let stdout = run_wasmtime(&[
|
|
"compile",
|
|
"tests/all/cli_tests/simple.wat",
|
|
"-o",
|
|
cwasm.to_str().unwrap(),
|
|
])?;
|
|
assert_eq!(stdout, "");
|
|
let args: &[&str] = &["run", "--allow-precompiled", "-"];
|
|
let output = run_wasmtime_for_output(args, Some(&cwasm))?;
|
|
if output.status.success() {
|
|
bail!("wasmtime should fail loading precompiled modules from piped files, but suceeded");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "wasi-threads")]
|
|
#[test]
|
|
fn run_threads() -> Result<()> {
|
|
let wasm = build_wasm("tests/all/cli_tests/threads.wat")?;
|
|
let stdout = run_wasmtime(&[
|
|
"run",
|
|
"--wasi-modules",
|
|
"experimental-wasi-threads",
|
|
"--wasm-features",
|
|
"threads",
|
|
"--disable-cache",
|
|
wasm.path().to_str().unwrap(),
|
|
])?;
|
|
|
|
assert!(
|
|
stdout
|
|
== "Called _start\n\
|
|
Running wasi_thread_start\n\
|
|
Running wasi_thread_start\n\
|
|
Running wasi_thread_start\n\
|
|
Done\n"
|
|
);
|
|
Ok(())
|
|
}
|