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:

  1. Doesn't adding 'static (as suggested by the error) limit the usefulness of the TypeErasedHandle type? Doesn't it disallow conversion from a useful subset of ConcreteHandle<T>?

  2. How can I get an intuitive understanding of what it means to add + 'static to a type bound like this? I understand the use of 'static for objects that are truly required to have indefinite lifetime, but that doesn't apply here; the lifetime should match the lifetime of the TypeErasedHandle struct. (Right?)

  3. If I add a generic lifetime parameter like this:

    struct TypeErasedHandle<'a>(NonNull<dyn 'a + Trait>); 

    then all users of the TypeErasedHandle type must supply a lifetime, which doesn't seem proper given the type's semantics. I want the lifetime to always be "the lifetime of the TypeErasedHandle struct itself". Is it not possible to express that?

  4. Should I store the output of pointer.to_raw_parts() instead of the fat pointer itself? (has the same problem)

  5. Would you take a different approach altogether?

8 posts - 5 participants

Read full topic