Using dyn without allocation
In the previous chapter, we showed how you can invoke async methods from a dyn Trait
value in a natural fashion. In those examples, though, we assume that it was ok to allocate a Box
for every call to an async function. For most applications, this is true, but for some applications, it is not. This could be because they intend to run in a kernel or embedded context, where no allocator is available, or it could be because of a very tight loop in which allocation introduces too much overhead. The good news is that our design allows you to avoid using Box, though it does take a bit of work on your part.
In general, functions that accept a &dyn Trait
as argument don't control how memory is allocated. So the count
function that we saw before can be used equally well on a no-std or kernel platform:
#![allow(unused)] fn main() { async fn count(iter: &mut dyn AsyncIterator) { // Whether or not `iter.next()` will allocate a box // depends on the underlying type; this `count` fn // doesn't have to know, and so it works equally // well in a no-std or std environment. ... } }
The decision about whether to use box or some other way of returning a future is made by the type implementing the async trait. In the previous example, the type was YieldingRangeIterator
, and its impl didn't make any kind of explicit choice, and thus the default is that it will allocate a Box
:
#![allow(unused)] fn main() { impl AsyncIterator for YieldingRangeIterator { type Item = u32; async fn next(&mut self) { // The default behavior here is to allocate a `Box` // when `next` is called through a `dyn AsyncIterator` // (no `Box` is allocated when `next` is called through // static dispatch, in that case the future itself is // returned.) ... } } }
If you want to use YieldingRangeIterator
in a context without Box
, you can do that by wrapping it in an adapter type. This adapter type will implement an alternative memory allocation strategy, such as using pre-allocated stack storage.
For the most part, there is no need to implement your own adapter type, beacuse the dyner
crate (to be published by rust-lang) includes a number of useful ones. For example, to pre-allocate the next
future on the stack, which is useful both for performance or no-std scenarios, you could use an "inline" adapter type, like the InlineAsyncIterator
type provided by the dyner
crate:
#![allow(unused)] fn main() { use dyner::InlineAsyncIterator; // ^^^^^^^^^^^^^^^^^^^ // Inline adapter type async fn count_range(mut x: YieldingRangeIterator) -> usize { // allocates stack space for the `next` future: let inline_x = InlineAsyncIterator::new(x); // invoke the `count` fn, which will no use that stack space // when it runs count(&mut inline_x).await } }
Dyner provides some other strategies, such as the CachedAsyncIterator
(which caches the returned Box
and re-uses the memory in between calls) and the BoxInAllocatorAsyncIterator
(which uses a Box
, but with a custom allocator).
How you apply an existing "adapter" strategy to your own traits
The InlineAsyncIterator
adapts an AsyncIterator
to pre-allocate stack space for the returned futures, but what if you want to apply that inline stategy to one of your traits? You can do that by using the #[inline_adapter]
attribute macro applied to your trait definition:
#![allow(unused)] fn main() { #[inline_adapter(InlineMyTrait)] trait MyTrait { async fn some_function(&mut self); } }
This will create an adapter type called InlineMyTrait
(the name is given as an argument to the attribute macro). You would then use it by invoking new
:
#![allow(unused)] fn main() { fn foo(x: impl MyTrait) { let mut w = InlineMyTrait::new(x); bar(&mut w); } fn bar(x: &mut dyn MyTrait) { x.some_function(); } }
If the trait is not defined in your crate, and hence you cannot use an attribute macro, you can use this alternate form, but it requires copying the trait definition:
#![allow(unused)] fn main() { dyner::inline::adapter_struct! { struct InlineAsyncIterator for trait MyTrait { async fn foo(&mut self); } } }