diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f419320..3b68cae 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,6 +11,9 @@ env: jobs: build: + strategy: + matrix: + features: ["", "--no-default-features"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -21,12 +24,15 @@ jobs: - name: Clippy run: cargo clippy --all-features --all-targets -- -D warnings - name: Build - run: cargo build --verbose + run: cargo build --verbose ${{ matrix.features }} - name: Run tests - run: cargo test --verbose + run: cargo test --verbose ${{ matrix.features }} miri: name: "Miri" + strategy: + matrix: + features: ["", "--no-default-features"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -36,4 +42,4 @@ jobs: rustup override set nightly cargo miri setup - name: Test with Miri - run: cargo miri test + run: cargo miri test ${{ matrix.features }} diff --git a/Cargo.toml b/Cargo.toml index af44896..2a9681d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "stackfuture" description = "StackFuture is a wrapper around futures that stores the wrapped future in space provided by the caller." -version = "0.1.1" +version = "0.2.0" edition = "2021" license = "MIT" authors = ["Microsoft"] @@ -16,3 +16,7 @@ categories = ["asynchronous", "no-std", "rust-patterns"] [dev-dependencies] futures = { version = "0.3", features = ["executor"] } + +[features] +default = ["alloc"] +alloc = [] diff --git a/src/lib.rs b/src/lib.rs index 6cd51c9..3f0bb8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,12 @@ use core::ptr; use core::task::Context; use core::task::Poll; +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "alloc")] +use alloc::boxed::Box; + /// A wrapper that stores a future in space allocated by the container /// /// Often this space comes from the calling function's stack, but it could just @@ -138,11 +144,27 @@ impl<'a, T, const STACK_SIZE: usize> StackFuture<'a, T, { STACK_SIZE }> { F: Future + Send + 'a, // the bounds here should match those in the _phantom field { Self::try_from(future).unwrap_or_else(|f| { - panic!( - "cannot create StackFuture, required size is {}, available space is {}", - mem::size_of_val(&f), - STACK_SIZE - ) + match (Self::has_alignment_for_val(&f), Self::has_space_for_val(&f)) { + (false, false) => panic!( + "cannot create StackFuture, required size is {}, available space is {}; required alignment is {} but maximum alignment is {}", + mem::size_of_val(&f), + STACK_SIZE, + mem::align_of::(), + mem::align_of::() + ), + (false, true) => panic!( + "cannot create StackFuture, required alignment is {} but maximum alignment is {}", + mem::align_of::(), + mem::align_of::() + ), + (true, false) => panic!( + "cannot create StackFuture, required size is {}, available space is {}", + mem::size_of_val(&f), + STACK_SIZE + ), + // If we have space and alignment, then `try_from` would have succeeded + (true, true) => unreachable!(), + } }) } @@ -158,15 +180,7 @@ impl<'a, T, const STACK_SIZE: usize> StackFuture<'a, T, { STACK_SIZE }> { where F: Future + Send + 'a, // the bounds here should match those in the _phantom field { - if mem::align_of::() > mem::align_of::() { - panic!( - "cannot create StackFuture, required alignment is {} but maximum alignment is {}", - mem::align_of::(), - mem::align_of::() - ) - } - - if Self::has_space_for_val(&future) { + if Self::has_space_for_val(&future) && Self::has_alignment_for_val(&future) { let mut result = StackFuture { data: [MaybeUninit::uninit(); STACK_SIZE], poll_fn: Self::poll_inner::, @@ -193,6 +207,24 @@ impl<'a, T, const STACK_SIZE: usize> StackFuture<'a, T, { STACK_SIZE }> { } } + /// Creates a StackFuture from the given future, boxing if necessary + /// + /// This version will succeed even if the future is larger than `STACK_SIZE`. If the future + /// is too large, `from_or_box` will allocate a `Box` on the heap and store the resulting + /// boxed future in the `StackFuture`. + /// + /// The same thing also happens if the wrapped future's alignment is larger than StackFuture's + /// alignment. + /// + /// This function requires the "alloc" crate feature. + #[cfg(feature = "alloc")] + pub fn from_or_box(future: F) -> Self + where + F: Future + Send + 'a, // the bounds here should match those in the _phantom field + { + Self::try_from(future).unwrap_or_else(|future| Self::from(Box::pin(future))) + } + /// A wrapper around the inner future's poll function, which we store in the poll_fn field /// of this struct. fn poll_inner(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -238,14 +270,26 @@ impl<'a, T, const STACK_SIZE: usize> StackFuture<'a, T, { STACK_SIZE }> { } /// Determines whether this `StackFuture` can hold a value of type `F` - fn has_space_for() -> bool { + pub fn has_space_for() -> bool { Self::required_space::() <= STACK_SIZE } /// Determines whether this `StackFuture` can hold the referenced value - fn has_space_for_val(_: &F) -> bool { + pub fn has_space_for_val(_: &F) -> bool { Self::has_space_for::() } + + /// Determines whether this `StackFuture`'s alignment is compatible with the + /// type `F`. + pub fn has_alignment_for() -> bool { + mem::align_of::() <= mem::align_of::() + } + + /// Determines whether this `StackFuture`'s alignment is compatible with the + /// referenced value. + pub fn has_alignment_for_val(_: &F) -> bool { + Self::has_alignment_for::() + } } impl<'a, T, const STACK_SIZE: usize> Future for StackFuture<'a, T, { STACK_SIZE }> { diff --git a/src/tests.rs b/src/tests.rs index 728bea9..fe29d82 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -98,6 +98,42 @@ fn test_alignment() { assert!(is_aligned(f.as_mut_ptr::(), 8)); } +#[test] +#[should_panic] +fn test_alignment_failure() { + // A test to make sure we store the wrapped future with the correct alignment + + #[repr(align(256))] + struct BigAlignment(u32); + + impl Future for BigAlignment { + type Output = Never; + + fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + Poll::Pending + } + } + StackFuture::<'_, _, 1016>::from(BigAlignment(42)); +} + +#[cfg(feature = "alloc")] +#[test] +fn test_boxed_alignment() { + // A test to make sure we store the wrapped future with the correct alignment + + #[repr(align(256))] + struct BigAlignment(u32); + + impl Future for BigAlignment { + type Output = Never; + + fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + Poll::Pending + } + } + StackFuture::<'_, _, 1016>::from_or_box(BigAlignment(42)); +} + /// Returns whether `ptr` is aligned with the given alignment /// /// `alignment` must be a power of two. @@ -142,3 +178,10 @@ fn try_from() { Err(big_future) => assert!(StackFuture::<_, 1500>::try_from(big_future).is_ok()), }; } + +#[cfg(feature = "alloc")] +#[test] +fn from_or_box() { + let big_future = StackFuture::<_, 1000>::from(async {}); + StackFuture::<_, 32>::from_or_box(big_future); +}