I'm trying to write a function that takes a reference to some object, &'a thing
, and returns a scope-guard object, whose lifetime needs to be bounded by the lifetime 'a
; however, it should not be considered to capture a reference to thing
. It just needs to run its Drop
impl before thing
ceases to exist. With the most straightforward implementation of a lifetime-bounded scope guard I can think of, the compiler thinks the guard does capture a reference to the thing, and so I get E0502 errors ("cannot borrow X as mutable because it is already borrowed as mutable"). What's the best way to code it so that the lifetime is bounded as desired but the reference isn't captured?
The concrete code requires some background, because I am, in fact, trying to do some black magic. I'm writing a program for a #[no_std]
environment. It does have a debug console, and so I'd like to log progress messages there, and panic messages as well. A global heap allocator is available, but memory is limited, and unwinding and threads are not available. Crucially, the debug console is strictly line-oriented. If you feed it a message in chunks of less than a full line it will mangle the message.
So to deal with the line-oriented console I have copied just barely enough of std::io
into my crate that I can use LineWriter
. Memory is limited, so I want to create the LineWriter
only once, in my startup glue. I pass a mutable reference to it to the main program. The challenge is to make the same LineWriter
also available to the #[panic_handler]
function. Now, if the panic handler has been called, the main program isn't going to execute any more code, so it is safe for the panic handler to steal the main program's &mut
reference; the dynamic lifetime of that reference has ended, even though the compiler cannot prove it.
Version 0 of the code I wrote for this, which did work, looked like this:
static PANIC_CONSOLE: Mutex<
UnsafeCell<Option<&'static mut ConsoleWriter<'static>>>,
> = Mutex::new(UnsafeCell::new(None));
unsafe fn set_console<'a, 'b: 'a>(writer: &'a mut ConsoleWriter<'b>) {
unsafe fn duplicate<'a, 'b: 'a>(
p: &'a mut ConsoleWriter<'b>,
) -> &'static mut ConsoleWriter<'static> {
mem::transmute(ptr::from_ref(&p).read())
}
unsafe {
let guard = PANIC_CONSOLE.lock();
*guard.get() = Some(duplicate(writer));
}
}
unsafe fn clear_console() {
unsafe {
let guard = PANIC_CONSOLE.lock();
*guard.get() = None;
}
}
unsafe fn get_console() -> Option<&'static mut ConsoleWriter<'static>> {
let guard = PANIC_CONSOLE.lock();
unsafe { &mut *guard.get() }.take()
}
where Mutex
is a fake mutex that exists to satisfy the Sync
bound on mutable statics. (Remember, this is a strictly single-threaded environment.) The panic handler called get_console
, and the startup glue called set_console
immediately after creating the ConsoleWriter
and clear_console
right before it went out of scope. (The environment does have a meaningful notion of executing one program after another; it's normal for programs to finish their work and exit.)
This works, as I said, but it is an API with sharp edges. If the startup glue happens to forget to call clear_console
at the right time, and a panic happens after the ConsoleWriter
goes out of scope, which is a possibility I can't rule out for certain, the panic handler will scribble on deallocated stack memory, with unpredictable consequences. It occurred to me that I could eliminate that problem by having set_console
return a scope guard that clears the cell on drop:
struct CellGuard<'a> {
closure: fn(),
lifetime: PhantomData<&'a ()>,
}
impl<'a> CellGuard<'a> {
fn new(f: fn()) -> Self {
Self { closure: f, lifetime: PhantomData }
}
}
impl<'a> Drop for CellGuard<'a> {
fn drop(&mut self) {
(self.closure)()
}
}
static PANIC_CONSOLE: Mutex<
UnsafeCell<Option<&'static mut ConsoleWriter<'static>>>,
> = Mutex::new(UnsafeCell::new(None));
unsafe fn set_console<'a, 'b: 'a>(writer: &'a mut ConsoleWriter<'b>) {
unsafe fn duplicate<'a, 'b: 'a>(
p: &'a mut ConsoleWriter<'b>,
) -> &'static mut ConsoleWriter<'static> {
mem::transmute(ptr::from_ref(&p).read())
}
unsafe {
let mguard = PANIC_CONSOLE.lock();
*mguard.get() = Some(duplicate(writer));
}
CellGuard::new(|| unsafe {
let mguard = PANIC_CONSOLE.lock();
*mguard.get() = None;
})
}
clear_console
goes away, get_console
is unchanged. It's still an unsafe API because of the shenanigans with stealing a mutable reference, but one way to misuse it has been eliminated. Strictly better, right?
Except now I get E0502 on the startup glue:
error[E0502]: cannot borrow `console` as mutable because it is already borrowed as mutable
--> src/glue/entry.rs:44:25
|
30 | let _panic_console_guard = panic::set_console(&mut console);
| ----------- first mutable borrow occurs here
...
44 | crate::main(&argv, &envp, &mut console);
| ^^^^^^^^^^^ second mutable borrow occurs here
...
71 | }
| - first mutable borrow might be used here, when `_panic_console_guard` is dropped and runs the `Drop` code for type `glue::panic::CellGuard`
So now I'm stuck. Apparently giving a CellGuard
a lifetime derived from the &console
reference means the compiler thinks CellGuard
holds an actual copy of the reference, even though it does not? (Pay no attention to what's in the UnsafeCell
) Surely there's some way to separate the lifetime from the reference itself?
(I'm open to alternative ideas, but I really have put a lot of thought into this and all the other possibilities I can think of are ruled out by one of the environmental limitations that I have listed, and there's more of them, so be prepared for me to come back at you with "that won't work because ...".)