Jul 29, 2016 — Rust
It’s easy to find articles describing how to wrap Rust closures to pass to C libraries that will carry a
void* to your callback, but what can you do when no userdata value is provided? This post is based on work I’ve done for the
Let’s imagine that the C library provides an API roughly analogous to the following:
We currently have a safe wrapper type named
Object and we want to enable consumers of our library to push callbacks onto it without writing any
unsafe code, but how can we do that without a userdata pointer? While the lack of a userdata pointer prevents us from carrying any state with the callback (ruling out closures), it’s still possible to wrap ordinary functions.
First, let’s think about the signature we want the users of our wrapper to conform to. The FFI demands
unsafe extern fn(*mut ffi_Object) -> u32, but we want to allow such callbacks to be totally safe. This means no
extern, and we should replace
*mut ffi_Object with a safe analogue. Since the callback does not take ownership, this is
Now we’re going to need to define a function that converts from our safe signature to the unsafe signature. At first, you might try to write:
but this leads to a dead end. A
fn(&mut Object) -> u32 is a function pointer, a runtime value, state! Exactly what we can’t pack into an
ffi_Callback. We’re going to need to build, at compile time, a different
ffi_Callback for each function we’re ever passed. Luckily, Rust has the useful property that each function gets its own unique type, so we can use a type parameter - our
wrap_callback function is going to be generic. These unique types can’t be written directly, so let’s add a dummy parameter
_: F to allow inference to fill them in for us.
Now that we know the function’s signature, what about its contents? First, let’s not forget to reintroduce a restriction we just accidentally loosened: this signature accepts closures with state, and it’s not possible for us to support that. Let’s be sure we’re being passed ordinary functions (or equally-valid stateless closures):
It’s a little unfortunate that we have to runtime assert this and can’t check it at compile time, but it’s the best we can do in current Rust. Make sure this requirement is displayed in your library’s documentation.
Alright, now what? Well, we need to define a function with the signature demanded by
wrap_callback, which accepts a function as a type parameter and returns an
ffi_Callback, this function will accept the same and be an
First, let’s convert our
*mut ffi_Object into a safe
Object we can use:
Now we need to obtain a value of type
F in order to call it. We just asserted that
F is zero-sized, so there’s exactly one possible value, but the compiler doesn’t know that. We’ll have to unsafely construct that value somehow - probably the simplest way is
mem::zeroed. Then, we’re going to call that function with a reference to the
Object, and save the result:
Lastly, since our
Object is an owned type, we need to avoid dropping it, then return the result we just generated. A future post will cover how to handle this in a less risky way.
Now we’re back in
wrap_callback, ready to return the function satisfying
ffi_Closure we just built:
And that’s it! Using it is simple, and as we hoped, contains no use of
Depending on your needs, you can make
wrap_callback public or use it internally.
Here’s the code in this article all put together, including a dummy version of
Object which I omitted for brevity. If you want to experiment, you can use this code on the Rust Playground. I’m interested in feedback and can accept it by email or the comments section of wherever you found this post.
Addendum: Safer, Less Usable
If you’re not satisfied with the runtime assert, it can be eliminated if you’re willing to sacrifice usability:
Thanks to Lalaland on
#rust for this suggestion.
Addendum: The Past
There was once a time when those unique per-function types we made use of were not zero-sized! This was just an implementation flaw and has since been resolved, but it was possible to work around it using closures that captured no state, and were zero-sized:
Tad Hardesty is a CS Student and enthusiast in many things.