Thursday , April 15 2021

Neat Rust Tricks: Passing Rust Closures to C • Sean Griffin, Hacker News


One of Rust’s biggest selling points is how well it can interoperate with C. It’s able to call into C libraries and produce APIs that C can call into with very little fuss. However, when dealing with sufficiently complex APIs, mismatches between language concepts can become a problem. In this post we’re going to look at how to handle callback functions when working with C from Rust.

Our hypothetical library has aWidgetstruct, which periodically generates events. We want to take a callback function from users that is called whenever one of these events occurs. More concretely, we want to provide this signature:

impl Widget {     fn register_callback (& ​​mut self, callback: impl FnMut (Event)) {         // ...     } }

Unlike Rust, C has no concept of closures. Instead it has function pointers. To put this in Rust terms, you can takefn (Event), but notimpl FnMut (Event). Function pointers can only work with the arguments passed to them (or global state likestaticvariables), while closures can capture (or “close over ”) Any arbitrary state in the environment they were created. Because of this, C APIs often let you pass a “data pointer” when registering a callback, and then pass that pointer to your function whenever it’s called. For example, the C version of that signature might look like this:

void widget_register_callback (     widget_t * widget,     void * data,     void  callback) (void *, event_t) );

If the callback is long lived, or the ownership semantics are complex, the C API may also have you provide a destructor function:

void widget_register_callback (     widget_t * widget,     void * data,     void  callback) (void *, event_t),     void  destroy) (void  );

If you’re not familiar with C’s syntax, here’s the equivalent signature in Rust:

fn widget_register_callback (     widget: Widget,     data: * mut (),     callback: fn  mut (), Event),     destroy: fn  mut ()), );

So instead of taking a closure which automatically captures whatever state it needs to, in C we need to manually shove any state we want to keep into a struct, and pass that along with our callback function. Bridging these two APIs in Rust is surprisingly easy. The language does most of the work for us. The way we handle this is to pass the actual closure as our data pointer. Let’s look more concretely at what this means:

fn register_c_callback(widget: & mut ffi :: widget_t, callback: F ) where     F: FnMut (ffi :: event_t)   'static, {     // Safety: We've carefully reviewed the docs for the C function     // we're calling, and the variants we need to uphold are:     // - widget is a valid pointer     // - We're using Rust references so we know this is true.     // - data is valid until its destructor is called     // - We've added a `` static` bound to ensure that is true.     let data=Box :: into_raw (Box :: new (callback));     unsafe {         ffi :: widget_register_callback (             widget,             data as * mut _,             call_closure ::,             drop_box ::,         );     } }  extern "C" fn call_closure(     data: * mut libc :: c_void,     event: ffi :: event_t, ) where     F: FnMut (ffi :: event_t), {     let callback_ptr=data as * mut F;     // Safety: We've carefully reviewed the documentation for our     // C lib and know this pointer is valid and non-null.     let callback=unsafe {& mut * callback_ptr};     callback (event); }  extern "C" fn drop_box ::(data: * mut libc :: c_void) {     unsafe {Box :: from_raw (data as * mut T); } }

There’s a lot going on here, so let’s look at each piece one at a time.

fn register_c_callback(widget: & mut ffi :: widget_t, callback: F ) where     F: FnMut (ffi :: event_t)   'static,

The signature of this function is pretty standard for Rust. We don’t care if the function has mutable state, so we takeFnMutinstead ofFn. The'staticbound is needed since the callback given to us is going to be called afterregister_c_callbackreturns. This assumes that we need the function to be valid for some unknown period of time, which is the most common case in my experience. It’s possible to have less strict bounds for this, but it’s hard to do safely1.

let data=Box :: into_raw (Box :: new (callback));

Since we need the closure to live for some unknown period of time, we need to move it onto the heap. We then immediately callBox :: into_rawon it, which will give us a raw pointer to the closure, and prevent the memory from being de -allocated.

ffi :: widget_register_callback (     widget,     data as * mut _,     call_closure ::,     drop_box ::, );

Here we’re actually calling the underlying C function. Since the type ofdatais* mut F, and our C API expectsvoid *, we need to explicitly cast it. Finally, we’re passing our two function pointers. Both of these functions are generic, so we’re giving it the concrete type of the closure it’s calling. This is one of the few times you’ll ever see an explicitturbofishwithout actually calling the function.

extern "C" fn call_closure(     data: * mut libc :: c_void,     event: ffi :: event_t, ) where     F: FnMut (ffi :: event_t),

Here we declare the first of our two functions we’re passing to C. Since it’s meant to be called from C code, the function is defined asextern "C"to tell the Rust compiler to use C’s ABI here. Usuallyextern "C"functions also need# [no_mangle]to disable Rust’s automatic name mangling. However, this function is never called by name2 ****************. We give it to C by passing function pointers directly, so# [no_mangle]isn’t needed here.

let callback_ptr=data as * mut F; let callback=unsafe {& mut * callback_ptr}; callback (event);

Since our C API wanted a function that takesvoid *as it’s first argument, that’s how we declared the function(3). This means that the first thing we need to do is cast it to a pointer of the right type. We then turn that raw pointer into a Rust reference. Finally, with a& mut F, we can actually call the Rust closure with our event.

extern "C" fn drop_box ::(data: * mut libc :: c_void) {     unsafe {Box :: from_raw (data as * mut T); } }

And lastly, we have our destructor function.Box :: from_rawwill recreate a newBoxfrom our raw pointer. Once we have a regular RustBox, it’ll automatically be dropped when this function returns, freeing the underlying memory, and calling the destructors of any values ​​captured by our closure.

It’s surprisingly little code for such a complex operation(4). And true to Rust, this is done as a zero cost abstraction. This is all done usingmonomorphized generic functions, so there’s no indirection or allocation beyond what’s absolutely required.

And this composesreally well. At this point any additional code we write can operate purely in safe Rust. For example, if we want to wrap thatffi :: event_tin a Rust abstraction, we can just add more closures!

impl Widget {     fn register_callback (& ​​mut self, callback: impl FnMut (Event)) {         register_c_callback (             & mut self.ffi_widget,             move | ffi_event | {                 let event=Event :: from_raw (ffi_event);                 callback (event);             }         );     } }

Your closures can even have mutable state with no fuss!

let mut x=0; widget.register_callback (move | _ | {     x =1;     println! ("I was called {} times", x); });

This will print an incrementing number every time it’s called as you’d expect. With this little bit of code, we’ve got a full bridge between Rust closures and our C API. You can do anything you would be able to do with the same API written natively in Rust, and consumers of this code never need to know the difference.

All of the code in this article is based on real code from Diesel, which allows you to use Rust closures as the implementation of custom SQL functions on SQLite. You can find the codehereandhere.

When I first started working on this feature for Diesel, I knew I wanted an API that let you use a closure, and I expected making that work with SQLite’s C library to be much harder than it actually was . But ultimately the code required boils down to a bit of pointer juggling wrapped in anextern "C" fn. The fact that the language can handle this with so little work really shows how powerful Rust’s abstractions are, and how well they can compose into unexpected use cases.

  

               

47

    Kudos  

               

47

    Kudos  

Brave Browser
(Read More)
Payeer

About admin

Leave a Reply

Your email address will not be published. Required fields are marked *