【问题标题】:Why do I get "does not live long enough" errors when reimplementing C++ signals & slots in Rust?为什么在 Rust 中实现 C++ 信号和槽时会出​​现“寿命不够长”的错误?
【发布时间】:2018-04-10 16:31:48
【问题描述】:

作为学习练习,我一直在将一个相当标准的 C++ 信号实现翻译成 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();
    }
}

对于简单的测试代码,这将按预期进行编译和运行:

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!");
}

但是,如果我尝试与设想的用例更相似的东西,它不会编译:

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!");
}

这给出了以下错误:

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

看来我的问题源于SignalData,我将侦听器集合存储为引用:listeners: BTreeMap&lt;usize, &amp;'l Notifiable&lt;E&gt;&gt;。生命周期的要求似乎一直向外传播。

Connection 类(至少在 C++ 中)的目的是允许从Listener 端断开连接,并管理连接的生命周期,当信号退出时从信号中删除Listener 条目范围。

Connection 的生命周期必须小于或等于 Signal 和相关 Listener 的生命周期。但是,ListenerSignal 的生命周期应该是完全独立的。

有没有办法改变我的实现来实现这一点,还是从根本上破坏了?

【问题讨论】:

  • 不相关,但对于惯用代码:if let 而不是带有一只手臂的matchOption::is_some()Option::map_or。这些将缩短您的实施时间
  • 你在 C++ 中使用了引用,还是使用了智能指针?
  • @MatthieuM。好吧,从技术上讲是std::function...(我猜它又会存储对*this 的引用)。我最初确实尝试使用Fn,但遇到了问题。
  • 使用 nightly 和 #![feature(nll)] 编译会提供更好的错误消息。
  • @user673679:Rust 不代表“由调用者决定”,除非函数被标记为 unsafe。这使得实现观察者变得更加棘手......但更安全。

标签: rust signals-slots


【解决方案1】:

信号/插槽很复杂。 真的很复杂

在 C++ 中,您可以使用 Boost.Signals,这是一个由 C++ 专家制作的库,它...啊等等,不。尽管 Boost.Signals 的作者拥有专业知识,但并没有设法使其成为线程安全的,您应该改用 Boost.Signals2

很有可能,您自己开发的 C++ 实现需要小心使用,以免引发未定义的行为。

这种库的直接移植在 Rust 中不起作用。 Rust 的目标是提前处理未定义的行为:您必须清楚地将不安全代码标记为...unsafe


总的来说,信号/插槽实现类似于观察者,观察者传统上需要循环所有权。这在垃圾收集语言中效果很好,但在手动管理内存时需要更多的深思熟虑。

最直接且容易出错的解决方案是使用一对Rc/Weak(或Arc/Weak 用于多线程代码)。开发人员可以策略性地放置 Weak 指针以打破循环(以免它们泄漏)。

在 Rust 中,还有另一个障碍:循环所有权意味着别名。默认情况下,Rust 要求别名内容是不可变的,这是非常有限的。要重新获得可变性,您需要内部可变性,可以使用CellRefCell(或Mutex 用于多线程代码)。

好消息:尽管有所有错综复杂的地方,但如果您的代码编译它会是安全的(尽管它可能仍然会泄漏)。


另一个解决方案是为了避免这种设计固有的所有堆分配,而是转向消息传递方案。您可以通过对象的 ID 向对象发送消息,而不是直接调用对象的方法。该方案的一个难点在于消息是异步的,因此被调用的方法只能通过回传消息来传达结果。

Citybound 是一款使用 Rust 开发的游戏,使用细粒度的 actor 与这种方案进行通信。还有actix actor 框架,从 TechEmpower 基准测试中可以看出,它在性能方面进行了非常精细的调整(在Round 15 中排名第 7)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-20
    • 2016-01-22
    • 1970-01-01
    • 1970-01-01
    • 2016-03-31
    相关资源
    最近更新 更多