Search code examples
rustsignals-slots

Why do I get "does not live long enough" errors when reimplementing C++ signals & slots in Rust?


As a learning exercise, I've been translating a fairly standard C++ signal implementation into Rust:

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::{Rc, Weak};

trait Notifiable<E> {
    fn notify(&self, e: &E);
}

struct SignalData<'l, E>
where
    E: 'l,
{
    listeners: BTreeMap<usize, &'l Notifiable<E>>,
}

struct Signal<'l, E>
where
    E: 'l,
{
    next_id: usize,
    data: Rc<RefCell<SignalData<'l, E>>>,
}

struct Connection<'l, E>
where
    E: 'l,
{
    id: usize,
    data: Weak<RefCell<SignalData<'l, E>>>,
}

impl<'l, E> Signal<'l, E>
where
    E: 'l,
{
    pub fn new() -> Self {
        Self {
            next_id: 1,
            data: Rc::new(RefCell::new(SignalData {
                listeners: BTreeMap::new(),
            })),
        }
    }

    pub fn connect(&mut self, l: &'l Notifiable<E>) -> Connection<'l, E> {
        let id = self.get_next_id();
        self.data.borrow_mut().listeners.insert(id, l);
        Connection {
            id: id,
            data: Rc::downgrade(&self.data),
        }
    }

    pub fn disconnect(&mut self, connection: &mut Connection<'l, E>) {
        self.data.borrow_mut().listeners.remove(&connection.id);
        connection.data = Weak::new();
    }

    pub fn is_connected_to(&self, connection: &Connection<'l, E>) -> bool {
        match connection.data.upgrade() {
            Some(data) => Rc::ptr_eq(&data, &self.data),
            None => false,
        }
    }

    pub fn clear(&mut self) {
        self.data.borrow_mut().listeners.clear();
    }

    pub fn is_empty(&self) -> bool {
        self.data.borrow().listeners.is_empty()
    }

    pub fn notify(&self, e: &E) {
        for (_, l) in &self.data.borrow().listeners {
            l.notify(e);
        }
    }

    fn get_next_id(&mut self) -> usize {
        let id = self.next_id;
        self.next_id += 1;
        id
    }
}

impl<'l, E> Connection<'l, E>
where
    E: 'l,
{
    pub fn new() -> Self {
        Connection {
            id: 0,
            data: Weak::new(),
        }
    }

    pub fn is_connected(&self) -> bool {
        match self.data.upgrade() {
            Some(_) => true,
            None => false,
        }
    }

    pub fn disconnect(&mut self) {
        match self.data.upgrade() {
            Some(data) => {
                data.borrow_mut().listeners.remove(&self.id);
                self.data = Weak::new();
            }
            None => (),
        }
    }
}

impl<'l, E> Drop for Connection<'l, E>
where
    E: 'l,
{
    fn drop(&mut self) {
        self.disconnect();
    }
}

This compiles and behaves as expected for simple test code:

struct Event {}
struct Listener {}

impl Notifiable<Event> for Listener {
    fn notify(&self, _e: &Event) {
        println!("1: event");
    }
}

fn main() {
    let l1 = Listener {};
    let l2 = Listener {};
    let mut s = Signal::<Event>::new();
    let c1 = s.connect(&l1);
    let mut c2 = s.connect(&l2);

    println!("c2: {}", c2.is_connected());
    s.disconnect(&mut c2);
    println!("c2: {}", c2.is_connected());

    let e = Event {};
    s.notify(&e);

    println!("done!");
}

However, if I try something more similar to the envisaged use case, it doesn't compile:

struct Event {}
struct System<'l> {
    signal: Signal<'l, Event>,
}
struct Listener<'l> {
    connection: Connection<'l, Event>,
}

impl<'l> Notifiable<Event> for Listener<'l> {
    fn notify(&self, _e: &Event) {
        println!("1: event");
    }
}

fn main() {
    let mut listener = Listener {
        connection: Connection::new(),
    };
    let mut system = System {
        signal: Signal::new(),
    };

    listener.connection = system.signal.connect(&listener);

    println!("is_connected(): {}", listener.connection.is_connected());
    system.signal.disconnect(&mut listener.connection);
    println!("is_connected(): {}", listener.connection.is_connected());

    let e = Event {};
    system.signal.notify(&e);

    println!("done!");
}

Which gives the following error:

error[E0597]: `listener` does not live long enough
   --> src/main.rs:147:50
    |
147 |     listener.connection = system.signal.connect(&listener);
    |                                                  ^^^^^^^^ borrowed value does not live long enough
...
157 | }
    | - `listener` dropped here while still borrowed
    |
    = note: values in a scope are dropped in the opposite order they are created

It seems that my problems stem from SignalData, where I store the collection of listeners as references: listeners: BTreeMap<usize, &'l Notifiable<E>>. That lifetime requirement seems to propagate ever outwards.

The purpose of the Connection class (at least in C++) is to allow disconnection from the Listener end, and manage the lifetime of the connection, removing the Listener entry from the signal when it goes out of scope.

The lifetime of the Connection must be less than or equal to that of both the Signal and the relevant Listener. However, the lifetime of the Listener and Signal should otherwise be entirely independent.

Is there a way to alter my implementation to achieve this, or is it fundamentally broken?


Solution

  • Signals/Slots are complicated. Really complicated.

    In C++, you can use Boost.Signals, a library crafted by C++ experts which... ah wait no. Despite their expertise, the authors of Boost.Signals didn't manage to make it thread-safe, you should use Boost.Signals2 instead.

    Chances are, your homegrown C++ implementation requires care to use, lest it invokes undefined behavior.

    A straightforward port of such a library will not work in Rust. The goal of Rust is to be upfront about undefined behavior: you have to clearly mark unsafe code as... unsafe.


    In fine, a Signals/Slots implementation is akin to an Observer, and an Observer traditionally requires a cyclic ownership. This works well in Garbage Collected languages, but requires more forethought when memory is managed manually.

    The most straightforward, and error-prone, solution is to use a pair of Rc/Weak (or Arc/Weak for multi-threaded code). It is up to the developer to strategically place Weak pointers to break the cycles (lest they leak).

    In Rust, there is another hurdle: cyclic ownership implies aliasing. By default, Rust requires that aliased content be immutable which is quite limiting. To regain mutability, you will need interior mutability, either with Cell or RefCell (or Mutex for multi-threaded code).

    The good news: despite all the intricacies, if your code compile it will be safe (though it may still leak).


    Another solution, to avoid all the heap allocations inherent to such a design, is to move toward a message-passing scheme instead. Instead of directly invoking a method on an object, you can send a message to an object via its ID. A difficulty of this scheme is that messages are asynchronous, so the method called can only communicate a result by sending a message back.

    Citybound is a game developed in Rust using fine-grained actors communicating with such a scheme. There is also the actix actor framework, which has been quite finely tuned performance wise as can be seen on the TechEmpower benchmarks (placed 7 in Round 15).