I've tried to reply to this on reddit, here follows a transcript:
They focus on size_of
and while that is entirely doable (LLVM doesn't do anything interesting about computing sizes, it's a bunch of really simple "algorithms" and we already replicate most of it)...
There is no point. Really, what they actually want is function-scoped statics and mem::uninitialized()
in them (or even None
, but that wastes a few bytes), e.g.:
fn to_static<F: 'static>(closure: F) -> &'static F {
static CLOSURE: UnsafeCell<F> = UnsafeCell::new(unsafe {
mem::uninitialized()
});
unsafe {
*CLOSURE.get() = closure;
&*CLOSURE.get()
}
}
It doesn't even have to use the same static
location for each F
(in cross-crate situations, for example), since the function returns the address in which the value was written, so something like this would not be hard to support.
The problem with that, however, as I hope many of you will notice, is that calling this function twice for the same F
will end up invalidating the first value, which for closures means re-entrance leads to UB.
As such, the API they want is wildly unsafe.
There is a solution, if they can move the closure storage to the callback invocation site: instead of returning a reference which is easy to invalidate, store that reference into an Option<&Fn()>
which can only be accessed by two functions:
One function is similar to the above to_static
, except it converts the reference to &Fn()
, wraps it in Some
and stores it in the Option<&Fn()>
callback holder.
Another function takes the &Fn()
out of the Option<&Fn()>
callback holder, replacing it with None
, and calls that closure, discarding the &Fn()
afterwards.
Except that still has the re-entrance issue: they could either guard against re-entrance by keeping the Some
until the call has ended and refusing to register a new callback (but this goes against their desired operational model).
Or they could move the closure data on the stack and only then call it, but this is pretty tricky to do, the best I could come up with is:
trait VirtualCall {
unsafe fn move_call(&self);
}
impl<F: Fn()> VirtualCall for F {
unsafe fn move_call(&self) {
ptr::read(self)();
}
}
static CALLBACK: Cell<Option<&'static VirtualCall>> = Cell::new(None);
pub fn register<F: Fn() + 'static>(closure: F) {
static CLOSURE: UnsafeCell<F> = UnsafeCell::new(unsafe {
mem::uninitialized()
});
unsafe {
*CLOSURE.get() = closure;
CALLBACK.set(Some(&*CLOSURE.get()));
}
}
pub fn invoke() {
if let Some(cb) = CALLBACK.get() {
CALLBACK.set(None);
unsafe {
cb.move_call();
}
}
}
The last 3 items (CALLBACK
, register
and invoke
) have to be separate for everything with a callback (spi_write
in this case), and preferably encapsulated.
They could maybe get away with a macro to define these items, and have the CALLBACK
hidden inside a struct
which only provides register
and invoke
as methods.
Also, single-threaded contexts are assumed - this scheme can be made safe via lifetime-based 0-cost proof tokens for "interrupts are disabled" (unless NMIs exist, ofc).