Lifetime struggle (?) on Sunday morning - please help

Dear all,
I am relative new to Rust, coming from a long experience with garbage-collected languages (Java and Python).

I want to use a library some_library_i_cannot_modify with a struct Middle that keeps a reference to struct Inner.

I would like to create my own struct MyOuter that encapsulates the use of that library and exposes higher-level methods. This is where I get an error borrowed value does not live long enough.

I understand that my method MyOuter::new() cannot return a dangling reference to Inner. I therefore try to extend life time of Inner by passing the owned version of Inner to my struct MyOuter.

But now it complains "cannot move out of inner because it is borrowed".

I'm sure I could juggle with lifetimes somehow here, but I don't see how ...

Some help would be greatly appreciated.
Thanks in advance,
Vito

mod some_library_i_cannot_modify {
    pub struct Inner {
        pub inner_string: String,
    }

    pub struct Middle<'a> {
        pub ref_inner: &'a Inner,
    }
}

use crate::some_library_i_cannot_modify::{Inner, Middle};
struct MyOuter<'a> {
    middle: Middle<'a>,
    inner: Inner,
}

impl <'a> MyOuter<'a> {
    fn new() -> Self {
        let inner = Inner {
            inner_string: String::from("inner"),
        };

        let middle: Middle<'a> = Middle {
            ref_inner: &inner,
        };
        // Error happens here: 
        MyOuter { middle, inner }
    }

    fn do_something(&self) {
        todo!("implement")
    }
}

fn main() {
    let outer = MyOuter::new();
    outer.do_something();
}


(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `inner` does not live long enough
  --> src/main.rs:24:24
   |
17 | impl <'a> MyOuter<'a> {
   |       -- lifetime `'a` defined here
18 |     fn new() -> Self {
19 |         let inner = Inner {
   |             ----- binding `inner` declared here
...
23 |         let middle: Middle<'a> = Middle {
   |                     ---------- type annotation requires that `inner` is borrowed for `'a`
24 |             ref_inner: &inner,
   |                        ^^^^^^ borrowed value does not live long enough
...
28 |     }
   |     - `inner` dropped here while still borrowed

error[E0505]: cannot move out of `inner` because it is borrowed
  --> src/main.rs:27:27
   |
17 | impl <'a> MyOuter<'a> {
   |       -- lifetime `'a` defined here
18 |     fn new() -> Self {
19 |         let inner = Inner {
   |             ----- binding `inner` declared here
...
23 |         let middle: Middle<'a> = Middle {
   |                     ---------- type annotation requires that `inner` is borrowed for `'a`
24 |             ref_inner: &inner,
   |                        ------ borrow of `inner` occurs here
...
27 |         MyOuter { middle, inner }
   |                           ^^^^^ move out of `inner` occurs here
   |
note: if `Inner` implemented `Clone`, you could clone the value
  --> src/main.rs:2:5
   |
2  |     pub struct Inner {
   |     ^^^^^^^^^^^^^^^^ consider implementing `Clone` for this type
...
24 |             ref_inner: &inner,
   |                         ----- you could clone this value

Some errors have detailed explanations: E0505, E0597.
For more information about an error, try `rustc --explain E0505`.
error: could not compile `playground` (bin "playground") due to 2 previous errors

Self-referential structs are considered an antipattern and are best to be avoided. There are some crates like ouroboros and yoke that help with self-referential structs, but I personally would change the interface such that MyOuter isn't self-referential anymore. With the snippet you gave I'd construct Middle in MyOuter::do_something, but that's just an example and might not work for your actual use-case:

impl MyOuter {
    fn new() -> Self {
        let inner = Inner {
            inner_string: String::from("inner"),
        };
        
        MyOuter { inner }
    }

    fn do_something(&self) {
        let middle = Middle {
            ref_inner: &self.inner,
        };
        
        todo!("implement")
    }
}

Playground.

3 Likes

Dear @jofas,

thanks for your reply. It makes sense that what I showed is considered an antipattern, thanks for the link!

Thanks also for taking the time to propose some code. I am not sure that I want to (or can) call the code involving Middle every time I call do_something(), because it is part of the initialization.

    fn do_something(&self) {
        let middle = Middle {
            ref_inner: &self.inner,
        };
        
        todo!("implement")
    }

Maybe my problem is easier to explain with the real code:

I'm trying to transform this code:

to something like this:

Essentially, I want to (1) hide the low-level access to the bluetooth drivers inside a module rather than having them scattered all over the main() method and (2) split it into two phases: initialization phase and usage of the methods.

Thanks again and have a good evening!
Vito

if you are okay with making your Inner struct clonable #[derive(Clone)] you can do the unspeakable
Please don't tell anyone that i shared the following code with you:


mod some_library_i_cannot_modify {
    #[derive(Clone)]
    pub struct Inner {
        pub inner_string: String,
    }

    pub struct Middle<'a> {
        pub ref_inner: &'a Inner,
    }
}

use crate::some_library_i_cannot_modify::{Inner, Middle};
struct MyOuter {
    middle: Middle<'static>, 
    inner: Inner,        
}

impl MyOuter {
    fn new() -> Self {
        let inner = Inner {
            inner_string: String::from("Hello, world!"),
        };
        let inner = Box::new(inner);

        let boxed_inner = Box::leak(Box::clone(&inner)); 

        let middle = Middle {
            ref_inner: boxed_inner,
        };

        
        MyOuter {
            middle,
            inner: *inner,
        }
    }
}

So, without changing that cursed library, I tried doing this by boxing it and leaking it. Since using Box allocates it on the heap, Box::leak allows us to keep it after escaping the scope by turning it into a 'static reference. This obviously creates a struct that cannot be deallocated (of course, everything can be deallocated if you unsafely manifest it with std::alloc::dealloc). So, use it only if that struct lives as long as your program (e.g., cache system, global structs, etc.).
i have a question for other people, can we use a Phantom marker to tie the lifetime of Inner with Middle?

I had the same issue trying to write a wrapper to another library, and tried doing some tricks with ouroboros, mentioned above, but they completely failed.

My bet is the authors usually have serious reasons to make structs like this, and the least evil is to make 2 structs similar to theirs, but have a better api:

struct MyHci {
    hci: HciConnector,
}

impl MyHci {
    pub fn new(some_data: ...) -> Self {
         // init HciConnector
        
         Self { hci } 
    } 

    pub fn connect<'a>(&'a self) -> MyBle<'a> {
        MyBle { ble: Ble(&self.hci) }
    }
}

struct MyBle<'a> {
    ble: Ble<'a>
}

fn main() {
    let myhci = MyHci::new(...);
    let con = myhci.connect();
}

Rust lifetimes are not value livenesses, so what you call "the lifetime of Inner" isn't representable by a Rust lifetime (those '_ things). It's a poor choice of terminology that causes much confusion.

2 Likes

Thanks a lot for your reply. It was interesting for me to learn about Box::leak() which I didn't know.

Unfortunately I cannot modify the library which means that I cannot add cloning to it. Also, I'm not sure the internal of the library (which has to do with low-level Wifi drivers) would support cloning easily.

1 Like

That's true, value livnesse is more of a runtime behavior while 'lifetimes' are a compile-time reference guarantee, thanks for the feedback, i would look more into this

Thanks @culebron for your reply!

the least evil is to make 2 structs similar to theirs, but have a better api

That's an interesting approach... I'll give that a try!

My bet is the authors usually have serious reasons to make structs like this

I think I'll ask the same question on a different forum, more related to embedded programming.

It looks like a pattern in that community, that they create/initialize all objects linked to the hardware resources in the main() method and then work with them.