I'm trying to build an async dynamic registration system using the inventory crate.
The sync code is easy, and works fine; however, once async is becoming involved, and I need the registration to run in tokio
, I'm having troubles with the lifetime of a reference passed into the registration function.
It's easier to illustrate with a code example:
I've got a collector, which collects (in this example), some string flags:
#[derive(Debug, Default)]
struct Collector {
flags: RwLock<Vec<String>>,
}
impl Collector {
pub async fn add(&self, flag: &str) {
let mut lock = self.flags.write().await;
lock.push(flag.to_owned());
}
}
Using inventory
, the user can submit functions, which can call Collector::add
, to add their own flags:
type CollectFn<'a> = fn(&'a Collector)
-> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
struct CollectFunc<'a>(CollectFn<'a>);
inventory::collect!(CollectFunc<'static>); // notice the 'static lifetime
The user will write a function that adheres to the CollectFn
signature and submits it:
struct FooFlag {}
impl<'a> FooFlag {
pub(crate) fn ctor(collector: &'a Collector) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>
where
Self: Sync + 'a,
{
Box::pin(async move {
collector.add("Foo").await;
})
}
}
inventory::submit!(CollectFunc(FooFlag::ctor));
The signature of the ctor
function is inspired by how the async-trait
creates boxed futures.
However, due to inventory::collect!
requiring me to specify a 'static
lifetime, it means that I require a Collector
with a static lifetime.
This code fails to compile:
#[tokio::main]
async fn main() {
let collector = Collector::default();
for func in inventory::iter::<CollectFunc<'_>>() {
(func.0)(collector_ref).await;
}
println!("flags: {:?}", collector.flags);
}
Of course, for this particular case, this is incorrect. Collector
needs to have a lifetime that's longer than the iteration loop. This is due to not storing the futures anywhere or not dropping collector
, of course. Once we do this (and we can, with safe Rust), things start to break.
I can circumvent this with std::mem::transmute
; but this is ... well ... unsafe and could spawn nasal daemons.
This code compiles, works, and even runs perfectly fine under miri
:
#[tokio::main]
async fn main() {
let collector = Collector::default();
let collector_ref =
unsafe { std::mem::transmute::<&Collector, &'static Collector>(&collector) };
for func in inventory::iter::<CollectFunc<'_>>() {
(func.0)(collector_ref).await;
}
println!("flags: {:?}", collector.flags);
}
Is there any way for me to avoid this unsafe
transmute
?
Thank you!
Full Code Listing (minimal reproducible example)
# Cargo.toml
[package]
name = "reproduce"
version = "0.1.0"
edition = "2021"
[dependencies]
inventory = "0.3.15"
tokio = { version = "1.41.0", features = ["full"] }
// main.rs
use std::future::Future;
use std::pin::Pin;
use tokio::sync::RwLock;
#[derive(Debug, Default)]
struct Collector {
flags: RwLock<Vec<String>>,
}
impl Collector {
pub async fn add(&self, flag: &str) {
let mut lock = self.flags.write().await;
lock.push(flag.to_owned());
}
}
type CollectFn<'a> = fn(&'a Collector)
-> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
struct CollectFunc<'a>(CollectFn<'a>);
inventory::collect!(CollectFunc<'static>);
struct FooFlag {}
impl<'a> FooFlag {
pub(crate) fn ctor(collector: &'a Collector) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>
where
Self: Sync + 'a,
{
Box::pin(async move {
collector.add("Foo").await;
})
}
}
inventory::submit!(CollectFunc(FooFlag::ctor));
#[tokio::main]
async fn main() {
let collector = Collector::default();
let collector_ref =
unsafe { std::mem::transmute::<&Collector, &'static Collector>(&collector) };
for func in inventory::iter::<CollectFunc<'_>>() {
(func.0)(collector_ref).await;
}
println!("flags: {:?}", collector.flags);
}