Skip to content

Commit

Permalink
Port wasmtime-fiber to no_std and allow async feature in no_std
Browse files Browse the repository at this point in the history
… Wasmtime.

This PR allows a `no_std` Wasmtime build to be configured with the
`async` feature. (Previously, a minimal `no_std` configuration could
only run with sync entry points, without suspending of stacks.)

The main hurdle to this support was the `wasmtime-fiber` crate.
Fortunately, the "unix" variant of fibers was almost entirely portable
to a `no_std` environment, owing to the fact that it implements
stack-switching manually in assembly itself. I moved the per-ISA
implementations to a shared submodule and built the nostd platform
backend for `wasmtime-fiber` with a stripped-down version of the unix
backend.

The nostd backend does not support mmap'd stacks, does not support
custom stack allocators, and does not propagate panics.

I've updated the `min-platform` example to ensure this builds but have
not yet actually tested it (I am in the middle of a larger porting
effort); hence, a draft PR for initial feedback.
  • Loading branch information
cfallin committed Nov 27, 2024
1 parent 6691006 commit dc4fe18
Show file tree
Hide file tree
Showing 17 changed files with 293 additions and 61 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,12 @@ jobs:
-p wasmtime --no-default-features --features wat
-p wasmtime --no-default-features --features profiling
-p wasmtime --no-default-features --features cache
-p wasmtime --no-default-features --features async
-p wasmtime --no-default-features --features async,std
-p wasmtime --no-default-features --features pooling-allocator
-p wasmtime --no-default-features --features cranelift
-p wasmtime --no-default-features --features component-model
-p wasmtime --no-default-features --features runtime,component-model
-p wasmtime --no-default-features --features cranelift,wat,async,cache
-p wasmtime --no-default-features --features cranelift,wat,async,std,cache
-p wasmtime --no-default-features --features winch
-p wasmtime --no-default-features --features wmemcheck
-p wasmtime --no-default-features --features wmemcheck,cranelift,runtime
Expand Down
6 changes: 3 additions & 3 deletions crates/asm-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cfg_if::cfg_if! {
#[macro_export]
macro_rules! asm_func {
($name:expr, $body:expr $(, $($args:tt)*)?) => {
std::arch::global_asm!(
core::arch::global_asm!(
concat!(
".p2align 4\n",
".private_extern _", $name, "\n",
Expand All @@ -29,7 +29,7 @@ cfg_if::cfg_if! {
#[macro_export]
macro_rules! asm_func {
($name:expr, $body:expr $(, $($args:tt)*)?) => {
std::arch::global_asm!(
core::arch::global_asm!(
concat!(
".def ", $name, "\n",
".scl 2\n",
Expand Down Expand Up @@ -65,7 +65,7 @@ cfg_if::cfg_if! {
#[macro_export]
macro_rules! asm_func {
($name:expr, $body:expr $(, $($args:tt)*)?) => {
std::arch::global_asm!(
core::arch::global_asm!(
concat!(
".p2align 4\n",
".hidden ", $name, "\n",
Expand Down
9 changes: 8 additions & 1 deletion crates/fiber/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ workspace = true
anyhow = { workspace = true }
cfg-if = { workspace = true }
wasmtime-versioned-export-macros = { workspace = true }
wasmtime-asm-macros = { workspace = true }

[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["mm", "param"] }
wasmtime-asm-macros = { workspace = true }

[target.'cfg(windows)'.dependencies.windows-sys]
workspace = true
Expand All @@ -33,3 +33,10 @@ wasmtime-versioned-export-macros = { workspace = true }

[dev-dependencies]
backtrace = "0.3.68"

[features]
default = []

# Assume presence of the standard library. Allows propagating
# panic-unwinds across fiber invocations.
std = []
76 changes: 57 additions & 19 deletions crates/fiber/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
#![no_std]

// no_std support: we can only support `panic=abort` builds, because the
// `core::panic` module otherwise does not provide us the catch/resume
// unwinding primitives we need to propagate unwinding across stacks.
#[cfg(all(panic = "unwind", not(feature = "std")))]
compile_error!(
"no_std builds with wasmtime-fiber (e.g., Wasmtime async) only support `panic=abort`."
);

#[cfg(any(feature = "std", unix, windows))]
#[macro_use]
extern crate std;
extern crate alloc;

use alloc::boxed::Box;
use anyhow::Error;
use std::any::Any;
use std::cell::Cell;
use std::io;
use std::marker::PhantomData;
use std::ops::Range;
use core::any::Any;
use core::cell::Cell;
use core::marker::PhantomData;
use core::ops::Range;
#[cfg(feature = "std")]
use std::panic::{self, AssertUnwindSafe};

cfg_if::cfg_if! {
if #[cfg(windows)] {
if #[cfg(not(feature = "std"))] {
mod nostd;
use nostd as imp;
} else if #[cfg(windows)] {
mod windows;
use windows as imp;
} else if #[cfg(unix)] {
Expand All @@ -18,6 +37,11 @@ cfg_if::cfg_if! {
}
}

// Our own stack switcher routines are used on Unix and no_std
// platforms, but not on Windows (it has its own fiber API).
#[cfg(any(unix, not(feature = "std")))]
pub(crate) mod stackswitch;

/// Represents an execution stack to use for a fiber.
pub struct FiberStack(imp::FiberStack);

Expand All @@ -29,14 +53,16 @@ fn _assert_send_sync() {
_assert_sync::<FiberStack>();
}

pub type Result<T> = imp::Result<T>;

impl FiberStack {
/// Creates a new fiber stack of the given size.
pub fn new(size: usize) -> io::Result<Self> {
pub fn new(size: usize) -> Result<Self> {
Ok(Self(imp::FiberStack::new(size)?))
}

/// Creates a new fiber stack of the given size.
pub fn from_custom(custom: Box<dyn RuntimeFiberStack>) -> io::Result<Self> {
pub fn from_custom(custom: Box<dyn RuntimeFiberStack>) -> Result<Self> {
Ok(Self(imp::FiberStack::from_custom(custom)?))
}

Expand All @@ -53,11 +79,7 @@ impl FiberStack {
///
/// The caller must properly allocate the stack space with a guard page and
/// make the pages accessible for correct behavior.
pub unsafe fn from_raw_parts(
bottom: *mut u8,
guard_size: usize,
len: usize,
) -> io::Result<Self> {
pub unsafe fn from_raw_parts(bottom: *mut u8, guard_size: usize, len: usize) -> Result<Self> {
Ok(Self(imp::FiberStack::from_raw_parts(
bottom, guard_size, len,
)?))
Expand Down Expand Up @@ -96,7 +118,7 @@ pub unsafe trait RuntimeFiberStackCreator: Send + Sync {
///
/// This is useful to plugin previously allocated memory instead of mmap'ing a new stack for
/// every instance.
fn new_stack(&self, size: usize) -> Result<Box<dyn RuntimeFiberStack>, Error>;
fn new_stack(&self, size: usize) -> core::result::Result<Box<dyn RuntimeFiberStack>, Error>;
}

/// A fiber stack backed by custom memory.
Expand Down Expand Up @@ -138,7 +160,7 @@ impl<'a, Resume, Yield, Return> Fiber<'a, Resume, Yield, Return> {
pub fn new(
stack: FiberStack,
func: impl FnOnce(Resume, &mut Suspend<Resume, Yield, Return>) -> Return + 'a,
) -> io::Result<Self> {
) -> Result<Self> {
let inner = imp::Fiber::new(&stack.0, func)?;

Ok(Self {
Expand All @@ -164,7 +186,7 @@ impl<'a, Resume, Yield, Return> Fiber<'a, Resume, Yield, Return> {
///
/// 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> {
pub fn resume(&self, val: Resume) -> core::result::Result<Return, Yield> {
assert!(!self.done.replace(true), "cannot resume a finished fiber");
let result = Cell::new(RunResult::Resuming(val));
self.inner.resume(&self.stack().0, &result);
Expand All @@ -175,7 +197,12 @@ impl<'a, Resume, Yield, Return> Fiber<'a, Resume, Yield, Return> {
Err(y)
}
RunResult::Returned(r) => Ok(r),
RunResult::Panicked(payload) => std::panic::resume_unwind(payload),
RunResult::Panicked(_payload) => {
#[cfg(feature = "std")]
panic::resume_unwind(_payload);
#[cfg(not(feature = "std"))]
unreachable!()
}
}
}

Expand Down Expand Up @@ -220,7 +247,17 @@ impl<Resume, Yield, Return> Suspend<Resume, Yield, Return> {
inner,
_phantom: PhantomData,
};
let result = panic::catch_unwind(AssertUnwindSafe(|| (func)(initial, &mut suspend)));

let result;
#[cfg(feature = "std")]
{
result = panic::catch_unwind(AssertUnwindSafe(|| (func)(initial, &mut suspend)));
}
#[cfg(not(feature = "std"))]
{
result = Ok((func)(initial, &mut suspend));
}

suspend.inner.switch::<Resume, Yield, Return>(match result {
Ok(result) => RunResult::Returned(result),
Err(panic) => RunResult::Panicked(panic),
Expand All @@ -234,9 +271,10 @@ impl<A, B, C> Drop for Fiber<'_, A, B, C> {
}
}

#[cfg(test)]
#[cfg(all(test))]
mod tests {
use super::{Fiber, FiberStack};
use alloc::string::ToString;
use std::cell::Cell;
use std::panic::{self, AssertUnwindSafe};
use std::rc::Rc;
Expand Down
174 changes: 174 additions & 0 deletions crates/fiber/src/nostd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! no_std implementation of fibers.
//!
//! This is a very stripped-down version of the Unix platform support,
//! but without mmap or guard pages, because on no_std systems we do
//! not assume that virtual memory exists.
//!
//! The stack layout is nevertheless the same (modulo the guard page)
//! as on Unix because we share its low-level implementations:
//!
//! ```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
//! | |
//! 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::stackswitch::*;
use crate::{RunResult, RuntimeFiberStack};
use alloc::boxed::Box;
use alloc::{vec, vec::Vec};
use core::cell::Cell;
use core::ops::Range;

// The no_std implementation is infallible in practice, but we use
// `anyhow::Error` here absent any better alternative.
pub type Result<T> = core::result::Result<T, anyhow::Error>;

pub struct FiberStack {
base: BasePtr,
len: usize,
/// Backing storage, if owned. Allocated once at startup and then
/// not reallocated afterward.
storage: Vec<u8>,
}

struct BasePtr(*mut u8);

unsafe impl Send for BasePtr {}
unsafe impl Sync for BasePtr {}

const STACK_ALIGN: usize = 16;

/// Align a pointer by incrementing it up to `align - 1`
/// bytes. `align` must be a power of two. Also updates the length as
/// appropriate so that `ptr + len` points to the same endpoint.
fn align_ptr(ptr: *mut u8, len: usize, align: usize) -> (*mut u8, usize) {
let ptr = ptr as usize;
let aligned = (ptr + align - 1) & !(align - 1);
let new_len = len - (aligned - ptr);
(aligned as *mut u8, new_len)
}

impl FiberStack {
pub fn new(size: usize) -> Result<Self> {
let mut storage = vec![0; size];
let (base, len) = align_ptr(storage.as_mut_ptr(), size, STACK_ALIGN);
Ok(FiberStack {
storage,
base: BasePtr(base),
len,
})
}

pub unsafe fn from_raw_parts(base: *mut u8, guard_size: usize, len: usize) -> Result<Self> {
Ok(FiberStack {
storage: vec![],
base: BasePtr(base.offset(isize::try_from(guard_size).unwrap())),
len,
})
}

pub fn is_from_raw_parts(&self) -> bool {
self.storage.is_empty()
}

pub fn from_custom(_custom: Box<dyn RuntimeFiberStack>) -> Result<Self> {
unimplemented!("Custom fiber stacks not supported in no_std fiber library")
}

pub fn top(&self) -> Option<*mut u8> {
Some(self.base.0.wrapping_byte_add(self.len))
}

pub fn range(&self) -> Option<Range<usize>> {
let base = self.base.0 as usize;
Some(base..base + self.len)
}

pub fn guard_range(&self) -> Option<Range<*mut u8>> {
None
}
}

pub struct Fiber;

pub struct Suspend {
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, &mut 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: &FiberStack, func: F) -> Result<Self>
where
F: FnOnce(A, &mut super::Suspend<A, B, C>) -> C,
{
unsafe {
let data = Box::into_raw(Box::new(func)).cast();
wasmtime_fiber_init(stack.top().unwrap(), fiber_start::<F, A, B, C>, data);
}

Ok(Self)
}

pub(crate) fn resume<A, B, C>(&self, stack: &FiberStack, 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 addr = stack.top().unwrap().cast::<usize>().offset(-1);
addr.write(result as *const _ as usize);

wasmtime_fiber_switch(stack.top().unwrap());

// null this out to help catch use-after-free
addr.write(0);
}
}
}

impl Suspend {
pub(crate) fn switch<A, B, C>(&mut self, result: RunResult<A, B, C>) -> A {
unsafe {
// Calculate 0xAff8 and then write to it
(*self.result_location::<A, B, C>()).set(result);

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());
ret.cast()
}
}
Loading

0 comments on commit dc4fe18

Please sign in to comment.