Update:
Turns out the real problem was not the one I described earlier, in the old message, but rather: Can there be a value T (owned by Owner) that is borrowed by a static Borrower? To access the value via the borrower, there could be borrower.get() -> Option<BorrowerGuard>. If the Owner (and the value) still exist, a BorrowerGuard is created to allow access to the value (while preventing the Owner from dropping somehow), and if the Owner does no longer exist, returns None.
Turns out this problem has no solution (I think), since we can't rely on drop, as @quinedot pointed out (read for more infos, especially the latest messages).
Old message
Hi!
Can someone helps me understand why is *const T covariant over T (is it really?)?
I'm onto a problem where Borrower<T> needs to be... fullyvariant I guess? It means that Borrower<T> can accept T regardless of its lifetime. (Borrower<T> can outlive T and T can outlive Borrower<T>). Borrower<T> stores a raw pointer of T.
My code has a tiny bit of unsafe code, that should prevent UB by not dereferencing *const T when it doesn't exist. I do manage lifetimes manually. (Borrower::get returns Some with a guard that ensures data isn't mutated during the process, and if the data (T) has outlived Borrower, it just returns None)
How to define Borrower<T> so that its lifetime isn't related / tight to T?
The full context is:
I want to make a signal. A signal is a value that can mutate, and every time the signal is mutated, a set of callbacks are called (with a ref to the new value). These callbacks are not defined at the start of the signal struct, they are defined later via Signal::register. The callbacks are FnMut, and aren't static, as they have to be able to reference and mutate local scoped vars. This way, Signal::register should return a guard, so that the FnMut is registred as a callback only when the guard is alive.
fn main() {
let signal: Signal<usize> = Signal::new(0);
let mut my_mut = 3;
println!("my_mut: {}", my_mut) // 3
{
let callback = RefCell::new(|new_val: &usize| my_mut = 4); // inside a Cell cause it's a FnMut
let guard = signal.register(&callback); // callback is borrowed until guard dies
*signal.write() = 1; // callback is called with &1, and my_mut becomes 4
// (Signal::write returns a guard that call the callbacks on drop)
drop(guard); // callback is no longer borrowed, and the callback is unregistred from signal
drop(callback); // my_mut is no longer borrowed
// I can drop signal here if I want, as soon as guard is dropped
}
println!("my_mut: {}", my_mut) // my_mut is accessible here and equal to 4
drop(signal);
}
Why is this related?
This way, I need signal to access the callbacks (when signal.write), but the callbacks are owned by the guard (got from signal.register). My solution was to make the signal store a borrower to the value that the guard holds.
I don't think this is very suitable but it's the only way I found.
Am I understanding correctly that the Drop implementation for the guard unregisters the callback, and you're counting on this to no longer call the callback (via some stashed reference/pointer)? That's probably unsound as someone could std::mem::forget the guard, so the drop implementation is never ran.
I'm still half-guessing at your design, but this feels a bit like scoped threads to me. I don't know if there's a safe and sound interface that doesn't involve something like a closure environment (ala scoped threads or GhostCell), or linear types (which we don't have).
Didn't think about forget, an unfortunately you are right. If the owner doesn't run drop, then the borrower cannot know that the value does no longer exist, and produces UB as soon as you try do access it. I can't imagine a solution for this.
By the way, in our embedded codebase we explicitly prohibit forget and similar stuff like Rc, so our code actually uses things similar to this. It is super helpful and convenient, Rust code is just a lot more pleasant to write when you can pass references in async tasks and conceal lifetimes sometimes.
Do you have a clippy lint or similar to forbid those things? Otherwise it seems very easy to get unsoundness by mistake. Especially if a dependency happens to do something like that internally.
We don't have a lint, but it really isn't easy to do it by accident. We don't have allocations, don't use forget in usual code (only with some unsafe stuff, which is thoroughly audited etc) . And we basically always are checking the code for our dependencies for various reasons - to make sure they will not blow up the stack, there isn't bad unsafe code (it happens sometimes), it won't allocate etc. And we usually only exercise that property (of relying on !Forget when using them with our own APIs.
Good to know. I actually use it intentionally at the end of some cli tools. It helps a shave hundreds of milliseconds off, by letting the OS just free that memory instead of running Drop for various very large hash maps (1GB+, tens of thousands of entries).
I usually tend to use box leak and work with mut reference for the rest of the program. Like, you can just do it at the place of construction instead of catching it before Drop. And I guess ManuallyDrop would do the same, but more clunky?
Idk if it's really safe to do that. Even if forget isn't used, imagine a thread panics with the data: drop will not be ran. Forget doesn't only exist to annoy rustaceans (even if it does)