diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9ce263c5cb46..59b2508b8e09 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: name: Doc - build the API documentation runs-on: ubuntu-latest env: - RUSTDOCFLAGS: -Dbroken_intra_doc_links + RUSTDOCFLAGS: -Dbroken_intra_doc_links --cfg nightlydoc OPENVINO_SKIP_LINKING: 1 steps: - uses: actions/checkout@v2 @@ -120,6 +120,8 @@ jobs: - run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features wat - run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features lightbeam - run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features jitdump + - run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features cache + - run: cargo check --manifest-path crates/wasmtime/Cargo.toml --features async # Check some feature combinations of the `wasmtime-c-api` crate - run: cargo check --manifest-path crates/c-api/Cargo.toml --no-default-features diff --git a/crates/fiber/Cargo.toml b/crates/fiber/Cargo.toml new file mode 100644 index 000000000000..d4b6b3f08f11 --- /dev/null +++ b/crates/fiber/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "wasmtime-fiber" +version = "0.22.0" +authors = ["The Wasmtime Project Developers"] +description = "Fiber support for Wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +edition = "2018" + +# We link to some native code with symbols that don't change often, so let Cargo +# know that we can't show up multiple times in a crate graph. If this is an +# issue in the future we should tweak the build script to set `#define` +# directives or similar to embed a version number of this crate in symbols. +links = "wasmtime-fiber-shims" + +[target.'cfg(unix)'.dependencies] +libc = "0.2.80" + +[target.'cfg(windows)'.dependencies.winapi] +version = "0.3.9" +features = [ + "fibersapi", + "winbase", +] + +[build-dependencies] +cc = "1.0" + +[dev-dependencies] +backtrace = "0.3" diff --git a/crates/fiber/build.rs b/crates/fiber/build.rs new file mode 100644 index 000000000000..034990bc3c54 --- /dev/null +++ b/crates/fiber/build.rs @@ -0,0 +1,23 @@ +use std::env; + +fn main() { + let mut build = cc::Build::new(); + let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + let family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap(); + let os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + if family == "windows" { + build.file("src/arch/windows.c"); + } else if arch == "x86_64" { + build.file("src/arch/x86_64.S"); + } else if arch == "aarch64" { + build.file("src/arch/aarch64.S"); + } else { + panic!( + "wasmtime doesn't support fibers on platform: {}", + env::var("TARGET").unwrap() + ); + } + build.define(&format!("CFG_TARGET_OS_{}", os), None); + build.define(&format!("CFG_TARGET_ARCH_{}", arch), None); + build.compile("wasmtime-fiber"); +} diff --git a/crates/fiber/src/arch/aarch64.S b/crates/fiber/src/arch/aarch64.S new file mode 100644 index 000000000000..d0ece452bd45 --- /dev/null +++ b/crates/fiber/src/arch/aarch64.S @@ -0,0 +1,111 @@ +// A WORD OF CAUTION +// +// This entire file basically needs to be kept in sync with itself. It's not +// really possible to modify just one bit of this file without understanding +// all the other bits. Documentation tries to reference various bits here and +// there but try to make sure to read over everything before tweaking things! +// +// Also at this time this file is heavily based off the x86_64 file, so you'll +// probably want to read that one as well. + +#define GLOBL(fnname) .globl fnname +#define TYPE(fnname) .type fnname,@function +#define FUNCTION(fnname) fnname +#define SIZE(fnname) .size fnname,.-fnname + +// fn(top_of_stack(%x0): *mut u8) +GLOBL(wasmtime_fiber_switch) +.p2align 2 +TYPE(wasmtime_fiber_switch) +FUNCTION(wasmtime_fiber_switch): + // Save all callee-saved registers on the stack since we're assuming + // they're clobbered as a result of the stack switch. + str lr, [sp, -16]! + stp x20, x19, [sp, -16]! + stp x22, x21, [sp, -16]! + stp x24, x23, [sp, -16]! + stp x26, x25, [sp, -16]! + stp x28, x27, [sp, -16]! + + // Load our previously saved stack pointer to resume to, and save off our + // current stack pointer on where to come back to eventually. + ldr x8, [x0, -0x10] + mov x9, sp + str x9, [x0, -0x10] + + // Switch to the new stack and restore all our callee-saved registers after + // the switch and return to our new stack. + mov sp, x8 + ldp x28, x27, [sp], 16 + ldp x26, x25, [sp], 16 + ldp x24, x23, [sp], 16 + ldp x22, x21, [sp], 16 + ldp x20, x19, [sp], 16 + ldr lr, [sp], 16 + ret +SIZE(wasmtime_fiber_switch) + +// fn( +// top_of_stack(%x0): *mut u8, +// entry_point(%x1): extern fn(*mut u8, *mut u8), +// entry_arg0(%x2): *mut u8, +// ) +GLOBL(wasmtime_fiber_init) +.p2align 2 +TYPE(wasmtime_fiber_init) +FUNCTION(wasmtime_fiber_init): + adr x8, wasmtime_fiber_start + stp x0, x8, [x0, -0x28] // x0 => x19, x8 => lr + stp x2, x1, [x0, -0x38] // x1 => x20, x2 => x21 + + // `wasmtime_fiber_switch` has an 0x60 byte stack, and we add 0x10 more for + // the original reserved 16 bytes. + add x8, x0, -0x70 + str x8, [x0, -0x10] + ret +SIZE(wasmtime_fiber_init) + +.p2align 2 +TYPE(wasmtime_fiber_start) +FUNCTION(wasmtime_fiber_start): +.cfi_startproc simple + + // See the x86_64 file for more commentary on what these CFI directives are + // doing. Like over there note that the relative offsets to registers here + // match the frame layout in `wasmtime_fiber_switch`. + .cfi_escape 0x0f, /* DW_CFA_def_cfa_expression */ \ + 4, /* the byte length of this expression */ \ + 0x6f, /* DW_OP_reg31(%sp) */ \ + 0x06, /* DW_OP_deref */ \ + 0x23, 0x60 /* DW_OP_plus_uconst 0x60 */ + + .cfi_rel_offset lr, -0x10 + .cfi_rel_offset x19, -0x18 + .cfi_rel_offset x20, -0x20 + .cfi_rel_offset x21, -0x28 + .cfi_rel_offset x22, -0x30 + .cfi_rel_offset x23, -0x38 + .cfi_rel_offset x24, -0x40 + .cfi_rel_offset x25, -0x48 + .cfi_rel_offset x26, -0x50 + .cfi_rel_offset x27, -0x58 + .cfi_rel_offset x29, -0x60 + + // Load our two arguments from the stack, where x1 is our start procedure + // and x0 is its first argument. This also blows away the stack space used + // by those two arguments. + mov x0, x21 + mov x1, x19 + + // ... and then we call the function! Note that this is a function call so + // our frame stays on the stack to backtrace through. + blr x20 + // .. technically we shouldn't get here, and I would like to write in an + // instruction which just aborts, but I don't know such an instruction in + // aarch64 lan, and I would like to write in an instruction which just + // aborts, but I don't know such an instruction in aarch64 land + .cfi_endproc +SIZE(wasmtime_fiber_start) + +.section .note.GNU-stack,"",%progbits + diff --git a/crates/fiber/src/arch/windows.c b/crates/fiber/src/arch/windows.c new file mode 100644 index 000000000000..9e017f9b0260 --- /dev/null +++ b/crates/fiber/src/arch/windows.c @@ -0,0 +1,5 @@ +#include + +LPVOID wasmtime_fiber_get_current() { + return GetCurrentFiber(); +} diff --git a/crates/fiber/src/arch/x86_64.S b/crates/fiber/src/arch/x86_64.S new file mode 100644 index 000000000000..9a5e5cc07293 --- /dev/null +++ b/crates/fiber/src/arch/x86_64.S @@ -0,0 +1,177 @@ +// A WORD OF CAUTION +// +// This entire file basically needs to be kept in sync with itself. It's not +// really possible to modify just one bit of this file without understanding +// all the other bits. Documentation tries to reference various bits here and +// there but try to make sure to read over everything before tweaking things! + +.text + +#if CFG_TARGET_OS_macos + +#define GLOBL(fnname) .globl _##fnname +#define TYPE(fnname) +#define FUNCTION(fnname) _##fnname +#define SIZE(fnname) + +#else + +#define GLOBL(fnname) .globl fnname +#define TYPE(fnname) .type fnname,@function +#define FUNCTION(fnname) fnname +#define SIZE(fnname) .size fnname,.-fnname + +#endif + +// fn(top_of_stack(%rdi): *mut u8) +GLOBL(wasmtime_fiber_switch) +.align 16 +TYPE(wasmtime_fiber_switch) +FUNCTION(wasmtime_fiber_switch): + // We're switching to arbitrary code somewhere else, so pessimistically + // assume that all callee-save register are clobbered. This means we need + // to save/restore all of them. + // + // Note that this order for saving is important since we use CFI directives + // below to point to where all the saved registers are. + pushq %rbp + pushq %rbx + pushq %r12 + pushq %r13 + pushq %r14 + pushq %r15 + + // Load pointer that we're going to resume at and store where we're going + // to get resumed from. This is in accordance with the diagram at the top + // of unix.rs. + movq -0x10(%rdi), %rax + mov %rsp, -0x10(%rdi) + + // Swap stacks and restore all our callee-saved registers + mov %rax, %rsp + popq %r15 + popq %r14 + popq %r13 + popq %r12 + popq %rbx + popq %rbp + ret +SIZE(wasmtime_fiber_switch) + +// fn( +// top_of_stack(%rdi): *mut u8, +// entry_point(%rsi): extern fn(*mut u8, *mut u8), +// entry_arg0(%rdx): *mut u8, +// ) +GLOBL(wasmtime_fiber_init) +.align 16 +TYPE(wasmtime_fiber_init) +FUNCTION(wasmtime_fiber_init): + // The first 16 bytes of the stack are reserved (see unix.rs) so we store + // the initial data used in `wasmtime_fiber_start` just below + // that. + movq %rdi, -0x18(%rdi) + movq %rsi, -0x20(%rdi) + movq %rdx, -0x28(%rdi) + + // After these arguments is the return address of where to switch to, + // which for the first run is `wasmtime_fiber_start`. + lea FUNCTION(wasmtime_fiber_start)(%rip), %rax + movq %rax, -0x30(%rdi) + + // And then we specify the stack pointer resumption should begin at. Our + // `wasmtime_fiber_switch` function saves 6 registers so we need to ensure + // that there's space for that as well. 0x30 + 6 * 8 == 0x60 here. + lea -0x60(%rdi), %rax + movq %rax, -0x10(%rdi) + ret +SIZE(wasmtime_fiber_init) + +// This is a pretty special function that has no real signature. Its use is to +// be the "base" function of all fibers. This entrypoint is used in +// `wasmtime_fiber_init` to bootstrap the execution of a new fiber. +// +// We also use this function as a persistent frame on the stack to emit dwarf +// information to unwind into the caller. This allows us to unwind from the +// fiber's stack back to the main stack that the fiber was called from. We use +// special dwarf directives here to do so since this is a pretty nonstandard +// function. +// +// If you're curious a decent introduction to CFI things and unwinding is at +// https://www.imperialviolet.org/2017/01/18/cfi.html +.align 16 +TYPE(wasmtime_fiber_start) +FUNCTION(wasmtime_fiber_start): +// Use the `simple` directive on the startproc here which indicates that some +// default settings for the platform are omitted, since this function is so +// nonstandard +.cfi_startproc simple + // This is where things get special, we're specifying a custom dwarf + // expression for how to calculate the CFA. The goal here is that we need + // to load the parent's stack pointer just before the call it made into + // `wasmtime_fiber_switch`. Note that the CFA value changes over time as + // well because a fiber may be resumed multiple times from different points + // on the original stack. This means that our custom CFA directive involves + // `DW_OP_deref`, which loads data from memory. + // + // The expression we're encoding here is that the CFA, the stack pointer of + // whatever called into `wasmtime_fiber_start`, is: + // + // *($rsp + 0x18) + 0x38 + // + // $rsp is the stack pointer of `wasmtime_fiber_start` at the time the next + // instruction after the `.cfi_escape` is executed. Our $rsp at the start + // of this function is 3 words below stack start (0xAff0 in + // the diagram in unix.rs). The $rsp to resume at is at 0xAff0, so we + // add an offset to $rsp to get to that memory location and then we + // dereference it. + // + // After dereferencing, though, we have the $rsp value for + // `wasmtime_fiber_switch` itself. That's a weird function which sort of + // and sort of doesn't exist on the stack. We want to point to the caller + // of `wasmtime_fiber_switch`, so to do that we need to skip the stack space + // reserved by `wasmtime_fiber_switch`, which is the 6 saved registers plus + // the return address of the caller's `call` instruction. Hence we offset + // another 0x38 bytes. + .cfi_escape 0x0f, /* DW_CFA_def_cfa_expression */ \ + 5, /* the byte length of this expression */ \ + 0x77, 0x18, /* DW_OP_breg7 (%rsp) + 0x18 */ \ + 0x06, /* DW_OP_deref */ \ + 0x23, 0x38 /* DW_OP_plus_uconst 0x38 */ + + // And now after we've indicated where our CFA is for our parent function, + // we can define that where all of the saved registers are located. This + // uses standard `.cfi` directives which indicate that these registers are + // all stored relative to the CFA. Note that this order is kept in sync + // with the above register spills in `wasmtime_fiber_switch`. + .cfi_rel_offset rip, -8 + .cfi_rel_offset rbp, -16 + .cfi_rel_offset rbx, -24 + .cfi_rel_offset r12, -32 + .cfi_rel_offset r13, -40 + .cfi_rel_offset r14, -48 + .cfi_rel_offset r15, -56 + + // Update the CFA expression after each adjustment of $rsp as we load + // registers to call the entrypoint. The major change is that the $rsp + // offset is decreasing by 8, and for the last adjustment a 0 offset means + // we can use DW_OP_reg7. + popq %rdi + .cfi_escape 0x0f, 5, 0x77, 0x10, 0x06, 0x23, 0x38 + popq %rax + .cfi_escape 0x0f, 5, 0x77, 0x08, 0x06, 0x23, 0x38 + popq %rsi + .cfi_escape 0x0f, 4, 0x57, 0x06, 0x23, 0x38 + + // And finally head off into the fiber. Note the `callq` keeps this frame + // on the stack so all our CFI directives can be read. Additionally this + // is not expected to ever return, but for safety we put a `ud2` at the end. + callq *%rax + ud2 + .cfi_endproc +SIZE(wasmtime_fiber_start) + +// Mark that we don't need executable stack. +#if defined(__ELF__) +.section .note.GNU-stack,"",%progbits +#endif diff --git a/crates/fiber/src/lib.rs b/crates/fiber/src/lib.rs new file mode 100644 index 000000000000..bafae2f01c13 --- /dev/null +++ b/crates/fiber/src/lib.rs @@ -0,0 +1,249 @@ +use std::any::Any; +use std::cell::Cell; +use std::io; +use std::marker::PhantomData; +use std::panic::{self, AssertUnwindSafe}; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +use windows as imp; + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +use unix as imp; + +pub struct Fiber<'a, Resume, Yield, Return> { + inner: imp::Fiber, + done: Cell, + _phantom: PhantomData<&'a (Resume, Yield, Return)>, +} + +pub struct Suspend { + inner: imp::Suspend, + _phantom: PhantomData<(Resume, Yield, Return)>, +} + +enum RunResult { + Executing, + Resuming(Resume), + Yield(Yield), + Returned(Return), + Panicked(Box), +} + +impl<'a, Resume, Yield, Return> Fiber<'a, Resume, Yield, Return> { + /// Creates a new fiber which will execute `func` on a new native stack of + /// size `stack_size`. + /// + /// This function returns a `Fiber` which, when resumed, will execute `func` + /// to completion. When desired the `func` can suspend itself via + /// `Fiber::suspend`. + pub fn new( + stack_size: usize, + func: impl FnOnce(Resume, &Suspend) -> Return + 'a, + ) -> io::Result> { + Ok(Fiber { + inner: imp::Fiber::new(stack_size, func)?, + done: Cell::new(false), + _phantom: PhantomData, + }) + } + + /// Resumes execution of this fiber. + /// + /// This function will transfer execution to the fiber and resume from where + /// it last left off. + /// + /// Returns `true` if the fiber finished or `false` if the fiber was + /// suspended in the middle of execution. + /// + /// # Panics + /// + /// Panics if the current thread is already executing a fiber or if this + /// fiber has already finished. + /// + /// Note that if the fiber itself panics during execution then the panic + /// will be propagated to this caller. + pub fn resume(&self, val: Resume) -> Result { + assert!(!self.done.replace(true), "cannot resume a finished fiber"); + let result = Cell::new(RunResult::Resuming(val)); + self.inner.resume(&result); + match result.into_inner() { + RunResult::Resuming(_) | RunResult::Executing => unreachable!(), + RunResult::Yield(y) => { + self.done.set(false); + Err(y) + } + RunResult::Returned(r) => Ok(r), + RunResult::Panicked(payload) => std::panic::resume_unwind(payload), + } + } + + /// Returns whether this fiber has finished executing. + pub fn done(&self) -> bool { + self.done.get() + } +} + +impl Suspend { + /// Suspend execution of a currently running fiber. + /// + /// This function will switch control back to the original caller of + /// `Fiber::resume`. This function will then return once the `Fiber::resume` + /// function is called again. + /// + /// # Panics + /// + /// Panics if the current thread is not executing a fiber from this library. + pub fn suspend(&self, value: Yield) -> Resume { + self.inner + .switch::(RunResult::Yield(value)) + } + + fn execute( + inner: imp::Suspend, + initial: Resume, + func: impl FnOnce(Resume, &Suspend) -> Return, + ) { + let suspend = Suspend { + inner, + _phantom: PhantomData, + }; + let result = panic::catch_unwind(AssertUnwindSafe(|| (func)(initial, &suspend))); + suspend.inner.switch::(match result { + Ok(result) => RunResult::Returned(result), + Err(panic) => RunResult::Panicked(panic), + }); + } +} + +impl Drop for Fiber<'_, A, B, C> { + fn drop(&mut self) { + debug_assert!(self.done.get(), "fiber dropped without finishing"); + } +} + +#[cfg(test)] +mod tests { + use super::Fiber; + use std::cell::Cell; + use std::panic::{self, AssertUnwindSafe}; + use std::rc::Rc; + + #[test] + fn small_stacks() { + Fiber::<(), (), ()>::new(0, |_, _| {}) + .unwrap() + .resume(()) + .unwrap(); + Fiber::<(), (), ()>::new(1, |_, _| {}) + .unwrap() + .resume(()) + .unwrap(); + } + + #[test] + fn smoke() { + let hit = Rc::new(Cell::new(false)); + let hit2 = hit.clone(); + let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |_, _| { + hit2.set(true); + }) + .unwrap(); + assert!(!hit.get()); + fiber.resume(()).unwrap(); + assert!(hit.get()); + } + + #[test] + fn suspend_and_resume() { + let hit = Rc::new(Cell::new(false)); + let hit2 = hit.clone(); + let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |_, s| { + s.suspend(()); + hit2.set(true); + s.suspend(()); + }) + .unwrap(); + assert!(!hit.get()); + assert!(fiber.resume(()).is_err()); + assert!(!hit.get()); + assert!(fiber.resume(()).is_err()); + assert!(hit.get()); + assert!(fiber.resume(()).is_ok()); + assert!(hit.get()); + } + + #[test] + fn backtrace_traces_to_host() { + #[inline(never)] // try to get this to show up in backtraces + fn look_for_me() { + run_test(); + } + fn assert_contains_host() { + let trace = backtrace::Backtrace::new(); + println!("{:?}", trace); + assert!( + trace + .frames() + .iter() + .flat_map(|f| f.symbols()) + .filter_map(|s| Some(s.name()?.to_string())) + .any(|s| s.contains("look_for_me")) + // TODO: apparently windows unwind routines don't unwind through fibers, so this will always fail. Is there a way we can fix that? + || cfg!(windows) + ); + } + + fn run_test() { + let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |(), s| { + assert_contains_host(); + s.suspend(()); + assert_contains_host(); + s.suspend(()); + assert_contains_host(); + }) + .unwrap(); + assert!(fiber.resume(()).is_err()); + assert!(fiber.resume(()).is_err()); + assert!(fiber.resume(()).is_ok()); + } + + look_for_me(); + } + + #[test] + fn panics_propagated() { + let a = Rc::new(Cell::new(false)); + let b = SetOnDrop(a.clone()); + let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |(), _s| { + drop(&b); + panic!(); + }) + .unwrap(); + assert!(panic::catch_unwind(AssertUnwindSafe(|| fiber.resume(()))).is_err()); + assert!(a.get()); + + struct SetOnDrop(Rc>); + + impl Drop for SetOnDrop { + fn drop(&mut self) { + self.0.set(true); + } + } + } + + #[test] + fn suspend_and_resume_values() { + let fiber = Fiber::new(1024 * 1024, move |first, s| { + assert_eq!(first, 2.0); + assert_eq!(s.suspend(4), 3.0); + "hello".to_string() + }) + .unwrap(); + assert_eq!(fiber.resume(2.0), Err(4)); + assert_eq!(fiber.resume(3.0), Ok("hello".to_string())); + } +} diff --git a/crates/fiber/src/unix.rs b/crates/fiber/src/unix.rs new file mode 100644 index 000000000000..2c1069041ebe --- /dev/null +++ b/crates/fiber/src/unix.rs @@ -0,0 +1,174 @@ +//! The unix fiber implementation has some platform-specific details +//! (naturally) but there's a few details of the stack layout which are common +//! amongst all platforms using this file. Remember that none of this applies to +//! Windows, which is entirely separate. +//! +//! The stack is expected to look pretty standard with a guard page at the end. +//! Currently allocation happens in this file but this is probably going to be +//! refactored to happen somewhere else. Otherwise though the stack layout is +//! expected to look like so: +//! +//! +//! ```text +//! 0xB000 +-----------------------+ <- top of stack +//! | &Cell | <- where to store results +//! 0xAff8 +-----------------------+ +//! | *const u8 | <- last sp to resume from +//! 0xAff0 +-----------------------+ <- 16-byte aligned +//! | | +//! ~ ... ~ <- actual native stack space to use +//! | | +//! 0x1000 +-----------------------+ +//! | guard page | +//! 0x0000 +-----------------------+ +//! ``` +//! +//! Here `0xAff8` is filled in temporarily while `resume` is running. The fiber +//! started with 0xB000 as a parameter so it knows how to find this. +//! Additionally `resumes` stores state at 0xAff0 to restart execution, and +//! `suspend`, which has 0xB000 so it can find this, will read that and write +//! its own resumption information into this slot as well. + +use crate::RunResult; +use std::cell::Cell; +use std::io; +use std::ptr; + +pub struct Fiber { + // Description of the mmap region we own. This should be abstracted + // eventually so we aren't personally mmap-ing this region. + mmap: *mut libc::c_void, + mmap_len: usize, +} + +pub struct Suspend { + top_of_stack: *mut u8, +} + +extern "C" { + fn wasmtime_fiber_init( + top_of_stack: *mut u8, + entry: extern "C" fn(*mut u8, *mut u8), + entry_arg0: *mut u8, + ); + fn wasmtime_fiber_switch(top_of_stack: *mut u8); +} + +extern "C" fn fiber_start(arg0: *mut u8, top_of_stack: *mut u8) +where + F: FnOnce(A, &super::Suspend) -> C, +{ + unsafe { + let inner = Suspend { top_of_stack }; + let initial = inner.take_resume::(); + super::Suspend::::execute(inner, initial, Box::from_raw(arg0.cast::())) + } +} + +impl Fiber { + pub fn new(stack_size: usize, func: F) -> io::Result + where + F: FnOnce(A, &super::Suspend) -> C, + { + let fiber = Fiber::alloc_with_stack(stack_size)?; + unsafe { + // Initialize the top of the stack to be resumed from + let top_of_stack = fiber.top_of_stack(); + let data = Box::into_raw(Box::new(func)).cast(); + wasmtime_fiber_init(top_of_stack, fiber_start::, data); + Ok(fiber) + } + } + + fn alloc_with_stack(stack_size: usize) -> io::Result { + unsafe { + // Round up our stack size request to the nearest multiple of the + // page size. + let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize; + let stack_size = if stack_size == 0 { + page_size + } else { + (stack_size + (page_size - 1)) & (!(page_size - 1)) + }; + + // Add in one page for a guard page and then ask for some memory. + let mmap_len = stack_size + page_size; + let mmap = libc::mmap( + ptr::null_mut(), + mmap_len, + libc::PROT_NONE, + libc::MAP_ANON | libc::MAP_PRIVATE, + -1, + 0, + ); + if mmap == libc::MAP_FAILED { + return Err(io::Error::last_os_error()); + } + let ret = Fiber { mmap, mmap_len }; + let res = libc::mprotect( + mmap.cast::().add(page_size).cast(), + stack_size, + libc::PROT_READ | libc::PROT_WRITE, + ); + if res != 0 { + Err(io::Error::last_os_error()) + } else { + Ok(ret) + } + } + } + + pub(crate) fn resume(&self, result: &Cell>) { + unsafe { + // Store where our result is going at the very tip-top of the + // stack, otherwise known as our reserved slot for this information. + // + // In the diagram above this is updating address 0xAff8 + let top_of_stack = self.top_of_stack(); + let addr = top_of_stack.cast::().offset(-1); + addr.write(result as *const _ as usize); + + wasmtime_fiber_switch(top_of_stack); + + // null this out to help catch use-after-free + addr.write(0); + } + } + + unsafe fn top_of_stack(&self) -> *mut u8 { + self.mmap.cast::().add(self.mmap_len) + } +} + +impl Drop for Fiber { + fn drop(&mut self) { + unsafe { + let ret = libc::munmap(self.mmap, self.mmap_len); + debug_assert!(ret == 0); + } + } +} + +impl Suspend { + pub(crate) fn switch(&self, result: RunResult) -> A { + unsafe { + // Calculate 0xAff8 and then write to it + (*self.result_location::()).set(result); + wasmtime_fiber_switch(self.top_of_stack); + self.take_resume::() + } + } + + unsafe fn take_resume(&self) -> A { + match (*self.result_location::()).replace(RunResult::Executing) { + RunResult::Resuming(val) => val, + _ => panic!("not in resuming state"), + } + } + + unsafe fn result_location(&self) -> *const Cell> { + let ret = self.top_of_stack.cast::<*const u8>().offset(-1).read(); + assert!(!ret.is_null()); + return ret.cast(); + } +} diff --git a/crates/fiber/src/windows.rs b/crates/fiber/src/windows.rs new file mode 100644 index 000000000000..46a513c20121 --- /dev/null +++ b/crates/fiber/src/windows.rs @@ -0,0 +1,126 @@ +use crate::RunResult; +use std::cell::Cell; +use std::io; +use std::ptr; +use winapi::shared::minwindef::*; +use winapi::um::fibersapi::*; +use winapi::um::winbase::*; + +pub struct Fiber { + fiber: LPVOID, + state: Box, +} + +pub struct Suspend { + state: *const StartState, +} + +struct StartState { + parent: Cell, + initial_closure: Cell<*mut u8>, + result_location: Cell<*const u8>, +} + +extern "C" { + fn wasmtime_fiber_get_current() -> LPVOID; +} + +unsafe extern "system" fn fiber_start(data: LPVOID) +where + F: FnOnce(A, &super::Suspend) -> C, +{ + let state = data.cast::(); + let func = Box::from_raw((*state).initial_closure.get().cast::()); + (*state).initial_closure.set(ptr::null_mut()); + let suspend = Suspend { state }; + let initial = suspend.take_resume::(); + super::Suspend::::execute(suspend, initial, *func); +} + +impl Fiber { + pub fn new(stack_size: usize, func: F) -> io::Result + where + F: FnOnce(A, &super::Suspend) -> C, + { + unsafe { + let state = Box::new(StartState { + initial_closure: Cell::new(Box::into_raw(Box::new(func)).cast()), + parent: Cell::new(ptr::null_mut()), + result_location: Cell::new(ptr::null()), + }); + let fiber = CreateFiber( + stack_size, + Some(fiber_start::), + &*state as *const StartState as *mut _, + ); + if fiber.is_null() { + drop(Box::from_raw(state.initial_closure.get().cast::())); + Err(io::Error::last_os_error()) + } else { + Ok(Fiber { fiber, state }) + } + } + } + + pub(crate) fn resume(&self, result: &Cell>) { + unsafe { + let is_fiber = IsThreadAFiber() != 0; + let parent_fiber = if is_fiber { + wasmtime_fiber_get_current() + } else { + ConvertThreadToFiber(ptr::null_mut()) + }; + assert!( + !parent_fiber.is_null(), + "failed to make current thread a fiber" + ); + self.state + .result_location + .set(result as *const _ as *const _); + self.state.parent.set(parent_fiber); + SwitchToFiber(self.fiber); + self.state.parent.set(ptr::null_mut()); + self.state.result_location.set(ptr::null()); + if !is_fiber { + let res = ConvertFiberToThread(); + assert!(res != 0, "failed to convert main thread back"); + } + } + } +} + +impl Drop for Fiber { + fn drop(&mut self) { + unsafe { + DeleteFiber(self.fiber); + } + } +} + +impl Suspend { + pub(crate) fn switch(&self, result: RunResult) -> A { + unsafe { + (*self.result_location::()).set(result); + debug_assert!(IsThreadAFiber() != 0); + let parent = (*self.state).parent.get(); + debug_assert!(!parent.is_null()); + SwitchToFiber(parent); + self.take_resume::() + } + } + unsafe fn take_resume(&self) -> A { + match (*self.result_location::()).replace(RunResult::Executing) { + RunResult::Resuming(val) => val, + _ => panic!("not in resuming state"), + } + } + + unsafe fn result_location(&self) -> *const Cell> { + let ret = (*self.state) + .result_location + .get() + .cast::>>(); + assert!(!ret.is_null()); + return ret; + } +} diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 52b36464cfea..d063f0de04ae 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -9,12 +9,16 @@ repository = "https://github.com/bytecodealliance/wasmtime" readme = "README.md" edition = "2018" +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "nightlydoc"] + [dependencies] wasmtime-runtime = { path = "../runtime", version = "0.22.0" } wasmtime-environ = { path = "../environ", version = "0.22.0" } wasmtime-jit = { path = "../jit", version = "0.22.0" } wasmtime-cache = { path = "../cache", version = "0.22.0", optional = true } wasmtime-profiling = { path = "../profiling", version = "0.22.0" } +wasmtime-fiber = { path = "../fiber", version = "0.22.0", optional = true } target-lexicon = { version = "0.11.0", default-features = false } wasmparser = "0.73" anyhow = "1.0.19" @@ -30,6 +34,7 @@ smallvec = "1.6.1" serde = { version = "1.0.94", features = ["derive"] } bincode = "1.2.1" indexmap = "1.6" +paste = "1.0.3" [target.'cfg(target_os = "windows")'.dependencies] winapi = "0.3.7" @@ -42,7 +47,7 @@ wasmtime-wasi = { path = "../wasi" } maintenance = { status = "actively-developed" } [features] -default = ['cache', 'wat', 'jitdump', 'parallel-compilation'] +default = ['async', 'cache', 'wat', 'jitdump', 'parallel-compilation'] # Enables experimental support for the lightbeam codegen backend, an alternative # to cranelift. Requires Nightly Rust currently, and this is not enabled by @@ -63,3 +68,7 @@ cache = ["wasmtime-cache"] # Enables support for new x64 backend. experimental_x64 = ["wasmtime-jit/experimental_x64"] + +# Enables support for "async stores" as well as defining host functions as +# `async fn` and calling functions asynchronously. +async = ["wasmtime-fiber"] diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index a662599dc8e2..6ddaa022c4d7 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -470,6 +470,7 @@ impl Config { /// /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html #[cfg(feature = "cache")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "cache")))] pub fn cache_config_load(&mut self, path: impl AsRef) -> Result<&mut Self> { self.cache_config = CacheConfig::from_file(Some(path.as_ref()))?; Ok(self) @@ -497,6 +498,7 @@ impl Config { /// /// [docs]: https://bytecodealliance.github.io/wasmtime/cli-cache.html #[cfg(feature = "cache")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "cache")))] pub fn cache_config_load_default(&mut self) -> Result<&mut Self> { self.cache_config = CacheConfig::from_file(None)?; Ok(self) diff --git a/crates/wasmtime/src/func.rs b/crates/wasmtime/src/func.rs index 7f68b97964cc..2fa7bd0e0cea 100644 --- a/crates/wasmtime/src/func.rs +++ b/crates/wasmtime/src/func.rs @@ -5,10 +5,13 @@ use anyhow::{bail, ensure, Context as _, Result}; use smallvec::{smallvec, SmallVec}; use std::cmp::max; use std::fmt; +use std::future::Future; use std::mem; use std::panic::{self, AssertUnwindSafe}; +use std::pin::Pin; use std::ptr::{self, NonNull}; use std::rc::Weak; +use std::task::{Context, Poll}; use wasmtime_environ::wasm::EntityIndex; use wasmtime_runtime::{ raise_user_trap, InstanceHandle, VMContext, VMFunctionBody, VMSharedSignatureIndex, @@ -34,6 +37,61 @@ use wasmtime_runtime::{ /// cloning process only performs a shallow clone, so two cloned `Func` /// instances are equivalent in their functionality. /// +/// # `Func` and `async` +/// +/// Functions from the perspective of WebAssembly are always synchronous. You +/// might have an `async` function in Rust, however, which you'd like to make +/// available from WebAssembly. Wasmtime supports asynchronously calling +/// WebAssembly through native stack switching. You can get some more +/// information about [asynchronous stores](Store::new_async), but from the +/// perspective of `Func` it's important to know that whether or not your +/// [`Store`] is asynchronous will dictate whether you call functions through +/// [`Func::call`] or [`Func::call_async`] (or the wrappers such as +/// [`Func::get0`] vs [`Func::get0_async`]). +/// +/// Note that asynchronous function APIs here are a bit trickier than their +/// synchronous bretheren. For example [`Func::new_async`] and +/// [`Func::wrapN_async`](Func::wrap1_async) take explicit state parameters to +/// allow you to close over the state in the returned future. It's recommended +/// that you pass state via these parameters instead of through the closure's +/// environment, which may give Rust lifetime errors. Additionally unlike +/// synchronous functions which can all get wrapped through [`Func::wrap`] +/// asynchronous functions need to explicitly wrap based on the number of +/// parameters that they have (e.g. no wasm parameters gives you +/// [`Func::wrap0_async`], one wasm parameter you'd use [`Func::wrap1_async`], +/// etc). Be sure to consult the documentation for [`Func::wrap`] for how the +/// wasm type signature is inferred from the Rust type signature. +/// +/// # To `Func::call` or to `Func::getN` +/// +/// There are four ways to call a `Func`. Half are asynchronus and half are +/// synchronous, corresponding to the type of store you're using. Within each +/// half you've got two choices: +/// +/// * Dynamically typed - if you don't statically know the signature of the +/// function that you're calling you'll be using [`Func::call`] or +/// [`Func::call_async`]. These functions take a variable-length slice of +/// "boxed" arguments in their [`Val`] representation. Additionally the +/// results are returned as an owned slice of [`Val`]. These methods are the +/// most heavily optimized due to the dynamic type checks that must occur, in +/// addition to some dynamic allocations for where to put all the arguments. +/// While this allows you to call all possible wasm function signatures, if +/// you're looking for a speedier alternative you can also use... +/// +/// * Statically typed - if you statically know the type signature of the wasm +/// function you're calling then you can use the [`Func::getN`](Func::get1) +/// family of functions where `N` is the number of wasm arguments the function +/// takes. Asynchronous users can use [`Func::getN_async`](Func::get1_async). +/// These functions will perform type validation up-front and then return a +/// specialized closure which can be used to invoke the wasm function. This +/// route involves no dynamic allocation and does not type-checks during +/// runtime, so it's recommended to use this where possible for maximal speed. +/// +/// Unfortunately a limitation of the code generation backend right now means +/// that the statically typed `getN` methods only work with wasm functions that +/// return 0 or 1 value. If 2 or more values are returned you'll need to use the +/// dynamic `call` API. We hope to fix this in the future, though! +/// /// # Examples /// /// One way to get a `Func` is from an [`Instance`] after you've instantiated @@ -148,15 +206,104 @@ pub struct Func { export: wasmtime_runtime::ExportFunction, } -macro_rules! getters { - ($( - $(#[$doc:meta])* - ($name:ident $(,$args:ident)*) - )*) => ($( - $(#[$doc])* +macro_rules! for_each_function_signature { + ($mac:ident) => { + $mac!(0); + $mac!(1 A1); + $mac!(2 A1 A2); + $mac!(3 A1 A2 A3); + $mac!(4 A1 A2 A3 A4); + $mac!(5 A1 A2 A3 A4 A5); + $mac!(6 A1 A2 A3 A4 A5 A6); + $mac!(7 A1 A2 A3 A4 A5 A6 A7); + $mac!(8 A1 A2 A3 A4 A5 A6 A7 A8); + $mac!(9 A1 A2 A3 A4 A5 A6 A7 A8 A9); + $mac!(10 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10); + $mac!(11 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11); + $mac!(12 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12); + $mac!(13 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13); + $mac!(14 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14); + $mac!(15 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 A15); + $mac!(16 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 A15 A16); + }; +} + +macro_rules! generate_get_methods { + ($num:tt $($args:ident)*) => (paste::paste! { + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func`] structure for more documentation. Returns an error + /// if the type parameters and return parameter provided don't match the + /// actual function's type signature. + /// + /// # Panics + /// + /// Panics if this is called on a function in an asynchronous store. #[allow(non_snake_case)] - pub fn $name<$($args,)* R>(&self) - -> anyhow::Result Result> + pub fn []<$($args,)* R>(&self) + -> Result Result> + where + $($args: WasmTy,)* + R: WasmTy, + { + assert!(!self.store().is_async(), concat!("must use `get", $num, "_async` on synchronous stores")); + self.[<_get $num>]::<$($args,)* R>() + } + + /// Extracts a natively-callable object from this `Func`, if the + /// signature matches. + /// + /// See the [`Func`] structure for more documentation. Returns an error + /// if the type parameters and return parameter provided don't match the + /// actual function's type signature. + /// + /// # Panics + /// + /// Panics if this is called on a function in a synchronous store. + #[allow(non_snake_case, warnings)] + #[cfg(feature = "async")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "async")))] + pub fn []<$($args,)* R>(&self) + -> Result WasmCall> + where + $($args: WasmTy + 'static,)* + R: WasmTy + 'static, + { + assert!(self.store().is_async(), concat!("must use `get", $num, "` on synchronous stores")); + + // TODO: ideally we wouldn't box up the future here but could + // instead name the future returned by `on_fiber`. Unfortunately + // that would require a bunch of inter-future borrows which are safe + // but gnarly to implement by hand. + // + // Otherwise the implementation here is pretty similar to + // `call_async` where we're just calling the `on_fiber` method to + // run the blocking work. Most of the goop here is juggling + // lifetimes and making sure everything lives long enough and + // closures have the right signatures. + // + // This is... less readable than ideal. + let func = self.[<_get $num>]::<$($args,)* R>()?; + let store = self.store().clone(); + Ok(move |$($args),*| { + let func = func.clone(); + let store = store.clone(); + WasmCall { + inner: Pin::from(Box::new(async move { + match store.on_fiber(|| func($($args,)*)).await { + Ok(result) => result, + Err(trap) => Err(trap), + } + })) + } + }) + } + + // Internal helper to share between `getN` and `getN_async` + #[allow(non_snake_case)] + fn [<_get $num>]<$($args,)* R>(&self) + -> Result Result + Clone> where $($args: WasmTy,)* R: WasmTy, @@ -228,7 +375,40 @@ macro_rules! getters { } }) } - )*) + }) +} + +macro_rules! generate_wrap_async_func { + ($num:tt $($args:ident)*) => (paste::paste!{ + /// Same as [`Func::wrap`], except the closure asynchronously produces + /// its result. For more information see the [`Func`] documentation. + /// + /// # Panics + /// + /// This function will panic if called with a non-asynchronous store. + #[allow(non_snake_case)] + #[cfg(feature = "async")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "async")))] + pub fn []( + store: &Store, + state: T, + func: impl for<'a> Fn(Caller<'a>, &'a T, $($args),*) -> Box + 'a> + 'static, + ) -> Func + where + T: 'static, + $($args: WasmTy,)* + R: WasmRet, + { + Func::wrap(store, move |caller: Caller<'_>, $($args: $args),*| { + let store = Store::upgrade(&caller.store).unwrap(); + let mut future = Pin::from(func(caller, &state, $($args),*)); + match store.block_on(future.as_mut()) { + Ok(ret) => ret.into_result(), + Err(e) => Err(e), + } + }) + } + }) } impl Func { @@ -324,6 +504,100 @@ impl Func { } } + /// Creates a new host-defined WebAssembly function which, when called, + /// will run the asynchronous computation defined by `func` to completion + /// and then return the result to WebAssembly. + /// + /// This function is the asynchronous analogue of [`Func::new`] and much of + /// that documentation applies to this as well. There are a few key + /// differences (besides being asynchronous) that are worth pointing out: + /// + /// * The state parameter `T` is passed to the provided function `F` on + /// each invocation. This is done so you can use the state in `T` in the + /// computation of the output future (the future can close over this + /// value). Unfortunately due to limitations of async-in-Rust right now + /// you **cannot** close over the captured variables in `F` itself in the + /// returned future. This means that you likely won't close over much + /// state in `F` and will instead use `T`. + /// + /// * The closure here returns a *boxed* future, not something that simply + /// implements a future. This is also unfortunately due to limitations in + /// Rust right now. + /// + /// Overall we're not super happy with this API signature and would love to + /// change it to make it more ergonomic. Despite this, however, you should + /// be able to still hook into asynchronous computations and plug them into + /// wasm. Improvements are always welcome with PRs! + /// + /// # Panics + /// + /// This function will panic if `store` is not an [asynchronous + /// store](Store::new_async). + /// + /// # Examples + /// + /// ``` + /// # use wasmtime::*; + /// # fn main() -> anyhow::Result<()> { + /// // Simulate some application-specific state as well as asynchronous + /// // functions to query that state. + /// struct MyDatabase { + /// // ... + /// } + /// + /// impl MyDatabase { + /// async fn get_row_count(&self) -> u32 { + /// // ... + /// # 100 + /// } + /// } + /// + /// let my_database = MyDatabase { + /// // ... + /// }; + /// + /// // Using `new_async` we can hook up into calling our async + /// // `get_row_count` function. + /// let store = Store::new_async(&Engine::default()); + /// let get_row_count_type = wasmtime::FuncType::new( + /// None, + /// Some(wasmtime::ValType::I32), + /// ); + /// let double = Func::new_async(&store, get_row_count_type, my_database, |_, database, params, results| { + /// Box::new(async move { + /// let count = database.get_row_count().await; + /// results[0] = Val::I32(count as i32); + /// Ok(()) + /// }) + /// }); + /// // ... + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "async")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "async")))] + pub fn new_async(store: &Store, ty: FuncType, state: T, func: F) -> Func + where + T: 'static, + F: for<'a> Fn( + Caller<'a>, + &'a T, + &'a [Val], + &'a mut [Val], + ) -> Box> + 'a> + + 'static, + { + assert!(store.is_async()); + Func::new(store, ty, move |caller, params, results| { + let store = Store::upgrade(&caller.store).unwrap(); + let mut future = Pin::from(func(caller, &state, params, results)); + match store.block_on(future.as_mut()) { + Ok(Ok(())) => Ok(()), + Ok(Err(trap)) | Err(trap) => Err(trap), + } + }) + } + pub(crate) unsafe fn from_caller_checked_anyfunc( store: &Store, anyfunc: *mut wasmtime_runtime::VMCallerCheckedAnyfunc, @@ -535,6 +809,8 @@ impl Func { func.into_func(store) } + for_each_function_signature!(generate_wrap_async_func); + pub(crate) fn sig_index(&self) -> VMSharedSignatureIndex { unsafe { self.export.anyfunc.as_ref().type_index } } @@ -579,9 +855,52 @@ impl Func { /// trap will occur. If a trap occurs while executing this function, then a /// trap will also be returned. /// - /// This function should not panic unless the underlying function itself + /// # Panics + /// + /// This function will panic if called on a function belonging to an async + /// store. Asynchronous stores must always use `call_async`. /// initiates a panic. pub fn call(&self, params: &[Val]) -> Result> { + assert!( + !self.store().is_async(), + "must use `call_async` on asynchronous stores", + ); + self._call(params) + } + + /// Invokes this function with the `params` given, returning the results + /// asynchronously. + /// + /// This function is the same as [`Func::call`] except that it is + /// asynchronous. This is only compatible with [asynchronous + /// stores](Store::new_async). + /// + /// It's important to note that the execution of WebAssembly will happen + /// synchronously in the `poll` method of the future returned from this + /// function. Wasmtime does not manage its own thread pool or similar to + /// execute WebAssembly in. Future `poll` methods are generally expected to + /// resolve quickly, so it's recommended that you run or poll this future + /// in a "blocking context". + /// + /// For more information see the documentation on [asynchronous + /// stores](Store::new_async). + /// + /// # Panics + /// + /// Panics if this is called on a function in a synchronous store. This + /// only works with functions defined within an asynchronous store. + #[cfg(feature = "async")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "async")))] + pub async fn call_async(&self, params: &[Val]) -> Result> { + assert!( + self.store().is_async(), + "must use `call` on synchronous stores", + ); + let result = self.store().on_fiber(|| self._call(params)).await??; + Ok(result) + } + + fn _call(&self, params: &[Val]) -> Result> { // We need to perform a dynamic check that the arguments given to us // match the signature of this function and are appropriate to pass to // this function. This involves checking to make sure we have the right @@ -669,127 +988,7 @@ impl Func { } } - getters! { - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get0) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// This function serves as an optimized version of the [`Func::call`] - /// method if the type signature of a function is statically known to - /// the program. This method is faster than `call` on a few metrics: - /// - /// * Runtime type-checking only happens once, when this method is - /// called. - /// * The result values, if any, aren't boxed into a vector. - /// * Arguments and return values don't go through boxing and unboxing. - /// * No trampolines are used to transfer control flow to/from JIT code, - /// instead this function jumps directly into JIT code. - /// - /// For more information about which Rust types match up to which wasm - /// types, see the documentation on [`Func::wrap`]. - /// - /// # Return - /// - /// This function will return `None` if the type signature asserted - /// statically does not match the runtime type signature. `Some`, - /// however, will be returned if the underlying function takes one - /// parameter of type `A` and returns the parameter `R`. Currently `R` - /// can either be `()` (no return values) or one wasm type. At this time - /// a multi-value return isn't supported. - /// - /// The returned closure will always return a `Result` and an - /// `Err` is returned if a trap happens while the wasm is executing. - (get1, A1) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get2, A1, A2) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get3, A1, A2, A3) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get4, A1, A2, A3, A4) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get5, A1, A2, A3, A4, A5) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get6, A1, A2, A3, A4, A5, A6) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get7, A1, A2, A3, A4, A5, A6, A7) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get8, A1, A2, A3, A4, A5, A6, A7, A8) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get9, A1, A2, A3, A4, A5, A6, A7, A8, A9) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get10, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get11, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get12, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get13, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get14, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14) - - /// Extracts a natively-callable object from this `Func`, if the - /// signature matches. - /// - /// See the [`Func::get1`] method for more documentation. - (get15, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15) - } + for_each_function_signature!(generate_get_methods); /// Get a reference to this function's store. pub fn store(&self) -> &Store { @@ -900,6 +1099,11 @@ pub unsafe trait WasmRet { #[doc(hidden)] type Abi: Copy; + // The "ok" version of this, meaning that which is returned if there is no + // error. + #[doc(hidden)] + type Ok: WasmTy; + // Same as `WasmTy::compatible_with_store`. #[doc(hidden)] fn compatible_with_store<'a>(&self, store: WeakStore<'a>) -> bool; @@ -932,6 +1136,10 @@ pub unsafe trait WasmRet { // Same as `WasmTy::store_to_args`. #[doc(hidden)] unsafe fn store_to_args(abi: Self::Abi, ptr: *mut u128); + + // Converts this result into an explicit `Result` to match on. + #[doc(hidden)] + fn into_result(self) -> Result; } unsafe impl WasmTy for () { @@ -1335,6 +1543,7 @@ where T: WasmTy, { type Abi = ::Abi; + type Ok = T; #[inline] fn compatible_with_store<'a>(&self, store: WeakStore<'a>) -> bool { @@ -1369,6 +1578,11 @@ where unsafe fn store_to_args(abi: Self::Abi, ptr: *mut u128) { ::store_to_args(abi, ptr) } + + #[inline] + fn into_result(self) -> Result { + Ok(self) + } } unsafe impl WasmRet for Result @@ -1376,6 +1590,7 @@ where T: WasmTy, { type Abi = ::Abi; + type Ok = T; #[inline] fn compatible_with_store<'a>(&self, store: WeakStore<'a>) -> bool { @@ -1419,6 +1634,11 @@ where unsafe fn store_to_args(abi: Self::Abi, ptr: *mut u128) { ::store_to_args(abi, ptr); } + + #[inline] + fn into_result(self) -> Result { + self + } } /// Internal trait implemented for all arguments that can be passed to @@ -1431,6 +1651,14 @@ pub trait IntoFunc { fn into_func(self, store: &Store) -> Func; } +/// TODO +#[cfg(feature = "async")] +#[cfg_attr(nightlydoc, doc(cfg(feature = "async")))] +pub trait IntoFuncAsync { + #[doc(hidden)] + fn into_func(self, store: &Store, state: T) -> Func; +} + /// A structure representing the *caller's* context when creating a function /// via [`Func::wrap`]. /// @@ -1552,9 +1780,7 @@ unsafe fn raise_cross_store_trap() -> ! { } macro_rules! impl_into_func { - ($( - ($($args:ident)*) - )*) => ($( + ($num:tt $($args:ident)*) => { // Implement for functions without a leading `&Caller` parameter, // delegating to the implementation below which does have the leading // `Caller` parameter. @@ -1699,27 +1925,23 @@ macro_rules! impl_into_func { } } } - )*) + } } -impl_into_func! { - () - (A1) - (A1 A2) - (A1 A2 A3) - (A1 A2 A3 A4) - (A1 A2 A3 A4 A5) - (A1 A2 A3 A4 A5 A6) - (A1 A2 A3 A4 A5 A6 A7) - (A1 A2 A3 A4 A5 A6 A7 A8) - (A1 A2 A3 A4 A5 A6 A7 A8 A9) - (A1 A2 A3 A4 A5 A6 A7 A8 A9 A10) - (A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11) - (A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12) - (A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13) - (A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14) - (A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 A15) - (A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 A15 A16) +for_each_function_signature!(impl_into_func); + +/// Returned future from the [`Func::get1_async`] family of methods, used to +/// represent an asynchronous call into WebAssembly. +pub struct WasmCall { + inner: Pin>>>, +} + +impl Future for WasmCall { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.inner.as_mut().poll(cx) + } } #[test] diff --git a/crates/wasmtime/src/instance.rs b/crates/wasmtime/src/instance.rs index 47115d90ae90..ed4b1d670c91 100644 --- a/crates/wasmtime/src/instance.rs +++ b/crates/wasmtime/src/instance.rs @@ -90,10 +90,19 @@ impl Instance { /// see why it failed, or bubble it upwards. If you'd like to specifically /// check for trap errors, you can use `error.downcast::()`. /// + /// # Panics + /// + /// This function will panic if called within an asynchronous store + /// (created with [`Store::new_async`]). + /// /// [inst]: https://webassembly.github.io/spec/core/exec/modules.html#exec-instantiation /// [issue]: https://github.com/bytecodealliance/wasmtime/issues/727 /// [`ExternType`]: crate::ExternType pub fn new(store: &Store, module: &Module, imports: &[Extern]) -> Result { + assert!( + !store.is_async(), + "cannot instantiate synchronously within an asynchronous store" + ); let mut i = Instantiator::new(store, module, imports)?; loop { if let Some((instance, items)) = i.step()? { @@ -105,6 +114,50 @@ impl Instance { } } + /// Same as [`Instance::new`], except for usage in [asynchronous stores]. + /// + /// For more details about this function see the documentation on + /// [`Instance::new`]. The only difference between these two methods is that + /// this one will asynchronously invoke the wasm start function in case it + /// calls any imported function which is an asynchronous host function (e.g. + /// created with [`Func::new_async`](crate::Func::new_async). + /// + /// # Panics + /// + /// This function will panic if called within a non-asynchronous store + /// (created with [`Store::new`]). This is only compatible with asynchronous + /// stores created with [`Store::new_async`]. + /// + /// [asynchronous stores]: Store::new_async + #[cfg(feature = "async")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "async")))] + pub async fn new_async( + store: &Store, + module: &Module, + imports: &[Extern], + ) -> Result { + assert!( + store.is_async(), + "cannot instantiate asynchronously within a synchronous store" + ); + + assert!( + !store.is_async(), + "cannot instantiate synchronously within an asynchronous store" + ); + let mut i = Instantiator::new(store, module, imports)?; + loop { + if let Some((instance, items)) = i.step()? { + store + .on_fiber(|| Instantiator::start_raw(&instance)) + .await??; + if let Some(items) = items { + break Ok(Instance::from_wasmtime(&items, store)); + } + } + } + } + pub(crate) fn from_wasmtime(handle: &RuntimeInstance, store: &Store) -> Instance { Instance { items: handle.clone(), @@ -121,6 +174,33 @@ impl Instance { ty } + fn start(&self) -> Result<(), Error> { + let start_func = match self.handle.module().start_func { + Some(func) => func, + None => return Ok(()), + }; + + let f = match self + .handle + .lookup_by_declaration(&EntityIndex::Function(start_func)) + { + wasmtime_runtime::Export::Function(f) => f, + _ => unreachable!(), // valid modules shouldn't hit this + }; + let vmctx_ptr = self.handle.vmctx_ptr(); + unsafe { + super::func::invoke_wasm_and_catch_traps(vmctx_ptr, self.store(), || { + mem::transmute::< + *const VMFunctionBody, + unsafe extern "C" fn(*mut VMContext, *mut VMContext), + >(f.anyfunc.as_ref().func_ptr.as_ptr())( + f.anyfunc.as_ref().vmctx, vmctx_ptr + ) + })?; + } + Ok(()) + } + /// Returns the associated [`Store`] that this `Instance` is compiled into. /// /// This is the [`Store`] that generally serves as a sort of global cache diff --git a/crates/wasmtime/src/lib.rs b/crates/wasmtime/src/lib.rs index 9deb3638d060..7613458b4f4a 100644 --- a/crates/wasmtime/src/lib.rs +++ b/crates/wasmtime/src/lib.rs @@ -140,6 +140,38 @@ //! create a "wasi instance" and then add all of its items into a [`Linker`], //! which can then be used to instantiate a [`Module`] that uses WASI. //! +//! ## Crate Features +//! +//! The `wasmtime` crate comes with a number of compile-time features that can +//! be used to customize what features it supports. Some of these features are +//! just internal details, but some affect the public API of the `wasmtime` +//! crate. Be sure to check the API you're using to see if any crate features +//! are enabled. +//! +//! * `cache` - Enabled by default, this feature adds support for wasmtime to +//! perform internal caching of modules in a global location. This must still +//! be enabled explicitly through [`Config::cache_config_load`] or +//! [`Config::cache_config_load_default`]. +//! +//! * `wat` - Enabled by default, this feature adds support for accepting the +//! text format of WebAssembly in [`Module::new`]. The text format will be +//! automatically recognized and translated to binary when compiling a +//! module. +//! +//! * `parallel-compilation` - Enabled by default, this feature enables support +//! for compiling functions of a module in parallel with `rayon`. +//! +//! * `async` - Enabled by default, this feature enables APIs and runtime +//! support for defining asynchronous host functions and calling WebAssembly +//! asynchronously. +//! +//! * `jitdump` - Enabled by default, this feature compiles in support for the +//! jitdump runtime profilng format. The profiler can be selected with +//! [`Config::profiler`]. +//! +//! * `vtune` - Not enabled by default, this feature compiles in support for +//! supporting VTune profiling of JIT code. +//! //! ## Examples //! //! In addition to the examples below be sure to check out the [online embedding @@ -233,6 +265,8 @@ #![deny(missing_docs, broken_intra_doc_links)] #![doc(test(attr(deny(warnings))))] #![doc(test(attr(allow(dead_code, unused_variables, unused_mut))))] +#![cfg_attr(nightlydoc, feature(doc_cfg))] +#![cfg_attr(not(feature = "default"), allow(dead_code, unused_imports))] mod config; mod engine; diff --git a/crates/wasmtime/src/store.rs b/crates/wasmtime/src/store.rs index 10564d79dd43..20b5cc694fda 100644 --- a/crates/wasmtime/src/store.rs +++ b/crates/wasmtime/src/store.rs @@ -1,16 +1,20 @@ use crate::frame_info::StoreFrameInfo; use crate::sig_registry::SignatureRegistry; use crate::trampoline::StoreInstanceHandle; -use crate::{Engine, Module}; +use crate::{Engine, Module, Trap}; use anyhow::{bail, Result}; use std::any::Any; use std::cell::{Cell, RefCell}; use std::collections::HashSet; use std::convert::TryFrom; use std::fmt; +use std::future::Future; use std::hash::{Hash, Hasher}; +use std::pin::Pin; +use std::ptr; use std::rc::{Rc, Weak}; use std::sync::Arc; +use std::task::{Context, Poll}; use wasmtime_environ::wasm; use wasmtime_jit::{CompiledModule, ModuleCode, TypeTables}; use wasmtime_runtime::{ @@ -55,6 +59,7 @@ pub struct Store { } pub(crate) struct StoreInner { + is_async: bool, engine: Engine, interrupts: Arc, signatures: RefCell, @@ -73,10 +78,17 @@ pub(crate) struct StoreInner { instance_count: Cell, memory_count: Cell, table_count: Cell, +<<<<<<< HEAD /// An adjustment to add to the fuel consumed value in `interrupts` above /// to get the true amount of fuel consumed. fuel_adj: Cell, +======= + #[cfg(feature = "async")] + current_suspend: Cell<*const wasmtime_fiber::Suspend, (), Result<(), Trap>>>, + #[cfg(feature = "async")] + current_poll_cx: Cell<*mut Context<'static>>, +>>>>>>> Implement support for `async` functions in Wasmtime } struct HostInfoKey(VMExternRef); @@ -100,7 +112,69 @@ impl Hash for HostInfoKey { impl Store { /// Creates a new store to be associated with the given [`Engine`]. + /// + /// Note that this `Store` cannot be used with asynchronous host calls nor + /// can it be used to call functions asynchronously. For that you'll want to + /// use [`Store::new_async`]. pub fn new(engine: &Engine) -> Store { + Store::_new(engine, false) + } + + /// Creates a new async store to be associated with the given [`Engine`]. + /// + /// The returned store can optionally define host functions with `async`. + /// Instances created and functions called within the returned `Store` + /// *must* be called through their asynchronous APIs, however. For example + /// using [`Func::call`](crate::Func::call) will panic in the returned + /// store. + /// + /// # Asynchronous Wasm + /// + /// WebAssembly does not currently have a way to specify at the bytecode + /// level what is and isn't async. Host-defined functions, however, may be + /// defined as `async`. WebAssembly imports always appear synchronous, which + /// gives rise to a bit of an impedence mismatch here. To solve this + /// Wasmtime supports "asynchronous stores" which enables calling these + /// asynchronous functions in a way that looks synchronous to the executing + /// WebAssembly code. + /// + /// An asynchronous store must always invoke wasm code asynchronously, + /// meaning we'll always represent its computation as a + /// [`Future`](std::future::Future). The `poll` method of the futures + /// returned by Wasmtime will perform the actual work of calling the + /// WebAssembly. Wasmtime won't manage its own thread pools or similar, + /// that's left up to the embedder. + /// + /// To implement futures in a way that WebAssembly sees asynchronous host + /// functions as synchronous, all async Wasmtime futures will execute on a + /// separately allocated native stack from the thread otherwise executing + /// Wasmtime. This separate native stack can then be switched to and from. + /// Using this whenever an `async` host function returns a future that + /// resolves to `Pending` we switch away from the temporary stack back to + /// the main stack and propagate the `Pending` status. + /// + /// Note that the intention with supporting asynchronous WebAssembly in + /// Wasmtime is to primarily provide the *ability* to suspend wasm + /// computation while the host is waiting for a result. You'll likely need + /// to continue to massage the exact future returned to suit your + /// application's needs. For example the `Future::poll` method is not + /// expected to take a large amount of time, but executing WebAssembly can + /// take arbitrarily long. This means that you may want to run futures on a + /// dedicated thread pool in some scenarios, or perhaps have some form of + /// "fuel" in other scenarios (Wasmtime hopes to support fuel natively in + /// the future). + /// + /// In general it's encouraged that the integration with `async` and + /// wasmtime is designed early on in your embedding of Wasmtime to ensure + /// that it's planned that WebAssembly executes in the right context of your + /// application. + #[cfg(feature = "async")] + #[cfg_attr(nightlydoc, doc(cfg(feature = "async")))] + pub fn new_async(engine: &Engine) -> Store { + Store::_new(engine, true) + } + + fn _new(engine: &Engine, is_async: bool) -> Store { // Ensure that wasmtime_runtime's signal handlers are configured. Note // that at the `Store` level it means we should perform this // once-per-thread. Platforms like Unix, however, only require this @@ -110,6 +184,7 @@ impl Store { Store { inner: Rc::new(StoreInner { + is_async, engine: engine.clone(), interrupts: Arc::new(Default::default()), signatures: RefCell::new(Default::default()), @@ -123,6 +198,10 @@ impl Store { memory_count: Default::default(), table_count: Default::default(), fuel_adj: Cell::new(0), + #[cfg(feature = "async")] + current_suspend: Cell::new(ptr::null()), + #[cfg(feature = "async")] + current_poll_cx: Cell::new(ptr::null_mut()), }), } } @@ -491,6 +570,186 @@ impl Store { } } } + + pub(crate) fn is_async(&self) -> bool { + self.inner.is_async + } + + /// Blocks on the asynchronous computation represented by `future` and + /// produces the result here, in-line. + /// + /// This function is designed to only work when it's currently executing on + /// a native fiber. This fiber provides the ability for us to handle the + /// future's `Pending` state as "jump back to whomever called the fiber in + /// an asynchronous fashion and propagate `Pending`". This tight coupling + /// with `on_fiber` below is what powers the asynchronicity of calling wasm. + /// Note that the asynchronous part only applies to host functions, wasm + /// itself never really does anything asynchronous at this time. + /// + /// This function takes a `future` and will (appear to) synchronously wait + /// on the result. While this function is executing it will fiber switch + /// to-and-from the original frame calling `on_fiber` which should be a + /// guarantee due to how async stores are configured. + /// + /// The return value here is either the output of the future `T`, or a trap + /// which represents that the asynchronous computation was cancelled. It is + /// not recommended to catch the trap and try to keep executing wasm, so + /// we've tried to liberally document this. + #[cfg(feature = "async")] + pub(crate) fn block_on( + &self, + mut future: Pin<&mut dyn Future>, + ) -> Result { + debug_assert!(self.is_async()); + + // Take our current `Suspend` context which was configured as soon as + // our fiber started. Note that we must load it at the front here and + // save it on our stack frame. While we're polling the future other + // fibers may be started for recursive computations, and the current + // suspend context is only preserved at the edges of the fiber, not + // during the fiber itself. + // + // For a little bit of extra safety we also replace the current value + // with null to try to catch any accidental bugs on our part early. + // This is all pretty unsafe so we're trying to be careful... + // + // Note that there should be a segfaulting test in `async_functions.rs` + // if this `Reset` is removed. + let suspend = self.inner.current_suspend.replace(ptr::null()); + let _reset = Reset(&self.inner.current_suspend, suspend); + assert!(!suspend.is_null()); + + loop { + let future_result = unsafe { + let current_poll_cx = self.inner.current_poll_cx.replace(ptr::null_mut()); + let _reset = Reset(&self.inner.current_poll_cx, current_poll_cx); + assert!(!current_poll_cx.is_null()); + future.as_mut().poll(&mut *current_poll_cx) + }; + match future_result { + Poll::Ready(t) => break Ok(t), + Poll::Pending => {} + } + unsafe { + (*suspend).suspend(())?; + } + } + } + + /// Executes a synchronous computation `func` asynchronously on a new fiber. + /// + /// This function will convert the synchronous `func` into an asynchronous + /// future. This is done by running `func` in a fiber on a separate native + /// stack which can be suspended and resumed from. + /// + /// Most of the nitty-gritty here is how we juggle the various contexts + /// necessary to suspend the fiber later on and poll sub-futures. It's hoped + /// that the various comments are illuminating as to what's going on here. + #[cfg(feature = "async")] + pub(crate) async fn on_fiber(&self, func: impl FnOnce() -> R) -> Result { + debug_assert!(self.is_async()); + + // TODO: allocation of a fiber should be much more abstract where we + // shouldn't be allocating huge stacks on every async wasm function call. + let mut slot = None; + let fiber = wasmtime_fiber::Fiber::new(10 * 1024 * 1024, |keep_going, suspend| { + // First check and see if we were interrupted/dropped, and only + // continue if we haven't been. + keep_going?; + + // Configure our store's suspension context for the rest of the + // execution of this fiber. Note that a raw pointer is stored here + // which is only valid for the duration of this closure. + // Consequently we at least replace it with the previous value when + // we're done. This reset is also required for correctness because + // otherwise our value will overwrite another active fiber's value. + // There should be a test that segfaults in `async_functions.rs` if + // this `Replace` is removed. + let prev = self.inner.current_suspend.replace(suspend); + let _reset = Reset(&self.inner.current_suspend, prev); + + slot = Some(func()); + Ok(()) + }) + .map_err(|e| Trap::from(anyhow::Error::from(e)))?; + + // Once we have the fiber representing our synchronous computation, we + // wrap that in a custom future implementation which does the + // translation from the future protocol to our fiber API. + FiberFuture { fiber, store: self }.await?; + return Ok(slot.unwrap()); + + struct FiberFuture<'a> { + fiber: wasmtime_fiber::Fiber<'a, Result<(), Trap>, (), Result<(), Trap>>, + store: &'a Store, + } + + impl Future for FiberFuture<'_> { + type Output = Result<(), Trap>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + // We need to carry over this `cx` into our fiber's runtime + // for when it trys to poll sub-futures that are created. Doing + // this must be done unsafely, however, since `cx` is only alive + // for this one singular function call. Here we do a `transmute` + // to extend the lifetime of `Context` so it can be stored in + // our `Store`, and then we replace the current polling context + // with this one. + // + // Note that the replace is done for weird situations where + // futures might be switching contexts and there's multiple + // wasmtime futures in a chain of futures. + // + // On exit from this function, though, we reset the polling + // context back to what it was to signify that `Store` no longer + // has access to this pointer. + let cx = + unsafe { std::mem::transmute::<&mut Context<'_>, *mut Context<'static>>(cx) }; + let prev = self.store.inner.current_poll_cx.replace(cx); + let _reste = Reset(&self.store.inner.current_poll_cx, prev); + + // After that's set up we resume execution of the fiber, which + // may also start the fiber for the first time. This either + // returns `Ok` saying the fiber finished (yay!) or it returns + // `Err` with the payload passed to `suspend`, which in our case + // is `()`. If `Err` is returned that means the fiber polled a + // future but it said "Pending", so we propagate that here. + match self.fiber.resume(Ok(())) { + Ok(result) => Poll::Ready(result), + Err(()) => Poll::Pending, + } + } + } + + // Dropping futures is pretty special in that it means the future has + // been requested to be cancelled. Here we run the risk of dropping an + // in-progress fiber, and if we were to do nothing then the fiber would + // leak all its owned stack resources. + // + // To handle this we implement `Drop` here and, if the fiber isn't done, + // resume execution of the fiber saying "hey please stop you're + // interrupted". Our `Trap` created here (which has the stack trace + // of whomever dropped us) will then get propagated in whatever called + // `block_on`, and the idea is that the trap propagates all the way back + // up to the original fiber start, finishing execution. + // + // We don't actually care about the fiber's return value here (one one's + // around to look at it), we just assert the fiber finished to + // completion. + impl Drop for FiberFuture<'_> { + fn drop(&mut self) { + if self.fiber.done() { + return; + } + let result = self.fiber.resume(Err(Trap::new("future dropped"))); + // This resumption with an error should always complete the + // fiber. While it's technically possible for host code to catch + // the trap and re-resume, we'd ideally like to signal that to + // callers that they shouldn't be doing that. + debug_assert!(result.is_ok()); + } + } + } } unsafe impl TrapInfo for Store { @@ -599,3 +858,11 @@ impl Hash for ArcModuleCode { Arc::as_ptr(&self.0).hash(hasher) } } + +struct Reset<'a, T: Copy>(&'a Cell, T); + +impl Drop for Reset<'_, T> { + fn drop(&mut self) { + self.0.set(self.1); + } +} diff --git a/tests/all/async_functions.rs b/tests/all/async_functions.rs new file mode 100644 index 000000000000..cd964bec099a --- /dev/null +++ b/tests/all/async_functions.rs @@ -0,0 +1,303 @@ +use std::cell::Cell; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; +use wasmtime::*; + +fn async_store() -> Store { + let engine = Engine::default(); + Store::new_async(&engine) +} + +#[test] +fn smoke() { + let store = async_store(); + let func = Func::new_async( + &store, + FuncType::new(None, None), + (), + move |_caller, _state, _params, _results| Box::new(async { Ok(()) }), + ); + run(func.call_async(&[])).unwrap(); + run(func.call_async(&[])).unwrap(); + let future1 = func.call_async(&[]); + let future2 = func.call_async(&[]); + run(future2).unwrap(); + run(future1).unwrap(); +} + +#[test] +fn smoke_with_suspension() { + let store = async_store(); + let func = Func::new_async( + &store, + FuncType::new(None, None), + (), + move |_caller, _state, _params, _results| { + Box::new(async { + PendingOnce::default().await; + Ok(()) + }) + }, + ); + run(func.call_async(&[])).unwrap(); + run(func.call_async(&[])).unwrap(); + let future1 = func.call_async(&[]); + let future2 = func.call_async(&[]); + run(future2).unwrap(); + run(future1).unwrap(); +} + +#[test] +fn smoke_get_with_suspension() { + let store = async_store(); + let func = Func::new_async( + &store, + FuncType::new(None, None), + (), + move |_caller, _state, _params, _results| { + Box::new(async { + PendingOnce::default().await; + Ok(()) + }) + }, + ); + let func = func.get0_async::<()>().unwrap(); + run(func()).unwrap(); + run(func()).unwrap(); + let future1 = func(); + let future2 = func(); + run(future2).unwrap(); + run(future1).unwrap(); +} + +#[test] +fn recursive_call() { + let store = async_store(); + let async_wasm_func = Rc::new(Func::new_async( + &store, + FuncType::new(None, None), + (), + |_caller, _state, _params, _results| { + Box::new(async { + PendingOnce::default().await; + Ok(()) + }) + }, + )); + let weak = Rc::downgrade(&async_wasm_func); + + // Create an imported function which recursively invokes another wasm + // function asynchronously, although this one is just our own host function + // which suffices for this test. + let func2 = Func::new_async( + &store, + FuncType::new(None, None), + (), + move |_caller, _state, _params, _results| { + let async_wasm_func = weak.upgrade().unwrap(); + Box::new(async move { + async_wasm_func.call_async(&[]).await?; + Ok(()) + }) + }, + ); + + // Create an instance which calls an async import twice. + let module = Module::new( + store.engine(), + " + (module + (import \"\" \"\" (func)) + (func (export \"\") + ;; call imported function which recursively does an async + ;; call + call 0 + ;; do it again, and our various pointers all better align + call 0)) + ", + ) + .unwrap(); + + run(async { + let instance = Instance::new_async(&store, &module, &[func2.into()]).await?; + let func = instance.get_func("").unwrap(); + func.call_async(&[]).await + }) + .unwrap(); +} + +#[test] +fn suspend_while_suspending() { + let store = async_store(); + + // Create a synchronous function which calls our asynchronous function and + // runs it locally. This shouldn't generally happen but we know everything + // is synchronous in this test so it's fine for us to do this. + // + // The purpose of this test is intended to stress various cases in how + // we manage pointers in ways that are not necessarily common but are still + // possible in safe code. + let async_thunk = Rc::new(Func::new_async( + &store, + FuncType::new(None, None), + (), + |_caller, _state, _params, _results| Box::new(async { Ok(()) }), + )); + let weak = Rc::downgrade(&async_thunk); + let sync_call_async_thunk = Func::new( + &store, + FuncType::new(None, None), + move |_caller, _params, _results| { + let async_thunk = weak.upgrade().unwrap(); + run(async_thunk.call_async(&[]))?; + Ok(()) + }, + ); + + // A small async function that simply awaits once to pump the loops and + // then finishes. + let async_import = Func::new_async( + &store, + FuncType::new(None, None), + (), + move |_caller, _state, _params, _results| { + Box::new(async move { + PendingOnce::default().await; + Ok(()) + }) + }, + ); + + let module = Module::new( + store.engine(), + " + (module + (import \"\" \"\" (func $sync_call_async_thunk)) + (import \"\" \"\" (func $async_import)) + (func (export \"\") + ;; Set some store-local state and pointers + call $sync_call_async_thunk + ;; .. and hopefully it's all still configured correctly + call $async_import)) + ", + ) + .unwrap(); + run(async { + let instance = Instance::new_async( + &store, + &module, + &[sync_call_async_thunk.into(), async_import.into()], + ) + .await?; + let func = instance.get_func("").unwrap(); + func.call_async(&[]).await + }) + .unwrap(); +} + +#[test] +fn cancel_during_run() { + let store = async_store(); + let state = Rc::new(Cell::new(0)); + let state2 = state.clone(); + + let async_thunk = Func::new_async( + &store, + FuncType::new(None, None), + (), + move |_caller, _state, _params, _results| { + assert_eq!(state2.get(), 0); + state2.set(1); + let dtor = SetOnDrop(state2.clone()); + Box::new(async move { + drop(&dtor); + PendingOnce::default().await; + Ok(()) + }) + }, + ); + // Shouldn't have called anything yet... + assert_eq!(state.get(), 0); + + // Create our future, but as per async conventions this still doesn't + // actually do anything. No wasm or host function has been called yet. + let mut future = Pin::from(Box::new(async_thunk.call_async(&[]))); + assert_eq!(state.get(), 0); + + // Push the future forward one tick, which actually runs the host code in + // our async func. Our future is designed to be pending once, however. + let poll = future + .as_mut() + .poll(&mut Context::from_waker(&dummy_waker())); + assert!(poll.is_pending()); + assert_eq!(state.get(), 1); + + // Now that our future is running (on a separate, now-suspended fiber), drop + // the future and that should deallocate all the Rust bits as well. + drop(future); + assert_eq!(state.get(), 2); + + struct SetOnDrop(Rc>); + + impl Drop for SetOnDrop { + fn drop(&mut self) { + assert_eq!(self.0.get(), 1); + self.0.set(2); + } + } +} + +#[derive(Default)] +struct PendingOnce { + already_polled: bool, +} + +impl Future for PendingOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + if self.already_polled { + Poll::Ready(()) + } else { + self.already_polled = true; + cx.waker().wake_by_ref(); + Poll::Pending + } + } +} + +fn run(future: F) -> F::Output { + let mut f = Pin::from(Box::new(future)); + let waker = dummy_waker(); + let mut cx = Context::from_waker(&waker); + loop { + match f.as_mut().poll(&mut cx) { + Poll::Ready(val) => break val, + Poll::Pending => {} + } + } +} + +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/tests/all/main.rs b/tests/all/main.rs index c20b2dddce03..526c5eb4c6b1 100644 --- a/tests/all/main.rs +++ b/tests/all/main.rs @@ -1,3 +1,4 @@ +mod async_functions; mod cli_tests; mod custom_signal_handler; mod debug;