rc::Weak extending lifetime?

#1

I have a situation where I would like to pass to a 'static callback an rc::Weak that can be upgraded to the original Rc if it’s still around.

When the struct stored in the Rc has i32 or other value types, everything is fine. It works as expected. As soon as I add a reference, though, I get errors about contradicting lifetime requirements. I don’t understand why this is… it’s as if the rc::Weak is extending the lifetime of the reference, but that shouldn’t be true, should it? I mean, the whole point of it, I thought, was to move the borrow-checking to run time (effectively).

Apologies if the above is non-sensical; here’s some code to demonstrate the issue…

#![allow(unused)]

use std::cell::RefCell;
use std::rc::Rc;

trait Bar {
    fn baz(&mut self);
}

struct Foo<'a, T> where T: Bar {
    t: &'a mut T,
}

struct OtherFoo<'a, T> where T: Bar {
    foo : Rc<RefCell<Foo<'a, T>>>,
}

fn do_cb<F>(cb : F) where F : Fn() + 'static {
    
}

fn do_thing<'a, T>(t: &'a mut T) -> OtherFoo<'a, T> where T: Bar {
    let foo = Rc::new(RefCell::new(Foo{ t }));
    
    let weak_foo = Rc::downgrade(&foo);
    let cb = move || {
        // This is what breaks... if I comment it out, it's fine.
        if let Some(foo) = weak_foo.upgrade() {
            foo.borrow_mut().t.baz();
        }
    };
    do_cb(cb);
    OtherFoo { foo }
}

(Playground)

#2

The problem is that the value isn’t 'static because it captures the 'a input value. If the Weak is upgraded elsewhere, that elsewhere can retrieve and copy out the inner 'a value, but it has no idea what 'a is.

Reference counting with Rc<T> only really works to share ownership of T, but it cannot change the lifetime of T itself (in this case, RefCell<Foo<'a, T>>).

2 Likes
#3

Oh, that makes sense. I didn’t think about the possibility of copying it.

What I’m really trying to do is pass a non-'static callback to a library that only takes 'static callbacks and was thinking I could effectively move the borrow checking to run-time and eat the cost in exchange for the flexibility I need. Specifically, I’m trying to wrap JavaScript WebSocket objects as exposed by web_sys (a part of wasm-bindgen) such that I can have a Rust “socket” with callbacks for on-open, on-close, on-message, etc. but not require that these be 'static. Perhaps there’s a better way…

#4

The thing you’re trying to do fundamentally is sound, but Rc doesn’t have the right semantics for it – at least not by itself.

After all, once some code upgrades a Weak to a Rc, nothing prevents it from storing the Rc somewhere and extend its lifetime indefinitely. You need something that dynamically verifies that all references have been dropped, and aborts if not. This necessarily can’t be implemented in safe code – after all, if the check were skipped, the behavior would be unsound, yet the check itself isn’t unsafe, so there must be unsafe somewhere else to create the potential for unsoundness. But it’s possible to create a wrapper with a safe interface.

It feels like something that ought to exist already. From a quick search on crates.io, I wasn’t able to find anything, but I bet it’s out there somewhere…

Here’s a quick attempt at an implementation from scratch – warning: might be completely broken.

Some notes on implementation choices in the above:

  • rc_borrow has to accept a lambda rather than just returning an object with the check in its destructor, because otherwise you could bypass the check by calling mem::forget.
  • But the abort check has to be in a destructor anyway so that it runs even if the function panics, hence RcBorrowBomb.
  • As written it only handles an immutable reference, but you could pass an &RefCell<&mut Foo>, or upgrade it to handle mutability itself…
2 Likes