Hello, I plan on using a trait to define an abstract interface to a data storage, where several implementations may exist
Since the interface will involve I/O, and because I want to use async Rust, I have been considering to use the async-trait
crate. However, some methods might be very cheap and called repeatedly.
I understand that async-trait
imposes some overhead at runtime due to type-erasure / dynamic dispatch and heap allocation. To allow optimizations, I'd like to define my trait in such a way that some implementations may work without dynamic futures. To do this, I declared the trait as follows (simplified example):
use std::future::Future;
use std::pin::Pin;
trait GetAnInteger {
type GetFuture: Future<Output = i32>;
fn get(&self) -> Self::GetFuture;
}
Using an associated type, I can decide for each implementation whether I want to use a Pin<Box<dyn Future<…>…>>
or some other future which doesn't require any allocations on the heap.
Question 1: If the type GetAnInteger::GetFuture
is only restricted to be a Future<Output = i32>
, wouldn't it be possible to return a future that is !Unpin
and thus cannot be polled without using unsafe code, as poll
works on Pin<&mut self>
? Do I need to restrict GetFuture
to be Unpin
? (So far, the compiler didn't complain.)
Edit: I assume that I don't need Unpin
(and shouldn't demand it here), because I can always pin an owned value/future using tokio::pin!
(or similar macros, which internally use unsafe
but do not require the caller to deal with unsafe code).
Let's take a look at an efficient implementation:
struct EfficientGetAnInteger {
value: i32,
}
impl GetAnInteger for EfficientGetAnInteger {
type GetFuture = std::future::Ready<i32>;
fn get(&self) -> Self::GetFuture {
std::future::ready(self.value)
}
}
EfficientGetAnInteger
doesn't really need to do complicated stuff and simply uses Ready
to satisfy the asynchrnonous interface. No heap allocations or dynamic dispatch needed.
But for other implementations of the trait, things might get more complex, and it might not be easy to explicitly name the future that is returned. In that case, I can do the following:
struct DynGetAnInteger {
value: i32,
}
async fn some_async_stuff() {
println!("Let's pretend we do some async stuff here.");
}
impl GetAnInteger for DynGetAnInteger {
type GetFuture = Pin<Box<dyn Future<Output = i32> + Send>>;
fn get(&self) -> Self::GetFuture {
let retval: i32 = self.value;
Box::pin(async move {
// code might be added here in future
some_async_stuff().await;
// code might be added here in future
retval
})
}
}
Here, I don't need to explicitly name the type of the future created by the async move {…}
block because I declare the return type of the get
method (associated type GetFuture
) to be a Pin<Box<dyn Future<…>…>>
, which is the same that async-trait
does, I assume.
So far, my program works fine (here with the tokio
runtime):
#[tokio::main]
async fn main() {
let getter1 = EfficientGetAnInteger { value: 17 };
println!("Got value: {}", getter1.get().await);
let getter2 = DynGetAnInteger { value: 18 };
println!("Got value: {}", getter2.get().await);
}
Question 2: Is my approach the way to go with the current support of async in Rust? Or is there an easier way? It seems like I have to declare an associated type for each async method where I want a highly performant implementation to be possible. That seems very verbose, but I guess there is no other way?