I'm new to Rust, and I am trying to understand dyn object lifetimes when inside a Box or NonNull. My goal is to write an adapter for a C API. I have a few C struct types:
#[repr(C)]
struct ConcreteA {
// generated by bindgen
}
#[repr(C)]
struct ConcreteB {
// generated by bindgen
}
that all implement the same trait:
trait Trait {
// common methods
}
impl Trait for ConcreteA {
// implementation calls C functions
}
impl Trait for ConcreteB {
// implementation calls different C functions
}
The structs are allocated and initialized in C, so I created a generic "handle" smart pointer type that owns an instance of one of those structs:
use std::{ops::Deref, ptr::NonNull};
#[repr(transparent)]
struct ConcreteHandle<T: Trait>(NonNull<T>);
impl<T: Trait> ConcreteHandle<T> {
// The real code calls into C to allocate and initialize the memory; using a
// Box is for illustration.
fn new(v: T) -> Self {
Self(Box::into_non_null(Box::new(v)))
}
}
impl<T: Trait> Drop for ConcreteHandle<T> {
// The real code calls into C to free the memory; this is for illustration.
fn drop(&mut self) {
drop(unsafe { Box::from_non_null(self.0) });
}
}
impl<T: Trait> Deref for ConcreteHandle<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { self.0.as_ref() }
}
}
Semantically, the handle more-or-less is the underlying concrete object—the lifetime of the handle and the lifetime of the underlying object are tied together. All good so far.
My trouble starts when I try to create a type-erased handle (to make it easier to hold a heterogeneous collection), like this (playground):
#[derive(Debug)]
#[repr(transparent)]
struct TypeErasedHandle(NonNull<dyn Trait>);
impl<T: Trait> From<ConcreteHandle<T>> for TypeErasedHandle {
fn from(h: ConcreteHandle<T>) -> Self {
Self(NonNull::new(h.0.as_ptr()).unwrap())
}
}
impl Drop for TypeErasedHandle {
fn drop(&mut self) {
drop(unsafe { Box::from_non_null(self.0) });
}
}
impl Deref for TypeErasedHandle {
type Target = dyn Trait;
fn deref(&self) -> &Self::Target {
unsafe { self.0.as_ref() }
}
}
I get this error:
error[E0310]: the parameter type `T` may not live long enough
--> src/main.rs:63:14
|
63 | Self(NonNull::new(h.0.as_ptr()).unwrap())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| the parameter type `T` must be valid for the static lifetime...
| ...so that the type `T` will meet its required lifetime bounds
|
help: consider adding an explicit lifetime bound
|
61 | impl<T: Trait + 'static> From<ConcreteHandle<T>> for TypeErasedHandle {
| +++++++++
For more information about this error, try `rustc --explain E0310`.
If I do what the error suggests and add 'static to the type bound, then the toy example compiles and runs, but I have several questions:
-
Doesn't adding
'static(as suggested by the error) limit the usefulness of theTypeErasedHandletype? Doesn't it disallow conversion from a useful subset ofConcreteHandle<T>? -
How can I get an intuitive understanding of what it means to add
+ 'staticto a type bound like this? I understand the use of'staticfor objects that are truly required to have indefinite lifetime, but that doesn't apply here; the lifetime should match the lifetime of theTypeErasedHandlestruct. (Right?) -
If I add a generic lifetime parameter like this:
struct TypeErasedHandle<'a>(NonNull<dyn 'a + Trait>);then all users of the
TypeErasedHandletype must supply a lifetime, which doesn't seem proper given the type's semantics. I want the lifetime to always be "the lifetime of theTypeErasedHandlestruct itself". Is it not possible to express that? -
Should I store the output of(has the same problem)pointer.to_raw_parts()instead of the fat pointer itself? -
Would you take a different approach altogether?