diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b3dfe2d70..71dc0a3b3c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -208,6 +208,7 @@ jobs: name: Test runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: build: [stable, beta, nightly, windows, macos] include: @@ -382,6 +383,7 @@ jobs: name: Build wasmtime runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: include: - build: x86_64-linux diff --git a/Cargo.lock b/Cargo.lock index 102a126904..2551a2e8ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,6 +261,12 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + [[package]] name = "cap-fs-ext" version = "0.13.9" @@ -315,6 +321,17 @@ dependencies = [ "unsafe-io", ] +[[package]] +name = "cap-tempfile" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2f6f45ddb06ff26f4cf2ba9838d5826d52e1a5f6b321d71f114bb38cf34a57" +dependencies = [ + "cap-std", + "rand 0.8.3", + "uuid", +] + [[package]] name = "cap-time-ext" version = "0.13.9" @@ -1073,6 +1090,10 @@ dependencies = [ name = "example-fib-debug-wasm" version = "0.0.0" +[[package]] +name = "example-tokio-wasm" +version = "0.0.0" + [[package]] name = "example-wasi-wasm" version = "0.0.0" @@ -1551,6 +1572,28 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "mio" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + [[package]] name = "more-asserts" version = "0.2.1" @@ -1580,6 +1623,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + [[package]] name = "num-bigint" version = "0.2.6" @@ -2732,6 +2784,7 @@ dependencies = [ "pretty_env_logger", "target-lexicon", "tempfile", + "tokio", "wasi-cap-std-sync", "wasi-common", "wasmtime", @@ -2787,6 +2840,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "tokio" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" +dependencies = [ + "autocfg 1.0.1", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.5.8" @@ -2938,9 +3018,9 @@ dependencies = [ [[package]] name = "unsafe-io" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0301dd0f2c21baed606faa2717fbfbb1a68b7e289ea29b40bc21a16f5ae9f5aa" +checksum = "fe39acfe60d3754452ea6881613c3240100b23ffd94a627c138863f8cd314b1b" dependencies = [ "rustc_version", "winapi", @@ -2970,6 +3050,15 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", +] + [[package]] name = "vec_map" version = "0.8.2" @@ -3025,6 +3114,7 @@ name = "wasi-cap-std-sync" version = "0.26.0" dependencies = [ "anyhow", + "async-trait", "bitflags", "cap-fs-ext", "cap-rand", @@ -3085,6 +3175,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "wasi-tokio" +version = "0.26.0" +dependencies = [ + "anyhow", + "bitflags", + "cap-fs-ext", + "cap-std", + "cap-tempfile", + "cap-time-ext", + "fs-set-times", + "lazy_static", + "libc", + "posish", + "system-interface", + "tempfile", + "tokio", + "tracing", + "unsafe-io", + "wasi-cap-std-sync", + "wasi-common", + "wiggle", + "winapi", +] + [[package]] name = "wasm-encoder" version = "0.4.1" @@ -3263,6 +3378,7 @@ dependencies = [ "target-lexicon", "tempfile", "test-programs", + "tokio", "tracing-subscriber", "wasmparser", "wasmtime", @@ -3491,6 +3607,7 @@ dependencies = [ "anyhow", "wasi-cap-std-sync", "wasi-common", + "wasi-tokio", "wasmtime", "wasmtime-wiggle", "wiggle", diff --git a/Cargo.toml b/Cargo.toml index e23bddb5e3..ae79508b1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ tempfile = "3.1.0" test-programs = { path = "crates/test-programs" } wasmtime-fuzzing = { path = "crates/fuzzing" } wasmtime-runtime = { path = "crates/runtime" } +tokio = { version = "1.5.0", features = ["rt", "time", "macros", "rt-multi-thread"] } tracing-subscriber = "0.2.16" wast = "35.0.0" @@ -78,8 +79,10 @@ members = [ "crates/wiggle/wasmtime", "crates/wasi-common", "crates/wasi-common/cap-std-sync", + "crates/wasi-common/tokio", "examples/fib-debug/wasm", "examples/wasi/wasm", + "examples/tokio/wasm", "fuzz", ] @@ -107,5 +110,9 @@ maintenance = { status = "actively-developed" } name = "host_segfault" harness = false +[[example]] +name = "tokio" +required-features = ["wasmtime-wasi/tokio"] + [profile.dev.package.backtrace] debug = false # FIXME(#1813) diff --git a/crates/bench-api/src/lib.rs b/crates/bench-api/src/lib.rs index b2884d7738..6cf6104279 100644 --- a/crates/bench-api/src/lib.rs +++ b/crates/bench-api/src/lib.rs @@ -83,9 +83,8 @@ use std::env; use std::os::raw::{c_int, c_void}; use std::path::Path; use std::slice; -use wasi_cap_std_sync::WasiCtxBuilder; use wasmtime::{Config, Engine, Instance, Linker, Module, Store}; -use wasmtime_wasi::Wasi; +use wasmtime_wasi::sync::{Wasi, WasiCtxBuilder}; pub type ExitCode = c_int; pub const OK: ExitCode = 0; diff --git a/crates/c-api/src/wasi.rs b/crates/c-api/src/wasi.rs index 441fa6fa54..302fcde8b7 100644 --- a/crates/c-api/src/wasi.rs +++ b/crates/c-api/src/wasi.rs @@ -12,9 +12,10 @@ use std::slice; use std::str; use wasmtime::{Extern, Linker, Trap}; use wasmtime_wasi::{ - snapshots::preview_0::Wasi as WasiSnapshot0, - snapshots::preview_1::Wasi as WasiPreview1, - sync::{Dir, WasiCtxBuilder}, + sync::{ + snapshots::preview_0::Wasi as WasiSnapshot0, snapshots::preview_1::Wasi as WasiPreview1, + Dir, WasiCtxBuilder, + }, WasiCtx, }; diff --git a/crates/misc/run-examples/src/main.rs b/crates/misc/run-examples/src/main.rs index 12746b2a38..362f9e1774 100644 --- a/crates/misc/run-examples/src/main.rs +++ b/crates/misc/run-examples/src/main.rs @@ -45,10 +45,13 @@ fn main() -> anyhow::Result<()> { .arg(target))?; } println!("======== Rust example `{}` ============", example); - run(Command::new("cargo") - .arg("run") - .arg("--example") - .arg(&example))?; + let mut cargo_cmd = Command::new("cargo"); + cargo_cmd.arg("run").arg("--example").arg(&example); + + if example.contains("tokio") { + cargo_cmd.arg("--features").arg("wasmtime-wasi/tokio"); + } + run(&mut cargo_cmd)?; println!("======== C/C++ example `{}` ============", example); for extension in ["c", "cc"].iter() { diff --git a/crates/test-programs/Cargo.toml b/crates/test-programs/Cargo.toml index 4cd5bcbc12..7c4d9c3ad4 100644 --- a/crates/test-programs/Cargo.toml +++ b/crates/test-programs/Cargo.toml @@ -14,7 +14,7 @@ cfg-if = "1.0" wasi-common = { path = "../wasi-common", version = "0.26.0" } wasi-cap-std-sync = { path = "../wasi-common/cap-std-sync", version = "0.26.0" } wasmtime = { path = "../wasmtime", version = "0.26.0" } -wasmtime-wasi = { path = "../wasi", version = "0.26.0" } +wasmtime-wasi = { path = "../wasi", version = "0.26.0", features = ["tokio"] } target-lexicon = "0.12.0" pretty_env_logger = "0.4.0" tempfile = "3.1.0" @@ -22,6 +22,7 @@ os_pipe = "0.9" anyhow = "1.0.19" wat = "1.0.37" cap-std = "0.13" +tokio = { version = "1.5.0", features = ["rt-multi-thread"] } [features] test_programs = [] diff --git a/crates/test-programs/build.rs b/crates/test-programs/build.rs index abef0738f0..6894f66a81 100644 --- a/crates/test-programs/build.rs +++ b/crates/test-programs/build.rs @@ -40,7 +40,9 @@ mod wasi_tests { File::create(out_dir.join("wasi_tests.rs")).expect("error generating test source file"); build_tests("wasi-tests", &out_dir).expect("building tests"); test_directory(&mut out, "wasi-cap-std-sync", "cap_std_sync", &out_dir) - .expect("generating tests"); + .expect("generating wasi-cap-std-sync tests"); + test_directory(&mut out, "wasi-tokio", "tokio", &out_dir) + .expect("generating wasi-tokio tests"); } fn build_tests(testsuite: &str, out_dir: &Path) -> io::Result<()> { @@ -173,6 +175,7 @@ mod wasi_tests { match testsuite { "wasi-cap-std-sync" => cap_std_sync_ignore(name), "wasi-virtfs" => virtfs_ignore(name), + "wasi-tokio" => tokio_ignore(name), _ => panic!("unknown test suite: {}", testsuite), } } @@ -200,6 +203,10 @@ mod wasi_tests { .contains(&name) } + /// Tokio should support the same things as cap_std_sync + fn tokio_ignore(name: &str) -> bool { + cap_std_sync_ignore(name) + } /// Virtfs barely works at all and is not suitable for any purpose fn virtfs_ignore(name: &str) -> bool { [ @@ -260,7 +267,7 @@ mod wasi_tests { /// Mark tests which require inheriting parent process stdio fn inherit_stdio(testsuite: &str, name: &str) -> bool { match testsuite { - "wasi-cap-std-sync" => match name { + "wasi-cap-std-sync" | "wasi-tokio" => match name { "poll_oneoff_stdio" => true, _ => false, }, diff --git a/crates/test-programs/tests/wasm_tests/runtime/cap_std_sync.rs b/crates/test-programs/tests/wasm_tests/runtime/cap_std_sync.rs index 1610880115..32aa3cbedb 100644 --- a/crates/test-programs/tests/wasm_tests/runtime/cap_std_sync.rs +++ b/crates/test-programs/tests/wasm_tests/runtime/cap_std_sync.rs @@ -1,10 +1,26 @@ use anyhow::Context; use std::path::Path; -use wasi_cap_std_sync::WasiCtxBuilder; use wasi_common::pipe::WritePipe; use wasmtime::{Linker, Module, Store}; +use wasmtime_wasi::sync::{Wasi, WasiCtxBuilder}; pub fn instantiate(data: &[u8], bin_name: &str, workspace: Option<&Path>) -> anyhow::Result<()> { + run(data, bin_name, workspace, false) +} +pub fn instantiate_inherit_stdio( + data: &[u8], + bin_name: &str, + workspace: Option<&Path>, +) -> anyhow::Result<()> { + run(data, bin_name, workspace, true) +} + +fn run( + data: &[u8], + bin_name: &str, + workspace: Option<&Path>, + inherit_stdio: bool, +) -> anyhow::Result<()> { let stdout = WritePipe::new_in_memory(); let stderr = WritePipe::new_in_memory(); @@ -15,41 +31,29 @@ pub fn instantiate(data: &[u8], bin_name: &str, workspace: Option<&Path>) -> any // Additionally register any preopened directories if we have them. let mut builder = WasiCtxBuilder::new(); - builder = builder - .arg(bin_name)? - .arg(".")? - .stdout(Box::new(stdout.clone())) - .stderr(Box::new(stderr.clone())); + if inherit_stdio { + builder = builder.inherit_stdio(); + } else { + builder = builder + .stdout(Box::new(stdout.clone())) + .stderr(Box::new(stderr.clone())); + } + + builder = builder.arg(bin_name)?.arg(".")?; if let Some(workspace) = workspace { println!("preopen: {:?}", workspace); let preopen_dir = unsafe { cap_std::fs::Dir::open_ambient_dir(workspace) }?; builder = builder.preopened_dir(preopen_dir, ".")?; } - - #[cfg(windows)] - { - builder = builder - .env("ERRNO_MODE_WINDOWS", "1")? - .env("NO_DANGLING_FILESYSTEM", "1")? - .env("NO_FD_ALLOCATE", "1")? - .env("NO_RENAME_DIR_TO_EMPTY_DIR", "1")? - } - #[cfg(all(unix, not(target_os = "macos")))] - { - builder = builder.env("ERRNO_MODE_UNIX", "1")?; - } - #[cfg(target_os = "macos")] - { - builder = builder - .env("ERRNO_MODE_MACOS", "1")? - .env("NO_FD_ALLOCATE", "1")?; + for (var, val) in super::test_suite_environment() { + builder = builder.env(var, val)?; } // cap-std-sync does not yet support the sync family of fdflags builder = builder.env("NO_FDFLAGS_SYNC_SUPPORT", "1")?; - let wasi = wasmtime_wasi::Wasi::new(&store, builder.build()?); + let wasi = Wasi::new(&store, builder.build()?); let mut linker = Linker::new(&store); @@ -82,41 +86,3 @@ pub fn instantiate(data: &[u8], bin_name: &str, workspace: Option<&Path>) -> any } } } - -pub fn instantiate_inherit_stdio( - data: &[u8], - bin_name: &str, - workspace: Option<&Path>, -) -> anyhow::Result<()> { - let r = { - let store = Store::default(); - - // Create our wasi context. - // Additionally register any preopened directories if we have them. - let mut builder = WasiCtxBuilder::new(); - - builder = builder.arg(bin_name)?.arg(".")?.inherit_stdio(); - - if let Some(workspace) = workspace { - println!("preopen: {:?}", workspace); - let preopen_dir = unsafe { cap_std::fs::Dir::open_ambient_dir(workspace) }?; - builder = builder.preopened_dir(preopen_dir, ".")?; - } - - let snapshot1 = wasmtime_wasi::Wasi::new(&store, builder.build()?); - - let mut linker = Linker::new(&store); - - snapshot1.add_to_linker(&mut linker)?; - - let module = Module::new(store.engine(), &data).context("failed to create wasm module")?; - let instance = linker.instantiate(&module)?; - let start = instance.get_typed_func::<(), ()>("_start")?; - start.call(()).map_err(anyhow::Error::from) - }; - - match r { - Ok(()) => Ok(()), - Err(trap) => Err(trap.context(format!("error while testing Wasm module '{}'", bin_name,))), - } -} diff --git a/crates/test-programs/tests/wasm_tests/runtime/mod.rs b/crates/test-programs/tests/wasm_tests/runtime/mod.rs index 035b1e83ea..9a4fe8202a 100644 --- a/crates/test-programs/tests/wasm_tests/runtime/mod.rs +++ b/crates/test-programs/tests/wasm_tests/runtime/mod.rs @@ -1 +1,34 @@ pub mod cap_std_sync; +pub mod tokio; + +// Configure the test suite environment. +// Test programs use these environment variables to determine what behavior +// is expected: different errnos are expected on windows, mac, and other unixes, +// and other filesystem operations are supported or not. +pub fn test_suite_environment() -> &'static [(&'static str, &'static str)] { + #[cfg(windows)] + { + &[ + ("ERRNO_MODE_WINDOWS", "1"), + // Windows does not support dangling links or symlinks in the filesystem. + ("NO_DANGLING_FILESYSTEM", "1"), + // Windows does not support fd_allocate. + ("NO_FD_ALLOCATE", "1"), + // Windows does not support renaming a directory to an empty directory - + // empty directory must be deleted. + ("NO_RENAME_DIR_TO_EMPTY_DIR", "1"), + ] + } + #[cfg(all(unix, not(target_os = "macos")))] + { + &[("ERRNO_MODE_UNIX", "1")] + } + #[cfg(target_os = "macos")] + { + &[ + ("ERRNO_MODE_MACOS", "1"), + // MacOS does not support fd_allocate + ("NO_FD_ALLOCATE", "1"), + ] + } +} diff --git a/crates/test-programs/tests/wasm_tests/runtime/tokio.rs b/crates/test-programs/tests/wasm_tests/runtime/tokio.rs new file mode 100644 index 0000000000..28577015bc --- /dev/null +++ b/crates/test-programs/tests/wasm_tests/runtime/tokio.rs @@ -0,0 +1,96 @@ +use anyhow::Context; +use std::path::Path; +use wasi_common::pipe::WritePipe; +use wasmtime::{Config, Engine, Linker, Module, Store}; +use wasmtime_wasi::tokio::{Wasi, WasiCtxBuilder}; + +pub fn instantiate(data: &[u8], bin_name: &str, workspace: Option<&Path>) -> anyhow::Result<()> { + run(data, bin_name, workspace, false) +} +pub fn instantiate_inherit_stdio( + data: &[u8], + bin_name: &str, + workspace: Option<&Path>, +) -> anyhow::Result<()> { + run(data, bin_name, workspace, true) +} + +fn run( + data: &[u8], + bin_name: &str, + workspace: Option<&Path>, + inherit_stdio: bool, +) -> anyhow::Result<()> { + let stdout = WritePipe::new_in_memory(); + let stdout_ = stdout.clone(); + let stderr = WritePipe::new_in_memory(); + let stderr_ = stderr.clone(); + + let r = tokio::runtime::Runtime::new() + .expect("create runtime") + .block_on(async move { + let mut config = Config::new(); + config.async_support(true); + Wasi::add_to_config(&mut config); + let engine = Engine::new(&config)?; + let store = Store::new(&engine); + + // Create our wasi context. + let mut builder = WasiCtxBuilder::new(); + + if inherit_stdio { + builder = builder.inherit_stdio(); + } else { + builder = builder + .stdout(Box::new(stdout_.clone())) + .stderr(Box::new(stderr_.clone())); + } + + builder = builder.arg(bin_name)?.arg(".")?; + + if let Some(workspace) = workspace { + println!("preopen: {:?}", workspace); + let preopen_dir = unsafe { cap_std::fs::Dir::open_ambient_dir(workspace) }?; + builder = builder.preopened_dir(preopen_dir, ".")?; + } + + for (var, val) in super::test_suite_environment() { + builder = builder.env(var, val)?; + } + + // tokio does not yet support the sync family of fdflags, because cap-std-sync + // does not. + builder = builder.env("NO_FDFLAGS_SYNC_SUPPORT", "1")?; + + Wasi::set_context(&store, builder.build()?) + .map_err(|_| anyhow::anyhow!("wasi set_context failed"))?; + + let module = + Module::new(store.engine(), &data).context("failed to create wasm module")?; + let linker = Linker::new(&store); + let instance = linker.instantiate_async(&module).await?; + let start = instance.get_typed_func::<(), ()>("_start")?; + start.call_async(()).await.map_err(anyhow::Error::from) + }); + + match r { + Ok(()) => Ok(()), + Err(trap) => { + let stdout = stdout + .try_into_inner() + .expect("sole ref to stdout") + .into_inner(); + if !stdout.is_empty() { + println!("guest stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); + } + let stderr = stderr + .try_into_inner() + .expect("sole ref to stderr") + .into_inner(); + if !stderr.is_empty() { + println!("guest stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); + } + Err(trap.context(format!("error while testing Wasm module '{}'", bin_name,))) + } + } +} diff --git a/crates/test-programs/wasi-tests/src/bin/poll_oneoff.rs b/crates/test-programs/wasi-tests/src/bin/poll_oneoff_files.rs similarity index 73% rename from crates/test-programs/wasi-tests/src/bin/poll_oneoff.rs rename to crates/test-programs/wasi-tests/src/bin/poll_oneoff_files.rs index 6407a32f3c..d457a80544 100644 --- a/crates/test-programs/wasi-tests/src/bin/poll_oneoff.rs +++ b/crates/test-programs/wasi-tests/src/bin/poll_oneoff_files.rs @@ -55,7 +55,10 @@ unsafe fn test_timeout() { event.userdata, CLOCK_ID, "the event.userdata should contain clock_id specified by the user" ); - assert!(after - before >= timeout, "poll_oneoff should sleep for the specified interval"); + assert!( + after - before >= timeout, + "poll_oneoff should sleep for the specified interval" + ); } // Like test_timeout, but uses `CLOCKID_REALTIME`, as WASI libc's sleep @@ -90,20 +93,22 @@ unsafe fn test_sleep() { event.userdata, CLOCK_ID, "the event.userdata should contain clock_id specified by the user" ); - assert!(after - before >= timeout, "poll_oneoff should sleep for the specified interval"); + assert!( + after - before >= timeout, + "poll_oneoff should sleep for the specified interval" + ); } -unsafe fn test_fd_readwrite(fd: wasi::Fd, error_code: wasi::Errno) { - let fd_readwrite = wasi::SubscriptionFdReadwrite { - file_descriptor: fd, - }; +unsafe fn test_fd_readwrite(readable_fd: wasi::Fd, writable_fd: wasi::Fd, error_code: wasi::Errno) { let r#in = [ wasi::Subscription { userdata: 1, u: wasi::SubscriptionU { tag: wasi::EVENTTYPE_FD_READ, u: wasi::SubscriptionUU { - fd_read: fd_readwrite, + fd_read: wasi::SubscriptionFdReadwrite { + file_descriptor: readable_fd, + }, }, }, }, @@ -112,13 +117,15 @@ unsafe fn test_fd_readwrite(fd: wasi::Fd, error_code: wasi::Errno) { u: wasi::SubscriptionU { tag: wasi::EVENTTYPE_FD_WRITE, u: wasi::SubscriptionUU { - fd_write: fd_readwrite, + fd_write: wasi::SubscriptionFdReadwrite { + file_descriptor: writable_fd, + }, }, }, }, ]; let out = poll_oneoff_impl(&r#in).unwrap(); - assert_eq!(out.len(), 2, "should return 2 events"); + assert_eq!(out.len(), 2, "should return 2 events, got: {:?}", out); assert_eq!( out[0].userdata, 1, "the event.userdata should contain fd userdata specified by the user" @@ -143,26 +150,64 @@ unsafe fn test_fd_readwrite(fd: wasi::Fd, error_code: wasi::Errno) { unsafe fn test_fd_readwrite_valid_fd(dir_fd: wasi::Fd) { // Create a file in the scratch directory. - let file_fd = wasi::path_open( + let nonempty_file = wasi::path_open( dir_fd, 0, - "file", + "readable_file", wasi::OFLAGS_CREAT, - wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_WRITE | wasi::RIGHTS_POLL_FD_READWRITE, + wasi::RIGHTS_FD_WRITE, 0, 0, ) - .expect("opening a file"); + .expect("create writable file"); + // Write to file + let contents = &[1u8]; + let ciovec = wasi::Ciovec { + buf: contents.as_ptr() as *const _, + buf_len: contents.len(), + }; + wasi::fd_write(nonempty_file, &[ciovec]).expect("write"); + wasi::fd_close(nonempty_file).expect("close"); + + // Now open the file for reading + let readable_fd = wasi::path_open( + dir_fd, + 0, + "readable_file", + 0, + wasi::RIGHTS_FD_READ | wasi::RIGHTS_POLL_FD_READWRITE, + 0, + 0, + ) + .expect("opening a readable file"); + assert_gt!( - file_fd, + readable_fd, + libc::STDERR_FILENO as wasi::Fd, + "file descriptor range check", + ); + // Create a file in the scratch directory. + let writable_fd = wasi::path_open( + dir_fd, + 0, + "writable_file", + wasi::OFLAGS_CREAT, + wasi::RIGHTS_FD_WRITE | wasi::RIGHTS_POLL_FD_READWRITE, + 0, + 0, + ) + .expect("opening a writable file"); + assert_gt!( + writable_fd, libc::STDERR_FILENO as wasi::Fd, "file descriptor range check", ); - test_fd_readwrite(file_fd, wasi::ERRNO_SUCCESS); + test_fd_readwrite(readable_fd, writable_fd, wasi::ERRNO_SUCCESS); - wasi::fd_close(file_fd).expect("closing a file"); - wasi::path_unlink_file(dir_fd, "file").expect("removing a file"); + wasi::fd_close(readable_fd).expect("closing readable_file"); + wasi::path_unlink_file(dir_fd, "readable_file").expect("removing readable_file"); + wasi::path_unlink_file(dir_fd, "writable_file").expect("removing writable_file"); } unsafe fn test_fd_readwrite_invalid_fd() { diff --git a/crates/test-programs/wasi-tests/src/bin/poll_oneoff_stdio.rs b/crates/test-programs/wasi-tests/src/bin/poll_oneoff_stdio.rs index 19f60d076c..b5c3bad73e 100644 --- a/crates/test-programs/wasi-tests/src/bin/poll_oneoff_stdio.rs +++ b/crates/test-programs/wasi-tests/src/bin/poll_oneoff_stdio.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::mem::MaybeUninit; use wasi_tests::{assert_errno, STDERR_FD, STDIN_FD, STDOUT_FD}; @@ -46,7 +47,7 @@ unsafe fn test_stdin_read() { let out = poll_oneoff_impl(&r#in).unwrap(); // The result should be either a timeout, or that stdin is ready for reading. // Both are valid behaviors that depend on the test environment. - assert!(out.len() >= 1, "should return at least 1 event"); + assert!(out.len() >= 1, "stdin read should return at least 1 event"); for event in out { if event.r#type == wasi::EVENTTYPE_CLOCK { assert_errno!(event.error, wasi::ERRNO_SUCCESS); @@ -66,55 +67,61 @@ unsafe fn test_stdin_read() { } } +fn writable_subs(h: &HashMap) -> Vec { + println!("writable subs: {:?}", h); + h.iter() + .map(|(ud, fd)| wasi::Subscription { + userdata: *ud, + u: wasi::SubscriptionU { + tag: wasi::EVENTTYPE_FD_WRITE, + u: wasi::SubscriptionUU { + fd_write: wasi::SubscriptionFdReadwrite { + file_descriptor: *fd, + }, + }, + }, + }) + .collect() +} + unsafe fn test_stdout_stderr_write() { - let stdout_readwrite = wasi::SubscriptionFdReadwrite { - file_descriptor: STDOUT_FD, - }; - let stderr_readwrite = wasi::SubscriptionFdReadwrite { - file_descriptor: STDERR_FD, - }; - let r#in = [ - wasi::Subscription { - userdata: 1, - u: wasi::SubscriptionU { - tag: wasi::EVENTTYPE_FD_WRITE, - u: wasi::SubscriptionUU { - fd_write: stdout_readwrite, + let mut writable: HashMap = + vec![(1, STDOUT_FD), (2, STDERR_FD)].into_iter().collect(); + + let clock = wasi::Subscription { + userdata: CLOCK_ID, + u: wasi::SubscriptionU { + tag: wasi::EVENTTYPE_CLOCK, + u: wasi::SubscriptionUU { + clock: wasi::SubscriptionClock { + id: wasi::CLOCKID_MONOTONIC, + timeout: 10_000_000u64, // 10 milliseconds + precision: 0, + flags: 0, }, }, }, - wasi::Subscription { - userdata: 2, - u: wasi::SubscriptionU { - tag: wasi::EVENTTYPE_FD_WRITE, - u: wasi::SubscriptionUU { - fd_write: stderr_readwrite, - }, - }, - }, - ]; - let out = poll_oneoff_impl(&r#in).unwrap(); - assert_eq!(out.len(), 2, "should return 2 events"); - assert_eq!( - out[0].userdata, 1, - "the event.userdata should contain fd userdata specified by the user" - ); - assert_errno!(out[0].error, wasi::ERRNO_SUCCESS); - assert_eq!( - out[0].r#type, - wasi::EVENTTYPE_FD_WRITE, - "the event.type should equal FD_WRITE" - ); - assert_eq!( - out[1].userdata, 2, - "the event.userdata should contain fd userdata specified by the user" - ); - assert_errno!(out[1].error, wasi::ERRNO_SUCCESS); - assert_eq!( - out[1].r#type, - wasi::EVENTTYPE_FD_WRITE, - "the event.type should equal FD_WRITE" - ); + }; + while !writable.is_empty() { + let mut subs = writable_subs(&writable); + subs.push(clock.clone()); + let out = poll_oneoff_impl(&subs).unwrap(); + for event in out { + match event.userdata { + CLOCK_ID => { + panic!("timed out with the following pending subs: {:?}", writable) + } + ud => { + if let Some(_) = writable.remove(&ud) { + assert_eq!(event.r#type, wasi::EVENTTYPE_FD_WRITE); + assert_errno!(event.error, wasi::ERRNO_SUCCESS); + } else { + panic!("Unknown userdata {}, pending sub: {:?}", ud, writable) + } + } + } + } + } } unsafe fn test_poll_oneoff() { diff --git a/crates/wasi-common/cap-std-sync/Cargo.toml b/crates/wasi-common/cap-std-sync/Cargo.toml index d1e22339fc..3d1ee935dd 100644 --- a/crates/wasi-common/cap-std-sync/Cargo.toml +++ b/crates/wasi-common/cap-std-sync/Cargo.toml @@ -13,13 +13,14 @@ include = ["src/**/*", "LICENSE" ] [dependencies] wasi-common = { path = "../", version = "0.26.0" } +async-trait = "0.1" anyhow = "1.0" cap-std = "0.13.9" cap-fs-ext = "0.13.9" cap-time-ext = "0.13.9" cap-rand = "0.13.9" fs-set-times = "0.3.1" -unsafe-io = "0.6.2" +unsafe-io = "0.6.5" system-interface = { version = "0.6.3", features = ["cap_std_impls"] } tracing = "0.1.19" bitflags = "1.2" diff --git a/crates/wasi-common/cap-std-sync/src/dir.rs b/crates/wasi-common/cap-std-sync/src/dir.rs index 91126f8638..0b846ccf15 100644 --- a/crates/wasi-common/cap-std-sync/src/dir.rs +++ b/crates/wasi-common/cap-std-sync/src/dir.rs @@ -15,13 +15,8 @@ impl Dir { pub fn from_cap_std(dir: cap_std::fs::Dir) -> Self { Dir(dir) } -} -impl WasiDir for Dir { - fn as_any(&self) -> &dyn Any { - self - } - fn open_file( + pub fn open_file_( &self, symlink_follow: bool, path: &str, @@ -29,7 +24,7 @@ impl WasiDir for Dir { read: bool, write: bool, fdflags: FdFlags, - ) -> Result, Error> { + ) -> Result { use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt}; let mut opts = cap_std::fs::OpenOptions::new(); @@ -81,26 +76,67 @@ impl WasiDir for Dir { if fdflags.contains(wasi_common::file::FdFlags::NONBLOCK) { f.set_fd_flags(system_interface::fs::FdFlags::NONBLOCK)?; } - Ok(Box::new(File::from_cap_std(f))) + Ok(File::from_cap_std(f)) } - fn open_dir(&self, symlink_follow: bool, path: &str) -> Result, Error> { + pub fn open_dir_(&self, symlink_follow: bool, path: &str) -> Result { let d = if symlink_follow { self.0.open_dir(Path::new(path))? } else { self.0.open_dir_nofollow(Path::new(path))? }; - Ok(Box::new(Dir::from_cap_std(d))) + Ok(Dir::from_cap_std(d)) } - fn create_dir(&self, path: &str) -> Result<(), Error> { + pub fn rename_(&self, src_path: &str, dest_dir: &Self, dest_path: &str) -> Result<(), Error> { + self.0 + .rename(Path::new(src_path), &dest_dir.0, Path::new(dest_path))?; + Ok(()) + } + pub fn hard_link_( + &self, + src_path: &str, + target_dir: &Self, + target_path: &str, + ) -> Result<(), Error> { + let src_path = Path::new(src_path); + let target_path = Path::new(target_path); + self.0.hard_link(src_path, &target_dir.0, target_path)?; + Ok(()) + } +} + +#[async_trait::async_trait(?Send)] +impl WasiDir for Dir { + fn as_any(&self) -> &dyn Any { + self + } + async fn open_file( + &self, + symlink_follow: bool, + path: &str, + oflags: OFlags, + read: bool, + write: bool, + fdflags: FdFlags, + ) -> Result, Error> { + let f = self.open_file_(symlink_follow, path, oflags, read, write, fdflags)?; + Ok(Box::new(f)) + } + + async fn open_dir(&self, symlink_follow: bool, path: &str) -> Result, Error> { + let d = self.open_dir_(symlink_follow, path)?; + Ok(Box::new(d)) + } + + async fn create_dir(&self, path: &str) -> Result<(), Error> { self.0.create_dir(Path::new(path))?; Ok(()) } - fn readdir( + async fn readdir( &self, cursor: ReaddirCursor, - ) -> Result>>, Error> { + ) -> Result> + Send>, Error> { // cap_std's read_dir does not include . and .., we should prepend these. // Why does the Ok contain a tuple? We can't construct a cap_std::fs::DirEntry, and we don't // have enough info to make a ReaddirEntity yet. @@ -165,24 +201,24 @@ impl WasiDir for Dir { Ok(Box::new(rd)) } - fn symlink(&self, src_path: &str, dest_path: &str) -> Result<(), Error> { + async fn symlink(&self, src_path: &str, dest_path: &str) -> Result<(), Error> { self.0.symlink(src_path, dest_path)?; Ok(()) } - fn remove_dir(&self, path: &str) -> Result<(), Error> { + async fn remove_dir(&self, path: &str) -> Result<(), Error> { self.0.remove_dir(Path::new(path))?; Ok(()) } - fn unlink_file(&self, path: &str) -> Result<(), Error> { + async fn unlink_file(&self, path: &str) -> Result<(), Error> { self.0.remove_file_or_symlink(Path::new(path))?; Ok(()) } - fn read_link(&self, path: &str) -> Result { + async fn read_link(&self, path: &str) -> Result { let link = self.0.read_link(Path::new(path))?; Ok(link) } - fn get_filestat(&self) -> Result { + async fn get_filestat(&self) -> Result { let meta = self.0.dir_metadata()?; Ok(Filestat { device_id: meta.dev(), @@ -195,7 +231,11 @@ impl WasiDir for Dir { ctim: meta.created().map(|t| Some(t.into_std())).unwrap_or(None), }) } - fn get_path_filestat(&self, path: &str, follow_symlinks: bool) -> Result { + async fn get_path_filestat( + &self, + path: &str, + follow_symlinks: bool, + ) -> Result { let meta = if follow_symlinks { self.0.metadata(Path::new(path))? } else { @@ -212,16 +252,19 @@ impl WasiDir for Dir { ctim: meta.created().map(|t| Some(t.into_std())).unwrap_or(None), }) } - fn rename(&self, src_path: &str, dest_dir: &dyn WasiDir, dest_path: &str) -> Result<(), Error> { + async fn rename( + &self, + src_path: &str, + dest_dir: &dyn WasiDir, + dest_path: &str, + ) -> Result<(), Error> { let dest_dir = dest_dir .as_any() .downcast_ref::() .ok_or(Error::badf().context("failed downcast to cap-std Dir"))?; - self.0 - .rename(Path::new(src_path), &dest_dir.0, Path::new(dest_path))?; - Ok(()) + self.rename_(src_path, dest_dir, dest_path) } - fn hard_link( + async fn hard_link( &self, src_path: &str, target_dir: &dyn WasiDir, @@ -231,12 +274,9 @@ impl WasiDir for Dir { .as_any() .downcast_ref::() .ok_or(Error::badf().context("failed downcast to cap-std Dir"))?; - let src_path = Path::new(src_path); - let target_path = Path::new(target_path); - self.0.hard_link(src_path, &target_dir.0, target_path)?; - Ok(()) + self.hard_link_(src_path, target_dir, target_path) } - fn set_times( + async fn set_times( &self, path: &str, atime: Option, @@ -280,7 +320,7 @@ mod test { let preopen_dir = unsafe { cap_std::fs::Dir::open_ambient_dir(tempdir.path()) } .expect("open ambient temporary dir"); let preopen_dir = Dir::from_cap_std(preopen_dir); - wasi_common::WasiDir::open_dir(&preopen_dir, false, ".") + run(wasi_common::WasiDir::open_dir(&preopen_dir, false, ".")) .expect("open the same directory via WasiDir abstraction"); } @@ -294,9 +334,8 @@ mod test { fn readdir_into_map(dir: &dyn WasiDir) -> HashMap { let mut out = HashMap::new(); - for readdir_result in dir - .readdir(ReaddirCursor::from(0)) - .expect("readdir succeeds") + for readdir_result in + run(dir.readdir(ReaddirCursor::from(0))).expect("readdir succeeds") { let entity = readdir_result.expect("readdir entry is valid"); out.insert(entity.name.clone(), entity); @@ -322,16 +361,15 @@ mod test { assert!(entities.get(".").is_some()); assert!(entities.get("..").is_some()); - preopen_dir - .open_file( - false, - "file1", - OFlags::CREATE, - true, - false, - FdFlags::empty(), - ) - .expect("create file1"); + run(preopen_dir.open_file( + false, + "file1", + OFlags::CREATE, + true, + false, + FdFlags::empty(), + )) + .expect("create file1"); let entities = readdir_into_map(&preopen_dir); assert_eq!(entities.len(), 3, "should be ., .., file1 {:?}", entities); @@ -348,4 +386,41 @@ mod test { FileType::RegularFile ); } + + fn run(future: F) -> F::Output { + use std::pin::Pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + + let mut f = Pin::from(Box::new(future)); + let waker = dummy_waker(); + let mut cx = Context::from_waker(&waker); + match f.as_mut().poll(&mut cx) { + Poll::Ready(val) => return val, + Poll::Pending => { + panic!("Cannot wait on pending future: must enable wiggle \"async\" future and execute on an async Store") + } + } + + fn dummy_waker() -> Waker { + return unsafe { Waker::from_raw(clone(5 as *const _)) }; + + unsafe fn clone(ptr: *const ()) -> RawWaker { + assert_eq!(ptr as usize, 5); + const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + RawWaker::new(ptr, &VTABLE) + } + + unsafe fn wake(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + + unsafe fn wake_by_ref(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + + unsafe fn drop(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + } + } } diff --git a/crates/wasi-common/cap-std-sync/src/file.rs b/crates/wasi-common/cap-std-sync/src/file.rs index f89a090ea1..0105d61583 100644 --- a/crates/wasi-common/cap-std-sync/src/file.rs +++ b/crates/wasi-common/cap-std-sync/src/file.rs @@ -20,27 +20,28 @@ impl File { } } +#[async_trait::async_trait(?Send)] impl WasiFile for File { fn as_any(&self) -> &dyn Any { self } - fn datasync(&self) -> Result<(), Error> { + async fn datasync(&self) -> Result<(), Error> { self.0.sync_data()?; Ok(()) } - fn sync(&self) -> Result<(), Error> { + async fn sync(&self) -> Result<(), Error> { self.0.sync_all()?; Ok(()) } - fn get_filetype(&self) -> Result { + async fn get_filetype(&self) -> Result { let meta = self.0.metadata()?; Ok(filetype_from(&meta.file_type())) } - fn get_fdflags(&self) -> Result { + async fn get_fdflags(&self) -> Result { let fdflags = self.0.get_fd_flags()?; Ok(from_sysif_fdflags(fdflags)) } - fn set_fdflags(&mut self, fdflags: FdFlags) -> Result<(), Error> { + async fn set_fdflags(&mut self, fdflags: FdFlags) -> Result<(), Error> { if fdflags.intersects( wasi_common::file::FdFlags::DSYNC | wasi_common::file::FdFlags::SYNC @@ -50,7 +51,7 @@ impl WasiFile for File { } Ok(self.0.set_fd_flags(to_sysif_fdflags(fdflags))?) } - fn get_filestat(&self) -> Result { + async fn get_filestat(&self) -> Result { let meta = self.0.metadata()?; Ok(Filestat { device_id: meta.dev(), @@ -63,19 +64,19 @@ impl WasiFile for File { ctim: meta.created().map(|t| Some(t.into_std())).unwrap_or(None), }) } - fn set_filestat_size(&self, size: u64) -> Result<(), Error> { + async fn set_filestat_size(&self, size: u64) -> Result<(), Error> { self.0.set_len(size)?; Ok(()) } - fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error> { + async fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error> { self.0.advise(offset, len, convert_advice(advice))?; Ok(()) } - fn allocate(&self, offset: u64, len: u64) -> Result<(), Error> { + async fn allocate(&self, offset: u64, len: u64) -> Result<(), Error> { self.0.allocate(offset, len)?; Ok(()) } - fn set_times( + async fn set_times( &self, atime: Option, mtime: Option, @@ -84,32 +85,46 @@ impl WasiFile for File { .set_times(convert_systimespec(atime), convert_systimespec(mtime))?; Ok(()) } - fn read_vectored(&self, bufs: &mut [io::IoSliceMut]) -> Result { + async fn read_vectored<'a>(&self, bufs: &mut [io::IoSliceMut<'a>]) -> Result { let n = self.0.read_vectored(bufs)?; Ok(n.try_into()?) } - fn read_vectored_at(&self, bufs: &mut [io::IoSliceMut], offset: u64) -> Result { + async fn read_vectored_at<'a>( + &self, + bufs: &mut [io::IoSliceMut<'a>], + offset: u64, + ) -> Result { let n = self.0.read_vectored_at(bufs, offset)?; Ok(n.try_into()?) } - fn write_vectored(&self, bufs: &[io::IoSlice]) -> Result { + async fn write_vectored<'a>(&self, bufs: &[io::IoSlice<'a>]) -> Result { let n = self.0.write_vectored(bufs)?; Ok(n.try_into()?) } - fn write_vectored_at(&self, bufs: &[io::IoSlice], offset: u64) -> Result { + async fn write_vectored_at<'a>( + &self, + bufs: &[io::IoSlice<'a>], + offset: u64, + ) -> Result { let n = self.0.write_vectored_at(bufs, offset)?; Ok(n.try_into()?) } - fn seek(&self, pos: std::io::SeekFrom) -> Result { + async fn seek(&self, pos: std::io::SeekFrom) -> Result { Ok(self.0.seek(pos)?) } - fn peek(&self, buf: &mut [u8]) -> Result { + async fn peek(&self, buf: &mut [u8]) -> Result { let n = self.0.peek(buf)?; Ok(n.try_into()?) } - fn num_ready_bytes(&self) -> Result { + async fn num_ready_bytes(&self) -> Result { Ok(self.0.num_ready_bytes()?) } + async fn readable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } + async fn writable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } } pub fn filetype_from(ft: &cap_std::fs::FileType) -> FileType { diff --git a/crates/wasi-common/cap-std-sync/src/sched.rs b/crates/wasi-common/cap-std-sync/src/sched.rs index 43af68a952..cbda527109 100644 --- a/crates/wasi-common/cap-std-sync/src/sched.rs +++ b/crates/wasi-common/cap-std-sync/src/sched.rs @@ -1,15 +1,40 @@ #[cfg(unix)] -mod unix; +pub mod unix; #[cfg(unix)] -pub use unix::*; +pub use unix::poll_oneoff; #[cfg(windows)] -mod windows; +pub mod windows; #[cfg(windows)] -pub use windows::*; +pub use windows::poll_oneoff; -use wasi_common::sched::WasiSched; +use std::thread; +use std::time::Duration; +use wasi_common::{ + sched::{Poll, WasiSched}, + Error, +}; +pub struct SyncSched {} +impl SyncSched { + pub fn new() -> Self { + Self {} + } +} +#[async_trait::async_trait(?Send)] +impl WasiSched for SyncSched { + async fn poll_oneoff<'a>(&self, poll: &mut Poll<'a>) -> Result<(), Error> { + poll_oneoff(poll).await + } + async fn sched_yield(&self) -> Result<(), Error> { + thread::yield_now(); + Ok(()) + } + async fn sleep(&self, duration: Duration) -> Result<(), Error> { + std::thread::sleep(duration); + Ok(()) + } +} pub fn sched_ctx() -> Box { Box::new(SyncSched::new()) } diff --git a/crates/wasi-common/cap-std-sync/src/sched/unix.rs b/crates/wasi-common/cap-std-sync/src/sched/unix.rs index da2ec1d143..7b232a4ed9 100644 --- a/crates/wasi-common/cap-std-sync/src/sched/unix.rs +++ b/crates/wasi-common/cap-std-sync/src/sched/unix.rs @@ -1,117 +1,97 @@ use cap_std::time::Duration; use std::convert::TryInto; -use std::ops::Deref; use std::os::unix::io::{AsRawFd, RawFd}; use wasi_common::{ file::WasiFile, sched::{ subscription::{RwEventFlags, Subscription}, - Poll, WasiSched, + Poll, }, Error, ErrorExt, }; use poll::{PollFd, PollFlags}; -pub struct SyncSched; - -impl SyncSched { - pub fn new() -> Self { - SyncSched +pub async fn poll_oneoff<'a>(poll: &mut Poll<'a>) -> Result<(), Error> { + if poll.is_empty() { + return Ok(()); } -} + let mut pollfds = Vec::new(); + for s in poll.rw_subscriptions() { + match s { + Subscription::Read(f) => { + let raw_fd = wasi_file_raw_fd(f.file).ok_or( + Error::invalid_argument().context("read subscription fd downcast failed"), + )?; + pollfds.push(unsafe { PollFd::new(raw_fd, PollFlags::POLLIN) }); + } -impl WasiSched for SyncSched { - fn poll_oneoff<'a>(&self, poll: &'a Poll<'a>) -> Result<(), Error> { - if poll.is_empty() { - return Ok(()); + Subscription::Write(f) => { + let raw_fd = wasi_file_raw_fd(f.file).ok_or( + Error::invalid_argument().context("write subscription fd downcast failed"), + )?; + pollfds.push(unsafe { PollFd::new(raw_fd, PollFlags::POLLOUT) }); + } + Subscription::MonotonicClock { .. } => unreachable!(), } - let mut pollfds = Vec::new(); - let timeout = poll.earliest_clock_deadline(); - for s in poll.rw_subscriptions() { - match s { - Subscription::Read(f) => { - let raw_fd = wasi_file_raw_fd(f.file.deref()).ok_or( - Error::invalid_argument().context("read subscription fd downcast failed"), - )?; - pollfds.push(unsafe { PollFd::new(raw_fd, PollFlags::POLLIN) }); - } + } - Subscription::Write(f) => { - let raw_fd = wasi_file_raw_fd(f.file.deref()).ok_or( - Error::invalid_argument().context("write subscription fd downcast failed"), - )?; - pollfds.push(unsafe { PollFd::new(raw_fd, PollFlags::POLLOUT) }); - } - Subscription::MonotonicClock { .. } => unreachable!(), - } - } - - let ready = loop { - let poll_timeout = if let Some(t) = timeout { - let duration = t.duration_until().unwrap_or(Duration::from_secs(0)); - (duration.as_millis() + 1) // XXX try always rounding up? - .try_into() - .map_err(|_| Error::overflow().context("poll timeout"))? - } else { - libc::c_int::max_value() - }; - tracing::debug!( - poll_timeout = tracing::field::debug(poll_timeout), - poll_fds = tracing::field::debug(&pollfds), - "poll" - ); - match poll::poll(&mut pollfds, poll_timeout) { - Ok(ready) => break ready, - Err(_) => { - let last_err = std::io::Error::last_os_error(); - if last_err.raw_os_error().unwrap() == libc::EINTR { - continue; - } else { - return Err(last_err.into()); - } - } - } - }; - if ready > 0 { - for (rwsub, pollfd) in poll.rw_subscriptions().zip(pollfds.into_iter()) { - if let Some(revents) = pollfd.revents() { - let (nbytes, rwsub) = match rwsub { - Subscription::Read(sub) => { - let ready = sub.file.num_ready_bytes()?; - (std::cmp::max(ready, 1), sub) - } - Subscription::Write(sub) => (0, sub), - _ => unreachable!(), - }; - if revents.contains(PollFlags::POLLNVAL) { - rwsub.error(Error::badf()); - } else if revents.contains(PollFlags::POLLERR) { - rwsub.error(Error::io()); - } else if revents.contains(PollFlags::POLLHUP) { - rwsub.complete(nbytes, RwEventFlags::HANGUP); - } else { - rwsub.complete(nbytes, RwEventFlags::empty()); - }; - } - } + let ready = loop { + let poll_timeout = if let Some(t) = poll.earliest_clock_deadline() { + let duration = t.duration_until().unwrap_or(Duration::from_secs(0)); + (duration.as_millis() + 1) // XXX try always rounding up? + .try_into() + .map_err(|_| Error::overflow().context("poll timeout"))? } else { - timeout - .expect("timed out") - .result() - .expect("timer deadline is past") - .unwrap() + libc::c_int::max_value() + }; + tracing::debug!( + poll_timeout = tracing::field::debug(poll_timeout), + poll_fds = tracing::field::debug(&pollfds), + "poll" + ); + match poll::poll(&mut pollfds, poll_timeout) { + Ok(ready) => break ready, + Err(_) => { + let last_err = std::io::Error::last_os_error(); + if last_err.raw_os_error().unwrap() == libc::EINTR { + continue; + } else { + return Err(last_err.into()); + } + } } - Ok(()) - } - fn sched_yield(&self) -> Result<(), Error> { - std::thread::yield_now(); - Ok(()) - } - fn sleep(&self, duration: Duration) -> Result<(), Error> { - std::thread::sleep(duration); - Ok(()) + }; + if ready > 0 { + for (rwsub, pollfd) in poll.rw_subscriptions().zip(pollfds.into_iter()) { + if let Some(revents) = pollfd.revents() { + let (nbytes, rwsub) = match rwsub { + Subscription::Read(sub) => { + let ready = sub.file.num_ready_bytes().await?; + (std::cmp::max(ready, 1), sub) + } + Subscription::Write(sub) => (0, sub), + _ => unreachable!(), + }; + if revents.contains(PollFlags::POLLNVAL) { + rwsub.error(Error::badf()); + } else if revents.contains(PollFlags::POLLERR) { + rwsub.error(Error::io()); + } else if revents.contains(PollFlags::POLLHUP) { + rwsub.complete(nbytes, RwEventFlags::HANGUP); + } else { + rwsub.complete(nbytes, RwEventFlags::empty()); + }; + } + } + } else { + poll.earliest_clock_deadline() + .expect("timed out") + .result() + .expect("timer deadline is past") + .unwrap() } + Ok(()) } fn wasi_file_raw_fd(f: &dyn WasiFile) -> Option { diff --git a/crates/wasi-common/cap-std-sync/src/sched/windows.rs b/crates/wasi-common/cap-std-sync/src/sched/windows.rs index f40cb08d72..41f3f1dda3 100644 --- a/crates/wasi-common/cap-std-sync/src/sched/windows.rs +++ b/crates/wasi-common/cap-std-sync/src/sched/windows.rs @@ -1,3 +1,13 @@ +// The windows scheduler is unmaintained and due for a rewrite. +// +// Rather than use a polling mechanism for file read/write readiness, +// it checks readiness just once, before sleeping for any timer subscriptions. +// Checking stdin readiness uses a worker thread which, once started, lives for the +// lifetime of the process. +// +// We suspect there are bugs in this scheduler, however, we have not +// taken the time to improve it. See bug #2880. + use anyhow::Context; use std::ops::Deref; use std::os::windows::io::{AsRawHandle, RawHandle}; @@ -9,132 +19,127 @@ use wasi_common::{ file::WasiFile, sched::{ subscription::{RwEventFlags, Subscription}, - Poll, WasiSched, + Poll, }, Error, ErrorExt, }; -pub struct SyncSched {} -impl SyncSched { - pub fn new() -> Self { - Self {} - } +pub async fn poll_oneoff<'a>(poll: &mut Poll<'a>) -> Result<(), Error> { + poll_oneoff_(poll, wasi_file_is_stdin, wasi_file_raw_handle).await } -impl WasiSched for SyncSched { - fn poll_oneoff<'a>(&self, poll: &'a Poll<'a>) -> Result<(), Error> { - if poll.is_empty() { - return Ok(()); +// For reuse by wasi-tokio, which has a different WasiFile -> RawHandle translator. +pub async fn poll_oneoff_<'a>( + poll: &mut Poll<'a>, + file_is_stdin: impl Fn(&dyn WasiFile) -> bool, + file_to_handle: impl Fn(&dyn WasiFile) -> Option, +) -> Result<(), Error> { + if poll.is_empty() { + return Ok(()); + } + + let mut ready = false; + let waitmode = if let Some(t) = poll.earliest_clock_deadline() { + if let Some(duration) = t.duration_until() { + WaitMode::Timeout(duration) + } else { + WaitMode::Immediate } + } else { + if ready { + WaitMode::Immediate + } else { + WaitMode::Infinite + } + }; - let mut ready = false; - let timeout = poll.earliest_clock_deadline(); - - let mut stdin_read_subs = Vec::new(); - let mut immediate_subs = Vec::new(); - for s in poll.rw_subscriptions() { - match s { - Subscription::Read(r) if r.file.as_any().is::() => { + let mut stdin_read_subs = Vec::new(); + let mut immediate_reads = Vec::new(); + let mut immediate_writes = Vec::new(); + for s in poll.rw_subscriptions() { + match s { + Subscription::Read(r) => { + if file_is_stdin(r.file.deref()) { stdin_read_subs.push(r); + } else if file_to_handle(r.file.deref()).is_some() { + immediate_reads.push(r); + } else { + return Err( + Error::invalid_argument().context("read subscription fd downcast failed") + ); } - Subscription::Read(rw) | Subscription::Write(rw) => { - if wasi_file_raw_handle(rw.file.deref()).is_some() { - immediate_subs.push(s); - } else { - return Err(Error::invalid_argument() - .context("read/write subscription fd downcast failed")); - } - } - Subscription::MonotonicClock { .. } => unreachable!(), } + Subscription::Write(w) => { + if file_to_handle(w.file.deref()).is_some() { + immediate_writes.push(w); + } else { + return Err( + Error::invalid_argument().context("write subscription fd downcast failed") + ); + } + } + Subscription::MonotonicClock { .. } => unreachable!(), } + } - if !stdin_read_subs.is_empty() { - let waitmode = if let Some(t) = timeout { - if let Some(duration) = t.duration_until() { - WaitMode::Timeout(duration) - } else { - WaitMode::Immediate - } - } else { - if ready { - WaitMode::Immediate - } else { - WaitMode::Infinite - } - }; - let state = STDIN_POLL - .lock() - .map_err(|_| Error::trap("failed to take lock of STDIN_POLL"))? - .poll(waitmode)?; - for readsub in stdin_read_subs.into_iter() { - match state { - PollState::Ready => { - readsub.complete(1, RwEventFlags::empty()); - ready = true; - } - PollState::NotReady | PollState::TimedOut => {} - PollState::Error(ref e) => { - // Unfortunately, we need to deliver the Error to each of the - // subscriptions, but there is no Clone on std::io::Error. So, we convert it to the - // kind, and then back to std::io::Error, and finally to anyhow::Error. - // When its time to turn this into an errno elsewhere, the error kind will - // be inspected. - let ekind = e.kind(); - let ioerror = std::io::Error::from(ekind); - readsub.error(ioerror.into()); - ready = true; - } - } - } - } - for sub in immediate_subs { - match sub { - Subscription::Read(r) => { - // XXX This doesnt strictly preserve the behavior in the earlier - // implementation, which would always do complete(0) for reads from - // stdout/err. - match r.file.num_ready_bytes() { - Ok(ready_bytes) => { - r.complete(ready_bytes, RwEventFlags::empty()); - ready = true; - } - Err(e) => { - r.error(e); - ready = true; - } - } - } - Subscription::Write(w) => { - // Everything is always ready for writing, apparently? - w.complete(0, RwEventFlags::empty()); + if !stdin_read_subs.is_empty() { + let state = STDIN_POLL + .lock() + .map_err(|_| Error::trap("failed to take lock of STDIN_POLL"))? + .poll(waitmode)?; + for readsub in stdin_read_subs.into_iter() { + match state { + PollState::Ready => { + readsub.complete(1, RwEventFlags::empty()); ready = true; } - Subscription::MonotonicClock { .. } => unreachable!(), - } - } - - if !ready { - if let Some(t) = timeout { - if let Some(duration) = t.duration_until() { - thread::sleep(duration); + PollState::NotReady | PollState::TimedOut => {} + PollState::Error(ref e) => { + // Unfortunately, we need to deliver the Error to each of the + // subscriptions, but there is no Clone on std::io::Error. So, we convert it to the + // kind, and then back to std::io::Error, and finally to anyhow::Error. + // When its time to turn this into an errno elsewhere, the error kind will + // be inspected. + let ekind = e.kind(); + let ioerror = std::io::Error::from(ekind); + readsub.error(ioerror.into()); + ready = true; } } } + } + for r in immediate_reads { + match r.file.num_ready_bytes().await { + Ok(ready_bytes) => { + r.complete(ready_bytes, RwEventFlags::empty()); + ready = true; + } + Err(e) => { + r.error(e); + ready = true; + } + } + } + for w in immediate_writes { + // Everything is always ready for writing, apparently? + w.complete(0, RwEventFlags::empty()); + ready = true; + } - Ok(()) - } - fn sched_yield(&self) -> Result<(), Error> { - thread::yield_now(); - Ok(()) - } - fn sleep(&self, duration: Duration) -> Result<(), Error> { - std::thread::sleep(duration); - Ok(()) + if !ready { + if let WaitMode::Timeout(duration) = waitmode { + thread::sleep(duration); + } } + + Ok(()) } -fn wasi_file_raw_handle(f: &dyn WasiFile) -> Option { +pub fn wasi_file_is_stdin(f: &dyn WasiFile) -> bool { + f.as_any().is::() +} + +pub fn wasi_file_raw_handle(f: &dyn WasiFile) -> Option { let a = f.as_any(); if a.is::() { Some( @@ -172,6 +177,7 @@ enum PollState { Error(std::io::Error), } +#[derive(Copy, Clone)] enum WaitMode { Timeout(Duration), Infinite, diff --git a/crates/wasi-common/cap-std-sync/src/stdio.rs b/crates/wasi-common/cap-std-sync/src/stdio.rs index 92a76d47ff..c6afa8f9b4 100644 --- a/crates/wasi-common/cap-std-sync/src/stdio.rs +++ b/crates/wasi-common/cap-std-sync/src/stdio.rs @@ -22,31 +22,32 @@ pub fn stdin() -> Stdin { Stdin(std::io::stdin()) } +#[async_trait::async_trait(?Send)] impl WasiFile for Stdin { fn as_any(&self) -> &dyn Any { self } - fn datasync(&self) -> Result<(), Error> { + async fn datasync(&self) -> Result<(), Error> { Ok(()) } - fn sync(&self) -> Result<(), Error> { + async fn sync(&self) -> Result<(), Error> { Ok(()) } - fn get_filetype(&self) -> Result { + async fn get_filetype(&self) -> Result { Ok(FileType::Unknown) } - fn get_fdflags(&self) -> Result { + async fn get_fdflags(&self) -> Result { Ok(FdFlags::empty()) } - fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { + async fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { Err(Error::badf()) } - fn get_filestat(&self) -> Result { + async fn get_filestat(&self) -> Result { let meta = self.0.as_file_view().metadata()?; Ok(Filestat { device_id: 0, inode: 0, - filetype: self.get_filetype()?, + filetype: self.get_filetype().await?, nlink: 0, size: meta.len(), atim: meta.accessed().ok(), @@ -54,35 +55,43 @@ impl WasiFile for Stdin { ctim: meta.created().ok(), }) } - fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { + async fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { Err(Error::badf()) } - fn advise(&self, _offset: u64, _len: u64, _advice: Advice) -> Result<(), Error> { + async fn advise(&self, _offset: u64, _len: u64, _advice: Advice) -> Result<(), Error> { Err(Error::badf()) } - fn allocate(&self, _offset: u64, _len: u64) -> Result<(), Error> { + async fn allocate(&self, _offset: u64, _len: u64) -> Result<(), Error> { Err(Error::badf()) } - fn read_vectored(&self, bufs: &mut [io::IoSliceMut]) -> Result { + async fn read_vectored<'a>(&self, bufs: &mut [io::IoSliceMut<'a>]) -> Result { let n = self.0.as_file_view().read_vectored(bufs)?; Ok(n.try_into().map_err(|_| Error::range())?) } - fn read_vectored_at(&self, _bufs: &mut [io::IoSliceMut], _offset: u64) -> Result { + async fn read_vectored_at<'a>( + &self, + _bufs: &mut [io::IoSliceMut<'a>], + _offset: u64, + ) -> Result { Err(Error::seek_pipe()) } - fn write_vectored(&self, _bufs: &[io::IoSlice]) -> Result { + async fn write_vectored<'a>(&self, _bufs: &[io::IoSlice<'a>]) -> Result { Err(Error::badf()) } - fn write_vectored_at(&self, _bufs: &[io::IoSlice], _offset: u64) -> Result { + async fn write_vectored_at<'a>( + &self, + _bufs: &[io::IoSlice<'a>], + _offset: u64, + ) -> Result { Err(Error::badf()) } - fn seek(&self, _pos: std::io::SeekFrom) -> Result { + async fn seek(&self, _pos: std::io::SeekFrom) -> Result { Err(Error::seek_pipe()) } - fn peek(&self, _buf: &mut [u8]) -> Result { + async fn peek(&self, _buf: &mut [u8]) -> Result { Err(Error::seek_pipe()) } - fn set_times( + async fn set_times( &self, atime: Option, mtime: Option, @@ -91,9 +100,15 @@ impl WasiFile for Stdin { .set_times(convert_systimespec(atime), convert_systimespec(mtime))?; Ok(()) } - fn num_ready_bytes(&self) -> Result { + async fn num_ready_bytes(&self) -> Result { Ok(self.0.num_ready_bytes()?) } + async fn readable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } + async fn writable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } } #[cfg(windows)] impl AsRawHandle for Stdin { @@ -110,31 +125,32 @@ impl AsRawFd for Stdin { macro_rules! wasi_file_write_impl { ($ty:ty) => { + #[async_trait::async_trait(?Send)] impl WasiFile for $ty { fn as_any(&self) -> &dyn Any { self } - fn datasync(&self) -> Result<(), Error> { + async fn datasync(&self) -> Result<(), Error> { Ok(()) } - fn sync(&self) -> Result<(), Error> { + async fn sync(&self) -> Result<(), Error> { Ok(()) } - fn get_filetype(&self) -> Result { + async fn get_filetype(&self) -> Result { Ok(FileType::Unknown) } - fn get_fdflags(&self) -> Result { + async fn get_fdflags(&self) -> Result { Ok(FdFlags::APPEND) } - fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { + async fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { Err(Error::badf()) } - fn get_filestat(&self) -> Result { + async fn get_filestat(&self) -> Result { let meta = self.0.as_file_view().metadata()?; Ok(Filestat { device_id: 0, inode: 0, - filetype: self.get_filetype()?, + filetype: self.get_filetype().await?, nlink: 0, size: meta.len(), atim: meta.accessed().ok(), @@ -142,39 +158,46 @@ macro_rules! wasi_file_write_impl { ctim: meta.created().ok(), }) } - fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { + async fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { Err(Error::badf()) } - fn advise(&self, _offset: u64, _len: u64, _advice: Advice) -> Result<(), Error> { + async fn advise(&self, _offset: u64, _len: u64, _advice: Advice) -> Result<(), Error> { Err(Error::badf()) } - fn allocate(&self, _offset: u64, _len: u64) -> Result<(), Error> { + async fn allocate(&self, _offset: u64, _len: u64) -> Result<(), Error> { Err(Error::badf()) } - fn read_vectored(&self, _bufs: &mut [io::IoSliceMut]) -> Result { - Err(Error::badf()) - } - fn read_vectored_at( + async fn read_vectored<'a>( &self, - _bufs: &mut [io::IoSliceMut], + _bufs: &mut [io::IoSliceMut<'a>], + ) -> Result { + Err(Error::badf()) + } + async fn read_vectored_at<'a>( + &self, + _bufs: &mut [io::IoSliceMut<'a>], _offset: u64, ) -> Result { Err(Error::badf()) } - fn write_vectored(&self, bufs: &[io::IoSlice]) -> Result { + async fn write_vectored<'a>(&self, bufs: &[io::IoSlice<'a>]) -> Result { let n = self.0.as_file_view().write_vectored(bufs)?; Ok(n.try_into().map_err(|c| Error::range().context(c))?) } - fn write_vectored_at(&self, _bufs: &[io::IoSlice], _offset: u64) -> Result { + async fn write_vectored_at<'a>( + &self, + _bufs: &[io::IoSlice<'a>], + _offset: u64, + ) -> Result { Err(Error::seek_pipe()) } - fn seek(&self, _pos: std::io::SeekFrom) -> Result { + async fn seek(&self, _pos: std::io::SeekFrom) -> Result { Err(Error::seek_pipe()) } - fn peek(&self, _buf: &mut [u8]) -> Result { + async fn peek(&self, _buf: &mut [u8]) -> Result { Err(Error::badf()) } - fn set_times( + async fn set_times( &self, atime: Option, mtime: Option, @@ -183,9 +206,15 @@ macro_rules! wasi_file_write_impl { .set_times(convert_systimespec(atime), convert_systimespec(mtime))?; Ok(()) } - fn num_ready_bytes(&self) -> Result { + async fn num_ready_bytes(&self) -> Result { Ok(0) } + async fn readable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } + async fn writable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } } #[cfg(windows)] impl AsRawHandle for $ty { diff --git a/crates/wasi-common/src/clocks.rs b/crates/wasi-common/src/clocks.rs index babf0acff4..679759caf9 100644 --- a/crates/wasi-common/src/clocks.rs +++ b/crates/wasi-common/src/clocks.rs @@ -5,12 +5,12 @@ pub enum SystemTimeSpec { Absolute(SystemTime), } -pub trait WasiSystemClock { +pub trait WasiSystemClock: Send + Sync { fn resolution(&self) -> Duration; fn now(&self, precision: Duration) -> SystemTime; } -pub trait WasiMonotonicClock { +pub trait WasiMonotonicClock: Send + Sync { fn resolution(&self) -> Duration; fn now(&self, precision: Duration) -> Instant; } diff --git a/crates/wasi-common/src/dir.rs b/crates/wasi-common/src/dir.rs index 1c7e3b2c56..9c49c6c8aa 100644 --- a/crates/wasi-common/src/dir.rs +++ b/crates/wasi-common/src/dir.rs @@ -6,9 +6,10 @@ use std::cell::Ref; use std::ops::Deref; use std::path::PathBuf; -pub trait WasiDir { +#[wiggle::async_trait] +pub trait WasiDir: Send + Sync { fn as_any(&self) -> &dyn Any; - fn open_file( + async fn open_file( &self, symlink_follow: bool, path: &str, @@ -17,26 +18,33 @@ pub trait WasiDir { write: bool, fdflags: FdFlags, ) -> Result, Error>; - fn open_dir(&self, symlink_follow: bool, path: &str) -> Result, Error>; - fn create_dir(&self, path: &str) -> Result<(), Error>; - fn readdir( + async fn open_dir(&self, symlink_follow: bool, path: &str) -> Result, Error>; + async fn create_dir(&self, path: &str) -> Result<(), Error>; + // XXX the iterator here needs to be asyncified as well! + async fn readdir( &self, cursor: ReaddirCursor, - ) -> Result>>, Error>; - fn symlink(&self, old_path: &str, new_path: &str) -> Result<(), Error>; - fn remove_dir(&self, path: &str) -> Result<(), Error>; - fn unlink_file(&self, path: &str) -> Result<(), Error>; - fn read_link(&self, path: &str) -> Result; - fn get_filestat(&self) -> Result; - fn get_path_filestat(&self, path: &str, follow_symlinks: bool) -> Result; - fn rename(&self, path: &str, dest_dir: &dyn WasiDir, dest_path: &str) -> Result<(), Error>; - fn hard_link( + ) -> Result> + Send>, Error>; + async fn symlink(&self, old_path: &str, new_path: &str) -> Result<(), Error>; + async fn remove_dir(&self, path: &str) -> Result<(), Error>; + async fn unlink_file(&self, path: &str) -> Result<(), Error>; + async fn read_link(&self, path: &str) -> Result; + async fn get_filestat(&self) -> Result; + async fn get_path_filestat(&self, path: &str, follow_symlinks: bool) + -> Result; + async fn rename( + &self, + path: &str, + dest_dir: &dyn WasiDir, + dest_path: &str, + ) -> Result<(), Error>; + async fn hard_link( &self, path: &str, target_dir: &dyn WasiDir, target_path: &str, ) -> Result<(), Error>; - fn set_times( + async fn set_times( &self, path: &str, atime: Option, diff --git a/crates/wasi-common/src/error.rs b/crates/wasi-common/src/error.rs index cf132b59ee..20277554fc 100644 --- a/crates/wasi-common/src/error.rs +++ b/crates/wasi-common/src/error.rs @@ -23,7 +23,7 @@ //! The real value of using `anyhow::Error` here is being able to use //! `anyhow::Result::context` to aid in debugging of errors. -pub use anyhow::Error; +pub use anyhow::{Context, Error}; /// Internal error type for the `wasi-common` crate. /// Contains variants of the WASI `$errno` type are added according to what is actually used internally by diff --git a/crates/wasi-common/src/file.rs b/crates/wasi-common/src/file.rs index 65fba7cd4a..c718b7ad25 100644 --- a/crates/wasi-common/src/file.rs +++ b/crates/wasi-common/src/file.rs @@ -4,30 +4,41 @@ use std::any::Any; use std::cell::{Ref, RefMut}; use std::ops::{Deref, DerefMut}; -pub trait WasiFile { +#[wiggle::async_trait] +pub trait WasiFile: Send { fn as_any(&self) -> &dyn Any; - fn datasync(&self) -> Result<(), Error>; // write op - fn sync(&self) -> Result<(), Error>; // file op - fn get_filetype(&self) -> Result; // file op - fn get_fdflags(&self) -> Result; // file op - fn set_fdflags(&mut self, flags: FdFlags) -> Result<(), Error>; // file op - fn get_filestat(&self) -> Result; // split out get_length as a read & write op, rest is a file op - fn set_filestat_size(&self, _size: u64) -> Result<(), Error>; // write op - fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error>; // file op - fn allocate(&self, offset: u64, len: u64) -> Result<(), Error>; // write op - fn set_times( + async fn datasync(&self) -> Result<(), Error>; // write op + async fn sync(&self) -> Result<(), Error>; // file op + async fn get_filetype(&self) -> Result; // file op + async fn get_fdflags(&self) -> Result; // file op + async fn set_fdflags(&mut self, flags: FdFlags) -> Result<(), Error>; // file op + async fn get_filestat(&self) -> Result; // split out get_length as a read & write op, rest is a file op + async fn set_filestat_size(&self, _size: u64) -> Result<(), Error>; // write op + async fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error>; // file op + async fn allocate(&self, offset: u64, len: u64) -> Result<(), Error>; // write op + async fn set_times( &self, atime: Option, mtime: Option, ) -> Result<(), Error>; - fn read_vectored(&self, bufs: &mut [std::io::IoSliceMut]) -> Result; // read op - fn read_vectored_at(&self, bufs: &mut [std::io::IoSliceMut], offset: u64) - -> Result; // file op - fn write_vectored(&self, bufs: &[std::io::IoSlice]) -> Result; // write op - fn write_vectored_at(&self, bufs: &[std::io::IoSlice], offset: u64) -> Result; // file op - fn seek(&self, pos: std::io::SeekFrom) -> Result; // file op that generates a new stream from a file will supercede this - fn peek(&self, buf: &mut [u8]) -> Result; // read op - fn num_ready_bytes(&self) -> Result; // read op + async fn read_vectored<'a>(&self, bufs: &mut [std::io::IoSliceMut<'a>]) -> Result; // read op + async fn read_vectored_at<'a>( + &self, + bufs: &mut [std::io::IoSliceMut<'a>], + offset: u64, + ) -> Result; // file op + async fn write_vectored<'a>(&self, bufs: &[std::io::IoSlice<'a>]) -> Result; // write op + async fn write_vectored_at<'a>( + &self, + bufs: &[std::io::IoSlice<'a>], + offset: u64, + ) -> Result; // file op + async fn seek(&self, pos: std::io::SeekFrom) -> Result; // file op that generates a new stream from a file will supercede this + async fn peek(&self, buf: &mut [u8]) -> Result; // read op + async fn num_ready_bytes(&self) -> Result; // read op + + async fn readable(&mut self) -> Result<(), Error>; + async fn writable(&mut self) -> Result<(), Error>; } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -111,11 +122,11 @@ impl FileEntry { Ok(()) } - pub fn get_fdstat(&self) -> Result { + pub async fn get_fdstat(&self) -> Result { Ok(FdStat { - filetype: self.file.get_filetype()?, + filetype: self.file.get_filetype().await?, caps: self.caps, - flags: self.file.get_fdflags()?, + flags: self.file.get_fdflags().await?, }) } } diff --git a/crates/wasi-common/src/lib.rs b/crates/wasi-common/src/lib.rs index 4575b423b6..63910d4a60 100644 --- a/crates/wasi-common/src/lib.rs +++ b/crates/wasi-common/src/lib.rs @@ -66,7 +66,7 @@ pub use cap_rand::RngCore; pub use clocks::{SystemTimeSpec, WasiClocks, WasiMonotonicClock, WasiSystemClock}; pub use ctx::{WasiCtx, WasiCtxBuilder}; pub use dir::WasiDir; -pub use error::{Error, ErrorExt, ErrorKind}; +pub use error::{Context, Error, ErrorExt, ErrorKind}; pub use file::WasiFile; pub use sched::{Poll, WasiSched}; pub use string_array::StringArrayError; diff --git a/crates/wasi-common/src/pipe.rs b/crates/wasi-common/src/pipe.rs index 8bbc8d73b7..2f39f5cb9c 100644 --- a/crates/wasi-common/src/pipe.rs +++ b/crates/wasi-common/src/pipe.rs @@ -105,30 +105,31 @@ impl From<&str> for ReadPipe> { } } -impl WasiFile for ReadPipe { +#[wiggle::async_trait] +impl WasiFile for ReadPipe { fn as_any(&self) -> &dyn Any { self } - fn datasync(&self) -> Result<(), Error> { + async fn datasync(&self) -> Result<(), Error> { Ok(()) // trivial: no implementation needed } - fn sync(&self) -> Result<(), Error> { + async fn sync(&self) -> Result<(), Error> { Ok(()) // trivial } - fn get_filetype(&self) -> Result { + async fn get_filetype(&self) -> Result { Ok(FileType::Pipe) } - fn get_fdflags(&self) -> Result { + async fn get_fdflags(&self) -> Result { Ok(FdFlags::empty()) } - fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { + async fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { Err(Error::badf()) } - fn get_filestat(&self) -> Result { + async fn get_filestat(&self) -> Result { Ok(Filestat { device_id: 0, inode: 0, - filetype: self.get_filetype()?, + filetype: self.get_filetype().await?, nlink: 0, size: 0, // XXX no way to get a size out of a Read :( atim: None, @@ -136,44 +137,58 @@ impl WasiFile for ReadPipe { ctim: None, }) } - fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { + async fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { Err(Error::badf()) } - fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error> { + async fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error> { Err(Error::badf()) } - fn allocate(&self, offset: u64, len: u64) -> Result<(), Error> { + async fn allocate(&self, offset: u64, len: u64) -> Result<(), Error> { Err(Error::badf()) } - fn read_vectored(&self, bufs: &mut [io::IoSliceMut]) -> Result { + async fn read_vectored<'a>(&self, bufs: &mut [io::IoSliceMut<'a>]) -> Result { let n = self.borrow().read_vectored(bufs)?; Ok(n.try_into()?) } - fn read_vectored_at(&self, bufs: &mut [io::IoSliceMut], offset: u64) -> Result { + async fn read_vectored_at<'a>( + &self, + bufs: &mut [io::IoSliceMut<'a>], + offset: u64, + ) -> Result { Err(Error::badf()) } - fn write_vectored(&self, bufs: &[io::IoSlice]) -> Result { + async fn write_vectored<'a>(&self, bufs: &[io::IoSlice<'a>]) -> Result { Err(Error::badf()) } - fn write_vectored_at(&self, bufs: &[io::IoSlice], offset: u64) -> Result { + async fn write_vectored_at<'a>( + &self, + bufs: &[io::IoSlice<'a>], + offset: u64, + ) -> Result { Err(Error::badf()) } - fn seek(&self, pos: std::io::SeekFrom) -> Result { + async fn seek(&self, pos: std::io::SeekFrom) -> Result { Err(Error::badf()) } - fn peek(&self, buf: &mut [u8]) -> Result { + async fn peek(&self, buf: &mut [u8]) -> Result { Err(Error::badf()) } - fn set_times( + async fn set_times( &self, atime: Option, mtime: Option, ) -> Result<(), Error> { Err(Error::badf()) } - fn num_ready_bytes(&self) -> Result { + async fn num_ready_bytes(&self) -> Result { Ok(0) } + async fn readable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } + async fn writable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } } /// A virtual pipe write end. @@ -249,30 +264,31 @@ impl WritePipe>> { } } -impl WasiFile for WritePipe { +#[wiggle::async_trait] +impl WasiFile for WritePipe { fn as_any(&self) -> &dyn Any { self } - fn datasync(&self) -> Result<(), Error> { + async fn datasync(&self) -> Result<(), Error> { Ok(()) } - fn sync(&self) -> Result<(), Error> { + async fn sync(&self) -> Result<(), Error> { Ok(()) } - fn get_filetype(&self) -> Result { + async fn get_filetype(&self) -> Result { Ok(FileType::Pipe) } - fn get_fdflags(&self) -> Result { + async fn get_fdflags(&self) -> Result { Ok(FdFlags::APPEND) } - fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { + async fn set_fdflags(&mut self, _fdflags: FdFlags) -> Result<(), Error> { Err(Error::badf()) } - fn get_filestat(&self) -> Result { + async fn get_filestat(&self) -> Result { Ok(Filestat { device_id: 0, inode: 0, - filetype: self.get_filetype()?, + filetype: self.get_filetype().await?, nlink: 0, size: 0, // XXX no way to get a size out of a Write :( atim: None, @@ -280,42 +296,56 @@ impl WasiFile for WritePipe { ctim: None, }) } - fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { + async fn set_filestat_size(&self, _size: u64) -> Result<(), Error> { Err(Error::badf()) } - fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error> { + async fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error> { Err(Error::badf()) } - fn allocate(&self, offset: u64, len: u64) -> Result<(), Error> { + async fn allocate(&self, offset: u64, len: u64) -> Result<(), Error> { Err(Error::badf()) } - fn read_vectored(&self, bufs: &mut [io::IoSliceMut]) -> Result { + async fn read_vectored<'a>(&self, bufs: &mut [io::IoSliceMut<'a>]) -> Result { Err(Error::badf()) } - fn read_vectored_at(&self, bufs: &mut [io::IoSliceMut], offset: u64) -> Result { + async fn read_vectored_at<'a>( + &self, + bufs: &mut [io::IoSliceMut<'a>], + offset: u64, + ) -> Result { Err(Error::badf()) } - fn write_vectored(&self, bufs: &[io::IoSlice]) -> Result { + async fn write_vectored<'a>(&self, bufs: &[io::IoSlice<'a>]) -> Result { let n = self.borrow().write_vectored(bufs)?; Ok(n.try_into()?) } - fn write_vectored_at(&self, bufs: &[io::IoSlice], offset: u64) -> Result { + async fn write_vectored_at<'a>( + &self, + bufs: &[io::IoSlice<'a>], + offset: u64, + ) -> Result { Err(Error::badf()) } - fn seek(&self, pos: std::io::SeekFrom) -> Result { + async fn seek(&self, pos: std::io::SeekFrom) -> Result { Err(Error::badf()) } - fn peek(&self, buf: &mut [u8]) -> Result { + async fn peek(&self, buf: &mut [u8]) -> Result { Err(Error::badf()) } - fn set_times( + async fn set_times( &self, atime: Option, mtime: Option, ) -> Result<(), Error> { Err(Error::badf()) } - fn num_ready_bytes(&self) -> Result { + async fn num_ready_bytes(&self) -> Result { Ok(0) } + async fn readable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } + async fn writable(&mut self) -> Result<(), Error> { + Err(Error::badf()) + } } diff --git a/crates/wasi-common/src/sched.rs b/crates/wasi-common/src/sched.rs index 10d12d6621..fecac2c5ca 100644 --- a/crates/wasi-common/src/sched.rs +++ b/crates/wasi-common/src/sched.rs @@ -1,18 +1,22 @@ use crate::clocks::WasiMonotonicClock; use crate::file::WasiFile; use crate::Error; -use cap_std::time::{Duration, Instant}; -use std::cell::Ref; +use cap_std::time::Instant; pub mod subscription; +pub use cap_std::time::Duration; -use subscription::{MonotonicClockSubscription, RwSubscription, Subscription, SubscriptionResult}; +pub use subscription::{ + MonotonicClockSubscription, RwEventFlags, RwSubscription, Subscription, SubscriptionResult, +}; +#[wiggle::async_trait] pub trait WasiSched { - fn poll_oneoff(&self, poll: &Poll) -> Result<(), Error>; - fn sched_yield(&self) -> Result<(), Error>; - fn sleep(&self, duration: Duration) -> Result<(), Error>; + async fn poll_oneoff<'a>(&self, poll: &mut Poll<'a>) -> Result<(), Error>; + async fn sched_yield(&self) -> Result<(), Error>; + async fn sleep(&self, duration: Duration) -> Result<(), Error>; } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Userdata(u64); impl From for Userdata { fn from(u: u64) -> Userdata { @@ -26,6 +30,8 @@ impl From for u64 { } } +pub type PollResults = Vec<(SubscriptionResult, Userdata)>; + pub struct Poll<'a> { subs: Vec<(Subscription<'a>, Userdata)>, } @@ -50,11 +56,11 @@ impl<'a> Poll<'a> { ud, )); } - pub fn subscribe_read(&mut self, file: Ref<'a, dyn WasiFile>, ud: Userdata) { + pub fn subscribe_read(&mut self, file: &'a mut dyn WasiFile, ud: Userdata) { self.subs .push((Subscription::Read(RwSubscription::new(file)), ud)); } - pub fn subscribe_write(&mut self, file: Ref<'a, dyn WasiFile>, ud: Userdata) { + pub fn subscribe_write(&mut self, file: &'a mut dyn WasiFile, ud: Userdata) { self.subs .push((Subscription::Write(RwSubscription::new(file)), ud)); } @@ -67,7 +73,7 @@ impl<'a> Poll<'a> { pub fn is_empty(&self) -> bool { self.subs.is_empty() } - pub fn earliest_clock_deadline(&'a self) -> Option<&MonotonicClockSubscription<'a>> { + pub fn earliest_clock_deadline(&self) -> Option<&MonotonicClockSubscription<'a>> { self.subs .iter() .filter_map(|(s, _ud)| match s { @@ -76,8 +82,8 @@ impl<'a> Poll<'a> { }) .min_by(|a, b| a.deadline.cmp(&b.deadline)) } - pub fn rw_subscriptions(&'a self) -> impl Iterator> { - self.subs.iter().filter_map(|(s, _ud)| match s { + pub fn rw_subscriptions<'b>(&'b mut self) -> impl Iterator> { + self.subs.iter_mut().filter_map(|(s, _ud)| match s { Subscription::Read { .. } | Subscription::Write { .. } => Some(s), _ => None, }) diff --git a/crates/wasi-common/src/sched/subscription.rs b/crates/wasi-common/src/sched/subscription.rs index 799cfc665f..cd861f6df0 100644 --- a/crates/wasi-common/src/sched/subscription.rs +++ b/crates/wasi-common/src/sched/subscription.rs @@ -3,7 +3,7 @@ use crate::file::WasiFile; use crate::Error; use bitflags::bitflags; use cap_std::time::{Duration, Instant}; -use std::cell::{Cell, Ref}; +use std::cell::Cell; bitflags! { pub struct RwEventFlags: u32 { @@ -12,12 +12,12 @@ bitflags! { } pub struct RwSubscription<'a> { - pub file: Ref<'a, dyn WasiFile>, + pub file: &'a mut dyn WasiFile, status: Cell>>, } impl<'a> RwSubscription<'a> { - pub fn new(file: Ref<'a, dyn WasiFile>) -> Self { + pub fn new(file: &'a mut dyn WasiFile) -> Self { Self { file, status: Cell::new(None), @@ -29,8 +29,8 @@ impl<'a> RwSubscription<'a> { pub fn error(&self, error: Error) { self.status.set(Some(Err(error))) } - pub fn result(self) -> Option> { - self.status.into_inner() + pub fn result(&self) -> Option> { + self.status.take() } } @@ -62,6 +62,7 @@ pub enum Subscription<'a> { MonotonicClock(MonotonicClockSubscription<'a>), } +#[derive(Debug)] pub enum SubscriptionResult { Read(Result<(u64, RwEventFlags), Error>), Write(Result<(u64, RwEventFlags), Error>), diff --git a/crates/wasi-common/src/snapshots/preview_0.rs b/crates/wasi-common/src/snapshots/preview_0.rs index 02e6289cd2..6eff280a47 100644 --- a/crates/wasi-common/src/snapshots/preview_0.rs +++ b/crates/wasi-common/src/snapshots/preview_0.rs @@ -1,12 +1,14 @@ -use crate::file::{FileCaps, FileEntryExt, TableFileExt}; +use crate::file::{FileCaps, FileEntryExt, FileEntryMutExt, TableFileExt, WasiFile}; use crate::sched::{ subscription::{RwEventFlags, SubscriptionResult}, - Poll, + Poll, Userdata, }; use crate::snapshots::preview_1::types as snapshot1_types; use crate::snapshots::preview_1::wasi_snapshot_preview1::WasiSnapshotPreview1 as Snapshot1; use crate::{Error, ErrorExt, WasiCtx}; use cap_std::time::Duration; +use std::cell::RefMut; +use std::collections::HashSet; use std::convert::{TryFrom, TryInto}; use std::io::{IoSlice, IoSliceMut}; use std::ops::Deref; @@ -16,6 +18,7 @@ use wiggle::GuestPtr; wiggle::from_witx!({ witx: ["$WASI_ROOT/phases/old/snapshot_0/witx/wasi_unstable.witx"], errors: { errno => Error }, + async: *, }); impl wiggle::GuestErrorType for types::Errno { @@ -332,79 +335,80 @@ convert_flags_bidirectional!( // This implementation, wherever possible, delegates directly to the Snapshot1 implementation, // performing the no-op type conversions along the way. -impl<'a> wasi_unstable::WasiUnstable for WasiCtx { - fn args_get<'b>( +#[wiggle::async_trait] +impl wasi_unstable::WasiUnstable for WasiCtx { + async fn args_get<'a>( &self, - argv: &GuestPtr<'b, GuestPtr<'b, u8>>, - argv_buf: &GuestPtr<'b, u8>, + argv: &GuestPtr<'a, GuestPtr<'a, u8>>, + argv_buf: &GuestPtr<'a, u8>, ) -> Result<(), Error> { - Snapshot1::args_get(self, argv, argv_buf) + Snapshot1::args_get(self, argv, argv_buf).await } - fn args_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { - Snapshot1::args_sizes_get(self) + async fn args_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { + Snapshot1::args_sizes_get(self).await } - fn environ_get<'b>( + async fn environ_get<'a>( &self, - environ: &GuestPtr<'b, GuestPtr<'b, u8>>, - environ_buf: &GuestPtr<'b, u8>, + environ: &GuestPtr<'a, GuestPtr<'a, u8>>, + environ_buf: &GuestPtr<'a, u8>, ) -> Result<(), Error> { - Snapshot1::environ_get(self, environ, environ_buf) + Snapshot1::environ_get(self, environ, environ_buf).await } - fn environ_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { - Snapshot1::environ_sizes_get(self) + async fn environ_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { + Snapshot1::environ_sizes_get(self).await } - fn clock_res_get(&self, id: types::Clockid) -> Result { - Snapshot1::clock_res_get(self, id.into()) + async fn clock_res_get(&self, id: types::Clockid) -> Result { + Snapshot1::clock_res_get(self, id.into()).await } - fn clock_time_get( + async fn clock_time_get( &self, id: types::Clockid, precision: types::Timestamp, ) -> Result { - Snapshot1::clock_time_get(self, id.into(), precision) + Snapshot1::clock_time_get(self, id.into(), precision).await } - fn fd_advise( + async fn fd_advise( &self, fd: types::Fd, offset: types::Filesize, len: types::Filesize, advice: types::Advice, ) -> Result<(), Error> { - Snapshot1::fd_advise(self, fd.into(), offset, len, advice.into()) + Snapshot1::fd_advise(self, fd.into(), offset, len, advice.into()).await } - fn fd_allocate( + async fn fd_allocate( &self, fd: types::Fd, offset: types::Filesize, len: types::Filesize, ) -> Result<(), Error> { - Snapshot1::fd_allocate(self, fd.into(), offset, len) + Snapshot1::fd_allocate(self, fd.into(), offset, len).await } - fn fd_close(&self, fd: types::Fd) -> Result<(), Error> { - Snapshot1::fd_close(self, fd.into()) + async fn fd_close(&self, fd: types::Fd) -> Result<(), Error> { + Snapshot1::fd_close(self, fd.into()).await } - fn fd_datasync(&self, fd: types::Fd) -> Result<(), Error> { - Snapshot1::fd_datasync(self, fd.into()) + async fn fd_datasync(&self, fd: types::Fd) -> Result<(), Error> { + Snapshot1::fd_datasync(self, fd.into()).await } - fn fd_fdstat_get(&self, fd: types::Fd) -> Result { - Ok(Snapshot1::fd_fdstat_get(self, fd.into())?.into()) + async fn fd_fdstat_get(&self, fd: types::Fd) -> Result { + Ok(Snapshot1::fd_fdstat_get(self, fd.into()).await?.into()) } - fn fd_fdstat_set_flags(&self, fd: types::Fd, flags: types::Fdflags) -> Result<(), Error> { - Snapshot1::fd_fdstat_set_flags(self, fd.into(), flags.into()) + async fn fd_fdstat_set_flags(&self, fd: types::Fd, flags: types::Fdflags) -> Result<(), Error> { + Snapshot1::fd_fdstat_set_flags(self, fd.into(), flags.into()).await } - fn fd_fdstat_set_rights( + async fn fd_fdstat_set_rights( &self, fd: types::Fd, fs_rights_base: types::Rights, @@ -416,24 +420,29 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { fs_rights_base.into(), fs_rights_inheriting.into(), ) + .await } - fn fd_filestat_get(&self, fd: types::Fd) -> Result { - Ok(Snapshot1::fd_filestat_get(self, fd.into())?.into()) + async fn fd_filestat_get(&self, fd: types::Fd) -> Result { + Ok(Snapshot1::fd_filestat_get(self, fd.into()).await?.into()) } - fn fd_filestat_set_size(&self, fd: types::Fd, size: types::Filesize) -> Result<(), Error> { - Snapshot1::fd_filestat_set_size(self, fd.into(), size) + async fn fd_filestat_set_size( + &self, + fd: types::Fd, + size: types::Filesize, + ) -> Result<(), Error> { + Snapshot1::fd_filestat_set_size(self, fd.into(), size).await } - fn fd_filestat_set_times( + async fn fd_filestat_set_times( &self, fd: types::Fd, atim: types::Timestamp, mtim: types::Timestamp, fst_flags: types::Fstflags, ) -> Result<(), Error> { - Snapshot1::fd_filestat_set_times(self, fd.into(), atim, mtim, fst_flags.into()) + Snapshot1::fd_filestat_set_times(self, fd.into(), atim, mtim, fst_flags.into()).await } // NOTE on fd_read, fd_pread, fd_write, fd_pwrite implementations: @@ -444,7 +453,11 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { // The bodies of these functions is mostly about converting the GuestPtr and types::-based // representation to a std::io::IoSlice(Mut) representation. - fn fd_read(&self, fd: types::Fd, iovs: &types::IovecArray<'_>) -> Result { + async fn fd_read<'a>( + &self, + fd: types::Fd, + iovs: &types::IovecArray<'a>, + ) -> Result { let table = self.table(); let f = table.get_file(u32::from(fd))?.get_cap(FileCaps::READ)?; @@ -462,14 +475,14 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { .map(|s| IoSliceMut::new(&mut *s)) .collect(); - let bytes_read = f.read_vectored(&mut ioslices)?; + let bytes_read = f.read_vectored(&mut ioslices).await?; Ok(types::Size::try_from(bytes_read)?) } - fn fd_pread( + async fn fd_pread<'a>( &self, fd: types::Fd, - iovs: &types::IovecArray<'_>, + iovs: &types::IovecArray<'a>, offset: types::Filesize, ) -> Result { let table = self.table(); @@ -491,14 +504,14 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { .map(|s| IoSliceMut::new(&mut *s)) .collect(); - let bytes_read = f.read_vectored_at(&mut ioslices, offset)?; + let bytes_read = f.read_vectored_at(&mut ioslices, offset).await?; Ok(types::Size::try_from(bytes_read)?) } - fn fd_write( + async fn fd_write<'a>( &self, fd: types::Fd, - ciovs: &types::CiovecArray<'_>, + ciovs: &types::CiovecArray<'a>, ) -> Result { let table = self.table(); let f = table.get_file(u32::from(fd))?.get_cap(FileCaps::WRITE)?; @@ -516,15 +529,15 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { .iter() .map(|s| IoSlice::new(s.deref())) .collect(); - let bytes_written = f.write_vectored(&ioslices)?; + let bytes_written = f.write_vectored(&ioslices).await?; Ok(types::Size::try_from(bytes_written)?) } - fn fd_pwrite( + async fn fd_pwrite<'a>( &self, fd: types::Fd, - ciovs: &types::CiovecArray<'_>, + ciovs: &types::CiovecArray<'a>, offset: types::Filesize, ) -> Result { let table = self.table(); @@ -545,77 +558,81 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { .iter() .map(|s| IoSlice::new(s.deref())) .collect(); - let bytes_written = f.write_vectored_at(&ioslices, offset)?; + let bytes_written = f.write_vectored_at(&ioslices, offset).await?; Ok(types::Size::try_from(bytes_written)?) } - fn fd_prestat_get(&self, fd: types::Fd) -> Result { - Ok(Snapshot1::fd_prestat_get(self, fd.into())?.into()) + async fn fd_prestat_get(&self, fd: types::Fd) -> Result { + Ok(Snapshot1::fd_prestat_get(self, fd.into()).await?.into()) } - fn fd_prestat_dir_name( + async fn fd_prestat_dir_name<'a>( &self, fd: types::Fd, - path: &GuestPtr, + path: &GuestPtr<'a, u8>, path_max_len: types::Size, ) -> Result<(), Error> { - Snapshot1::fd_prestat_dir_name(self, fd.into(), path, path_max_len) + Snapshot1::fd_prestat_dir_name(self, fd.into(), path, path_max_len).await } - fn fd_renumber(&self, from: types::Fd, to: types::Fd) -> Result<(), Error> { - Snapshot1::fd_renumber(self, from.into(), to.into()) + async fn fd_renumber(&self, from: types::Fd, to: types::Fd) -> Result<(), Error> { + Snapshot1::fd_renumber(self, from.into(), to.into()).await } - fn fd_seek( + async fn fd_seek( &self, fd: types::Fd, offset: types::Filedelta, whence: types::Whence, ) -> Result { - Snapshot1::fd_seek(self, fd.into(), offset, whence.into()) + Snapshot1::fd_seek(self, fd.into(), offset, whence.into()).await } - fn fd_sync(&self, fd: types::Fd) -> Result<(), Error> { - Snapshot1::fd_sync(self, fd.into()) + async fn fd_sync(&self, fd: types::Fd) -> Result<(), Error> { + Snapshot1::fd_sync(self, fd.into()).await } - fn fd_tell(&self, fd: types::Fd) -> Result { - Snapshot1::fd_tell(self, fd.into()) + async fn fd_tell(&self, fd: types::Fd) -> Result { + Snapshot1::fd_tell(self, fd.into()).await } - fn fd_readdir( + async fn fd_readdir<'a>( &self, fd: types::Fd, - buf: &GuestPtr, + buf: &GuestPtr<'a, u8>, buf_len: types::Size, cookie: types::Dircookie, ) -> Result { - Snapshot1::fd_readdir(self, fd.into(), buf, buf_len, cookie) + Snapshot1::fd_readdir(self, fd.into(), buf, buf_len, cookie).await } - fn path_create_directory( + async fn path_create_directory<'a>( &self, dirfd: types::Fd, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, ) -> Result<(), Error> { - Snapshot1::path_create_directory(self, dirfd.into(), path) + Snapshot1::path_create_directory(self, dirfd.into(), path).await } - fn path_filestat_get( + async fn path_filestat_get<'a>( &self, dirfd: types::Fd, flags: types::Lookupflags, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, ) -> Result { - Ok(Snapshot1::path_filestat_get(self, dirfd.into(), flags.into(), path)?.into()) + Ok( + Snapshot1::path_filestat_get(self, dirfd.into(), flags.into(), path) + .await? + .into(), + ) } - fn path_filestat_set_times( + async fn path_filestat_set_times<'a>( &self, dirfd: types::Fd, flags: types::Lookupflags, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, atim: types::Timestamp, mtim: types::Timestamp, fst_flags: types::Fstflags, @@ -629,15 +646,16 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { mtim, fst_flags.into(), ) + .await } - fn path_link( + async fn path_link<'a>( &self, src_fd: types::Fd, src_flags: types::Lookupflags, - src_path: &GuestPtr<'_, str>, + src_path: &GuestPtr<'a, str>, target_fd: types::Fd, - target_path: &GuestPtr<'_, str>, + target_path: &GuestPtr<'a, str>, ) -> Result<(), Error> { Snapshot1::path_link( self, @@ -647,13 +665,14 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { target_fd.into(), target_path, ) + .await } - fn path_open( + async fn path_open<'a>( &self, dirfd: types::Fd, dirflags: types::Lookupflags, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, oflags: types::Oflags, fs_rights_base: types::Rights, fs_rights_inheriting: types::Rights, @@ -668,49 +687,54 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { fs_rights_base.into(), fs_rights_inheriting.into(), fdflags.into(), - )? + ) + .await? .into()) } - fn path_readlink( + async fn path_readlink<'a>( &self, dirfd: types::Fd, - path: &GuestPtr<'_, str>, - buf: &GuestPtr, + path: &GuestPtr<'a, str>, + buf: &GuestPtr<'a, u8>, buf_len: types::Size, ) -> Result { - Snapshot1::path_readlink(self, dirfd.into(), path, buf, buf_len) + Snapshot1::path_readlink(self, dirfd.into(), path, buf, buf_len).await } - fn path_remove_directory( + async fn path_remove_directory<'a>( &self, dirfd: types::Fd, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, ) -> Result<(), Error> { - Snapshot1::path_remove_directory(self, dirfd.into(), path) + Snapshot1::path_remove_directory(self, dirfd.into(), path).await } - fn path_rename( + async fn path_rename<'a>( &self, src_fd: types::Fd, - src_path: &GuestPtr<'_, str>, + src_path: &GuestPtr<'a, str>, dest_fd: types::Fd, - dest_path: &GuestPtr<'_, str>, + dest_path: &GuestPtr<'a, str>, ) -> Result<(), Error> { - Snapshot1::path_rename(self, src_fd.into(), src_path, dest_fd.into(), dest_path) + Snapshot1::path_rename(self, src_fd.into(), src_path, dest_fd.into(), dest_path).await } - fn path_symlink( + async fn path_symlink<'a>( &self, - src_path: &GuestPtr<'_, str>, + src_path: &GuestPtr<'a, str>, dirfd: types::Fd, - dest_path: &GuestPtr<'_, str>, + dest_path: &GuestPtr<'a, str>, ) -> Result<(), Error> { - Snapshot1::path_symlink(self, src_path, dirfd.into(), dest_path) + Snapshot1::path_symlink(self, src_path, dirfd.into(), dest_path).await } - fn path_unlink_file(&self, dirfd: types::Fd, path: &GuestPtr<'_, str>) -> Result<(), Error> { - Snapshot1::path_unlink_file(self, dirfd.into(), path) + async fn path_unlink_file<'a>( + &self, + dirfd: types::Fd, + path: &GuestPtr<'a, str>, + ) -> Result<(), Error> { + Snapshot1::path_unlink_file(self, dirfd.into(), path).await } // NOTE on poll_oneoff implementation: @@ -720,10 +744,10 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { // The implementations are identical, but the `types::` in scope locally is different. // The bodies of these functions is mostly about converting the GuestPtr and types::-based // representation to use the Poll abstraction. - fn poll_oneoff( + async fn poll_oneoff<'a>( &self, - subs: &GuestPtr, - events: &GuestPtr, + subs: &GuestPtr<'a, types::Subscription>, + events: &GuestPtr<'a, types::Event>, nsubscriptions: types::Size, ) -> Result { if nsubscriptions == 0 { @@ -741,7 +765,9 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { .flags .contains(types::Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME) { - self.sched.sleep(Duration::from_nanos(clocksub.timeout))?; + self.sched + .sleep(Duration::from_nanos(clocksub.timeout)) + .await?; events.write(types::Event { userdata: sub.userdata, error: types::Errno::Success, @@ -754,6 +780,10 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { } let table = self.table(); + let mut sub_fds: HashSet = HashSet::new(); + // We need these refmuts to outlive Poll, which will hold the &mut dyn WasiFile inside + let mut read_refs: Vec<(RefMut<'_, dyn WasiFile>, Userdata)> = Vec::new(); + let mut write_refs: Vec<(RefMut<'_, dyn WasiFile>, Userdata)> = Vec::new(); let mut poll = Poll::new(); let subs = subs.as_array(nsubscriptions); @@ -792,22 +822,34 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { }, types::SubscriptionU::FdRead(readsub) => { let fd = readsub.file_descriptor; - let file = table - .get_file(u32::from(fd))? + if sub_fds.contains(&fd) { + return Err(Error::invalid_argument() + .context("Fd can be subscribed to at most once per poll")); + } else { + sub_fds.insert(fd); + } + let file_ref = table + .get_file_mut(u32::from(fd))? .get_cap(FileCaps::POLL_READWRITE)?; - poll.subscribe_read(file, sub.userdata.into()); + read_refs.push((file_ref, sub.userdata.into())); } types::SubscriptionU::FdWrite(writesub) => { let fd = writesub.file_descriptor; - let file = table - .get_file(u32::from(fd))? + if sub_fds.contains(&fd) { + return Err(Error::invalid_argument() + .context("Fd can be subscribed to at most once per poll")); + } else { + sub_fds.insert(fd); + } + let file_ref = table + .get_file_mut(u32::from(fd))? .get_cap(FileCaps::POLL_READWRITE)?; - poll.subscribe_write(file, sub.userdata.into()); + write_refs.push((file_ref, sub.userdata.into())); } } } - self.sched.poll_oneoff(&poll)?; + self.sched.poll_oneoff(&mut poll).await?; let results = poll.results(); let num_results = results.len(); @@ -882,41 +924,45 @@ impl<'a> wasi_unstable::WasiUnstable for WasiCtx { Ok(num_results.try_into().expect("results fit into memory")) } - fn proc_exit(&self, status: types::Exitcode) -> wiggle::Trap { - Snapshot1::proc_exit(self, status) + async fn proc_exit(&self, status: types::Exitcode) -> wiggle::Trap { + Snapshot1::proc_exit(self, status).await } - fn proc_raise(&self, _sig: types::Signal) -> Result<(), Error> { + async fn proc_raise(&self, _sig: types::Signal) -> Result<(), Error> { Err(Error::trap("proc_raise unsupported")) } - fn sched_yield(&self) -> Result<(), Error> { - Snapshot1::sched_yield(self) + async fn sched_yield(&self) -> Result<(), Error> { + Snapshot1::sched_yield(self).await } - fn random_get(&self, buf: &GuestPtr, buf_len: types::Size) -> Result<(), Error> { - Snapshot1::random_get(self, buf, buf_len) + async fn random_get<'a>( + &self, + buf: &GuestPtr<'a, u8>, + buf_len: types::Size, + ) -> Result<(), Error> { + Snapshot1::random_get(self, buf, buf_len).await } - fn sock_recv( + async fn sock_recv<'a>( &self, _fd: types::Fd, - _ri_data: &types::IovecArray<'_>, + _ri_data: &types::IovecArray<'a>, _ri_flags: types::Riflags, ) -> Result<(types::Size, types::Roflags), Error> { Err(Error::trap("sock_recv unsupported")) } - fn sock_send( + async fn sock_send<'a>( &self, _fd: types::Fd, - _si_data: &types::CiovecArray<'_>, + _si_data: &types::CiovecArray<'a>, _si_flags: types::Siflags, ) -> Result { Err(Error::trap("sock_send unsupported")) } - fn sock_shutdown(&self, _fd: types::Fd, _how: types::Sdflags) -> Result<(), Error> { + async fn sock_shutdown(&self, _fd: types::Fd, _how: types::Sdflags) -> Result<(), Error> { Err(Error::trap("sock_shutdown unsupported")) } } diff --git a/crates/wasi-common/src/snapshots/preview_1.rs b/crates/wasi-common/src/snapshots/preview_1.rs index 0ee4044c37..553066628b 100644 --- a/crates/wasi-common/src/snapshots/preview_1.rs +++ b/crates/wasi-common/src/snapshots/preview_1.rs @@ -2,17 +2,18 @@ use crate::{ dir::{DirCaps, DirEntry, DirEntryExt, DirFdStat, ReaddirCursor, ReaddirEntity, TableDirExt}, file::{ Advice, FdFlags, FdStat, FileCaps, FileEntry, FileEntryExt, FileEntryMutExt, FileType, - Filestat, OFlags, TableFileExt, + Filestat, OFlags, TableFileExt, WasiFile, }, sched::{ subscription::{RwEventFlags, SubscriptionResult}, - Poll, + Poll, Userdata, }, Error, ErrorExt, ErrorKind, SystemTimeSpec, WasiCtx, }; use anyhow::Context; use cap_std::time::{Duration, SystemClock}; use std::cell::{Ref, RefMut}; +use std::collections::HashSet; use std::convert::{TryFrom, TryInto}; use std::io::{IoSlice, IoSliceMut}; use std::ops::{Deref, DerefMut}; @@ -22,6 +23,10 @@ use wiggle::GuestPtr; wiggle::from_witx!({ witx: ["$WASI_ROOT/phases/snapshot/witx/wasi_snapshot_preview1.witx"], errors: { errno => Error }, + // Note: not every function actually needs to be async, however, nearly all of them do, and + // keeping that set the same in this macro and the wasmtime_wiggle / lucet_wiggle macros is + // tedious, and there is no cost to having a sync function be async in this case. + async: * }); impl wiggle::GuestErrorType for types::Errno { @@ -188,8 +193,9 @@ impl TryFrom for types::Errno { } } -impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { - fn args_get<'b>( +#[wiggle::async_trait] +impl wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { + async fn args_get<'b>( &self, argv: &GuestPtr<'b, GuestPtr<'b, u8>>, argv_buf: &GuestPtr<'b, u8>, @@ -197,11 +203,11 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { self.args.write_to_guest(argv_buf, argv) } - fn args_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { + async fn args_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { Ok((self.args.number_elements(), self.args.cumulative_size())) } - fn environ_get<'b>( + async fn environ_get<'b>( &self, environ: &GuestPtr<'b, GuestPtr<'b, u8>>, environ_buf: &GuestPtr<'b, u8>, @@ -209,11 +215,11 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { self.env.write_to_guest(environ_buf, environ) } - fn environ_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { + async fn environ_sizes_get(&self) -> Result<(types::Size, types::Size), Error> { Ok((self.env.number_elements(), self.env.cumulative_size())) } - fn clock_res_get(&self, id: types::Clockid) -> Result { + async fn clock_res_get(&self, id: types::Clockid) -> Result { let resolution = match id { types::Clockid::Realtime => Ok(self.clocks.system.resolution()), types::Clockid::Monotonic => Ok(self.clocks.monotonic.resolution()), @@ -224,7 +230,7 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { Ok(resolution.as_nanos().try_into()?) } - fn clock_time_get( + async fn clock_time_get( &self, id: types::Clockid, precision: types::Timestamp, @@ -249,7 +255,7 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { } } - fn fd_advise( + async fn fd_advise( &self, fd: types::Fd, offset: types::Filesize, @@ -259,11 +265,12 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { self.table() .get_file(u32::from(fd))? .get_cap(FileCaps::ADVISE)? - .advise(offset, len, advice.into())?; + .advise(offset, len, advice.into()) + .await?; Ok(()) } - fn fd_allocate( + async fn fd_allocate( &self, fd: types::Fd, offset: types::Filesize, @@ -272,11 +279,12 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { self.table() .get_file(u32::from(fd))? .get_cap(FileCaps::ALLOCATE)? - .allocate(offset, len)?; + .allocate(offset, len) + .await?; Ok(()) } - fn fd_close(&self, fd: types::Fd) -> Result<(), Error> { + async fn fd_close(&self, fd: types::Fd) -> Result<(), Error> { let mut table = self.table(); let fd = u32::from(fd); @@ -302,20 +310,21 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { Ok(()) } - fn fd_datasync(&self, fd: types::Fd) -> Result<(), Error> { + async fn fd_datasync(&self, fd: types::Fd) -> Result<(), Error> { self.table() .get_file(u32::from(fd))? .get_cap(FileCaps::DATASYNC)? - .datasync()?; + .datasync() + .await?; Ok(()) } - fn fd_fdstat_get(&self, fd: types::Fd) -> Result { + async fn fd_fdstat_get(&self, fd: types::Fd) -> Result { let table = self.table(); let fd = u32::from(fd); if table.is::(fd) { let file_entry: Ref = table.get(fd)?; - let fdstat = file_entry.get_fdstat()?; + let fdstat = file_entry.get_fdstat().await?; Ok(types::Fdstat::from(&fdstat)) } else if table.is::(fd) { let dir_entry: Ref = table.get(fd)?; @@ -326,14 +335,15 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { } } - fn fd_fdstat_set_flags(&self, fd: types::Fd, flags: types::Fdflags) -> Result<(), Error> { + async fn fd_fdstat_set_flags(&self, fd: types::Fd, flags: types::Fdflags) -> Result<(), Error> { self.table() .get_file_mut(u32::from(fd))? .get_cap(FileCaps::FDSTAT_SET_FLAGS)? .set_fdflags(FdFlags::from(flags)) + .await } - fn fd_fdstat_set_rights( + async fn fd_fdstat_set_rights( &self, fd: types::Fd, fs_rights_base: types::Rights, @@ -355,35 +365,42 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { } } - fn fd_filestat_get(&self, fd: types::Fd) -> Result { + async fn fd_filestat_get(&self, fd: types::Fd) -> Result { let table = self.table(); let fd = u32::from(fd); if table.is::(fd) { let filestat = table .get_file(fd)? .get_cap(FileCaps::FILESTAT_GET)? - .get_filestat()?; + .get_filestat() + .await?; Ok(filestat.into()) } else if table.is::(fd) { let filestat = table .get_dir(fd)? .get_cap(DirCaps::FILESTAT_GET)? - .get_filestat()?; + .get_filestat() + .await?; Ok(filestat.into()) } else { Err(Error::badf()) } } - fn fd_filestat_set_size(&self, fd: types::Fd, size: types::Filesize) -> Result<(), Error> { + async fn fd_filestat_set_size( + &self, + fd: types::Fd, + size: types::Filesize, + ) -> Result<(), Error> { self.table() .get_file(u32::from(fd))? .get_cap(FileCaps::FILESTAT_SET_SIZE)? - .set_filestat_size(size)?; + .set_filestat_size(size) + .await?; Ok(()) } - fn fd_filestat_set_times( + async fn fd_filestat_set_times( &self, fd: types::Fd, atim: types::Timestamp, @@ -407,18 +424,24 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .expect("checked that entry is file") .get_cap(FileCaps::FILESTAT_SET_TIMES)? .set_times(atim, mtim) + .await } else if table.is::(fd) { table .get_dir(fd) .expect("checked that entry is dir") .get_cap(DirCaps::FILESTAT_SET_TIMES)? .set_times(".", atim, mtim, false) + .await } else { Err(Error::badf()) } } - fn fd_read(&self, fd: types::Fd, iovs: &types::IovecArray<'_>) -> Result { + async fn fd_read<'a>( + &self, + fd: types::Fd, + iovs: &types::IovecArray<'a>, + ) -> Result { let table = self.table(); let f = table.get_file(u32::from(fd))?.get_cap(FileCaps::READ)?; @@ -436,14 +459,14 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .map(|s| IoSliceMut::new(&mut *s)) .collect(); - let bytes_read = f.read_vectored(&mut ioslices)?; + let bytes_read = f.read_vectored(&mut ioslices).await?; Ok(types::Size::try_from(bytes_read)?) } - fn fd_pread( + async fn fd_pread<'a>( &self, fd: types::Fd, - iovs: &types::IovecArray<'_>, + iovs: &types::IovecArray<'a>, offset: types::Filesize, ) -> Result { let table = self.table(); @@ -465,14 +488,14 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .map(|s| IoSliceMut::new(&mut *s)) .collect(); - let bytes_read = f.read_vectored_at(&mut ioslices, offset)?; + let bytes_read = f.read_vectored_at(&mut ioslices, offset).await?; Ok(types::Size::try_from(bytes_read)?) } - fn fd_write( + async fn fd_write<'a>( &self, fd: types::Fd, - ciovs: &types::CiovecArray<'_>, + ciovs: &types::CiovecArray<'a>, ) -> Result { let table = self.table(); let f = table.get_file(u32::from(fd))?.get_cap(FileCaps::WRITE)?; @@ -490,15 +513,15 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .iter() .map(|s| IoSlice::new(s.deref())) .collect(); - let bytes_written = f.write_vectored(&ioslices)?; + let bytes_written = f.write_vectored(&ioslices).await?; Ok(types::Size::try_from(bytes_written)?) } - fn fd_pwrite( + async fn fd_pwrite<'a>( &self, fd: types::Fd, - ciovs: &types::CiovecArray<'_>, + ciovs: &types::CiovecArray<'a>, offset: types::Filesize, ) -> Result { let table = self.table(); @@ -519,12 +542,12 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .iter() .map(|s| IoSlice::new(s.deref())) .collect(); - let bytes_written = f.write_vectored_at(&ioslices, offset)?; + let bytes_written = f.write_vectored_at(&ioslices, offset).await?; Ok(types::Size::try_from(bytes_written)?) } - fn fd_prestat_get(&self, fd: types::Fd) -> Result { + async fn fd_prestat_get(&self, fd: types::Fd) -> Result { let table = self.table(); let dir_entry: Ref = table.get(u32::from(fd)).map_err(|_| Error::badf())?; if let Some(ref preopen) = dir_entry.preopen_path() { @@ -536,10 +559,10 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { } } - fn fd_prestat_dir_name( + async fn fd_prestat_dir_name<'a>( &self, fd: types::Fd, - path: &GuestPtr, + path: &GuestPtr<'a, u8>, path_max_len: types::Size, ) -> Result<(), Error> { let table = self.table(); @@ -560,7 +583,7 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { Err(Error::not_supported()) } } - fn fd_renumber(&self, from: types::Fd, to: types::Fd) -> Result<(), Error> { + async fn fd_renumber(&self, from: types::Fd, to: types::Fd) -> Result<(), Error> { let mut table = self.table(); let from = u32::from(from); let to = u32::from(to); @@ -577,7 +600,7 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { Ok(()) } - fn fd_seek( + async fn fd_seek( &self, fd: types::Fd, offset: types::Filedelta, @@ -600,32 +623,35 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .table() .get_file(u32::from(fd))? .get_cap(required_caps)? - .seek(whence)?; + .seek(whence) + .await?; Ok(newoffset) } - fn fd_sync(&self, fd: types::Fd) -> Result<(), Error> { + async fn fd_sync(&self, fd: types::Fd) -> Result<(), Error> { self.table() .get_file(u32::from(fd))? .get_cap(FileCaps::SYNC)? - .sync()?; + .sync() + .await?; Ok(()) } - fn fd_tell(&self, fd: types::Fd) -> Result { + async fn fd_tell(&self, fd: types::Fd) -> Result { // XXX should this be stream_position? let offset = self .table() .get_file(u32::from(fd))? .get_cap(FileCaps::TELL)? - .seek(std::io::SeekFrom::Current(0))?; + .seek(std::io::SeekFrom::Current(0)) + .await?; Ok(offset) } - fn fd_readdir( + async fn fd_readdir<'a>( &self, fd: types::Fd, - buf: &GuestPtr, + buf: &GuestPtr<'a, u8>, buf_len: types::Size, cookie: types::Dircookie, ) -> Result { @@ -635,7 +661,8 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .table() .get_dir(u32::from(fd))? .get_cap(DirCaps::READDIR)? - .readdir(ReaddirCursor::from(cookie))? + .readdir(ReaddirCursor::from(cookie)) + .await? { let entity = entity?; let dirent_raw = dirent_bytes(types::Dirent::try_from(&entity)?); @@ -675,22 +702,23 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { Ok(bufused) } - fn path_create_directory( + async fn path_create_directory<'a>( &self, dirfd: types::Fd, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, ) -> Result<(), Error> { self.table() .get_dir(u32::from(dirfd))? .get_cap(DirCaps::CREATE_DIRECTORY)? .create_dir(path.as_str()?.deref()) + .await } - fn path_filestat_get( + async fn path_filestat_get<'a>( &self, dirfd: types::Fd, flags: types::Lookupflags, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, ) -> Result { let filestat = self .table() @@ -699,15 +727,16 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .get_path_filestat( path.as_str()?.deref(), flags.contains(types::Lookupflags::SYMLINK_FOLLOW), - )?; + ) + .await?; Ok(types::Filestat::from(filestat)) } - fn path_filestat_set_times( + async fn path_filestat_set_times<'a>( &self, dirfd: types::Fd, flags: types::Lookupflags, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, atim: types::Timestamp, mtim: types::Timestamp, fst_flags: types::Fstflags, @@ -728,15 +757,16 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { mtim, flags.contains(types::Lookupflags::SYMLINK_FOLLOW), ) + .await } - fn path_link( + async fn path_link<'a>( &self, src_fd: types::Fd, src_flags: types::Lookupflags, - src_path: &GuestPtr<'_, str>, + src_path: &GuestPtr<'a, str>, target_fd: types::Fd, - target_path: &GuestPtr<'_, str>, + target_path: &GuestPtr<'a, str>, ) -> Result<(), Error> { let table = self.table(); let src_dir = table @@ -751,18 +781,20 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .context("symlink following on path_link is not supported")); } - src_dir.hard_link( - src_path.as_str()?.deref(), - target_dir.deref(), - target_path.as_str()?.deref(), - ) + src_dir + .hard_link( + src_path.as_str()?.deref(), + target_dir.deref(), + target_path.as_str()?.deref(), + ) + .await } - fn path_open( + async fn path_open<'a>( &self, dirfd: types::Fd, dirflags: types::Lookupflags, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, oflags: types::Oflags, fs_rights_base: types::Rights, fs_rights_inheriting: types::Rights, @@ -790,7 +822,7 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { let dir_caps = dir_entry.child_dir_caps(DirCaps::from(&fs_rights_base)); let file_caps = dir_entry.child_file_caps(FileCaps::from(&fs_rights_inheriting)); let dir = dir_entry.get_cap(DirCaps::OPEN)?; - let child_dir = dir.open_dir(symlink_follow, path.deref())?; + let child_dir = dir.open_dir(symlink_follow, path.deref()).await?; drop(dir); let fd = table.push(Box::new(DirEntry::new( dir_caps, file_caps, None, child_dir, @@ -808,25 +840,28 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { let write = file_caps.contains(FileCaps::WRITE) || file_caps.contains(FileCaps::ALLOCATE) || file_caps.contains(FileCaps::FILESTAT_SET_SIZE); - let file = dir.open_file(symlink_follow, path.deref(), oflags, read, write, fdflags)?; + let file = dir + .open_file(symlink_follow, path.deref(), oflags, read, write, fdflags) + .await?; drop(dir); let fd = table.push(Box::new(FileEntry::new(file_caps, file)))?; Ok(types::Fd::from(fd)) } } - fn path_readlink( + async fn path_readlink<'a>( &self, dirfd: types::Fd, - path: &GuestPtr<'_, str>, - buf: &GuestPtr, + path: &GuestPtr<'a, str>, + buf: &GuestPtr<'a, u8>, buf_len: types::Size, ) -> Result { let link = self .table() .get_dir(u32::from(dirfd))? .get_cap(DirCaps::READLINK)? - .read_link(path.as_str()?.deref())? + .read_link(path.as_str()?.deref()) + .await? .into_os_string() .into_string() .map_err(|_| Error::illegal_byte_sequence().context("link contents"))?; @@ -840,23 +875,24 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { Ok(link_len as types::Size) } - fn path_remove_directory( + async fn path_remove_directory<'a>( &self, dirfd: types::Fd, - path: &GuestPtr<'_, str>, + path: &GuestPtr<'a, str>, ) -> Result<(), Error> { self.table() .get_dir(u32::from(dirfd))? .get_cap(DirCaps::REMOVE_DIRECTORY)? .remove_dir(path.as_str()?.deref()) + .await } - fn path_rename( + async fn path_rename<'a>( &self, src_fd: types::Fd, - src_path: &GuestPtr<'_, str>, + src_path: &GuestPtr<'a, str>, dest_fd: types::Fd, - dest_path: &GuestPtr<'_, str>, + dest_path: &GuestPtr<'a, str>, ) -> Result<(), Error> { let table = self.table(); let src_dir = table @@ -865,36 +901,44 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { let dest_dir = table .get_dir(u32::from(dest_fd))? .get_cap(DirCaps::RENAME_TARGET)?; - src_dir.rename( - src_path.as_str()?.deref(), - dest_dir.deref(), - dest_path.as_str()?.deref(), - ) + src_dir + .rename( + src_path.as_str()?.deref(), + dest_dir.deref(), + dest_path.as_str()?.deref(), + ) + .await } - fn path_symlink( + async fn path_symlink<'a>( &self, - src_path: &GuestPtr<'_, str>, + src_path: &GuestPtr<'a, str>, dirfd: types::Fd, - dest_path: &GuestPtr<'_, str>, + dest_path: &GuestPtr<'a, str>, ) -> Result<(), Error> { self.table() .get_dir(u32::from(dirfd))? .get_cap(DirCaps::SYMLINK)? .symlink(src_path.as_str()?.deref(), dest_path.as_str()?.deref()) + .await } - fn path_unlink_file(&self, dirfd: types::Fd, path: &GuestPtr<'_, str>) -> Result<(), Error> { + async fn path_unlink_file<'a>( + &self, + dirfd: types::Fd, + path: &GuestPtr<'a, str>, + ) -> Result<(), Error> { self.table() .get_dir(u32::from(dirfd))? .get_cap(DirCaps::UNLINK_FILE)? .unlink_file(path.as_str()?.deref()) + .await } - fn poll_oneoff( + async fn poll_oneoff<'a>( &self, - subs: &GuestPtr, - events: &GuestPtr, + subs: &GuestPtr<'a, types::Subscription>, + events: &GuestPtr<'a, types::Event>, nsubscriptions: types::Size, ) -> Result { if nsubscriptions == 0 { @@ -912,7 +956,9 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { .flags .contains(types::Subclockflags::SUBSCRIPTION_CLOCK_ABSTIME) { - self.sched.sleep(Duration::from_nanos(clocksub.timeout))?; + self.sched + .sleep(Duration::from_nanos(clocksub.timeout)) + .await?; events.write(types::Event { userdata: sub.userdata, error: types::Errno::Success, @@ -925,6 +971,10 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { } let table = self.table(); + let mut sub_fds: HashSet = HashSet::new(); + // We need these refmuts to outlive Poll, which will hold the &mut dyn WasiFile inside + let mut read_refs: Vec<(RefMut<'_, dyn WasiFile>, Userdata)> = Vec::new(); + let mut write_refs: Vec<(RefMut<'_, dyn WasiFile>, Userdata)> = Vec::new(); let mut poll = Poll::new(); let subs = subs.as_array(nsubscriptions); @@ -963,22 +1013,41 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { }, types::SubscriptionU::FdRead(readsub) => { let fd = readsub.file_descriptor; - let file = table - .get_file(u32::from(fd))? + if sub_fds.contains(&fd) { + return Err(Error::invalid_argument() + .context("Fd can be subscribed to at most once per poll")); + } else { + sub_fds.insert(fd); + } + let file_ref = table + .get_file_mut(u32::from(fd))? .get_cap(FileCaps::POLL_READWRITE)?; - poll.subscribe_read(file, sub.userdata.into()); + read_refs.push((file_ref, sub.userdata.into())); } types::SubscriptionU::FdWrite(writesub) => { let fd = writesub.file_descriptor; - let file = table - .get_file(u32::from(fd))? + if sub_fds.contains(&fd) { + return Err(Error::invalid_argument() + .context("Fd can be subscribed to at most once per poll")); + } else { + sub_fds.insert(fd); + } + let file_ref = table + .get_file_mut(u32::from(fd))? .get_cap(FileCaps::POLL_READWRITE)?; - poll.subscribe_write(file, sub.userdata.into()); + write_refs.push((file_ref, sub.userdata.into())); } } } - self.sched.poll_oneoff(&poll)?; + for (f, ud) in read_refs.iter_mut() { + poll.subscribe_read(f.deref_mut(), *ud); + } + for (f, ud) in write_refs.iter_mut() { + poll.subscribe_write(f.deref_mut(), *ud); + } + + self.sched.poll_oneoff(&mut poll).await?; let results = poll.results(); let num_results = results.len(); @@ -1053,7 +1122,7 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { Ok(num_results.try_into().expect("results fit into memory")) } - fn proc_exit(&self, status: types::Exitcode) -> wiggle::Trap { + async fn proc_exit(&self, status: types::Exitcode) -> wiggle::Trap { // Check that the status is within WASI's range. if status < 126 { wiggle::Trap::I32Exit(status as i32) @@ -1062,39 +1131,43 @@ impl<'a> wasi_snapshot_preview1::WasiSnapshotPreview1 for WasiCtx { } } - fn proc_raise(&self, _sig: types::Signal) -> Result<(), Error> { + async fn proc_raise(&self, _sig: types::Signal) -> Result<(), Error> { Err(Error::trap("proc_raise unsupported")) } - fn sched_yield(&self) -> Result<(), Error> { - self.sched.sched_yield() + async fn sched_yield(&self) -> Result<(), Error> { + self.sched.sched_yield().await } - fn random_get(&self, buf: &GuestPtr, buf_len: types::Size) -> Result<(), Error> { + async fn random_get<'a>( + &self, + buf: &GuestPtr<'a, u8>, + buf_len: types::Size, + ) -> Result<(), Error> { let mut buf = buf.as_array(buf_len).as_slice_mut()?; self.random.borrow_mut().try_fill_bytes(buf.deref_mut())?; Ok(()) } - fn sock_recv( + async fn sock_recv<'a>( &self, _fd: types::Fd, - _ri_data: &types::IovecArray<'_>, + _ri_data: &types::IovecArray<'a>, _ri_flags: types::Riflags, ) -> Result<(types::Size, types::Roflags), Error> { Err(Error::trap("sock_recv unsupported")) } - fn sock_send( + async fn sock_send<'a>( &self, _fd: types::Fd, - _si_data: &types::CiovecArray<'_>, + _si_data: &types::CiovecArray<'a>, _si_flags: types::Siflags, ) -> Result { Err(Error::trap("sock_send unsupported")) } - fn sock_shutdown(&self, _fd: types::Fd, _how: types::Sdflags) -> Result<(), Error> { + async fn sock_shutdown(&self, _fd: types::Fd, _how: types::Sdflags) -> Result<(), Error> { Err(Error::trap("sock_shutdown unsupported")) } } diff --git a/crates/wasi-common/tokio/Cargo.toml b/crates/wasi-common/tokio/Cargo.toml new file mode 100644 index 0000000000..4e65c63d56 --- /dev/null +++ b/crates/wasi-common/tokio/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "wasi-tokio" +version = "0.26.0" +authors = ["The Wasmtime Project Developers"] +description = "WASI implementation in Rust" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/bytecodealliance/wasmtime" +readme = "README.md" +edition = "2018" +include = ["src/**/*", "LICENSE" ] + +[dependencies] +wasi-common = { path = "../", version = "0.26.0" } +wasi-cap-std-sync = { path = "../cap-std-sync", version = "0.26.0" } +wiggle = { path = "../../wiggle", version = "0.26.0" } +tokio = { version = "1.5.0", features = [ "rt", "fs", "time", "io-util", "net", "io-std", "rt-multi-thread"] } +cap-std = "0.13.7" +cap-fs-ext = "0.13.7" +cap-time-ext = "0.13.7" +fs-set-times = "0.3.1" +unsafe-io = "0.6.5" +system-interface = { version = "0.6.3", features = ["cap_std_impls"] } +tracing = "0.1.19" +bitflags = "1.2" +anyhow = "1" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" +posish = "0.6.1" + + +[target.'cfg(windows)'.dependencies] +winapi = "0.3" +lazy_static = "1.4" + +[dev-dependencies] +tempfile = "3.1.0" +tokio = { version = "1.5.0", features = [ "macros" ] } +anyhow = "1" +cap-tempfile = "0.13.7" diff --git a/crates/wasi-common/tokio/src/dir.rs b/crates/wasi-common/tokio/src/dir.rs new file mode 100644 index 0000000000..a70b44e182 --- /dev/null +++ b/crates/wasi-common/tokio/src/dir.rs @@ -0,0 +1,209 @@ +use crate::{block_on_dummy_executor, file::File}; +use std::any::Any; +use std::path::PathBuf; +use wasi_common::{ + dir::{ReaddirCursor, ReaddirEntity, WasiDir}, + file::{FdFlags, Filestat, OFlags, WasiFile}, + Error, ErrorExt, +}; + +pub struct Dir(wasi_cap_std_sync::dir::Dir); + +impl Dir { + pub fn from_cap_std(dir: cap_std::fs::Dir) -> Self { + Dir(wasi_cap_std_sync::dir::Dir::from_cap_std(dir)) + } +} + +#[wiggle::async_trait] +impl WasiDir for Dir { + fn as_any(&self) -> &dyn Any { + self + } + async fn open_file( + &self, + symlink_follow: bool, + path: &str, + oflags: OFlags, + read: bool, + write: bool, + fdflags: FdFlags, + ) -> Result, Error> { + let f = block_on_dummy_executor(move || async move { + self.0 + .open_file_(symlink_follow, path, oflags, read, write, fdflags) + })?; + Ok(Box::new(File::from_inner(f))) + } + + async fn open_dir(&self, symlink_follow: bool, path: &str) -> Result, Error> { + let d = + block_on_dummy_executor(move || async move { self.0.open_dir_(symlink_follow, path) })?; + Ok(Box::new(Dir(d))) + } + + async fn create_dir(&self, path: &str) -> Result<(), Error> { + block_on_dummy_executor(|| self.0.create_dir(path)) + } + async fn readdir( + &self, + cursor: ReaddirCursor, + ) -> Result> + Send>, Error> { + struct I(Box> + Send>); + impl Iterator for I { + type Item = Result; + fn next(&mut self) -> Option { + tokio::task::block_in_place(move || self.0.next()) + } + } + + let inner = block_on_dummy_executor(move || self.0.readdir(cursor))?; + Ok(Box::new(I(inner))) + } + + async fn symlink(&self, src_path: &str, dest_path: &str) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.symlink(src_path, dest_path)) + } + async fn remove_dir(&self, path: &str) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.remove_dir(path)) + } + + async fn unlink_file(&self, path: &str) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.unlink_file(path)) + } + async fn read_link(&self, path: &str) -> Result { + block_on_dummy_executor(move || self.0.read_link(path)) + } + async fn get_filestat(&self) -> Result { + block_on_dummy_executor(|| self.0.get_filestat()) + } + async fn get_path_filestat( + &self, + path: &str, + follow_symlinks: bool, + ) -> Result { + block_on_dummy_executor(move || self.0.get_path_filestat(path, follow_symlinks)) + } + async fn rename( + &self, + src_path: &str, + dest_dir: &dyn WasiDir, + dest_path: &str, + ) -> Result<(), Error> { + let dest_dir = dest_dir + .as_any() + .downcast_ref::() + .ok_or(Error::badf().context("failed downcast to tokio Dir"))?; + block_on_dummy_executor( + move || async move { self.0.rename_(src_path, &dest_dir.0, dest_path) }, + ) + } + async fn hard_link( + &self, + src_path: &str, + target_dir: &dyn WasiDir, + target_path: &str, + ) -> Result<(), Error> { + let target_dir = target_dir + .as_any() + .downcast_ref::() + .ok_or(Error::badf().context("failed downcast to tokio Dir"))?; + block_on_dummy_executor(move || async move { + self.0.hard_link_(src_path, &target_dir.0, target_path) + }) + } + async fn set_times( + &self, + path: &str, + atime: Option, + mtime: Option, + follow_symlinks: bool, + ) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.set_times(path, atime, mtime, follow_symlinks)) + } +} + +#[cfg(test)] +mod test { + use super::Dir; + #[tokio::test(flavor = "multi_thread")] + async fn scratch_dir() { + let tempdir = tempfile::Builder::new() + .prefix("cap-std-sync") + .tempdir() + .expect("create temporary dir"); + let preopen_dir = unsafe { cap_std::fs::Dir::open_ambient_dir(tempdir.path()) } + .expect("open ambient temporary dir"); + let preopen_dir = Dir::from_cap_std(preopen_dir); + wasi_common::WasiDir::open_dir(&preopen_dir, false, ".") + .await + .expect("open the same directory via WasiDir abstraction"); + } + + // Readdir does not work on windows, so we won't test it there. + #[cfg(not(windows))] + #[tokio::test(flavor = "multi_thread")] + async fn readdir() { + use std::collections::HashMap; + use wasi_common::dir::{ReaddirCursor, ReaddirEntity, WasiDir}; + use wasi_common::file::{FdFlags, FileType, OFlags}; + + async fn readdir_into_map(dir: &dyn WasiDir) -> HashMap { + let mut out = HashMap::new(); + for readdir_result in dir + .readdir(ReaddirCursor::from(0)) + .await + .expect("readdir succeeds") + { + let entity = readdir_result.expect("readdir entry is valid"); + out.insert(entity.name.clone(), entity); + } + out + } + + let tempdir = tempfile::Builder::new() + .prefix("cap-std-sync") + .tempdir() + .expect("create temporary dir"); + let preopen_dir = unsafe { cap_std::fs::Dir::open_ambient_dir(tempdir.path()) } + .expect("open ambient temporary dir"); + let preopen_dir = Dir::from_cap_std(preopen_dir); + + let entities = readdir_into_map(&preopen_dir).await; + assert_eq!( + entities.len(), + 2, + "should just be . and .. in empty dir: {:?}", + entities + ); + assert!(entities.get(".").is_some()); + assert!(entities.get("..").is_some()); + + preopen_dir + .open_file( + false, + "file1", + OFlags::CREATE, + true, + false, + FdFlags::empty(), + ) + .await + .expect("create file1"); + + let entities = readdir_into_map(&preopen_dir).await; + assert_eq!(entities.len(), 3, "should be ., .., file1 {:?}", entities); + assert_eq!( + entities.get(".").expect(". entry").filetype, + FileType::Directory + ); + assert_eq!( + entities.get("..").expect(".. entry").filetype, + FileType::Directory + ); + assert_eq!( + entities.get("file1").expect("file1 entry").filetype, + FileType::RegularFile + ); + } +} diff --git a/crates/wasi-common/tokio/src/file.rs b/crates/wasi-common/tokio/src/file.rs new file mode 100644 index 0000000000..5907aa7f53 --- /dev/null +++ b/crates/wasi-common/tokio/src/file.rs @@ -0,0 +1,186 @@ +use crate::block_on_dummy_executor; +use std::any::Any; +use std::io; +#[cfg(windows)] +use std::os::windows::io::{AsRawHandle, RawHandle}; +use wasi_common::{ + file::{Advice, FdFlags, FileType, Filestat, WasiFile}, + Error, +}; + +pub struct File(wasi_cap_std_sync::file::File); + +impl File { + pub(crate) fn from_inner(file: wasi_cap_std_sync::file::File) -> Self { + File(file) + } + pub fn from_cap_std(file: cap_std::fs::File) -> Self { + Self::from_inner(wasi_cap_std_sync::file::File::from_cap_std(file)) + } +} + +pub struct Stdin(wasi_cap_std_sync::stdio::Stdin); + +pub fn stdin() -> Stdin { + Stdin(wasi_cap_std_sync::stdio::stdin()) +} + +pub struct Stdout(wasi_cap_std_sync::stdio::Stdout); + +pub fn stdout() -> Stdout { + Stdout(wasi_cap_std_sync::stdio::stdout()) +} + +pub struct Stderr(wasi_cap_std_sync::stdio::Stderr); + +pub fn stderr() -> Stderr { + Stderr(wasi_cap_std_sync::stdio::stderr()) +} + +macro_rules! wasi_file_impl { + ($ty:ty) => { + #[wiggle::async_trait] + impl WasiFile for $ty { + fn as_any(&self) -> &dyn Any { + self + } + async fn datasync(&self) -> Result<(), Error> { + block_on_dummy_executor(|| self.0.datasync()) + } + async fn sync(&self) -> Result<(), Error> { + block_on_dummy_executor(|| self.0.sync()) + } + async fn get_filetype(&self) -> Result { + block_on_dummy_executor(|| self.0.get_filetype()) + } + async fn get_fdflags(&self) -> Result { + block_on_dummy_executor(|| self.0.get_fdflags()) + } + async fn set_fdflags(&mut self, fdflags: FdFlags) -> Result<(), Error> { + block_on_dummy_executor(|| self.0.set_fdflags(fdflags)) + } + async fn get_filestat(&self) -> Result { + block_on_dummy_executor(|| self.0.get_filestat()) + } + async fn set_filestat_size(&self, size: u64) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.set_filestat_size(size)) + } + async fn advise(&self, offset: u64, len: u64, advice: Advice) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.advise(offset, len, advice)) + } + async fn allocate(&self, offset: u64, len: u64) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.allocate(offset, len)) + } + async fn read_vectored<'a>( + &self, + bufs: &mut [io::IoSliceMut<'a>], + ) -> Result { + block_on_dummy_executor(move || self.0.read_vectored(bufs)) + } + async fn read_vectored_at<'a>( + &self, + bufs: &mut [io::IoSliceMut<'a>], + offset: u64, + ) -> Result { + block_on_dummy_executor(move || self.0.read_vectored_at(bufs, offset)) + } + async fn write_vectored<'a>(&self, bufs: &[io::IoSlice<'a>]) -> Result { + block_on_dummy_executor(move || self.0.write_vectored(bufs)) + } + async fn write_vectored_at<'a>( + &self, + bufs: &[io::IoSlice<'a>], + offset: u64, + ) -> Result { + block_on_dummy_executor(move || self.0.write_vectored_at(bufs, offset)) + } + async fn seek(&self, pos: std::io::SeekFrom) -> Result { + block_on_dummy_executor(move || self.0.seek(pos)) + } + async fn peek(&self, buf: &mut [u8]) -> Result { + block_on_dummy_executor(move || self.0.peek(buf)) + } + async fn set_times( + &self, + atime: Option, + mtime: Option, + ) -> Result<(), Error> { + block_on_dummy_executor(move || self.0.set_times(atime, mtime)) + } + async fn num_ready_bytes(&self) -> Result { + block_on_dummy_executor(|| self.0.num_ready_bytes()) + } + + #[cfg(not(windows))] + async fn readable(&mut self) -> Result<(), Error> { + // The Inner impls OwnsRaw, which asserts exclusive use of the handle by the owned object. + // AsyncFd needs to wrap an owned `impl std::os::unix::io::AsRawFd`. Rather than introduce + // mutability to let it own the `Inner`, we are depending on the `&mut self` bound on this + // async method to ensure this is the only Future which can access the RawFd during the + // lifetime of the AsyncFd. + use tokio::io::{unix::AsyncFd, Interest}; + use unsafe_io::os::posish::AsRawFd; + let rawfd = self.0.as_raw_fd(); + match AsyncFd::with_interest(rawfd, Interest::READABLE) { + Ok(asyncfd) => { + let _ = asyncfd.readable().await?; + Ok(()) + } + Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { + // if e is EPERM, this file isnt supported by epoll because it is immediately + // available for reading: + Ok(()) + } + Err(e) => Err(e.into()), + } + } + #[cfg(windows)] + async fn readable(&mut self) -> Result<(), Error> { + // Windows uses a rawfd based scheduler :( + use wasi_common::ErrorExt; + Err(Error::badf()) + } + + #[cfg(not(windows))] + async fn writable(&mut self) -> Result<(), Error> { + // The Inner impls OwnsRaw, which asserts exclusive use of the handle by the owned object. + // AsyncFd needs to wrap an owned `impl std::os::unix::io::AsRawFd`. Rather than introduce + // mutability to let it own the `Inner`, we are depending on the `&mut self` bound on this + // async method to ensure this is the only Future which can access the RawFd during the + // lifetime of the AsyncFd. + use tokio::io::{unix::AsyncFd, Interest}; + use unsafe_io::os::posish::AsRawFd; + let rawfd = self.0.as_raw_fd(); + match AsyncFd::with_interest(rawfd, Interest::WRITABLE) { + Ok(asyncfd) => { + let _ = asyncfd.writable().await?; + Ok(()) + } + Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { + // if e is EPERM, this file isnt supported by epoll because it is immediately + // available for writing: + Ok(()) + } + Err(e) => Err(e.into()), + } + } + #[cfg(windows)] + async fn writable(&mut self) -> Result<(), Error> { + // Windows uses a rawfd based scheduler :( + use wasi_common::ErrorExt; + Err(Error::badf()) + } + } + #[cfg(windows)] + impl AsRawHandle for $ty { + fn as_raw_handle(&self) -> RawHandle { + self.0.as_raw_handle() + } + } + }; +} + +wasi_file_impl!(File); +wasi_file_impl!(Stdin); +wasi_file_impl!(Stdout); +wasi_file_impl!(Stderr); diff --git a/crates/wasi-common/tokio/src/lib.rs b/crates/wasi-common/tokio/src/lib.rs new file mode 100644 index 0000000000..e7dc7f42e6 --- /dev/null +++ b/crates/wasi-common/tokio/src/lib.rs @@ -0,0 +1,114 @@ +mod dir; +mod file; +pub mod sched; +pub mod stdio; + +use std::cell::RefCell; +use std::future::Future; +use std::path::Path; +use std::rc::Rc; +pub use wasi_cap_std_sync::{clocks_ctx, random_ctx}; +use wasi_common::{Error, Table, WasiCtx}; + +pub use dir::Dir; +pub use file::File; + +use crate::sched::sched_ctx; + +pub struct WasiCtxBuilder(wasi_common::WasiCtxBuilder); + +impl WasiCtxBuilder { + pub fn new() -> Self { + WasiCtxBuilder(WasiCtx::builder( + random_ctx(), + clocks_ctx(), + sched_ctx(), + Rc::new(RefCell::new(Table::new())), + )) + } + pub fn env(self, var: &str, value: &str) -> Result { + let s = self.0.env(var, value)?; + Ok(WasiCtxBuilder(s)) + } + pub fn envs(self, env: &[(String, String)]) -> Result { + let mut s = self; + for (k, v) in env { + s = s.env(k, v)?; + } + Ok(s) + } + pub fn inherit_env(self) -> Result { + let mut s = self.0; + for (key, value) in std::env::vars() { + s = s.env(&key, &value)?; + } + Ok(WasiCtxBuilder(s)) + } + pub fn arg(self, arg: &str) -> Result { + let s = self.0.arg(arg)?; + Ok(WasiCtxBuilder(s)) + } + pub fn args(self, arg: &[String]) -> Result { + let mut s = self; + for a in arg { + s = s.arg(&a)?; + } + Ok(s) + } + pub fn inherit_args(self) -> Result { + let mut s = self.0; + for arg in std::env::args() { + s = s.arg(&arg)?; + } + Ok(WasiCtxBuilder(s)) + } + pub fn stdin(self, f: Box) -> Self { + WasiCtxBuilder(self.0.stdin(f)) + } + pub fn stdout(self, f: Box) -> Self { + WasiCtxBuilder(self.0.stdout(f)) + } + pub fn stderr(self, f: Box) -> Self { + WasiCtxBuilder(self.0.stderr(f)) + } + pub fn inherit_stdin(self) -> Self { + self.stdin(Box::new(crate::stdio::stdin())) + } + pub fn inherit_stdout(self) -> Self { + self.stdout(Box::new(crate::stdio::stdout())) + } + pub fn inherit_stderr(self) -> Self { + self.stderr(Box::new(crate::stdio::stderr())) + } + pub fn inherit_stdio(self) -> Self { + self.inherit_stdin().inherit_stdout().inherit_stderr() + } + pub fn preopened_dir( + self, + dir: cap_std::fs::Dir, + guest_path: impl AsRef, + ) -> Result { + let dir = Box::new(Dir::from_cap_std(dir)); + Ok(WasiCtxBuilder(self.0.preopened_dir(dir, guest_path)?)) + } + pub fn build(self) -> Result { + self.0.build() + } +} + +// Much of this crate is implemented in terms of `async` methods from the +// wasi-cap-std-sync crate. These methods may be async in signature, however, +// they are synchronous in implementation (always Poll::Ready on first poll) +// and perform blocking syscalls. +// +// This function takes this blocking code and executes it using a dummy executor +// to assert its immediate readiness. We tell tokio this is a blocking operation +// with the block_in_place function. +pub(crate) fn block_on_dummy_executor<'a, F, Fut, T>(f: F) -> Result +where + F: FnOnce() -> Fut + Send + 'a, + Fut: Future>, + T: Send + 'static, +{ + tokio::task::block_in_place(move || wiggle::run_in_dummy_executor(f())) +} diff --git a/crates/wasi-common/tokio/src/sched.rs b/crates/wasi-common/tokio/src/sched.rs new file mode 100644 index 0000000000..1b97166641 --- /dev/null +++ b/crates/wasi-common/tokio/src/sched.rs @@ -0,0 +1,35 @@ +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::poll_oneoff; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use windows::poll_oneoff; + +use wasi_common::{ + sched::{Duration, Poll, WasiSched}, + Error, +}; + +pub fn sched_ctx() -> Box { + struct AsyncSched; + + #[wiggle::async_trait] + impl WasiSched for AsyncSched { + async fn poll_oneoff<'a>(&self, poll: &mut Poll<'a>) -> Result<(), Error> { + poll_oneoff(poll).await + } + async fn sched_yield(&self) -> Result<(), Error> { + tokio::task::yield_now().await; + Ok(()) + } + async fn sleep(&self, duration: Duration) -> Result<(), Error> { + tokio::time::sleep(duration).await; + Ok(()) + } + } + + Box::new(AsyncSched) +} diff --git a/crates/wasi-common/tokio/src/sched/unix.rs b/crates/wasi-common/tokio/src/sched/unix.rs new file mode 100644 index 0000000000..35e0234f77 --- /dev/null +++ b/crates/wasi-common/tokio/src/sched/unix.rs @@ -0,0 +1,91 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll as FPoll}; +use wasi_common::{ + sched::{ + subscription::{RwEventFlags, Subscription}, + Poll, + }, + Context as _, Error, +}; + +struct FirstReady<'a, T>(Vec + 'a>>>); + +impl<'a, T> FirstReady<'a, T> { + fn new() -> Self { + FirstReady(Vec::new()) + } + fn push(&mut self, f: impl Future + 'a) { + self.0.push(Box::pin(f)); + } +} + +impl<'a, T> Future for FirstReady<'a, T> { + type Output = T; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> FPoll { + let mut result = FPoll::Pending; + for f in self.as_mut().0.iter_mut() { + match f.as_mut().poll(cx) { + FPoll::Ready(r) => match result { + // First ready gets to set the result. But, continue the loop so all futures + // which are ready simultaneously (often on first poll) get to report their + // readiness. + FPoll::Pending => { + result = FPoll::Ready(r); + } + _ => {} + }, + _ => continue, + } + } + return result; + } +} + +pub async fn poll_oneoff<'a>(poll: &mut Poll<'a>) -> Result<(), Error> { + if poll.is_empty() { + return Ok(()); + } + + let duration = poll + .earliest_clock_deadline() + .map(|sub| sub.duration_until()); + + let mut futures = FirstReady::new(); + for s in poll.rw_subscriptions() { + match s { + Subscription::Read(f) => { + futures.push(async move { + f.file.readable().await.context("readable future")?; + f.complete( + f.file + .num_ready_bytes() + .await + .context("read num_ready_bytes")?, + RwEventFlags::empty(), + ); + Ok::<(), Error>(()) + }); + } + + Subscription::Write(f) => { + futures.push(async move { + f.file.writable().await.context("writable future")?; + f.complete(0, RwEventFlags::empty()); + Ok(()) + }); + } + Subscription::MonotonicClock { .. } => unreachable!(), + } + } + if let Some(Some(remaining_duration)) = duration { + match tokio::time::timeout(remaining_duration, futures).await { + Ok(r) => r?, + Err(_deadline_elapsed) => {} + } + } else { + futures.await?; + } + + Ok(()) +} diff --git a/crates/wasi-common/tokio/src/sched/windows.rs b/crates/wasi-common/tokio/src/sched/windows.rs new file mode 100644 index 0000000000..90b33aeb47 --- /dev/null +++ b/crates/wasi-common/tokio/src/sched/windows.rs @@ -0,0 +1,47 @@ +use crate::block_on_dummy_executor; +use std::os::windows::io::{AsRawHandle, RawHandle}; +use wasi_cap_std_sync::sched::windows::poll_oneoff_; +use wasi_common::{file::WasiFile, sched::Poll, Error}; + +pub async fn poll_oneoff<'a>(poll: &mut Poll<'a>) -> Result<(), Error> { + // Tokio doesn't provide us the AsyncFd primitive on Windows, so instead + // we use the blocking poll_oneoff implementation from the wasi-cap-std-crate. + // We provide a function specific to this crate's WasiFile types for downcasting + // to a RawHandle. + block_on_dummy_executor(move || poll_oneoff_(poll, wasi_file_is_stdin, wasi_file_raw_handle)) +} + +pub fn wasi_file_is_stdin(f: &dyn WasiFile) -> bool { + f.as_any().is::() +} + +fn wasi_file_raw_handle(f: &dyn WasiFile) -> Option { + let a = f.as_any(); + if a.is::() { + Some( + a.downcast_ref::() + .unwrap() + .as_raw_handle(), + ) + } else if a.is::() { + Some( + a.downcast_ref::() + .unwrap() + .as_raw_handle(), + ) + } else if a.is::() { + Some( + a.downcast_ref::() + .unwrap() + .as_raw_handle(), + ) + } else if a.is::() { + Some( + a.downcast_ref::() + .unwrap() + .as_raw_handle(), + ) + } else { + None + } +} diff --git a/crates/wasi-common/tokio/src/stdio.rs b/crates/wasi-common/tokio/src/stdio.rs new file mode 100644 index 0000000000..dd23c0555a --- /dev/null +++ b/crates/wasi-common/tokio/src/stdio.rs @@ -0,0 +1 @@ +pub use crate::file::{stderr, stdin, stdout, Stderr, Stdin, Stdout}; diff --git a/crates/wasi-common/tokio/tests/poll_oneoff.rs b/crates/wasi-common/tokio/tests/poll_oneoff.rs new file mode 100644 index 0000000000..fa04f4604e --- /dev/null +++ b/crates/wasi-common/tokio/tests/poll_oneoff.rs @@ -0,0 +1,158 @@ +use anyhow::{Context, Error}; +use cap_std::time::Duration; +use std::collections::HashMap; +use wasi_common::{ + file::{FdFlags, OFlags}, + sched::{Poll, RwEventFlags, SubscriptionResult, Userdata}, + WasiDir, WasiFile, +}; +use wasi_tokio::{clocks_ctx, sched::poll_oneoff, Dir}; + +#[tokio::test(flavor = "multi_thread")] +async fn empty_file_readable() -> Result<(), Error> { + let clocks = clocks_ctx(); + + let workspace = unsafe { cap_tempfile::tempdir().expect("create tempdir") }; + workspace.create_dir("d").context("create dir")?; + let d = workspace.open_dir("d").context("open dir")?; + let d = Dir::from_cap_std(d); + + let f = d + .open_file(false, "f", OFlags::CREATE, false, true, FdFlags::empty()) + .await + .context("create writable file f")?; + let to_write: Vec = vec![0]; + f.write_vectored(&vec![std::io::IoSlice::new(&to_write)]) + .await + .context("write to f")?; + drop(f); + + let mut f = d + .open_file(false, "f", OFlags::empty(), true, false, FdFlags::empty()) + .await + .context("open f as readable")?; + + let mut poll = Poll::new(); + poll.subscribe_read(&mut *f, Userdata::from(123)); + // Timeout bounds time in poll_oneoff + poll.subscribe_monotonic_clock( + &*clocks.monotonic, + clocks + .monotonic + .now(clocks.monotonic.resolution()) + .checked_add(Duration::from_millis(10)) + .unwrap(), + clocks.monotonic.resolution(), + Userdata::from(0), + ); + poll_oneoff(&mut poll).await?; + + let events = poll.results(); + + match events.get(0).expect("at least one event") { + (SubscriptionResult::Read(Ok((1, flags))), ud) => { + assert_eq!(*flags, RwEventFlags::empty()); + assert_eq!(*ud, Userdata::from(123)); + } + _ => panic!("expected (Read(Ok(1, empty), 123), got: {:?}", events[0]), + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn empty_file_writable() -> Result<(), Error> { + let clocks = clocks_ctx(); + + let workspace = unsafe { cap_tempfile::tempdir().expect("create tempdir") }; + workspace.create_dir("d").context("create dir")?; + let d = workspace.open_dir("d").context("open dir")?; + let d = Dir::from_cap_std(d); + + let mut writable_f = d + .open_file(false, "f", OFlags::CREATE, true, true, FdFlags::empty()) + .await + .context("create writable file")?; + + let mut poll = Poll::new(); + poll.subscribe_write(&mut *writable_f, Userdata::from(123)); + // Timeout bounds time in poll_oneoff + poll.subscribe_monotonic_clock( + &*clocks.monotonic, + clocks + .monotonic + .now(clocks.monotonic.resolution()) + .checked_add(Duration::from_millis(10)) + .unwrap(), + clocks.monotonic.resolution(), + Userdata::from(0), + ); + poll_oneoff(&mut poll).await?; + + let events = poll.results(); + + match events.get(0).expect("at least one event") { + (SubscriptionResult::Write(Ok((0, flags))), ud) => { + assert_eq!(*flags, RwEventFlags::empty()); + assert_eq!(*ud, Userdata::from(123)); + } + _ => panic!(""), + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn stdio_readable() -> Result<(), Error> { + let clocks = clocks_ctx(); + + let deadline = clocks + .monotonic + .now(clocks.monotonic.resolution()) + .checked_add(Duration::from_millis(10)) + .unwrap(); + + let mut waiting_on: HashMap> = vec![ + ( + 1, + Box::new(wasi_tokio::stdio::stdout()) as Box, + ), + (2, Box::new(wasi_tokio::stdio::stderr())), + ] + .into_iter() + .collect(); + + while !waiting_on.is_empty() { + let mut poll = Poll::new(); + + for (ix, file) in waiting_on.iter_mut() { + poll.subscribe_write(&mut **file, Userdata::from(*ix)); + } + // Timeout bounds time in poll_oneoff + poll.subscribe_monotonic_clock( + &*clocks.monotonic, + deadline, + clocks.monotonic.resolution(), + Userdata::from(999), + ); + poll_oneoff(&mut poll).await?; + let events = poll.results(); + + for e in events { + match e { + (SubscriptionResult::Write(Ok(_)), ud) => { + let _ = waiting_on.remove(&u64::from(ud)); + } + (SubscriptionResult::Write(Err(_)), ud) => { + panic!("error on ix {}", u64::from(ud)) + } + (SubscriptionResult::Read { .. }, _) => unreachable!(), + (SubscriptionResult::MonotonicClock { .. }, _) => { + panic!("timed out before stdin and stdout ready for reading") + } + } + } + } + + Ok(()) +} diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 9951e446a6..3fa9dad245 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -15,6 +15,7 @@ build = "build.rs" [dependencies] wasi-common = { path = "../wasi-common", version = "0.26.0" } wasi-cap-std-sync = { path = "../wasi-common/cap-std-sync", version = "0.26.0", optional = true } +wasi-tokio = { path = "../wasi-common/tokio", version = "0.26.0", optional = true } wiggle = { path = "../wiggle", default-features = false, version = "0.26.0" } wasmtime-wiggle = { path = "../wiggle/wasmtime", default-features = false, version = "0.26.0" } wasmtime = { path = "../wasmtime", default-features = false, version = "0.26.0" } @@ -23,3 +24,4 @@ anyhow = "1.0" [features] default = ["sync"] sync = ["wasi-cap-std-sync"] +tokio = ["wasi-tokio", "wasmtime/async", "wasmtime-wiggle/async"] diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index 2b71d7ab37..3edaa34505 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -7,10 +7,7 @@ //! Individual snapshots are available through //! `wasmtime_wasi::snapshots::preview_{0, 1}::Wasi::new(&Store, Rc>)`. -use std::cell::RefCell; -use std::rc::Rc; pub use wasi_common::{Error, WasiCtx, WasiCtxBuilder, WasiDir, WasiFile}; -use wasmtime::{Config, Linker, Store}; /// Re-export the commonly used wasi-cap-std-sync crate here. This saves /// consumers of this library from having to keep additional dependencies @@ -18,8 +15,36 @@ use wasmtime::{Config, Linker, Store}; #[cfg(feature = "sync")] pub mod sync { pub use wasi_cap_std_sync::*; + super::define_wasi!(block_on); } +/// Sync mode is the "default" of this crate, so we also export it at the top +/// level. +#[cfg(feature = "sync")] +pub use sync::*; + +/// Re-export the wasi-tokio crate here. This saves consumers of this library from having +/// to keep additional dependencies in sync. +#[cfg(feature = "tokio")] +pub mod tokio { + pub use wasi_tokio::*; + super::define_wasi!(async); +} + +// The only difference between these definitions for sync vs async is whether +// the wasmtime::Funcs generated are async (& therefore need an async Store and an executor to run) +// or whether they have an internal "dummy executor" that expects the implementation of all +// the async funcs to poll to Ready immediately. +#[doc(hidden)] +#[macro_export] +macro_rules! define_wasi { + ($async_mode: tt) => { + +use std::cell::RefCell; +use std::rc::Rc; +use wasmtime::{Config, Linker, Store}; +use wasi_common::WasiCtx; + /// An instantiated instance of all available wasi exports. Presently includes /// both the "preview1" snapshot and the "unstable" (preview0) snapshot. pub struct Wasi { @@ -79,6 +104,7 @@ necessary. Additionally [`Wasi::get_export`] can be used to do name-based resolution.", }, }, + $async_mode: * }); } pub mod preview_0 { @@ -106,6 +132,9 @@ necessary. Additionally [`Wasi::get_export`] can be used to do name-based resolution.", }, }, + $async_mode: * }); } } +} +} diff --git a/crates/wiggle/generate/src/config.rs b/crates/wiggle/generate/src/config.rs index a430a8547a..bed0e5c834 100644 --- a/crates/wiggle/generate/src/config.rs +++ b/crates/wiggle/generate/src/config.rs @@ -47,7 +47,9 @@ impl Parse for ConfigField { } else if lookahead.peek(Token![async]) { input.parse::()?; input.parse::()?; - Ok(ConfigField::Async(input.parse()?)) + Ok(ConfigField::Async(AsyncConf { + functions: input.parse()?, + })) } else { Err(lookahead.error()) } @@ -280,40 +282,64 @@ impl Parse for ErrorConfField { } #[derive(Clone, Default, Debug)] -/// Modules and funcs that should be async -pub struct AsyncConf(HashMap>); +/// Modules and funcs that have async signatures +pub struct AsyncConf { + functions: AsyncFunctions, +} -impl AsyncConf { - pub fn is_async(&self, module: &str, function: &str) -> bool { - self.0 - .get(module) - .and_then(|fs| fs.iter().find(|f| *f == function)) - .is_some() +#[derive(Clone, Debug)] +pub enum AsyncFunctions { + Some(HashMap>), + All, +} +impl Default for AsyncFunctions { + fn default() -> Self { + AsyncFunctions::Some(HashMap::default()) } } -impl Parse for AsyncConf { +impl AsyncConf { + pub fn is_async(&self, module: &str, function: &str) -> bool { + match &self.functions { + AsyncFunctions::Some(fs) => fs + .get(module) + .and_then(|fs| fs.iter().find(|f| *f == function)) + .is_some(), + AsyncFunctions::All => true, + } + } +} + +impl Parse for AsyncFunctions { fn parse(input: ParseStream) -> Result { let content; - let _ = braced!(content in input); - let items: Punctuated = - content.parse_terminated(Parse::parse)?; - let mut m: HashMap> = HashMap::new(); - use std::collections::hash_map::Entry; - for i in items { - let function_names = i - .function_names - .iter() - .map(|i| i.to_string()) - .collect::>(); - match m.entry(i.module_name.to_string()) { - Entry::Occupied(o) => o.into_mut().extend(function_names), - Entry::Vacant(v) => { - v.insert(function_names); + let lookahead = input.lookahead1(); + if lookahead.peek(syn::token::Brace) { + let _ = braced!(content in input); + let items: Punctuated = + content.parse_terminated(Parse::parse)?; + let mut functions: HashMap> = HashMap::new(); + use std::collections::hash_map::Entry; + for i in items { + let function_names = i + .function_names + .iter() + .map(|i| i.to_string()) + .collect::>(); + match functions.entry(i.module_name.to_string()) { + Entry::Occupied(o) => o.into_mut().extend(function_names), + Entry::Vacant(v) => { + v.insert(function_names); + } } } + Ok(AsyncFunctions::Some(functions)) + } else if lookahead.peek(Token![*]) { + let _: Token![*] = input.parse().unwrap(); + Ok(AsyncFunctions::All) + } else { + Err(lookahead.error()) } - Ok(AsyncConf(m)) } } diff --git a/crates/wiggle/src/lib.rs b/crates/wiggle/src/lib.rs index 7bcba7c065..f05a65cf5e 100644 --- a/crates/wiggle/src/lib.rs +++ b/crates/wiggle/src/lib.rs @@ -953,3 +953,40 @@ impl From for Trap { Trap::String(err.to_string()) } } + +pub fn run_in_dummy_executor(future: F) -> F::Output { + use std::pin::Pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + + let mut f = Pin::from(Box::new(future)); + let waker = dummy_waker(); + let mut cx = Context::from_waker(&waker); + match f.as_mut().poll(&mut cx) { + Poll::Ready(val) => return val, + Poll::Pending => { + panic!("Cannot wait on pending future: must enable wiggle \"async\" future and execute on an async Store") + } + } + + fn dummy_waker() -> Waker { + return unsafe { Waker::from_raw(clone(5 as *const _)) }; + + unsafe fn clone(ptr: *const ()) -> RawWaker { + assert_eq!(ptr as usize, 5); + const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + RawWaker::new(ptr, &VTABLE) + } + + unsafe fn wake(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + + unsafe fn wake_by_ref(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + + unsafe fn drop(ptr: *const ()) { + assert_eq!(ptr as usize, 5); + } + } +} diff --git a/crates/wiggle/tests/atoms_async.rs b/crates/wiggle/tests/atoms_async.rs index 0057c9203a..7d41d3a273 100644 --- a/crates/wiggle/tests/atoms_async.rs +++ b/crates/wiggle/tests/atoms_async.rs @@ -7,9 +7,7 @@ use wiggle_test::{impl_errno, HostMemory, MemArea, WasiCtx}; wiggle::from_witx!({ witx: ["$CARGO_MANIFEST_DIR/tests/atoms.witx"], - async: { - atoms::{int_float_args, double_int_return_float} - } + async: *, }); impl_errno!(types::Errno); diff --git a/crates/wiggle/wasmtime/Cargo.toml b/crates/wiggle/wasmtime/Cargo.toml index 06f17700ed..f51ea119bb 100644 --- a/crates/wiggle/wasmtime/Cargo.toml +++ b/crates/wiggle/wasmtime/Cargo.toml @@ -26,6 +26,11 @@ name = "atoms_async" path = "tests/atoms_async.rs" required-features = ["async", "wasmtime/wat"] +[[test]] +name = "atoms_sync" +path = "tests/atoms_sync.rs" +required-features = ["wasmtime/wat"] + [badges] maintenance = { status = "actively-developed" } diff --git a/crates/wiggle/wasmtime/macro/src/config.rs b/crates/wiggle/wasmtime/macro/src/config.rs index 76a23dd31f..30815817b2 100644 --- a/crates/wiggle/wasmtime/macro/src/config.rs +++ b/crates/wiggle/wasmtime/macro/src/config.rs @@ -1,4 +1,4 @@ -pub use wiggle_generate::config::AsyncConf; +use wiggle_generate::config::AsyncFunctions; use { proc_macro2::Span, std::collections::HashMap, @@ -16,7 +16,6 @@ pub struct Config { pub witx: WitxConf, pub ctx: CtxConf, pub modules: ModulesConf, - #[cfg(feature = "async")] pub async_: AsyncConf, } @@ -26,7 +25,6 @@ pub enum ConfigField { Witx(WitxConf), Ctx(CtxConf), Modules(ModulesConf), - #[cfg(feature = "async")] Async(AsyncConf), } @@ -39,6 +37,7 @@ mod kw { syn::custom_keyword!(name); syn::custom_keyword!(docs); syn::custom_keyword!(function_override); + syn::custom_keyword!(block_on); } impl Parse for ConfigField { @@ -67,17 +66,17 @@ impl Parse for ConfigField { } else if lookahead.peek(Token![async]) { input.parse::()?; input.parse::()?; - #[cfg(feature = "async")] - { - Ok(ConfigField::Async(input.parse()?)) - } - #[cfg(not(feature = "async"))] - { - Err(syn::Error::new( - input.span(), - "async not supported, enable cargo feature \"async\"", - )) - } + Ok(ConfigField::Async(AsyncConf { + blocking: false, + functions: input.parse()?, + })) + } else if lookahead.peek(kw::block_on) { + input.parse::()?; + input.parse::()?; + Ok(ConfigField::Async(AsyncConf { + blocking: true, + functions: input.parse()?, + })) } else { Err(lookahead.error()) } @@ -90,7 +89,6 @@ impl Config { let mut witx = None; let mut ctx = None; let mut modules = None; - #[cfg(feature = "async")] let mut async_ = None; for f in fields { match f { @@ -118,7 +116,6 @@ impl Config { } modules = Some(c); } - #[cfg(feature = "async")] ConfigField::Async(c) => { if async_.is_some() { return Err(Error::new(err_loc, "duplicate `async` field")); @@ -132,7 +129,6 @@ impl Config { witx: witx.ok_or_else(|| Error::new(err_loc, "`witx` field required"))?, ctx: ctx.ok_or_else(|| Error::new(err_loc, "`ctx` field required"))?, modules: modules.ok_or_else(|| Error::new(err_loc, "`modules` field required"))?, - #[cfg(feature = "async")] async_: async_.unwrap_or_default(), }) } @@ -276,3 +272,53 @@ impl Parse for ModulesConf { }) } } + +#[derive(Clone, Default, Debug)] +/// Modules and funcs that have async signatures +pub struct AsyncConf { + blocking: bool, + functions: AsyncFunctions, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Asyncness { + /// Wiggle function is synchronous, wasmtime Func is synchronous + Sync, + /// Wiggle function is asynchronous, but wasmtime Func is synchronous + Blocking, + /// Wiggle function and wasmtime Func are asynchronous. + Async, +} + +impl Asyncness { + pub fn is_sync(&self) -> bool { + match self { + Asyncness::Sync => true, + _ => false, + } + } +} + +impl AsyncConf { + pub fn is_async(&self, module: &str, function: &str) -> Asyncness { + let a = if self.blocking { + Asyncness::Blocking + } else { + Asyncness::Async + }; + match &self.functions { + AsyncFunctions::Some(fs) => { + if fs + .get(module) + .and_then(|fs| fs.iter().find(|f| *f == function)) + .is_some() + { + a + } else { + Asyncness::Sync + } + } + AsyncFunctions::All => a, + } + } +} diff --git a/crates/wiggle/wasmtime/macro/src/lib.rs b/crates/wiggle/wasmtime/macro/src/lib.rs index 24732c7f2e..01f8a8db48 100644 --- a/crates/wiggle/wasmtime/macro/src/lib.rs +++ b/crates/wiggle/wasmtime/macro/src/lib.rs @@ -6,7 +6,7 @@ use wiggle_generate::Names; mod config; -use config::{AsyncConf, ModuleConf, TargetConf}; +use config::{AsyncConf, Asyncness, ModuleConf, TargetConf}; /// Define the structs required to integrate a Wiggle implementation with Wasmtime. /// @@ -48,11 +48,6 @@ pub fn wasmtime_integration(args: TokenStream) -> TokenStream { let doc = config.load_document(); let names = Names::new(quote!(wasmtime_wiggle)); - #[cfg(feature = "async")] - let async_config = config.async_.clone(); - #[cfg(not(feature = "async"))] - let async_config = AsyncConf::default(); - let modules = config.modules.iter().map(|(name, module_conf)| { let module = doc .module(&witx::Id::new(name)) @@ -63,7 +58,7 @@ pub fn wasmtime_integration(args: TokenStream) -> TokenStream { &names, &config.target, &config.ctx.name, - &async_config, + &config.async_, ) }); quote!( #(#modules)* ).into() @@ -107,13 +102,24 @@ fn generate_module( let mut host_funcs = Vec::new(); for f in module.funcs() { + let asyncness = async_conf.is_async(module.name.as_str(), f.name.as_str()); + match asyncness { + Asyncness::Blocking => {} + Asyncness::Async => { + assert!( + cfg!(feature = "async"), + "generating async wasmtime Funcs requires cargo feature \"async\"" + ); + } + _ => {} + } generate_func( &module_id, &f, names, &target_module, ctx_type, - async_conf.is_async(module.name.as_str(), f.name.as_str()), + asyncness, &mut fns, &mut ctor_externs, &mut host_funcs, @@ -229,11 +235,12 @@ fn generate_func( names: &Names, target_module: &TokenStream2, ctx_type: &syn::Type, - is_async: bool, + asyncness: Asyncness, fns: &mut Vec, ctors: &mut Vec, host_funcs: &mut Vec<(witx::Id, TokenStream2)>, ) { + let rt = names.runtime_mod(); let name_ident = names.func(&func.name); let (params, results) = func.wasm_signature(); @@ -257,8 +264,16 @@ fn generate_func( _ => unimplemented!(), }; - let async_ = if is_async { quote!(async) } else { quote!() }; - let await_ = if is_async { quote!(.await) } else { quote!() }; + let async_ = if asyncness.is_sync() { + quote!() + } else { + quote!(async) + }; + let await_ = if asyncness.is_sync() { + quote!() + } else { + quote!(.await) + }; let runtime = names.runtime_mod(); let fn_ident = format_ident!("{}_{}", module_ident, name_ident); @@ -282,9 +297,10 @@ fn generate_func( } }); - if is_async { - let wrapper = format_ident!("wrap{}_async", params.len()); - ctors.push(quote! { + match asyncness { + Asyncness::Async => { + let wrapper = format_ident!("wrap{}_async", params.len()); + ctors.push(quote! { let #name_ident = wasmtime::Func::#wrapper( store, ctx.clone(), @@ -293,9 +309,24 @@ fn generate_func( Box::new(async move { Self::#fn_ident(&caller, &mut my_ctx.borrow_mut() #(, #arg_names)*).await }) } ); - }); - } else { - ctors.push(quote! { + }); + } + Asyncness::Blocking => { + // Emit a synchronous function. Self::#fn_ident returns a Future, so we need to + // use a dummy executor to let any synchronous code inside there execute correctly. If + // the future ends up Pending, this func will Trap. + ctors.push(quote! { + let my_ctx = ctx.clone(); + let #name_ident = wasmtime::Func::wrap( + store, + move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> { + #rt::run_in_dummy_executor(Self::#fn_ident(&caller, &mut my_ctx.borrow_mut() #(, #arg_names)*)) + } + ); + }); + } + Asyncness::Sync => { + ctors.push(quote! { let my_ctx = ctx.clone(); let #name_ident = wasmtime::Func::wrap( store, @@ -304,40 +335,62 @@ fn generate_func( } ); }); + } } - let host_wrapper = if is_async { - let wrapper = format_ident!("wrap{}_host_func_async", params.len()); - quote! { - config.#wrapper( - module, - field, - move |caller #(,#arg_decls)*| - -> Box>> { - Box::new(async move { - let ctx = caller.store() + let host_wrapper = match asyncness { + Asyncness::Async => { + let wrapper = format_ident!("wrap{}_host_func_async", params.len()); + quote! { + config.#wrapper( + module, + field, + move |caller #(,#arg_decls)*| + -> Box>> { + Box::new(async move { + let ctx = caller.store() + .get::>>() + .ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?; + let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*).await; + result + }) + } + ); + } + } + + Asyncness::Blocking => { + // Emit a synchronous host function. Self::#fn_ident returns a Future, so we need to + // use a dummy executor to let any synchronous code inside there execute correctly. If + // the future ends up Pending, this func will Trap. + quote! { + config.wrap_host_func( + module, + field, + move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> { + let ctx = caller + .store() .get::>>() .ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?; - let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*).await; - result - }) - } - ); + #rt::run_in_dummy_executor(Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*)) + }, + ); + } } - } else { - quote! { - config.wrap_host_func( - module, - field, - move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> { - let ctx = caller - .store() - .get::>>() - .ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?; - let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*); - result - }, - ); + Asyncness::Sync => { + quote! { + config.wrap_host_func( + module, + field, + move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> { + let ctx = caller + .store() + .get::>>() + .ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?; + Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*) + }, + ); + } } }; host_funcs.push((func.name.clone(), host_wrapper)); diff --git a/crates/wiggle/wasmtime/tests/atoms_sync.rs b/crates/wiggle/wasmtime/tests/atoms_sync.rs new file mode 100644 index 0000000000..eee48f5338 --- /dev/null +++ b/crates/wiggle/wasmtime/tests/atoms_sync.rs @@ -0,0 +1,176 @@ +use std::cell::RefCell; +use std::rc::Rc; + +wasmtime_wiggle::from_witx!({ + witx: ["$CARGO_MANIFEST_DIR/tests/atoms.witx"], + async: { + atoms::{double_int_return_float} + } +}); + +wasmtime_wiggle::wasmtime_integration!({ + target: crate, + witx: ["$CARGO_MANIFEST_DIR/tests/atoms.witx"], + ctx: Ctx, + modules: { atoms => { name: Atoms } }, + block_on: { + atoms::double_int_return_float + } +}); + +pub struct Ctx; +impl wiggle::GuestErrorType for types::Errno { + fn success() -> Self { + types::Errno::Ok + } +} + +#[wasmtime_wiggle::async_trait] +impl atoms::Atoms for Ctx { + fn int_float_args(&self, an_int: u32, an_float: f32) -> Result<(), types::Errno> { + println!("INT FLOAT ARGS: {} {}", an_int, an_float); + Ok(()) + } + async fn double_int_return_float( + &self, + an_int: u32, + ) -> Result { + Ok((an_int as f32) * 2.0) + } +} + +fn run_int_float_args(linker: &wasmtime::Linker) { + let shim_mod = shim_module(linker.store()); + let shim_inst = linker.instantiate(&shim_mod).unwrap(); + + let results = shim_inst + .get_func("int_float_args_shim") + .unwrap() + .call(&[0i32.into(), 123.45f32.into()]) + .unwrap(); + + assert_eq!(results.len(), 1, "one return value"); + assert_eq!( + results[0].unwrap_i32(), + types::Errno::Ok as i32, + "int_float_args errno" + ); +} + +fn run_double_int_return_float(linker: &wasmtime::Linker) { + let shim_mod = shim_module(linker.store()); + let shim_inst = linker.instantiate(&shim_mod).unwrap(); + + let input: i32 = 123; + let result_location: i32 = 0; + + let results = shim_inst + .get_func("double_int_return_float_shim") + .unwrap() + .call(&[input.into(), result_location.into()]) + .unwrap(); + + assert_eq!(results.len(), 1, "one return value"); + assert_eq!( + results[0].unwrap_i32(), + types::Errno::Ok as i32, + "double_int_return_float errno" + ); + + // The actual result is in memory: + let mem = shim_inst.get_memory("memory").unwrap(); + let mut result_bytes: [u8; 4] = [0, 0, 0, 0]; + mem.read(result_location as usize, &mut result_bytes) + .unwrap(); + let result = f32::from_le_bytes(result_bytes); + assert_eq!((input * 2) as f32, result); +} + +#[test] +fn test_sync_host_func() { + let store = store(); + + let ctx = Rc::new(RefCell::new(Ctx)); + let atoms = Atoms::new(&store, ctx.clone()); + + let mut linker = wasmtime::Linker::new(&store); + atoms.add_to_linker(&mut linker).unwrap(); + + run_int_float_args(&linker); +} + +#[test] +fn test_async_host_func() { + let store = store(); + + let ctx = Rc::new(RefCell::new(Ctx)); + let atoms = Atoms::new(&store, ctx.clone()); + + let mut linker = wasmtime::Linker::new(&store); + atoms.add_to_linker(&mut linker).unwrap(); + + run_double_int_return_float(&linker); +} + +#[test] +fn test_sync_config_host_func() { + let mut config = wasmtime::Config::new(); + Atoms::add_to_config(&mut config); + + let engine = wasmtime::Engine::new(&config).unwrap(); + let store = wasmtime::Store::new(&engine); + + assert!(Atoms::set_context(&store, Ctx).is_ok()); + + let linker = wasmtime::Linker::new(&store); + run_int_float_args(&linker); +} + +#[test] +fn test_async_config_host_func() { + let mut config = wasmtime::Config::new(); + Atoms::add_to_config(&mut config); + + let engine = wasmtime::Engine::new(&config).unwrap(); + let store = wasmtime::Store::new(&engine); + + assert!(Atoms::set_context(&store, Ctx).is_ok()); + + let linker = wasmtime::Linker::new(&store); + run_double_int_return_float(&linker); +} + +fn store() -> wasmtime::Store { + wasmtime::Store::new(&wasmtime::Engine::new(&wasmtime::Config::new()).unwrap()) +} + +// Wiggle expects the caller to have an exported memory. Wasmtime can only +// provide this if the caller is a WebAssembly module, so we need to write +// a shim module: +fn shim_module(store: &wasmtime::Store) -> wasmtime::Module { + wasmtime::Module::new( + store.engine(), + r#" + (module + (memory 1) + (export "memory" (memory 0)) + (import "atoms" "int_float_args" (func $int_float_args (param i32 f32) (result i32))) + (import "atoms" "double_int_return_float" (func $double_int_return_float (param i32 i32) (result i32))) + + (func $int_float_args_shim (param i32 f32) (result i32) + local.get 0 + local.get 1 + call $int_float_args + ) + (func $double_int_return_float_shim (param i32 i32) (result i32) + local.get 0 + local.get 1 + call $double_int_return_float + ) + (export "int_float_args_shim" (func $int_float_args_shim)) + (export "double_int_return_float_shim" (func $double_int_return_float_shim)) + ) + "#, + ) + .unwrap() +} diff --git a/examples/linking.rs b/examples/linking.rs index b4cee53a4a..824bb13410 100644 --- a/examples/linking.rs +++ b/examples/linking.rs @@ -4,7 +4,7 @@ use anyhow::Result; use wasmtime::*; -use wasmtime_wasi::{sync::WasiCtxBuilder, Wasi}; +use wasmtime_wasi::sync::{Wasi, WasiCtxBuilder}; fn main() -> Result<()> { let engine = Engine::default(); diff --git a/examples/tokio/main.c b/examples/tokio/main.c new file mode 100644 index 0000000000..f6920d8bae --- /dev/null +++ b/examples/tokio/main.c @@ -0,0 +1,5 @@ +int main(int argc, char *argv[]) { + // This example is specific to integrating with Rust's tokio ecosystem, so + // it isnt applicable to C/C++. + return 0; +} diff --git a/examples/tokio/main.rs b/examples/tokio/main.rs new file mode 100644 index 0000000000..491fe754db --- /dev/null +++ b/examples/tokio/main.rs @@ -0,0 +1,163 @@ +use anyhow::{anyhow, Error}; +use std::future::Future; +use tokio::time::Duration; +use wasmtime::{Config, Engine, Linker, Module, Store}; +// For this example we want to use the async version of wasmtime_wasi. +// Notably, this version of wasi uses a scheduler that will async yield +// when sleeping in `poll_oneoff`. +use wasmtime_wasi::tokio::{Wasi, WasiCtxBuilder}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + // Create an environment shared by all wasm execution. This contains + // the `Engine` and the `Module` we are executing. + let env = Environment::new()?; + + // The inputs to run_wasm are `Send`: we can create them here and send + // them to a new task that we spawn. + let inputs1 = Inputs::new(env.clone(), "Gussie"); + let inputs2 = Inputs::new(env.clone(), "Willa"); + let inputs3 = Inputs::new(env, "Sparky"); + + // Spawn some tasks. Insert sleeps before run_wasm so that the + // interleaving is easy to observe. + let join1 = tokio::task::spawn(async move { run_wasm(inputs1).await }); + let join2 = tokio::task::spawn(async move { + tokio::time::sleep(Duration::from_millis(750)).await; + run_wasm(inputs2).await + }); + let join3 = tokio::task::spawn(async move { + tokio::time::sleep(Duration::from_millis(1250)).await; + run_wasm(inputs3).await + }); + + // All tasks should join successfully. + join1.await??; + join2.await??; + join3.await??; + Ok(()) +} + +#[derive(Clone)] +struct Environment { + engine: Engine, + module: Module, +} + +impl Environment { + pub fn new() -> Result { + let mut config = Config::new(); + // We need this engine's `Store`s to be async, and consume fuel, so + // that they can co-operatively yield during execution. + config.async_support(true); + config.consume_fuel(true); + + // Install the host functions for `Wasi`. + Wasi::add_to_config(&mut config); + + let engine = Engine::new(&config)?; + let module = Module::from_file(&engine, "target/wasm32-wasi/debug/tokio-wasi.wasm")?; + + Ok(Self { engine, module }) + } +} + +struct Inputs { + env: Environment, + name: String, +} + +impl Inputs { + fn new(env: Environment, name: &str) -> Self { + Self { + env, + name: name.to_owned(), + } + } +} + +fn run_wasm(inputs: Inputs) -> impl Future> { + use std::pin::Pin; + use std::task::{Context, Poll}; + // IMPORTANT: The current wasmtime API is very challenging to use safely + // on an async runtime. This RFC describes a redesign of the API that will + // resolve these safety issues: + // https://github.com/alexcrichton/rfcs-2/blob/new-api/accepted/new-api.md + + // This is a "marker type future" which simply wraps some other future and + // the only purpose it serves is to forward the implementation of `Future` + // as well as have `unsafe impl Send` for itself, regardless of the + // underlying type. + // + // Note that the qctual safety of this relies on the fact that the inputs + // here are `Send`, the outputs (just () in this case) are `Send`, and the + // future itself is safe tu resume on other threads. + // + // For an in-depth discussion of the safety of moving Wasmtime's `Store` + // between threads, see + // https://docs.wasmtime.dev/examples-rust-multithreading.html. + struct UnsafeSend(T); + + // Note the `where` cause specifically ensures the output of the future to + // be `Send` is required. We specifically dont require `T` to be `Send` + // since that's the whole point of this function, but we require that + // everything used to construct `T` is `Send` below. + unsafe impl Send for UnsafeSend + where + T: Future, + T::Output: Send, + { + } + impl Future for UnsafeSend { + type Output = T::Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // Note that this `unsafe` is unrelated to `Send`, it only has to do with "pin + // projection" and should be safe since it's all we do with the `Pin`. + unsafe { self.map_unchecked_mut(|p| &mut p.0).poll(cx) } + } + } + + // This is a crucial assertion that needs to be here. The compiler + // typically checks this for us, but do to our `UnsafeSend` type the + // compiler isn't automatically checking this. The assertion here must + // assert that all arguments to this function are indeed `Send` because + // we're closing over them and sending them to other threads. It's only + // everything *internal* to the computation of this function which doesn't + // have to be `Send`. + fn assert_send(_t: &T) {} + assert_send(&inputs); + + // Wrap up the `_run_wasm` function, which is *not* `Send`, but is safe to + // resume on other threads. + UnsafeSend(_run_wasm(inputs)) +} + +async fn _run_wasm(inputs: Inputs) -> Result<(), Error> { + let store = Store::new(&inputs.env.engine); + + // WebAssembly execution will be paused for an async yield every time it + // consumes 10000 fuel. Fuel will be refilled u32::MAX times. + store.out_of_fuel_async_yield(u32::MAX, 10000); + + Wasi::set_context( + &store, + WasiCtxBuilder::new() + // Let wasi print to this process's stdout. + .inherit_stdout() + // Set an environment variable so the wasm knows its name. + .env("NAME", &inputs.name)? + .build()?, + ) + .map_err(|_| anyhow!("setting wasi context"))?; + + let linker = Linker::new(&store); + + // Instantiate + let instance = linker.instantiate_async(&inputs.env.module).await?; + instance + .get_typed_func::<(), ()>("_start")? + .call_async(()) + .await?; + + Ok(()) +} diff --git a/examples/tokio/wasm/Cargo.toml b/examples/tokio/wasm/Cargo.toml new file mode 100644 index 0000000000..5704f79630 --- /dev/null +++ b/examples/tokio/wasm/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "example-tokio-wasm" +version = "0.0.0" +authors = ["The Wasmtime Project Developers"] +edition = "2018" +publish = false + +[[bin]] +path = "tokio-wasi.rs" +name = "tokio-wasi" diff --git a/examples/tokio/wasm/tokio-wasi.rs b/examples/tokio/wasm/tokio-wasi.rs new file mode 100644 index 0000000000..3431e65a66 --- /dev/null +++ b/examples/tokio/wasm/tokio-wasi.rs @@ -0,0 +1,6 @@ +fn main() { + let name = std::env::var("NAME").unwrap(); + println!("Hello, world! My name is {}", name); + std::thread::sleep(std::time::Duration::from_secs(1)); + println!("Goodbye from {}", name); +} diff --git a/examples/wasi/main.rs b/examples/wasi/main.rs index f808be3eca..0c3f077d93 100644 --- a/examples/wasi/main.rs +++ b/examples/wasi/main.rs @@ -5,7 +5,7 @@ use anyhow::Result; use wasmtime::*; -use wasmtime_wasi::{sync::WasiCtxBuilder, Wasi}; +use wasmtime_wasi::sync::{Wasi, WasiCtxBuilder}; fn main() -> Result<()> { tracing_subscriber::FmtSubscriber::builder() diff --git a/scripts/publish.rs b/scripts/publish.rs index 39a7f04e8c..e121cedc3a 100644 --- a/scripts/publish.rs +++ b/scripts/publish.rs @@ -49,6 +49,7 @@ const CRATES_TO_PUBLISH: &[&str] = &[ // wasi-common "wasi-common", "wasi-cap-std-sync", + "wasi-tokio", // wasmtime "lightbeam", "wasmtime-fiber", diff --git a/src/commands/run.rs b/src/commands/run.rs index 4d6bce5b0b..eeec81b2b7 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -11,10 +11,7 @@ use std::{ }; use structopt::{clap::AppSettings, StructOpt}; use wasmtime::{Engine, Func, Linker, Module, Store, Trap, Val, ValType}; -use wasmtime_wasi::{ - sync::{Dir, WasiCtxBuilder}, - Wasi, -}; +use wasmtime_wasi::sync::{Dir, Wasi, WasiCtxBuilder}; #[cfg(feature = "wasi-nn")] use wasmtime_wasi_nn::{WasiNn, WasiNnCtx};