Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ env:

jobs:
build:
strategy:
matrix:
features: ["", "--no-default-features"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -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
Expand All @@ -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 }}
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -16,3 +16,7 @@ categories = ["asynchronous", "no-std", "rust-patterns"]

[dev-dependencies]
futures = { version = "0.3", features = ["executor"] }

[features]
default = ["alloc"]
alloc = []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, but I'd also add default = ["alloc"], since I'd wager most folks are going to be running this with an allocator.

Note that you'll need to change some of the CI stuff as well

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and this is most certainly a semver breaking change, so it'll need to coincide with a version bump to 0.2

76 changes: 60 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,11 +144,27 @@ impl<'a, T, const STACK_SIZE: usize> StackFuture<'a, T, { STACK_SIZE }> {
F: Future<Output = T> + 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::<F>(),
mem::align_of::<Self>()
),
(false, true) => panic!(
"cannot create StackFuture, required alignment is {} but maximum alignment is {}",
mem::align_of::<F>(),
mem::align_of::<Self>()
),
(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!(),
}
})
}

Expand All @@ -158,15 +180,7 @@ impl<'a, T, const STACK_SIZE: usize> StackFuture<'a, T, { STACK_SIZE }> {
where
F: Future<Output = T> + Send + 'a, // the bounds here should match those in the _phantom field
{
if mem::align_of::<F>() > mem::align_of::<Self>() {
panic!(
"cannot create StackFuture, required alignment is {} but maximum alignment is {}",
mem::align_of::<F>(),
mem::align_of::<Self>()
)
}

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::<F>,
Expand All @@ -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<F>(future: F) -> Self
where
F: Future<Output = T> + 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<F: Future>(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
Expand Down Expand Up @@ -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<F>() -> bool {
pub fn has_space_for<F>() -> bool {
Self::required_space::<F>() <= STACK_SIZE
}

/// Determines whether this `StackFuture` can hold the referenced value
fn has_space_for_val<F>(_: &F) -> bool {
pub fn has_space_for_val<F>(_: &F) -> bool {
Self::has_space_for::<F>()
}

/// Determines whether this `StackFuture`'s alignment is compatible with the
/// type `F`.
pub fn has_alignment_for<F>() -> bool {
mem::align_of::<F>() <= mem::align_of::<Self>()
}

/// Determines whether this `StackFuture`'s alignment is compatible with the
/// referenced value.
pub fn has_alignment_for_val<F>(_: &F) -> bool {
Self::has_alignment_for::<F>()
}
}

impl<'a, T, const STACK_SIZE: usize> Future for StackFuture<'a, T, { STACK_SIZE }> {
Expand Down
43 changes: 43 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,42 @@ fn test_alignment() {
assert!(is_aligned(f.as_mut_ptr::<BigAlignment>(), 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<Self::Output> {
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<Self::Output> {
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.
Expand Down Expand Up @@ -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);
}