Search code examples
rustraiiborrow-checkerinterior-mutability

How to make a subscriber object with RAII properties?


I'm talking to some hardware over a link with kind of a publisher/subscriber model. In C++, I did subscriptions with RAII to remember to always unsubscribe, but I can't seem to get the ownerships/borrows right in rust.

Naively, this is something like what I would like to do. send and receive probably needs to be &mut self, so as I understand, Subscription needs mutable access to the Transport.

struct Transport;

impl Transport {
    pub fn send(&mut self, cmd: &str) { unimplemented!() }
    pub fn subscribe(&mut self, cmd: &str) -> Subscription {
        self.send("subscribe-with-params");
        Subscription { trans: &mut self }
    }
}

struct Subscription {
    trans: &mut Transport,
}

impl Drop for Subscription {
    fn drop(&mut self) {
        self.trans.send("unsubscribe-with params");
    }
}

impl Subscription {
    fn receive(&mut self) -> &[u8] { /*blocking wait for data*/ }
}

fn test(t: Transport) {
    // Need to subscribe before command, as command might generate status messages
    let mut status_sub = t.subscribe("status-message");
    {
        let mut short_lived_sub = t.subscribe("command_reply");
        t.send("command");
        short_lived_sub.receive(); // Wait for ack
    }
    loop {
        println!("{:?}", status_sub.receive());
        /*processing of status */
    }
}

There are at least two problems here. One is how Subscription should keep some reference to it's "parent", the Transport, and another is the problem in fn test that I can't borrow Transport twice for two different subscriptions.

I have a feeling that I'm kind of asking the wrong question here, so maybe there's a good way of approaching this in a different way entirely?


Solution

  • It is problematic for your Subscription to hold a mutable reference to Transport because, as you discovered, you'll only be able to hold one at a time and you won't be able to do anything else with the transport in the meantime.

    Instead, you can use an Rc (for shared ownership) and RefCell (for interior mutability):

    use std::rc::Rc;
    use std::cell::RefCell;
    
    struct TransportInner;
    
    pub struct Transport {
        inner: Rc<RefCell<TransportInner>>,
    }
    
    pub struct Subscription { 
        trans: Rc<RefCell<TransportInner>>
    }
    
    impl TransportInner {
       pub fn send(&mut self, cmd: &str) { }
    }
    
    impl Transport {
       pub fn send(&mut self, cmd: &str) { 
           self.inner.borrow_mut().send(cmd)
       }
    
       pub fn subscribe(&mut self, cmd: &str) -> Subscription {
          self.send("subscribe-with-params");
          Subscription { trans: Rc::clone(&self.inner) }
       }
    }
    
    impl Drop for Subscription {
       fn drop(&mut self) {
          self.trans.borrow_mut().send("unsubscribe-with params");
       }
    }
    

    You can do this without splitting it into an inner and outer structure, but that would require the user to access the Transport via an Rc too, which could be unwieldy.

    If you need this to work across threads, you should use Arc<Mutex> instead.