Lifetime issue with PhantomData and borrows

Having a hard time figuring out a solution to this lifetime problem using the zeroconf-rs crate. Essentially I have a worker thread for registering mdns entries, each of which requires an MdnsService and an EventLoop. Somehow creating the EventLoop is holding a mutable borrow to the MdnsService and I can't quite figure out how to handle the lifetimes. My code is:


pub struct ServiceData<'a> {
    pub service: AvahiMdnsService,
    pub event_loop: AvahiEventLoop<'a>,
}

    fn service_registration_thread(rx_channel: Receiver<ChannelCommand>) {
        let mut services: Vec<ServiceData> = Vec::new();

        loop {
            for message in rx_channel.try_iter() {
                match message {
                    ChannelCommand::NewService(service_ref) => {
                        let mut txt_data = TxtRecord::new();
                        for item in service_ref.txt_records.iter() {
                            txt_data.insert(item.0, item.1).unwrap();
                        }
                        let service_type = ServiceType::new(
                            &service_ref.service_name.to_owned(),
                            &service_ref.protocol.to_owned(),
                        )
                        .unwrap();
136:                        let mut new_service = MdnsService::new(service_type, service_ref.port);
                        new_service.set_name(&service_ref.instance_name.to_owned());
                        new_service.set_txt_record(txt_data);
                        new_service
                            .set_registered_callback(Box::new(MDnsService::on_service_registered));
141:                        let event_loop = [new_service.register](https://github.com/windy1/zeroconf-rs/blob/850c028aa8e85fb3ee441b344e2dbecb8d7f9a0c/zeroconf/src/linux/service.rs#L109C1-L129)().unwrap();

                        let data = ServiceData {
                            service: new_service,
145:                            event_loop: event_loop,
                        };

                        services.push(data)
                    }
                }
            }

            for service in services.iter() {
                service.event_loop.poll(Duration::from_millis(0));
            }
        }
    }

I get the error:

cannot move out of `new_service` because it is borrowed
move out of `new_service` occurs hererustcClick for full compiler diagnostic
async_service.rs(141, 42): borrow of `new_service` occurs here
async_service.rs(136, 29): binding `new_service` declared here
async_service.rs(145, 41): borrow later used here
let mut new_service: AvahiMdnsService // size = 24 (0x18), align = 0x8

It seems to have something to do with how PhantomData in AvahiEventLoop is capturing the mutable borrow, but can't quite tell if there's a way to handle this in my code or if the library really needs to change. I've tried various permutations of Rc & RefCell around new_service but that doesn't seem to help with checking the ownership of that borrow on 141.

You're typically more likely get useful help if you paste the entire error of cargo build from the console. (Your code snippet also has some line numbers and code links copied out of your editor.)

Anyway, you can see from the documentation[1] that register does indeed hold an exclusive borrow of new_service. It's the same reason you need a lifetime on ServiceData<'_>. That also means that ServiceData<'_> is a self-referencial struct, which isn't something you want (it's nigh-on impossible to use).

If it weren't for the outer loop, you could try creating all the AvahiMdnsService structs and putting them in a Vec, then creating all the event loops (inline or in another Vec) and then polling them. But if I'm interpretting the outer loop correctly, and you want to indefinitely keep building the Vec length...

  • Push into the Vec
  • Make event loops from things in the Vec
  • (poll them)
  • Push more stuff into the Vec without killing the existing event loops
  • Make event loops from new things in the Vec
  • ...

and that won't work -- due to the borrow checker, and for good reason: pushing the Vec may reallocate the Vec and move all the contents, which would invalidate all the borrowed event loops (e.g. could cause pointers to dangle).

That's as far as I got -- this might be a job for some arena-based data structure. Perhaps someone else has some concrete suggestions.


  1. but not the source code :neutral_face: ↩ī¸Ž

The crate author just said there's a refactor in the works. I need a break from staring at that, but I do want to understand what's going on with it all. Maybe I removed PhantomData at one point, and the associated lifetimes and am confusing what's it causing what behavior there and how the lifetime annotations are interpreted. Like I don't quite understand how the function signature indicates that it's going to have an indefinite borrow.

fn register(&mut self) -> Result<EventLoop<'_>>
To me that reads it'll borrow self mutably for the life of the function then return an event loop with a result. That event loop has some inferred lifetime? Is that inferred to mean it'll have the same lifetime as all arguments in the function, including &mut self?

Forgot the full output:

error[E0597]: `new_service` does not live long enough
   --> src/something/async_service.rs:101:42
    |
96  |                         let mut new_service = MdnsService::new(service_type, service_ref.port);
    |                             --------------- binding `new_service` declared here
...
101 |                         let event_loop = new_service.register().unwrap();
    |                                          ^^^^^^^^^^^ borrowed value does not live long enough
...
109 |                     }
    |                     - `new_service` dropped here while still borrowed
...
113 |             for service in services.iter() {
    |                            -------- borrow later used here

error[E0505]: cannot move out of `new_service` because it is borrowed
   --> src/something/async_service.rs:104:38
    |
96  |                         let mut new_service = MdnsService::new(service_type, service_ref.port);
    |                             --------------- binding `new_service` declared here
...
101 |                         let event_loop = new_service.register().unwrap();
    |                                          ----------- borrow of `new_service` occurs here
...
104 |                             service: new_service,
    |                                      ^^^^^^^^^^^ move out of `new_service` occurs here
105 |                             event_loop: event_loop,
    |                                         ---------- borrow later used here

It's lifetime elision, not inference -- it means that the EventLoop<'_> has the same lifetime as the &mut self. You can think of it as "the EventLoop<'_> holds on to the &mut self". So long as the event loop is alive, the new_service remains exclusively borrowed.

Ellison and inference are just two sides of the same process, the first sentence of the doc you linked says that. The programmer elides what the compiler can infer and it's those inference rules I haven't quite internalized. Seem like this is the one that is applied in this case: " * If the receiver has type &Self or &mut Self, then the lifetime of that reference to Self is assigned to all elided output lifetime parameters." Gotta play with it more in a playground a bit, feels like it's doing something funky in this case, may just be the doc generator putting that <'_> that looks weird to me but I'm curious to find other instances of that rule playing out in other code I've written.

In this case though, I think the lifetime for the output could be adjusted. The output struct really only holds an Rc that's shared with the original structure so I'd think it'd be safe for them to have independent lifetimes where the output can outlive the original object. It compiles, but let's see if it blows up in my face ...

Well it didn't implode. Poking more, it's fine that the return struct outlive the &mut Self reference, Though it had other issues deeper in the crate's logic and data model. Easy enough to handle in my case, but certainly isn't quite sound as a whole. The current code is really just over-constrained but properly constraining how the structs interact with the C API it's wrapping may be a good chunk more work.

If you're lucky(?) it's unsound, but it's probably UB. Trivially circumventing the borrow checker with unsafe generally is. Note that Rust enforces API contracts regardless of implementation details.

I wrote a bunch more, but I think I'll just leave it at that.

1 Like