为什么在 Rust 中实现 C++ 信号和槽时会出现 "does not live long enough" 错误?
Why do I get "does not live long enough" errors when reimplementing C++ signals & slots in Rust?
作为学习练习,我一直在将相当标准的 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<usize, &'l Notifiable<E>>
。这种对生命周期的要求似乎一直在向外传播。
Connection
class(至少在 C++ 中)的目的是允许从 Listener
端断开连接,并管理连接的生命周期,删除 Listener
超出范围时从信号进入。
Connection
的生命周期必须小于或等于 Signal
和相关 Listener
的生命周期。但是,Listener
和 Signal
的生命周期应该是完全独立的。
有没有办法改变我的实现来实现这个,或者它从根本上被破坏了?
Signals/Slots 很复杂。 真复杂.
在 C++ 中,您可以使用 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
。
很有可能,您自己开发的 C++ 实现需要小心使用,以免调用未定义的行为。
这种库的直接移植在 Rust 中是行不通的。 Rust 的目标是提前处理未定义的行为:您必须清楚地将不安全代码标记为... unsafe
.
好吧,Signals/Slots 实现类似于观察者,而观察者传统上需要 循环所有权。这在垃圾收集语言中运行良好,但在手动管理内存时需要更多考虑。
最直接的 error-prone 解决方案是使用一对 Rc
/Weak
(或 Arc
/Weak
用于 multi-threaded代码)。由开发人员策略性地放置 Weak
指针来打破循环(以免它们泄漏)。
在 Rust 中,还有另一个障碍:循环所有权意味着 别名。默认情况下,Rust 要求别名内容是不可变的,这是非常有限的。要重新获得可变性,您将需要 内部可变性,使用 Cell
或 RefCell
(或 Mutex
用于 multi-threaded 代码)。
好消息:尽管存在所有复杂性,但如果您的代码编译通过,它将是安全的(尽管它仍然可能会泄漏)。
为了避免这种设计固有的所有堆分配,另一种解决方案是转向 message-passing 方案。您可以通过对象的 ID 向对象发送消息,而不是直接调用对象的方法。该方案的难点在于消息是异步的,所以调用的方法只能通过回传消息来传达结果。
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).
作为学习练习,我一直在将相当标准的 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<usize, &'l Notifiable<E>>
。这种对生命周期的要求似乎一直在向外传播。
Connection
class(至少在 C++ 中)的目的是允许从 Listener
端断开连接,并管理连接的生命周期,删除 Listener
超出范围时从信号进入。
Connection
的生命周期必须小于或等于 Signal
和相关 Listener
的生命周期。但是,Listener
和 Signal
的生命周期应该是完全独立的。
有没有办法改变我的实现来实现这个,或者它从根本上被破坏了?
Signals/Slots 很复杂。 真复杂.
在 C++ 中,您可以使用 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
。
很有可能,您自己开发的 C++ 实现需要小心使用,以免调用未定义的行为。
这种库的直接移植在 Rust 中是行不通的。 Rust 的目标是提前处理未定义的行为:您必须清楚地将不安全代码标记为... unsafe
.
好吧,Signals/Slots 实现类似于观察者,而观察者传统上需要 循环所有权。这在垃圾收集语言中运行良好,但在手动管理内存时需要更多考虑。
最直接的 error-prone 解决方案是使用一对 Rc
/Weak
(或 Arc
/Weak
用于 multi-threaded代码)。由开发人员策略性地放置 Weak
指针来打破循环(以免它们泄漏)。
在 Rust 中,还有另一个障碍:循环所有权意味着 别名。默认情况下,Rust 要求别名内容是不可变的,这是非常有限的。要重新获得可变性,您将需要 内部可变性,使用 Cell
或 RefCell
(或 Mutex
用于 multi-threaded 代码)。
好消息:尽管存在所有复杂性,但如果您的代码编译通过,它将是安全的(尽管它仍然可能会泄漏)。
为了避免这种设计固有的所有堆分配,另一种解决方案是转向 message-passing 方案。您可以通过对象的 ID 向对象发送消息,而不是直接调用对象的方法。该方案的难点在于消息是异步的,所以调用的方法只能通过回传消息来传达结果。
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).