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?
8 posts - 5 participants
