Passing Rust closures to C

Yes, exactly. The end of a lifetime is not and will likely never be an event that can be observed at run time, so it's okay for the compiler to relocate it (as long as future versions of the compiler continue to accept programs previous versions did). On the other hand, it's critical that destructors run at highly predictable times — for example, so that you can use a raw pointer into a local variable and know it'll stay valid until the end of the block.

Correct. In particular, the standard library provides impl<F: FnMut(...)> FnMut(...) for &mut F, so if your code accepts FnMut, that means it also accepts mutable references to the same kind of function. Thus, fn foo<F: FnMut(...)>(f: F) is more general than fn foo<F: FnMut(...)>(f: &mut F).

The simplest implementation is:

  • You have a type struct VirtualMachine<'a> {...}, that has a lifetime parameter. (This implicitly requires that the lifetime outlives any particular instance of the type.)
  • When you accept a function type, you bound it with 'a, like
    impl<'a> VirtualMachine<'a> {
        fn something_wants_a_closure<F: FnMut() + 'a>(&self, f: F) {...}
    }
    
  • All of the pointers the machine might use are dropped, or at least not ever used again, when the VirtualMachine struct is dropped.

This takes advantage of a pattern the borrow checker understands; even if you're using pointers internally, it looks just like "a struct containing references" from the outside.

The disadvantage of this strategy is that any closure passed in that borrows data must borrow data that exists for at least as long as the VirtualMachine does. This is entirely adequate if your machine is “one-shot” in some way, like executing one user input in a REPL, but not if you want the VM to be able to borrow shorter-lived data given to it by a normal Rust function calling it; you then need to do the same thing, creating another struct with lifetime that guarantees the pointers will be dropped when it is.

An alternative is that data the VM wants to refer to can be wrapped in Rc/Arc instead of & — then, the data can live as long as the VM needs it to, instead of obligating the VM to fit into the shape the borrow checker understands. This can be mixed with the VirtualMachine<'a> strategy.

3 Likes