Lifetime issue with dyn trait

Hello,

I am working on a proof-of-concept microservice using Rust and hex architecture. I am fairly new to Rust, so my code may not be idiomatic. I have a compilation error, 'lifetime may not live long enough', dealing with the lifetime of a trait reference. I have been going around in circles tracing, reviewing, tweaking, but to no avail. I suspect that I need to change the code to use 'static with the dyn traits, but when I did it, the code wouldn't build. I'd really appreciate any suggestions about how to resolve my issue. I am sure that I am missing a kernel of knowledge as a newcomer... :slight_smile:

Here is a link to a minimal working version of this app at gitlab:

Thank you for your time and interest!

Mike

ERROR MESSAGE:

error: lifetime may not live long enough
  --> src/main.rs:34:63
   |
29 | fn find_one_project(database_connection: &InMemoryDatabaseConnection) {
   |                                          - let's call the lifetime of this reference `'1`
...
34 |     let find_one_project_service = FindOneProjectService::new(find_one_project_persistence_adapter);
   |                                                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cast requires that `'1` must outlive `'static`
MAIN APPLICATION:
/////////////////////////////////////////////////////////////////////////
// main.rs
/////////////////////////////////////////////////////////////////////////
fn main() -> std::io::Result<()> {

    let database_connection = InMemoryDatabaseConnection::new().unwrap();
    find_one_project(&database_connection);

    Ok(())
}

fn find_one_project(database_connection: &InMemoryDatabaseConnection) {

    let find_one_project_repository = FindOneProjectRepository::new_with_db(database_connection).unwrap();
    let find_one_project_persistence_adapter = Box::new(FindOneProjectPersistenceAdapter::new(find_one_project_repository));

    let find_one_project_service = FindOneProjectService::new(find_one_project_persistence_adapter);

     ...

     Ok(())
}
/////////////////////////////////////////////////////////////////////////
// find_one_project_service.rs
/////////////////////////////////////////////////////////////////////////
pub struct FindOneProjectService<'a> {
    query_port: Box<(dyn FindOneProjectQueryPort<'a>)>
}

impl <'a>FindOneProjectService<'a> {
    pub fn new(query_port: Box<(dyn FindOneProjectQueryPort<'a>)>) -> Self {
        Self { query_port }
    }
}

impl <'a>FindOneProjectUseCase<'a> for Box<FindOneProjectService<'a>> {
    fn find_one(&self, query: FindOneProjectQuery) -> Result<Project, BoxError> {
        self.query_port.find_one(query.name)
    }
}

dyn Trait has a lifetime, and that lifetime is 'static for Box<dyn Trait> outside of function bodies.

There's too much missing to give a lot of solid advice, but you're probably overusing lifetimes (and maybe Box too). Why do your traits have lifetime parameters, for example?

Thanks for your response, Quinedot. The following link is to a minimal version of my app, so it is easier to follow things starting from main.rs rather than just looking at snippets. Have a great holiday weekend!

https://storage.googleapis.com/abitofhelp_public_share/rusttraitref.zip

Mike

If there's a repo or something I might look at it later (but I won't be downloading any random zip files, no offense).

1 Like

Actually, that is a much better idea than a zip. I will upload the code for sharing.

Here is a link to a minimal working version of this app at gitlab:

1 Like
Expand to see solutions for your case.
pub struct FindOneProjectService<'a> {
    query_port: Box<(dyn 'a + FindOneProjectQueryPort<'a>)>,
}

impl<'a> FindOneProjectService<'a> {
    pub fn new(query_port: Box<(dyn 'a + FindOneProjectQueryPort<'a>)>) -> Self {
        Self { query_port }
    }
}

or

pub trait FindOneProjectQueryPort<'a> {
    fn find_one(&self, name: AppId) -> Result<Project, BoxError>;
}

The general pattern here is Rust Playground

trait Trait<'a> {}

struct Struct<'a>(Box<dyn Trait<'a>>);

impl<'a> Trait<'a> for &'a str {}

fn f(s: &str) {
    Struct(Box::new(s) as _);
}

// won't compile:
error: lifetime may not live long enough
  --> src/lib.rs:11:16
   |
10 |     fn f(s: &str) {
   |             - let's call the lifetime of this reference `'1`
11 |         Struct(Box::new(s) as _);
   |                ^^^^^^^^^^^ cast requires that `'1` must outlive `'static`

Trait objects have their own rules: Lifetime elision - The Rust Reference

Box<dyn Trait<'a>> is syntactic sugar for Box<dyn 'static + Trait<'a>> in case of plain trait Trait<'a> {}, which follows the rule

If the trait has no lifetime bounds, then the lifetime is inferred in expressions and is 'static outside of expressions.

Your code FindOneProjectService::new(find_one_project_persistence_adapter /* non-static type*/); obviously can't meet the elided 'static bound, thus the error.

So one solution is using another elision rule:

If the trait is defined with a single lifetime bound then that bound is used.

trait Trait<'a>: 'a /* Note here! */ {}

Another solution is explicitly using the lifetime bound on trait objects:

Box<dyn 'a /* Note here! */ + Trait<'a>>
1 Like

Thank you very much for your detailed response. I learned a lot from it and will dig into the references tomorrow after some coffee!

Mike

I see no reason for the lifetimes on the traits here. In the example at least, you can get rid of them. (Best to avoid lifetimes on traits if possible; trait parameters are invariant, which means they're less flexible to work with than reference lifetimes or other covariant lifetimes.)

pub trait FindOneProjectQueryPort<'a> {
    fn find_one(&self, name: AppId) -> Result<Project, BoxError>;
}

pub trait FindOneProjectUseCase<'a> {
    fn find_one(&self, query: FindOneProjectQuery) -> Result<Project, BoxError>;
}

Lifetime-carrying structs are typically used for short-term borrows, not long term data structures in one's application.

pub struct FindOneProjectRepository<'a> {
    conn: &'a InMemoryDatabaseConnection,
}

If this is a temporary, the surrounding types should probably be temporary too.

// Here I'm showing updated code

pub struct FindOneProjectService<'a> {
    query_port: &'a dyn FindOneProjectQueryPort,
}

impl<'a> FindOneProjectService<'a> {
    pub fn new(query_port: &'a dyn FindOneProjectQueryPort) -> Self {
        Self { query_port }
    }
}

// I saw no reason for this to be implemented on the boxed form either...
impl FindOneProjectUseCase for FindOneProjectService<'_> {
    fn find_one(&self, query: FindOneProjectQuery) -> Result<Project, BoxError> {
        self.query_port.find_one(query.name)
    }
}

And then in main

    // Here I'm showing updated code

    let find_one_project_repository = FindOneProjectRepository::new_with_db(database_connection).unwrap();
    let find_one_project_persistence_adapter = FindOneProjectPersistenceAdapter::new(find_one_project_repository);
    let find_one_project_service = FindOneProjectService::new(&find_one_project_persistence_adapter);

Sometimes there's good reason to have a Box<dyn Trait + 'a>, when you need to type-erase something that is both owning and borrowing -- like an iterator map that owns a closure, say. But if you can avoid boxing and just use &'a dyn Trait /* + 'a */, some things get more ergonomic (and avoid allocations).


If this is not a temporary, perhaps your InMemoryDatabaseConnection should be contained in an Arc or similar, to allow cheap cloning without borrowing (less lifetime hassles). This would allow you to get rid of the lifetimes on the FindOneProject types instead.

3 Likes

The solution by vague is great when using references, such as the inmemory_database_connection variable in main(). The solution by quinedot is similar and helpful. quinedot mentioned something that I had not considered and it totally changed my solution... I was creating a reference of the database connection so I could share it with two separate repositories. I did not think about using an Arc to provide clones of the database connection, which eliminated a lot of the reference and lifetime code and made things simpler. I will update the code in gitlab to show both solutions.

Both solutions have been very, very instructive and helpful. I will return to them as I keep climbing Rust's learning curve. :slight_smile:

Thank you for your help and sharing of knowledge!
Mike

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.