X-Git-Url: https://git.ralfj.de/rust-101.git/blobdiff_plain/98dafe0138b8bf6584b8d9e86a74a580bb034a26..188b1ec1b8528e2326791feccc8077e15bd60182:/solutions/src/callbacks.rs diff --git a/solutions/src/callbacks.rs b/solutions/src/callbacks.rs new file mode 100644 index 0000000..93fcb17 --- /dev/null +++ b/solutions/src/callbacks.rs @@ -0,0 +1,66 @@ +use std::rc::Rc; +use std::cell::RefCell; + +#[derive(Clone)] +pub struct Callbacks { + callbacks: Vec>>, +} + +impl Callbacks { + pub fn new() -> Self { + Callbacks { callbacks: Vec::new() } /*@*/ + } + + pub fn register(&mut self, callback: F) { + let cell = Rc::new(RefCell::new(callback)); + self.callbacks.push(cell); /*@*/ + } + + pub fn call(&self, val: i32) { + for callback in self.callbacks.iter() { + // We have to *explicitly* borrow the contents of a `RefCell`. + //@ At run-time, the cell will keep track of the number of outstanding shared and mutable borrows, + //@ and panic if the rules are violated. Since this function is the only one that borrow the + //@ environments of the closures, and this function requires a *mutable* borrow of `self`, we know this cannot + //@ happen.
+ //@ For this check to be performed, `closure` is a *guard*: Rather than a normal borrow, `borrow_mut` returns + //@ a smart pointer (`RefMut`, in this case) that waits until is goes out of scope, and then + //@ appropriately updates the number of active borrows. + //@ + //@ The function would still typecheck with an immutable borrow of `self` (since we are + //@ relying on the interior mutability of `self`), but then it could happen that a callback + //@ will in turn trigger another round of callbacks, so that `call` would indirectly call itself. + //@ This is called reentrancy. It would imply that we borrow the closure a second time, and + //@ panic at run-time. I hope this also makes it clear that there's absolutely no hope of Rust + //@ performing these checks statically, at compile-time: It would have to detect reentrancy! + let mut closure = callback.borrow_mut(); + // Unfortunately, Rust's auto-dereference of pointers is not clever enough here. We thus have to explicitly + // dereference the smart pointer and obtain a mutable borrow of the target. + (&mut *closure)(val); + } + } +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + use std::cell::RefCell; + use super::*; + + #[test] + #[should_panic] + fn test_reentrant() { + let c = Rc::new(RefCell::new(Callbacks::new())); + c.borrow_mut().register(|val| println!("Callback called: {}", val) ); + + // If we change the two "borrow" below to "borrow_mut", you can get a panic even with a "call" that requires a + // mutable borrow. However, that panic is then triggered by our own, external `RefCell` (so it's kind of our fault), + // rather than being triggered by the `RefCell` in the `Callbacks`. + { + let c2 = c.clone(); + c.borrow_mut().register(move |val| c2.borrow().call(val+val) ); + } + + c.borrow().call(42); + } +} \ No newline at end of file