How do I solve this multiple borrowing problem?

#![allow(dead_code)]

use std::os::raw::c_void;
use std::marker::PhantomData;

pub struct Context<'a> {
    name: String,
    ctx: *mut c_void,
    _ctx: PhantomData<&'a c_void>,
}

impl<'a> Context<'a> {
    pub fn new(name: String) -> Self {
        Self {
            name,
            ctx: std::ptr::null_mut(),
            _ctx: PhantomData,
        }
    }
    
    pub fn get_device(&'a self) -> Device<'a> {
        Device {
            dev: std::ptr::null_mut(),
            _dev: PhantomData
        }
    }
}

pub struct Device<'a> {
    dev: *mut c_void,
    _dev: PhantomData<&'a c_void>,
}

impl<'a> Device<'a> {
    pub fn get_sub_device(&'a self, sub_name: String) -> SubDevice<'a> {
        SubDevice {
            name: sub_name,
            handle: std::ptr::null_mut(),
            _handle: PhantomData
        }
    }
}

pub struct SubDevice<'a> {
    name: String,
    handle: *mut c_void,
    _handle: PhantomData<&'a c_void>,
}

impl<'a> SubDevice<'a> {
    pub fn name(&'a self) -> &'a str {
        &self.name
    }
}

pub struct Manager<'a> {
    ctx: Context<'a>,
    dev: Option<Device<'a>>,
    sub_devs: Vec<SubDevice<'a>>,
}

impl<'a> Manager<'a> {
    pub fn new(name: String) -> Self {
        Manager {
            ctx: Context::new(name),
            dev: None,
            sub_devs: vec![],
        }
    }
    
    pub fn open_device(&'a mut self) {
        let dev = self.ctx.get_device();
        self.dev = Some(dev);
    }
    
    pub fn open_sub_device(&mut self, sub_name: String) -> Result<(), String> {
        match &mut self.dev {
            Some(dev) => {
                let sub_dev = dev.get_sub_device(sub_name);
                self.sub_devs.push(sub_dev);
                Ok(())
            }
            None => {
                Err("Not open".to_string())
            }
        }
    }
    
    pub fn sub_devs(&'a self) -> &'a Vec<SubDevice<'a>> {
        &self.sub_devs
    }
}

fn main() {
    let mut manager = Manager::new("TheManager".to_string());
    manager.open_device();
    for i in 1..10 {
        let name = format!("TheSubDev{}", i);
        let _ = manager.open_sub_device(name);
    }
    
    let sub_devs = manager.sub_devs();
    for sub in sub_devs {
        println!("{}", sub.name());
    }
}



















(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error: lifetime may not live long enough
  --> src/main.rs:79:31
   |
62 | impl<'a> Manager<'a> {
   |      -- lifetime `'a` defined here
...
76 |     pub fn open_sub_device(&mut self, sub_name: String) -> Result<(), String> {
   |                            - let's call the lifetime of this reference `'1`
...
79 |                 let sub_dev = dev.get_sub_device(sub_name);
   |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'a`

error[E0499]: cannot borrow `manager` as mutable more than once at a time
  --> src/main.rs:99:17
   |
96 |     manager.open_device();
   |     ------- first mutable borrow occurs here
...
99 |         let _ = manager.open_sub_device(name);
   |                 ^^^^^^^
   |                 |
   |                 second mutable borrow occurs here
   |                 first borrow later used here

error[E0502]: cannot borrow `manager` as immutable because it is also borrowed as mutable
   --> src/main.rs:102:20
    |
96  |     manager.open_device();
    |     ------- mutable borrow occurs here
...
102 |     let sub_devs = manager.sub_devs();
    |                    ^^^^^^^
    |                    |
    |                    immutable borrow occurs here
    |                    mutable borrow later used here

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

I've solved this problem, and the code is on Rust Playground. If anyone has better solution, please feel free to leave a comment. Thank you!

You may not need the lifetime annotations. I started by first removing them and making the constructors private to protect the interface from handing out dangling pointers. None of the handles have methods to return clones or copies, and the public methods that return &T already have an outer lifetime. With a careful data model, the inner lifetimes vanish. Rust Playground

There are some ways in which this won't work out in practice [1]. But starting from this simplified base, you can see that e.g. Manager::sub_devs() returns a slice that, by itself, cannot create any memory safety issues thanks to the "outer" lifetime on &self.

It isn't clear what the raw pointers are going to do or how you will provide a safe interface, but this is how I would start to approach a similar design.


  1. I'm alluding to subtyping and variance. ↩︎

Thank you for your reply. The solution is excellent if PhantomData markers are not used. In my case, the example corresponds to a third-party library I use in practice. Their library incorporates both lifetimes and PhantomData markers (similar to the Context, Device, and SubDevice in my example), and the creation and release of these elements follow a specific order. This means their lifetimes require strict constraints.

Try using the inner lifetime annotation without specifying the same lifetime for the outer loans. Like this: Rust Playground

When you end up with a function signature like fn get_device(&'a self) -> Device<'a>, you are "tying" two lifetimes together. I've been referring to these as outer and inner lifetimes, but that's just an unusual terminology that I'm using personally because it helps me think about what is actually happening [1].

The "outer" lifetime is the duration of the loan that needs to exist when the method is called. That's the lifetime on &'_ self. The "inner" lifetime is hidden inside the type as a generic parameter, as in Device<'_>. These two don't have to be the same duration. But giving the same annotation is forcing the compiler to find a minimum lifetime for the method call (the shorter "outer" lifetime) extending it to at least as long as the hidden longer "inner" lifetime. It makes them the same duration. And it fails to compile because the constraint cannot be satisfied.

By using the lifetime elision rules, we can allow the compiler to select a shorter duration when calling the methods: fn get_device(&self) -> Device<'a>. We still annotate the longer "inner" lifetime, because we want it to be at least as long as the annotation introduced on the impl<'a> Context<'a> that this method is bound to.


  1. It is probably better to use proper subtyping terminology, but I'm not a PL person. IIUC, what I call an "inner" lifetime would be a subtype, like 'inner: 'outer. I've tried to revise this response using "long" and "short" instead. ↩︎

1 Like

Thank you for your solution. This has been very helpful to me. It is more efficient and readable than my previous approach. I modified my code according to your method and it runs very well. I appreciate your assistance very much.