diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ebf23ac --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to +agree to a Contributor License Agreement (CLA) declaring that you have the right to, +and actually do, grant us the rights to use your contribution. For details, visit +https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need +to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the +instructions provided by the bot. You will only need to do this once across all repositories using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9c78f4e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "stackfuture" +version = "0.1.0" +edition = "2021" +license = "MIT" +authors = ["Microsoft"] + +[dependencies] + +[dev-dependencies] +futures = { version = "0.3", features = ["executor"] } diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 9e841e7..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..372f8d3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +StackFuture +Copyright (c) Microsoft Corporation. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5cd7cec..445f79a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,38 @@ -# Project - -> This repo has been populated by an initial template to help get you started. Please -> make sure to update the content to build a great experience for community-building. - -As the maintainer of this project, please make a few updates: - -- Improving this README.MD file to provide a great experience -- Updating SUPPORT.MD with content about this project's support experience -- Understanding the security reporting process in SECURITY.MD -- Remove this section from the README +# StackFuture + +This crate defines a `StackFuture` wrapper around futures that stores the wrapped future in space provided by the caller. +This can be used to emulate dynamic async traits without requiring heap allocation. +Below is an example of how use `StackFuture`: + +```rust +use stackfuture::*; + +trait PseudoAsyncTrait { + fn do_something(&self) -> StackFuture<'static, (), { 512 }>; +} + +impl PseudoAsyncTrait for i32 { + fn do_something(&self) -> StackFuture<'static, (), { 512 }> { + StackFuture::from(async { + // function body goes here + }) + } +} + +async fn use_dyn_async_trait(x: &dyn PseudoAsyncTrait) { + x.do_something().await; +} + +async fn call_with_dyn_async_trait() { + use_dyn_async_trait(&42).await; +} +``` + +This is most useful for cases where async functions in `dyn Trait` objects are needed but storing them in a `Box` is not feasible. +Such cases include embedded programming where allocation is not available, or in tight inner loops where the performance overhead for allocation is unacceptable. +Note that doing this involves tradeoffs. +In the case of `StackFuture`, you must set a compile-time limit on the maximum size of future that will be supported. +If you need to support async functions in `dyn Trait` objects but these constraints do not apply to you, you may be better served by the [`async-trait`](https://crates.io/crates/async-trait) crate. ## Contributing @@ -26,8 +50,8 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio ## Trademarks -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/SECURITY.md b/SECURITY.md index 869fdfe..f0c2dc1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,7 +14,7 @@ Instead, please report them to the Microsoft Security Response Center (MSRC) at If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0e58ad7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! This crate defines a `StackFuture` wrapper around futures that stores the wrapped +//! future in space provided by the caller. This can be used to emulate dyn async traits +//! without requiring heap allocation. +//! +//! For more details, see the documentation on the [`StackFuture`] struct. + +// std is needed to run tests, but otherwise we don't need it. +#![cfg_attr(not(test), no_std)] +#![warn(missing_docs)] + +use core::future::Future; +use core::marker::PhantomData; +use core::mem; +use core::mem::MaybeUninit; +use core::pin::Pin; +use core::ptr; +use core::task::Context; +use core::task::Poll; + +/// 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 +/// as well come from some other allocation. +/// +/// A `StackFuture` can be used to emulate async functions in dyn Trait objects. +/// For example: +/// +/// ``` +/// # use stackfuture::*; +/// trait PseudoAsyncTrait { +/// fn do_something(&self) -> StackFuture<'_, (), { 512 }>; +/// } +/// +/// impl PseudoAsyncTrait for i32 { +/// fn do_something(&self) -> StackFuture<'_, (), { 512 }> { +/// StackFuture::from(async { +/// // function body goes here +/// }) +/// } +/// } +/// +/// async fn use_dyn_async_trait(x: &dyn PseudoAsyncTrait) { +/// x.do_something().await; +/// } +/// +/// async fn call_with_dyn_async_trait() { +/// use_dyn_async_trait(&42).await; +/// } +/// ``` +/// +/// This example defines `PseudoAsyncTrait` with a single method `do_something`. +/// The `do_something` method can be called as if it were declared as +/// `async fn do_something(&self)`. To implement `do_something`, the easiest thing +/// to do is to wrap the body of the function in `StackFuture::from(async { ... })`, +/// which creates an anonymous future for the body and stores it in a `StackFuture`. +/// +/// Because `StackFuture` does not know the size of the future it wraps, the maximum +/// size of the future must be specified in the `STACK_SIZE` parameter. In the example +/// here, we've used a stack size of 512, which is probably much larger than necessary +/// but would accomodate many futures besides the simple one we've shown here. +/// +/// `StackFuture` ensures when wrapping a future that enough space is available, and +/// it also respects any alignment requirements for the wrapped future. Note that the +/// wrapped future's alignment must be less than or equal to that of the overall +/// `StackFuture` struct. +/// +/// The following example would panic because the future is too large: +/// ```should_panic +/// # use stackfuture::*; +/// fn large_stack_future() -> StackFuture<'static, u8, { 4 }> { +/// StackFuture::from(async { +/// let mut buf = [0u8; 256]; +/// fill_buf(&mut buf).await; +/// buf[0] +/// }) +/// } +/// +/// async fn fill_buf(buf: &mut [u8]) { +/// buf[0] = 42; +/// } +/// +/// let future = large_stack_future(); +/// ``` +/// +/// The following example would panic because the alignment requirements can't be met: +/// ```should_panic +/// # use stackfuture::*; +/// #[repr(align(4096))] +/// struct LargeAlignment(i32); +/// +/// fn large_alignment_future() -> StackFuture<'static, i32, { 8192 }> { +/// StackFuture::from(async { +/// let mut buf = LargeAlignment(0); +/// fill_buf(&mut buf).await; +/// buf.0 +/// }) +/// } +/// +/// async fn fill_buf(buf: &mut LargeAlignment) { +/// buf.0 = 42; +/// } +/// +/// let future = large_alignment_future(); +/// ``` +#[repr(C)] // Ensures the data first does not have any padding before it in the struct +pub struct StackFuture<'a, T, const STACK_SIZE: usize> { + /// An array of bytes that is used to store the wrapped future. + data: [MaybeUninit; STACK_SIZE], + /// Since the type of `StackFuture` does not know the underlying future that it is wrapping, + /// we keep a manual vtable that serves pointers to Poll::poll and Drop::drop. These are + /// generated and filled in by `StackFuture::from`. + /// + /// This field stores a pointer to the poll function wrapper. + poll_fn: fn(this: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, + /// Stores a pointer to the drop function wrapper + /// + /// See the documentation on `poll_fn` for more details. + drop_fn: fn(this: &mut Self), + /// StackFuture can be used similarly to a `dyn Future`. We keep a PhantomData + /// here so the type system knows this. + _phantom: PhantomData + Send + 'a>, +} + +impl<'a, T, const STACK_SIZE: usize> StackFuture<'a, T, { STACK_SIZE }> { + /// Creates a StackFuture from an existing future + /// + /// See the documentation on `StackFuture` for examples of how to use this. + pub fn from(future: F) -> Self + 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) { + let mut result = StackFuture { + data: [MaybeUninit::uninit(); STACK_SIZE], + poll_fn: Self::poll_inner::, + drop_fn: Self::drop_inner::, + _phantom: PhantomData, + }; + + // Ensure result.data is at the beginning of the struct so we don't need to do + // alignment adjustments. + assert_eq!(result.data.as_ptr() as usize, &result as *const _ as usize); + + // SAFETY: result.as_mut_ptr returns a pointer into result.data, which is an + // uninitialized array of bytes. result.as_mut_ptr ensures the returned pointer + // is correctly aligned, and the if expression we are in ensures the buffer is + // large enough. + // + // Because `future` is bound by `'a` and `StackFuture` is also bound by `'a`, + // we can be sure anything that `future` closes over will also outlive `result`. + unsafe { result.as_mut_ptr::().write(future) }; + + result + } else { + panic!( + "cannot create StackFuture, required size is {}, available space is {}", + mem::size_of::(), + STACK_SIZE + ); + } + } + + /// 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 { + self.as_pin_mut_ref::().poll(cx) + } + + /// A wrapper around the inner future's drop function, which we store in the drop_fn field + /// of this struct. + fn drop_inner(&mut self) { + // SAFETY: *this.as_mut_ptr() was previously written as type F + unsafe { ptr::drop_in_place(self.as_mut_ptr::()) } + } + + /// Returns a pointer into self.data that meets the alignment requirements for type `F` + /// + /// Before writing to the returned pointer, the caller must ensure that self.data is large + /// enough to hold F and any required padding. + fn as_mut_ptr(&mut self) -> *mut F { + assert!(Self::has_space_for::()); + self.data.as_mut_ptr().cast() + } + + /// Returns a pinned mutable reference to a type F stored in self.data + fn as_pin_mut_ref(self: Pin<&mut Self>) -> Pin<&mut F> { + // SAFETY: `StackFuture` is only created by `StackFuture::from`, which + // writes an `F` to `self.as_mut_ptr(), so it's okay to cast the `*mut F` + // to an `&mut F` with the same lifetime as `self`. + // + // For pinning, since self is already pinned, we know the wrapped future + // is also pinned. + // + // This function is only doing pointer arithmetic and casts, so we aren't moving + // any pinned data. + unsafe { self.map_unchecked_mut(|this| &mut *this.as_mut_ptr()) } + } + + /// Computes how much space is required to store a value of type `F` + fn required_space() -> usize { + mem::size_of::() + } + + /// Determines whether this `StackFuture` can hold a value of type `F` + 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 { + Self::has_space_for::() + } +} + +impl<'a, T, const STACK_SIZE: usize> Future for StackFuture<'a, T, { STACK_SIZE }> { + type Output = T; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + (self.as_mut().poll_fn)(self, cx) + } +} + +impl<'a, T, const STACK_SIZE: usize> Drop for StackFuture<'a, T, { STACK_SIZE }> { + fn drop(&mut self) { + (self.drop_fn)(self); + } +} + +#[cfg(test)] +mod tests { + use crate::StackFuture; + use core::task::Poll; + use futures::executor::block_on; + use futures::pin_mut; + use futures::Future; + use std::sync::Arc; + use std::task::Context; + use std::task::Wake; + + #[test] + fn create_and_run() { + // A smoke test. Make sure we can create a future, run it, and get a value out. + let f = StackFuture::<'_, _, 8>::from(async { 5 }); + assert_eq!(block_on(f), 5); + } + + /// A type that is uninhabited and therefore can never be constructed. + enum Never {} + + /// A future whose poll function always returns `Pending` + /// + /// Used to force a suspend point so we can test behaviors with suspended futures. + struct SuspendPoint; + impl Future for SuspendPoint { + type Output = Never; + + fn poll( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + Poll::Pending + } + } + + /// A waker that doesn't do anything. + /// + /// Needed so we can create a context and manually call poll. + struct Waker; + impl Wake for Waker { + fn wake(self: std::sync::Arc) { + unimplemented!() + } + } + + #[test] + fn destructor_runs() { + // A test to ensure `StackFuture` correctly calls the destructor of the underlying future. + // + // We do this by creating a manually implemented future whose destructor sets a boolean + // indicating it ran. We create such a value (the `let _ = DropMe(&mut destructed))` line + // below), then use `SuspendPoint.await` to suspend the future. + // + // The driver code creates a context and then calls poll once on the future so that the + // DropMe object will be created. We then let the future go out of scope so the destructor + // will run. + let mut destructed = false; + let _poll_result = { + let f = async { + struct DropMe<'a>(&'a mut bool); + impl Drop for DropMe<'_> { + fn drop(&mut self) { + *self.0 = true; + } + } + let _ = DropMe(&mut destructed); + SuspendPoint.await + }; + let f = StackFuture::<'_, _, 32>::from(f); + + let waker = Arc::new(Waker).into(); + let mut cx = Context::from_waker(&waker); + pin_mut!(f); + f.poll(&mut cx) + }; + assert!(destructed); + } + + #[test] + fn test_alignment() { + // A test to make sure we store the wrapped future with the correct alignment + + #[repr(align(8))] + struct BigAlignment(u32); + + impl Future for BigAlignment { + type Output = Never; + + fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + Poll::Pending + } + } + let mut f = StackFuture::<'_, _, 1016>::from(BigAlignment(42)); + assert!(is_aligned(f.as_mut_ptr::(), 8)); + } + + /// Returns whether `ptr` is aligned with the given alignment + /// + /// `alignment` must be a power of two. + fn is_aligned(ptr: *mut T, alignment: usize) -> bool { + (ptr as usize) & (alignment - 1) == 0 + } +}