Implement support for async functions in Wasmtime (#2434)
* Implement support for `async` functions in Wasmtime This is an implementation of [RFC 2] in Wasmtime which is to support `async`-defined host functions. At a high level support is added by executing WebAssembly code that might invoke an asynchronous host function on a separate native stack. When the host function's future is not ready we switch back to the main native stack to continue execution. There's a whole bunch of details in this commit, and it's a bit much to go over them all here in this commit message. The most important changes here are: * A new `wasmtime-fiber` crate has been written to manage the low-level details of stack-switching. Unixes use `mmap` to allocate a stack and Windows uses the native fibers implementation. We'll surely want to refactor this to move stack allocation elsewhere in the future. Fibers are intended to be relatively general with a lot of type paremters to fling values back and forth across suspension points. The whole crate is a giant wad of `unsafe` unfortunately and involves handwritten assembly with custom dwarf CFI directives to boot. Definitely deserves a close eye in review! * The `Store` type has two new methods -- `block_on` and `on_fiber` which bridge between the async and non-async worlds. Lots of unsafe fiddly bits here as we're trying to communicate context pointers between disparate portions of the code. Extra eyes and care in review is greatly appreciated. * The APIs for binding `async` functions are unfortunately pretty ugly in `Func`. This is mostly due to language limitations and compiler bugs (I believe) in Rust. Instead of `Func::wrap` we have a `Func::wrapN_async` family of methods, and we've also got a whole bunch of `Func::getN_async` methods now too. It may be worth rethinking the API of `Func` to try to make the documentation page actually grok'able. This isn't super heavily tested but the various test should suffice for engaging hopefully nearly all the infrastructure in one form or another. This is just the start though! [RFC 2]: https://github.com/bytecodealliance/rfcs/pull/2 * Add wasmtime-fiber to publish script * Save vector/float registers on ARM too. * Fix a typo * Update lock file * Implement periodically yielding with fuel consumption This commit implements APIs on `Store` to periodically yield execution of futures through the consumption of fuel. When fuel runs out a future's execution is yielded back to the caller, and then upon resumption fuel is re-injected. The goal of this is to allow cooperative multi-tasking with futures. * Fix compile without async * Save/restore the frame pointer in fiber switching Turns out this is another caller-saved register! * Simplify x86_64 fiber asm Take a leaf out of aarch64's playbook and don't have extra memory to load/store these arguments, instead leverage how `wasmtime_fiber_switch` already loads a bunch of data into registers which we can then immediately start using on a fiber's start without any extra memory accesses. * Add x86 support to wasmtime-fiber * Add ARM32 support to fiber crate * Make fiber build file probing more flexible * Use CreateFiberEx on Windows * Remove a stray no-longer-used trait declaration * Don't reach into `Caller` internals * Tweak async fuel to eventually run out. With fuel it's probably best to not provide any way to inject infinite fuel. * Fix some typos * Cleanup asm a bit * Use a shared header file to deduplicate some directives * Guarantee hidden visibility for functions * Enable gc-sections on macOS x86_64 * Add `.type` annotations for ARM * Update lock file * Fix compile error * Review comments
This commit is contained in:
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -1781,6 +1781,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1"
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
@@ -3120,6 +3126,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"libc",
|
||||
"log",
|
||||
"paste",
|
||||
"region",
|
||||
"rustc-demangle",
|
||||
"serde",
|
||||
@@ -3130,6 +3137,7 @@ dependencies = [
|
||||
"wasmparser",
|
||||
"wasmtime-cache",
|
||||
"wasmtime-environ",
|
||||
"wasmtime-fiber",
|
||||
"wasmtime-jit",
|
||||
"wasmtime-profiling",
|
||||
"wasmtime-runtime",
|
||||
@@ -3285,6 +3293,16 @@ dependencies = [
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime-fiber"
|
||||
version = "0.23.0"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"cc",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime-fuzz"
|
||||
version = "0.0.0"
|
||||
|
||||
30
crates/fiber/Cargo.toml
Normal file
30
crates/fiber/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "wasmtime-fiber"
|
||||
version = "0.23.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"
|
||||
25
crates/fiber/build.rs
Normal file
25
crates/fiber/build.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
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();
|
||||
|
||||
let family_file = format!("src/arch/{}.c", family);
|
||||
let arch_file = format!("src/arch/{}.S", arch);
|
||||
if fs::metadata(&family_file).is_ok() {
|
||||
build.file(&family_file);
|
||||
} else if fs::metadata(&arch_file).is_ok() {
|
||||
build.file(&arch_file);
|
||||
} 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");
|
||||
}
|
||||
122
crates/fiber/src/arch/aarch64.S
Normal file
122
crates/fiber/src/arch/aarch64.S
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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.
|
||||
|
||||
#include "header.h"
|
||||
|
||||
// fn(top_of_stack(%x0): *mut u8)
|
||||
HIDDEN(wasmtime_fiber_switch)
|
||||
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.
|
||||
stp lr, fp, [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]!
|
||||
stp d9, d8, [sp, -16]!
|
||||
stp d11, d10, [sp, -16]!
|
||||
stp d13, d12, [sp, -16]!
|
||||
stp d15, d14, [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 d15, d14, [sp], 16
|
||||
ldp d13, d12, [sp], 16
|
||||
ldp d11, d10, [sp], 16
|
||||
ldp d9, d8, [sp], 16
|
||||
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
|
||||
ldp lr, fp, [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,
|
||||
// )
|
||||
HIDDEN(wasmtime_fiber_init)
|
||||
GLOBL(wasmtime_fiber_init)
|
||||
.p2align 2
|
||||
TYPE(wasmtime_fiber_init)
|
||||
FUNCTION(wasmtime_fiber_init):
|
||||
adr x8, FUNCTION(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 0xa0 byte stack, and we add 0x10 more for
|
||||
// the original reserved 16 bytes.
|
||||
add x8, x0, -0xb0
|
||||
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 */ \
|
||||
5, /* the byte length of this expression */ \
|
||||
0x6f, /* DW_OP_reg31(%sp) */ \
|
||||
0x06, /* DW_OP_deref */ \
|
||||
0x23, 0xa0, 0x1 /* DW_OP_plus_uconst 0xa0 */
|
||||
|
||||
.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 land.
|
||||
.cfi_endproc
|
||||
SIZE(wasmtime_fiber_start)
|
||||
|
||||
// This omits the `.subsections_via_symbols` directive on macOS which means we
|
||||
// can't GC specific intrinsics from this file, but it enables usage of the
|
||||
// `adr` instruction above in lieu of figuring out a slightly more complicated
|
||||
// way of implementing that.
|
||||
#ifndef CFG_TARGET_OS_macos
|
||||
FOOTER
|
||||
#endif
|
||||
83
crates/fiber/src/arch/arm.S
Normal file
83
crates/fiber/src/arch/arm.S
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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.
|
||||
|
||||
#include "header.h"
|
||||
|
||||
// fn(top_of_stack(%r0): *mut u8)
|
||||
HIDDEN(wasmtime_fiber_switch)
|
||||
GLOBL(wasmtime_fiber_switch)
|
||||
TYPE(wasmtime_fiber_switch)
|
||||
FUNCTION(wasmtime_fiber_switch):
|
||||
// Save callee-saved registers
|
||||
push {r4-r11,lr}
|
||||
|
||||
// Swap stacks, recording our current stack pointer
|
||||
ldr r4, [r0, #-0x08]
|
||||
str sp, [r0, #-0x08]
|
||||
mov sp, r4
|
||||
|
||||
// Restore and return
|
||||
pop {r4-r11,lr}
|
||||
bx lr
|
||||
SIZE(wasmtime_fiber_switch)
|
||||
|
||||
// fn(
|
||||
// top_of_stack(%r0): *mut u8,
|
||||
// entry_point(%r1): extern fn(*mut u8, *mut u8),
|
||||
// entry_arg0(%r2): *mut u8,
|
||||
// )
|
||||
HIDDEN(wasmtime_fiber_init)
|
||||
GLOBL(wasmtime_fiber_init)
|
||||
TYPE(wasmtime_fiber_init)
|
||||
FUNCTION(wasmtime_fiber_init):
|
||||
adr r3, FUNCTION(wasmtime_fiber_start)
|
||||
str r3, [r0, #-0x0c] // => lr
|
||||
str r0, [r0, #-0x10] // => r11
|
||||
str r1, [r0, #-0x14] // => r10
|
||||
str r2, [r0, #-0x18] // => r9
|
||||
|
||||
add r3, r0, #-0x2c
|
||||
str r3, [r0, #-0x08]
|
||||
bx lr
|
||||
SIZE(wasmtime_fiber_init)
|
||||
|
||||
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`.
|
||||
//
|
||||
// TODO: this is only lightly tested. This gets backtraces in gdb but not
|
||||
// at runtime. Perhaps the libgcc at runtime was too old? Doesn't support
|
||||
// something here? Unclear. Will need investigation if someone ends up
|
||||
// needing this and it still doesn't work.
|
||||
.cfi_escape 0x0f, /* DW_CFA_def_cfa_expression */ \
|
||||
5, /* the byte length of this expression */ \
|
||||
0x7d, 0x00, /* DW_OP_breg14(%sp) + 0 */ \
|
||||
0x06, /* DW_OP_deref */ \
|
||||
0x23, 0x24 /* DW_OP_plus_uconst 0x24 */
|
||||
|
||||
.cfi_rel_offset lr, -0x04
|
||||
.cfi_rel_offset r11, -0x08
|
||||
.cfi_rel_offset r10, -0x0c
|
||||
.cfi_rel_offset r9, -0x10
|
||||
.cfi_rel_offset r8, -0x14
|
||||
.cfi_rel_offset r7, -0x18
|
||||
.cfi_rel_offset r6, -0x1c
|
||||
.cfi_rel_offset r5, -0x20
|
||||
.cfi_rel_offset r4, -0x24
|
||||
|
||||
mov r1, r11
|
||||
mov r0, r9
|
||||
blx r10
|
||||
.cfi_endproc
|
||||
SIZE(wasmtime_fiber_start)
|
||||
|
||||
FOOTER
|
||||
36
crates/fiber/src/arch/header.h
Normal file
36
crates/fiber/src/arch/header.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef __wasmtime_common_h
|
||||
#define __wasmtime_common_h
|
||||
|
||||
#if CFG_TARGET_OS_macos
|
||||
|
||||
.section __TEXT,__text,regular,pure_instructions
|
||||
|
||||
#define GLOBL(fnname) .globl _##fnname
|
||||
#define HIDDEN(fnname) .private_extern _##fnname
|
||||
#define TYPE(fnname)
|
||||
#define FUNCTION(fnname) _##fnname
|
||||
#define SIZE(fnname)
|
||||
|
||||
// Tells the linker it's safe to gc symbols away if not used.
|
||||
#define FOOTER .subsections_via_symbols
|
||||
|
||||
#else
|
||||
|
||||
.text
|
||||
|
||||
#define GLOBL(fnname) .globl fnname
|
||||
#define HIDDEN(fnname) .hidden fnname
|
||||
#ifdef CFG_TARGET_ARCH_arm
|
||||
#define TYPE(fnname) .type fnname,%function
|
||||
#else
|
||||
#define TYPE(fnname) .type fnname,@function
|
||||
#endif
|
||||
#define FUNCTION(fnname) fnname
|
||||
#define SIZE(fnname) .size fnname,.-fnname
|
||||
|
||||
// Mark that we don't need executable stack.
|
||||
#define FOOTER .section .note.GNU-stack,"",%progbits
|
||||
|
||||
#endif
|
||||
|
||||
#endif // __wasmtime_common_h
|
||||
5
crates/fiber/src/arch/windows.c
Normal file
5
crates/fiber/src/arch/windows.c
Normal file
@@ -0,0 +1,5 @@
|
||||
#include <windows.h>
|
||||
|
||||
LPVOID wasmtime_fiber_get_current() {
|
||||
return GetCurrentFiber();
|
||||
}
|
||||
107
crates/fiber/src/arch/x86.S
Normal file
107
crates/fiber/src/arch/x86.S
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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!
|
||||
//
|
||||
// This file is modeled after x86_64.S and comments are not copied over. For
|
||||
// reference be sure to review the other file. Note that the pointer size is
|
||||
// different so the reserved space at the top of the stack is 8 bytes, not 16
|
||||
// bytes. Still two pointers though.
|
||||
|
||||
#include "header.h"
|
||||
|
||||
// fn(top_of_stack: *mut u8)
|
||||
HIDDEN(wasmtime_fiber_switch)
|
||||
GLOBL(wasmtime_fiber_switch)
|
||||
TYPE(wasmtime_fiber_switch)
|
||||
FUNCTION(wasmtime_fiber_switch):
|
||||
// Load our stack-to-use
|
||||
mov 0x4(%esp), %eax
|
||||
mov -0x8(%eax), %ecx
|
||||
|
||||
// Save callee-saved registers
|
||||
push %ebp
|
||||
push %ebx
|
||||
push %esi
|
||||
push %edi
|
||||
|
||||
// Save our current stack and jump to the stack-to-use
|
||||
mov %esp, -0x8(%eax)
|
||||
mov %ecx, %esp
|
||||
|
||||
// Restore callee-saved registers
|
||||
pop %edi
|
||||
pop %esi
|
||||
pop %ebx
|
||||
pop %ebp
|
||||
ret
|
||||
SIZE(wasmtime_fiber_switch)
|
||||
|
||||
// fn(
|
||||
// top_of_stack: *mut u8,
|
||||
// entry_point: extern fn(*mut u8, *mut u8),
|
||||
// entry_arg0: *mut u8,
|
||||
// )
|
||||
HIDDEN(wasmtime_fiber_init)
|
||||
GLOBL(wasmtime_fiber_init)
|
||||
TYPE(wasmtime_fiber_init)
|
||||
FUNCTION(wasmtime_fiber_init):
|
||||
mov 4(%esp), %eax
|
||||
|
||||
// move top_of_stack to the 2nd argument
|
||||
mov %eax, -0x0c(%eax)
|
||||
|
||||
// move entry_arg0 to the 1st argument
|
||||
mov 12(%esp), %ecx
|
||||
mov %ecx, -0x10(%eax)
|
||||
|
||||
// Move our start function to the return address which the `ret` in
|
||||
// `wasmtime_fiber_start` will return to.
|
||||
lea FUNCTION(wasmtime_fiber_start), %ecx
|
||||
mov %ecx, -0x14(%eax)
|
||||
|
||||
// And move `entry_point` to get loaded into `%ebp` through the context
|
||||
// switch. This'll get jumped to in `wasmtime_fiber_start`.
|
||||
mov 8(%esp), %ecx
|
||||
mov %ecx, -0x18(%eax)
|
||||
|
||||
// Our stack from top-to-bottom looks like:
|
||||
//
|
||||
// * 8 bytes of reserved space per unix.rs (two-pointers space)
|
||||
// * 8 bytes of arguments (two arguments wasmtime_fiber_start forwards)
|
||||
// * 4 bytes of return address
|
||||
// * 16 bytes of saved registers
|
||||
//
|
||||
// Note that after the return address the stack is conveniently 16-byte
|
||||
// aligned as required, so we just leave the arguments on the stack in
|
||||
// `wasmtime_fiber_start` and immediately do the call.
|
||||
lea -0x24(%eax), %ecx
|
||||
mov %ecx, -0x08(%eax)
|
||||
ret
|
||||
SIZE(wasmtime_fiber_init)
|
||||
|
||||
TYPE(wasmtime_fiber_start)
|
||||
FUNCTION(wasmtime_fiber_start):
|
||||
.cfi_startproc simple
|
||||
.cfi_escape 0x0f, /* DW_CFA_def_cfa_expression */ \
|
||||
5, /* the byte length of this expression */ \
|
||||
0x74, 0x08, /* DW_OP_breg4 (%esp) + 8 */ \
|
||||
0x06, /* DW_OP_deref */ \
|
||||
0x23, 0x14 /* DW_OP_plus_uconst 0x14 */
|
||||
|
||||
.cfi_rel_offset eip, -4
|
||||
.cfi_rel_offset ebp, -8
|
||||
.cfi_rel_offset ebx, -12
|
||||
.cfi_rel_offset esi, -16
|
||||
.cfi_rel_offset edi, -20
|
||||
|
||||
// Our arguments and stack alignment are all prepped by
|
||||
// `wasmtime_fiber_init`.
|
||||
call *%ebp
|
||||
ud2
|
||||
.cfi_endproc
|
||||
SIZE(wasmtime_fiber_start)
|
||||
|
||||
FOOTER
|
||||
159
crates/fiber/src/arch/x86_64.S
Normal file
159
crates/fiber/src/arch/x86_64.S
Normal file
@@ -0,0 +1,159 @@
|
||||
// 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!
|
||||
|
||||
#include "header.h"
|
||||
|
||||
// fn(top_of_stack(%rdi): *mut u8)
|
||||
HIDDEN(wasmtime_fiber_switch)
|
||||
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,
|
||||
// )
|
||||
HIDDEN(wasmtime_fiber_init)
|
||||
GLOBL(wasmtime_fiber_init)
|
||||
.align 16
|
||||
TYPE(wasmtime_fiber_init)
|
||||
FUNCTION(wasmtime_fiber_init):
|
||||
// Here we're going to set up a stack frame as expected by
|
||||
// `wasmtime_fiber_switch`. The values we store here will get restored into
|
||||
// registers by that function and the `wasmtime_fiber_start` function will
|
||||
// take over and understands which values are in which registers.
|
||||
//
|
||||
// The first 16 bytes of stack are reserved for metadata, so we start
|
||||
// storing values beneath that.
|
||||
lea FUNCTION(wasmtime_fiber_start)(%rip), %rax
|
||||
movq %rax, -0x18(%rdi)
|
||||
movq %rdi, -0x20(%rdi) // loaded into rbp during switch
|
||||
movq %rsi, -0x28(%rdi) // loaded into rbx during switch
|
||||
movq %rdx, -0x30(%rdi) // loaded into r12 during switch
|
||||
|
||||
// And then we specify the stack pointer resumption should begin at. Our
|
||||
// `wasmtime_fiber_switch` function consumes 6 registers plus a return
|
||||
// pointer, and the top 16 bytes aree resereved, so that's:
|
||||
//
|
||||
// (6 + 1) * 16 + 16 = 0x48
|
||||
lea -0x48(%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 + 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 16 bytes below the top of the stack (0xAff0 in
|
||||
// the diagram in unix.rs). The $rsp to resume at is stored at that
|
||||
// location, so we dereference the stack pointer to load 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 */ \
|
||||
4, /* the byte length of this expression */ \
|
||||
0x57, /* DW_OP_reg7 (%rsp) */ \
|
||||
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
|
||||
|
||||
|
||||
// The body of this function is pretty similar. All our parameters are
|
||||
// already loaded into registers by the switch function. The
|
||||
// `wasmtime_fiber_init` routine arranged the various values to be
|
||||
// materialized into the registers used here. Our job is to then move the
|
||||
// values into the ABI-defined registers and call the entry-point. Note that
|
||||
// `callq` is used here to leave this frame on the stack so we can use the
|
||||
// dwarf info here for unwinding. The trailing `ud2` is just for safety.
|
||||
mov %r12,%rdi
|
||||
mov %rbp,%rsi
|
||||
callq *%rbx
|
||||
ud2
|
||||
.cfi_endproc
|
||||
SIZE(wasmtime_fiber_start)
|
||||
|
||||
FOOTER
|
||||
|
||||
249
crates/fiber/src/lib.rs
Normal file
249
crates/fiber/src/lib.rs
Normal file
@@ -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<bool>,
|
||||
_phantom: PhantomData<&'a (Resume, Yield, Return)>,
|
||||
}
|
||||
|
||||
pub struct Suspend<Resume, Yield, Return> {
|
||||
inner: imp::Suspend,
|
||||
_phantom: PhantomData<(Resume, Yield, Return)>,
|
||||
}
|
||||
|
||||
enum RunResult<Resume, Yield, Return> {
|
||||
Executing,
|
||||
Resuming(Resume),
|
||||
Yield(Yield),
|
||||
Returned(Return),
|
||||
Panicked(Box<dyn Any + Send>),
|
||||
}
|
||||
|
||||
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<Resume, Yield, Return>) -> Return + 'a,
|
||||
) -> io::Result<Fiber<'a, Resume, Yield, Return>> {
|
||||
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<Return, Yield> {
|
||||
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<Resume, Yield, Return> Suspend<Resume, Yield, Return> {
|
||||
/// 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::<Resume, Yield, Return>(RunResult::Yield(value))
|
||||
}
|
||||
|
||||
fn execute(
|
||||
inner: imp::Suspend,
|
||||
initial: Resume,
|
||||
func: impl FnOnce(Resume, &Suspend<Resume, Yield, Return>) -> Return,
|
||||
) {
|
||||
let suspend = Suspend {
|
||||
inner,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
let result = panic::catch_unwind(AssertUnwindSafe(|| (func)(initial, &suspend)));
|
||||
suspend.inner.switch::<Resume, Yield, Return>(match result {
|
||||
Ok(result) => RunResult::Returned(result),
|
||||
Err(panic) => RunResult::Panicked(panic),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B, C> 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<Cell<bool>>);
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
174
crates/fiber/src/unix.rs
Normal file
174
crates/fiber/src/unix.rs
Normal file
@@ -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<RunResult> | <- 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<F, A, B, C>(arg0: *mut u8, top_of_stack: *mut u8)
|
||||
where
|
||||
F: FnOnce(A, &super::Suspend<A, B, C>) -> C,
|
||||
{
|
||||
unsafe {
|
||||
let inner = Suspend { top_of_stack };
|
||||
let initial = inner.take_resume::<A, B, C>();
|
||||
super::Suspend::<A, B, C>::execute(inner, initial, Box::from_raw(arg0.cast::<F>()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Fiber {
|
||||
pub fn new<F, A, B, C>(stack_size: usize, func: F) -> io::Result<Fiber>
|
||||
where
|
||||
F: FnOnce(A, &super::Suspend<A, B, C>) -> 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::<F, A, B, C>, data);
|
||||
Ok(fiber)
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc_with_stack(stack_size: usize) -> io::Result<Fiber> {
|
||||
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::<u8>().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<A, B, C>(&self, result: &Cell<RunResult<A, B, C>>) {
|
||||
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::<usize>().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::<u8>().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<A, B, C>(&self, result: RunResult<A, B, C>) -> A {
|
||||
unsafe {
|
||||
// Calculate 0xAff8 and then write to it
|
||||
(*self.result_location::<A, B, C>()).set(result);
|
||||
wasmtime_fiber_switch(self.top_of_stack);
|
||||
self.take_resume::<A, B, C>()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn take_resume<A, B, C>(&self) -> A {
|
||||
match (*self.result_location::<A, B, C>()).replace(RunResult::Executing) {
|
||||
RunResult::Resuming(val) => val,
|
||||
_ => panic!("not in resuming state"),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn result_location<A, B, C>(&self) -> *const Cell<RunResult<A, B, C>> {
|
||||
let ret = self.top_of_stack.cast::<*const u8>().offset(-1).read();
|
||||
assert!(!ret.is_null());
|
||||
return ret.cast();
|
||||
}
|
||||
}
|
||||
130
crates/fiber/src/windows.rs
Normal file
130
crates/fiber/src/windows.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
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<StartState>,
|
||||
}
|
||||
|
||||
pub struct Suspend {
|
||||
state: *const StartState,
|
||||
}
|
||||
|
||||
struct StartState {
|
||||
parent: Cell<LPVOID>,
|
||||
initial_closure: Cell<*mut u8>,
|
||||
result_location: Cell<*const u8>,
|
||||
}
|
||||
|
||||
const FIBER_FLAG_FLOAT_SWITCH: DWORD = 1;
|
||||
|
||||
extern "C" {
|
||||
fn wasmtime_fiber_get_current() -> LPVOID;
|
||||
}
|
||||
|
||||
unsafe extern "system" fn fiber_start<F, A, B, C>(data: LPVOID)
|
||||
where
|
||||
F: FnOnce(A, &super::Suspend<A, B, C>) -> C,
|
||||
{
|
||||
let state = data.cast::<StartState>();
|
||||
let func = Box::from_raw((*state).initial_closure.get().cast::<F>());
|
||||
(*state).initial_closure.set(ptr::null_mut());
|
||||
let suspend = Suspend { state };
|
||||
let initial = suspend.take_resume::<A, B, C>();
|
||||
super::Suspend::<A, B, C>::execute(suspend, initial, *func);
|
||||
}
|
||||
|
||||
impl Fiber {
|
||||
pub fn new<F, A, B, C>(stack_size: usize, func: F) -> io::Result<Fiber>
|
||||
where
|
||||
F: FnOnce(A, &super::Suspend<A, B, C>) -> 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 = CreateFiberEx(
|
||||
0,
|
||||
stack_size,
|
||||
FIBER_FLAG_FLOAT_SWITCH,
|
||||
Some(fiber_start::<F, A, B, C>),
|
||||
&*state as *const StartState as *mut _,
|
||||
);
|
||||
if fiber.is_null() {
|
||||
drop(Box::from_raw(state.initial_closure.get().cast::<F>()));
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(Fiber { fiber, state })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resume<A, B, C>(&self, result: &Cell<RunResult<A, B, C>>) {
|
||||
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<A, B, C>(&self, result: RunResult<A, B, C>) -> A {
|
||||
unsafe {
|
||||
(*self.result_location::<A, B, C>()).set(result);
|
||||
debug_assert!(IsThreadAFiber() != 0);
|
||||
let parent = (*self.state).parent.get();
|
||||
debug_assert!(!parent.is_null());
|
||||
SwitchToFiber(parent);
|
||||
self.take_resume::<A, B, C>()
|
||||
}
|
||||
}
|
||||
unsafe fn take_resume<A, B, C>(&self) -> A {
|
||||
match (*self.result_location::<A, B, C>()).replace(RunResult::Executing) {
|
||||
RunResult::Resuming(val) => val,
|
||||
_ => panic!("not in resuming state"),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn result_location<A, B, C>(&self) -> *const Cell<RunResult<A, B, C>> {
|
||||
let ret = (*self.state)
|
||||
.result_location
|
||||
.get()
|
||||
.cast::<Cell<RunResult<A, B, C>>>();
|
||||
assert!(!ret.is_null());
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -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.23.0" }
|
||||
wasmtime-environ = { path = "../environ", version = "0.23.0" }
|
||||
wasmtime-jit = { path = "../jit", version = "0.23.0" }
|
||||
wasmtime-cache = { path = "../cache", version = "0.23.0", optional = true }
|
||||
wasmtime-profiling = { path = "../profiling", version = "0.23.0" }
|
||||
wasmtime-fiber = { path = "../fiber", version = "0.23.0", optional = true }
|
||||
target-lexicon = { version = "0.11.0", default-features = false }
|
||||
wasmparser = "0.75"
|
||||
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"
|
||||
@@ -43,7 +48,7 @@ wasi-cap-std-sync = { path = "../wasi-common/cap-std-sync" }
|
||||
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
|
||||
@@ -64,3 +69,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"]
|
||||
|
||||
@@ -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<Path>) -> 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)
|
||||
|
||||
@@ -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 not
|
||||
/// 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<impl Fn($($args,)*) -> Result<R, Trap>>
|
||||
pub fn [<get $num>]<$($args,)* R>(&self)
|
||||
-> Result<impl Fn($($args,)*) -> Result<R, Trap>>
|
||||
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 [<get $num _async>]<$($args,)* R>(&self)
|
||||
-> Result<impl Fn($($args,)*) -> WasmCall<R>>
|
||||
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<impl Fn($($args,)*) -> Result<R, Trap> + Clone>
|
||||
where
|
||||
$($args: WasmTy,)*
|
||||
R: WasmTy,
|
||||
@@ -228,7 +375,41 @@ 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 [<wrap $num _async>]<T, $($args,)* R>(
|
||||
store: &Store,
|
||||
state: T,
|
||||
func: impl for<'a> Fn(Caller<'a>, &'a T, $($args),*) -> Box<dyn Future<Output = R> + 'a> + 'static,
|
||||
) -> Func
|
||||
where
|
||||
T: 'static,
|
||||
$($args: WasmTy,)*
|
||||
R: WasmRet,
|
||||
{
|
||||
assert!(store.is_async());
|
||||
Func::wrap(store, move |caller: Caller<'_>, $($args: $args),*| {
|
||||
let store = caller.store();
|
||||
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 +505,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<T, F>(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<dyn Future<Output = Result<(), Trap>> + 'a>
|
||||
+ 'static,
|
||||
{
|
||||
assert!(store.is_async());
|
||||
Func::new(store, ty, move |caller, params, results| {
|
||||
let store = caller.store();
|
||||
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 +810,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 +856,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<Box<[Val]>> {
|
||||
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<Box<[Val]>> {
|
||||
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<Box<[Val]>> {
|
||||
// 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 +989,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<R, Trap>` 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 +1100,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 +1137,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<Self::Ok, Trap>;
|
||||
}
|
||||
|
||||
unsafe impl WasmTy for () {
|
||||
@@ -1335,6 +1544,7 @@ where
|
||||
T: WasmTy,
|
||||
{
|
||||
type Abi = <T as WasmTy>::Abi;
|
||||
type Ok = T;
|
||||
|
||||
#[inline]
|
||||
fn compatible_with_store<'a>(&self, store: WeakStore<'a>) -> bool {
|
||||
@@ -1369,6 +1579,11 @@ where
|
||||
unsafe fn store_to_args(abi: Self::Abi, ptr: *mut u128) {
|
||||
<Self as WasmTy>::store_to_args(abi, ptr)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_result(self) -> Result<T, Trap> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> WasmRet for Result<T, Trap>
|
||||
@@ -1376,6 +1591,7 @@ where
|
||||
T: WasmTy,
|
||||
{
|
||||
type Abi = <T as WasmTy>::Abi;
|
||||
type Ok = T;
|
||||
|
||||
#[inline]
|
||||
fn compatible_with_store<'a>(&self, store: WeakStore<'a>) -> bool {
|
||||
@@ -1419,6 +1635,11 @@ where
|
||||
unsafe fn store_to_args(abi: Self::Abi, ptr: *mut u128) {
|
||||
<T as WasmTy>::store_to_args(abi, ptr);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_result(self) -> Result<T, Trap> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal trait implemented for all arguments that can be passed to
|
||||
@@ -1552,9 +1773,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 +1918,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<R> {
|
||||
inner: Pin<Box<dyn Future<Output = Result<R, Trap>>>>,
|
||||
}
|
||||
|
||||
impl<R> Future for WasmCall<R> {
|
||||
type Output = Result<R, Trap>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
self.inner.as_mut().poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -90,10 +90,22 @@ 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::<Trap>()`.
|
||||
///
|
||||
/// # 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<Instance, Error> {
|
||||
assert!(
|
||||
!store.is_async(),
|
||||
"cannot instantiate synchronously within an asynchronous store"
|
||||
);
|
||||
|
||||
// NB: this is the same code as `Instance::new_async`. It's intentionally
|
||||
// small but should be kept in sync (modulo the async bits).
|
||||
let mut i = Instantiator::new(store, module, imports)?;
|
||||
loop {
|
||||
if let Some((instance, items)) = i.step()? {
|
||||
@@ -105,6 +117,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<Instance, Error> {
|
||||
assert!(
|
||||
store.is_async(),
|
||||
"cannot instantiate asynchronously within a synchronous store"
|
||||
);
|
||||
|
||||
// NB: this is the same code as `Instance::new`. It's intentionally
|
||||
// small but should be kept in sync (modulo the async bits).
|
||||
let mut i = Instantiator::new(store, module, imports)?;
|
||||
loop {
|
||||
if let Some((instance, items)) = i.step()? {
|
||||
if instance.handle.module().start_func.is_some() {
|
||||
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(),
|
||||
|
||||
@@ -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
|
||||
@@ -234,6 +266,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;
|
||||
|
||||
@@ -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<VMInterrupts>,
|
||||
signatures: RefCell<SignatureRegistry>,
|
||||
@@ -77,6 +82,21 @@ pub(crate) struct StoreInner {
|
||||
/// An adjustment to add to the fuel consumed value in `interrupts` above
|
||||
/// to get the true amount of fuel consumed.
|
||||
fuel_adj: Cell<i64>,
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
current_suspend: Cell<*const wasmtime_fiber::Suspend<Result<(), Trap>, (), Result<(), Trap>>>,
|
||||
#[cfg(feature = "async")]
|
||||
current_poll_cx: Cell<*mut Context<'static>>,
|
||||
out_of_gas_behavior: Cell<OutOfGas>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum OutOfGas {
|
||||
Trap,
|
||||
InjectFuel {
|
||||
injection_count: u32,
|
||||
fuel_to_inject: u64,
|
||||
},
|
||||
}
|
||||
|
||||
struct HostInfoKey(VMExternRef);
|
||||
@@ -100,7 +120,99 @@ 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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// # Execution in `poll`
|
||||
///
|
||||
/// The [`Future::poll`](std::future::Future::poll) method is the main
|
||||
/// driving force behind Rust's futures. That method's own documentation
|
||||
/// states "an implementation of `poll` should strive to return quickly, and
|
||||
/// should not block". This, however, can be at odds with executing
|
||||
/// WebAssembly code as part of the `poll` method itself. If your
|
||||
/// WebAssembly is untrusted then this could allow the `poll` method to take
|
||||
/// arbitrarily long in the worst case, likely blocking all other
|
||||
/// asynchronous tasks.
|
||||
///
|
||||
/// To remedy this situation you have a two possible ways to solve this:
|
||||
///
|
||||
/// * First you can spawn futures into a thread pool. Care must be taken for
|
||||
/// this because Wasmtime futures are not `Send` or `Sync`. If you ensure
|
||||
/// that the entire state of a `Store` is wrapped up in a single future,
|
||||
/// though, you can send the whole future at once to a separate thread. By
|
||||
/// doing this in a thread pool you are relaxing the requirement that
|
||||
/// `Future::poll` must be fast because your future is executing on a
|
||||
/// separate thread. This strategy, however, would likely still require
|
||||
/// some form of cancellation via [`Store::interrupt_handle`] to ensure
|
||||
/// wasm doesn't take *too* long to execute.
|
||||
///
|
||||
/// * Alternatively you can enable the
|
||||
/// [`Config::consume_fuel`](crate::Config::consume_fuel) method as well
|
||||
/// as [`Store::out_of_fuel_async_yield`] When doing so this will
|
||||
/// configure Wasmtime futures to yield periodically while they're
|
||||
/// executing WebAssembly code. After consuming the specified amount of
|
||||
/// fuel wasm futures will return `Poll::Pending` from their `poll`
|
||||
/// method, and will get automatically re-polled later. This enables the
|
||||
/// `Future::poll` method to take roughly a fixed amount of time since
|
||||
/// fuel is guaranteed to get consumed while wasm is executing. Note that
|
||||
/// to prevent infinite execution of wasm you'll still need to use
|
||||
/// [`Store::interrupt_handle`].
|
||||
///
|
||||
/// In either case special care needs to be taken when integrating
|
||||
/// asynchronous wasm into your application. You should carefully plan where
|
||||
/// WebAssembly will execute and what compute resources will be allotted to
|
||||
/// it. If Wasmtime doesn't support exactly what you'd like just yet, please
|
||||
/// feel free to open an issue!
|
||||
#[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 +222,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 +236,11 @@ 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()),
|
||||
out_of_gas_behavior: Cell::new(OutOfGas::Trap),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -496,6 +614,301 @@ impl Store {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configures a [`Store`] to generate a [`Trap`] whenever it runs out of
|
||||
/// fuel.
|
||||
///
|
||||
/// When a [`Store`] is configured to consume fuel with
|
||||
/// [`Config::consume_fuel`](crate::Config::consume_fuel) this method will
|
||||
/// configure what happens when fuel runs out. Specifically a WebAssembly
|
||||
/// trap will be raised and the current execution of WebAssembly will be
|
||||
/// aborted.
|
||||
///
|
||||
/// This is the default behavior for running out of fuel.
|
||||
pub fn out_of_fuel_trap(&self) {
|
||||
self.inner.out_of_gas_behavior.set(OutOfGas::Trap);
|
||||
}
|
||||
|
||||
/// Configures a [`Store`] to yield execution of async WebAssembly code
|
||||
/// periodically.
|
||||
///
|
||||
/// When a [`Store`] is configured to consume fuel with
|
||||
/// [`Config::consume_fuel`](crate::Config::consume_fuel) this method will
|
||||
/// configure what happens when fuel runs out. Specifically executing
|
||||
/// WebAssembly will be suspended and control will be yielded back to the
|
||||
/// caller. This is only suitable with use of [async
|
||||
/// stores](Store::new_async) because only then are futures used and yields
|
||||
/// are possible.
|
||||
///
|
||||
/// The purpose of this behavior is to ensure that futures which represent
|
||||
/// execution of WebAssembly do not execute too long inside their
|
||||
/// `Future::poll` method. This allows for some form of cooperative
|
||||
/// multitasking where WebAssembly will voluntarily yield control
|
||||
/// periodically (based on fuel consumption) back to the running thread.
|
||||
///
|
||||
/// Note that futures returned by this crate will automatically flag
|
||||
/// themselves to get re-polled if a yield happens. This means that
|
||||
/// WebAssembly will continue to execute, just after giving the host an
|
||||
/// opportunity to do something else.
|
||||
///
|
||||
/// The `fuel_to_inject` parameter indicates how much fuel should be
|
||||
/// automatically re-injected after fuel runs out. This is how much fuel
|
||||
/// will be consumed between yields of an async future.
|
||||
///
|
||||
/// The `injection_count` parameter indicates how many times this fuel will
|
||||
/// be injected. Multiplying the two parameters is the total amount of fuel
|
||||
/// this store is allowed before wasm traps.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method will panic if it is not called on an [async
|
||||
/// store](Store::new_async).
|
||||
pub fn out_of_fuel_async_yield(&self, injection_count: u32, fuel_to_inject: u64) {
|
||||
assert!(self.is_async());
|
||||
self.inner.out_of_gas_behavior.set(OutOfGas::InjectFuel {
|
||||
injection_count,
|
||||
fuel_to_inject,
|
||||
});
|
||||
}
|
||||
|
||||
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<T>(
|
||||
&self,
|
||||
mut future: Pin<&mut dyn Future<Output = T>>,
|
||||
) -> Result<T, Trap> {
|
||||
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<R>(&self, func: impl FnOnce() -> R) -> Result<R, Trap> {
|
||||
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<Self::Output> {
|
||||
// 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 (no 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Immediately raise a trap on an out-of-gas condition.
|
||||
fn out_of_gas_trap(&self) -> ! {
|
||||
#[derive(Debug)]
|
||||
struct OutOfGasError;
|
||||
|
||||
impl fmt::Display for OutOfGasError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("all fuel consumed by WebAssembly")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OutOfGasError {}
|
||||
unsafe {
|
||||
wasmtime_runtime::raise_lib_trap(wasmtime_runtime::Trap::User(Box::new(OutOfGasError)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields execution to the caller on out-of-gas
|
||||
///
|
||||
/// This only works on async futures and stores, and assumes that we're
|
||||
/// executing on a fiber. This will yield execution back to the caller once
|
||||
/// and when we come back we'll continue with `fuel_to_inject` more fuel.
|
||||
#[cfg(feature = "async")]
|
||||
fn out_of_gas_yield(&self, fuel_to_inject: u64) {
|
||||
// Small future that yields once and then returns ()
|
||||
#[derive(Default)]
|
||||
struct Yield {
|
||||
yielded: bool,
|
||||
}
|
||||
|
||||
impl Future for Yield {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
if self.yielded {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
// Flag ourselves as yielded to return next time, and also
|
||||
// flag the waker that we're already ready to get
|
||||
// re-enqueued for another poll.
|
||||
self.yielded = true;
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut future = Yield::default();
|
||||
match self.block_on(unsafe { Pin::new_unchecked(&mut future) }) {
|
||||
// If this finished successfully then we were resumed normally via a
|
||||
// `poll`, so inject some more fuel and keep going.
|
||||
Ok(()) => self.add_fuel(fuel_to_inject).unwrap(),
|
||||
// If the future was dropped while we were yielded, then we need to
|
||||
// clean up this fiber. Do so by raising a trap which will abort all
|
||||
// wasm and get caught on the other side to clean things up.
|
||||
Err(trap) => unsafe { wasmtime_runtime::raise_user_trap(trap.into()) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl TrapInfo for Store {
|
||||
@@ -519,19 +932,24 @@ unsafe impl TrapInfo for Store {
|
||||
}
|
||||
|
||||
fn out_of_gas(&self) {
|
||||
#[derive(Debug)]
|
||||
struct OutOfGas;
|
||||
|
||||
impl fmt::Display for OutOfGas {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("all fuel consumed by WebAssembly")
|
||||
match self.inner.out_of_gas_behavior.get() {
|
||||
OutOfGas::Trap => self.out_of_gas_trap(),
|
||||
#[cfg(feature = "async")]
|
||||
OutOfGas::InjectFuel {
|
||||
injection_count,
|
||||
fuel_to_inject,
|
||||
} => {
|
||||
if injection_count == 0 {
|
||||
self.out_of_gas_trap();
|
||||
}
|
||||
self.inner.out_of_gas_behavior.set(OutOfGas::InjectFuel {
|
||||
injection_count: injection_count - 1,
|
||||
fuel_to_inject,
|
||||
});
|
||||
self.out_of_gas_yield(fuel_to_inject);
|
||||
}
|
||||
|
||||
impl std::error::Error for OutOfGas {}
|
||||
|
||||
unsafe {
|
||||
wasmtime_runtime::raise_lib_trap(wasmtime_runtime::Trap::User(Box::new(OutOfGas)))
|
||||
#[cfg(not(feature = "async"))]
|
||||
OutOfGas::InjectFuel { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,3 +1022,11 @@ impl Hash for ArcModuleCode {
|
||||
Arc::as_ptr(&self.0).hash(hasher)
|
||||
}
|
||||
}
|
||||
|
||||
struct Reset<'a, T: Copy>(&'a Cell<T>, T);
|
||||
|
||||
impl<T: Copy> Drop for Reset<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
self.0.set(self.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const CRATES_TO_PUBLISH: &[&str] = &[
|
||||
"wasi-cap-std-sync",
|
||||
// wasmtime
|
||||
"lightbeam",
|
||||
"wasmtime-fiber",
|
||||
"wasmtime-environ",
|
||||
"wasmtime-runtime",
|
||||
"wasmtime-debug",
|
||||
|
||||
366
tests/all/async_functions.rs
Normal file
366
tests/all/async_functions.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
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<Cell<u32>>);
|
||||
|
||||
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<F: Future>(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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iloop_with_fuel() {
|
||||
let engine = Engine::new(Config::new().consume_fuel(true));
|
||||
let store = Store::new_async(&engine);
|
||||
store.out_of_fuel_async_yield(1_000, 10);
|
||||
let module = Module::new(
|
||||
&engine,
|
||||
"
|
||||
(module
|
||||
(func (loop br 0))
|
||||
(start 0)
|
||||
)
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
let instance = Instance::new_async(&store, &module, &[]);
|
||||
let mut f = Pin::from(Box::new(instance));
|
||||
let waker = dummy_waker();
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
|
||||
// This should yield a bunch of times...
|
||||
for _ in 0..100 {
|
||||
assert!(f.as_mut().poll(&mut cx).is_pending());
|
||||
}
|
||||
|
||||
// ... but it should eventually also finish.
|
||||
loop {
|
||||
match f.as_mut().poll(&mut cx) {
|
||||
Poll::Ready(_) => break,
|
||||
Poll::Pending => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuel_eventually_finishes() {
|
||||
let engine = Engine::new(Config::new().consume_fuel(true));
|
||||
let store = Store::new_async(&engine);
|
||||
store.out_of_fuel_async_yield(u32::max_value(), 10);
|
||||
let module = Module::new(
|
||||
&engine,
|
||||
"
|
||||
(module
|
||||
(func
|
||||
(local i32)
|
||||
i32.const 100
|
||||
local.set 0
|
||||
(loop
|
||||
local.get 0
|
||||
i32.const -1
|
||||
i32.add
|
||||
local.tee 0
|
||||
br_if 0)
|
||||
)
|
||||
(start 0)
|
||||
)
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
let instance = Instance::new_async(&store, &module, &[]);
|
||||
run(instance).unwrap();
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod async_functions;
|
||||
mod cli_tests;
|
||||
mod custom_signal_handler;
|
||||
mod debug;
|
||||
|
||||
Reference in New Issue
Block a user