Callbacks between Rust and C/C++

I am trying to implement a callback from rust but Im unsure how to use it.
I have the following binding from C and need to use the function ulViewSetFinishLoadingCallback. It wants a callback of the type ULFinishLoadingCallback. But how do I create the callback?
here is the code i want to use:

typedef void
(*ULFinishLoadingCallback) (void* user_data, ULView caller,
  unsigned long long frame_id, bool is_main_frame, ULString url);

///
/// Set callback for when the page finishes loading a URL into a frame.
///
ULExport void ulViewSetFinishLoadingCallback(ULView view,
                                             ULFinishLoadingCallback callback,
                                             void* user_data);
2 Likes

You can define an ordinary method with the appropriate parameters, then use it.

fn my_callback(user_data: *const (), caller: ULView, ...) {
    ...
}
ulViewSetFinishLoadingCallback(view, my_callback, user_data);

This should be an extern "C" fn, no?

Yes it needs to be extern "C". Also, if you want to wrap in a safe API, I suggest you to use user_data to be able to use closures. Something like:

pub fn ul_view_set_finish_loading_callback(
    view: ULView,
    callback: impl FnOnce(ULView, u64, bool, ULString),
) {
    extern "C" fn raw_callback(
        user_data: *const (),
        view: ULView,
        frame_id: u64,
        is_main_frame: bool,
        url: ULString,
    ) {
        let callback =
            unsafe { Box::from_raw(user_data as *mut () as *mut dyn FnOnce(ULView, u64, bool, ULString)) };
        callback(view, frame_id, is_main_frame, url);
    }

    unsafe {
        ulViewSetFinishLoadingCallback(
            view,
            raw_callback,
            Box::leak(Box::new(callback)) as &dyn FnOnce(ULView, u64, bool, ULString) as *const _
                as *const (),
        );
    }
}

You said it needed to be extern "C" but I don't see that in your example?

1 Like

My fault, thanks. Edited.

1 Like

Thanks! Got it to work like first example.

unsafe extern "C" fn callback(user_data: *mut std::ffi::c_void , caller: ul::ULView,  frame_id: u64,
            is_main_fram: bool, url: ul::ULString){}
    
    ul::ulViewSetFinishLoadingCallback(view,
                        Some(callback),
                        std::ptr::null_mut());

Still having errors with the other example

The first should be user_data as *mut () as *mut dyn FnOnce(ULView, u64, bool, ULString, edited.

About the seconds, what's the error?

Still giving some errors. For that part:

The other errors are:
mismatched types

types differ in mutability

note: expected raw pointer `*mut std::ffi::c_void`
         found raw pointer `*const ()`rustc(E0308)
ul.rs(215, 17): types differ in mutability

Guys, you can't just cast a *mut dyn MyTrait to an *const (). You are throwing away the vtable. And you definitely can't cast it the other way. You need two boxes for this.

type BoxType = Box<dyn FnOnce(ul::ULView, u64, bool, ul::ULString)>;

let inner: BoxType = Box::new(callback);
let outer: *mut BoxType = Box::into_raw(Box::new(inner));

ulViewSetFinishLoadingCallback(view, Some(raw_callback), outer as *mut std::ffi::c_void);
unsafe extern "C" fn raw_callback(user_data: *mut std::ffi::c_void, view: ULView, frame_id: u64, is_main_frame: bool, url: ul::ULString) {
    let outer = Box::from_raw(user_data as *mut BoxType);
    let inner = *outer;
    inner(view, frame_id, is_main_frame, url);
}

In the future please make sure to post code blocks instead of images, and also, please post full error messages

1 Like

First of all, let's translate this raw definition into Rust:

use ::libc::{c_void, c_ulonglong};

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[repr(transparent)]
pub
struct c_bool(pub u8); impl c_bool {
    const FALSE: Self = Self(0);
}

impl Into<bool> for c_bool {
    fn into (self: c_bool)
      -> bool
    {
        self != Self::FALSE
    }
}

type ULFinishLoadingCallback = Option<
    unsafe extern "C" fn(
        user_data: *mut c_void,
        caller: ULView,
        frame_id: c_ulonglong,
        is_main_frame: c_bool, // only use `bool` if you can trust the FFI to give you either 0 or 1 (any other bitpattern would be UB)
        url: ULString,
    )
>;

extern "C" {
    fn ulViewSetFinishLoadingCallback (
        view: ULView,
        callback: ULFinishLoadingCallback,
        user_data: *mut c_void,
    );
}

Then, before transforming it into nicer Rust, we need to think about the semantics: as usual with C signatures, the type-level signature does not give enough information.

Mainly:

  • Under what circumstances / which context is the callback called? Is it called exactly once, at most once, multiple times but sequentially, or multiple times and potentially in parallel?

  • what's the deal with user_data? Is it an owned state? i.e., does the API offer some way to free the state? Or to unregister the callback?

    Note that if the callback is to be called exactly once, then technically that call can play the role of the freeing one.

When you think about these questions, you will realize the kind of closure you need:

  • exactly once / at most once: you may use an FnOnce + Send closure; but for the "at most" once case, if the API offers no way of freeing it, you will have a memory leak;

  • (maybe) multiple times but sequentially / never in parallel ("no need for thread safety"): FnMut + Send. Again, you will need either a freeing context or the API may give you a guarantee about how and when the callback ceases to be used (e.g., by offering an unregistering function). In those cases, you may get away with a borrowing abstraction. although one which may need drop glue to unregister itself when dropped.

  • (maybe) multiple times and (maybe) in parallel: Fn + Send + Sync. Same disclaimers about releasing the resources or borrowing them without UAF danger apply.

Since I see no mention of all these questions whatsoever in this thread, I'd be very concerned about the safety / correctness of any solution that may have resulted :warning:


An orthogonal question to the previous ones is whether to use static or dynamic dispatch:

  • Box<dyn …> / Arc<dyn …>, &[mut] dyn … (usually through an extra layer of indirection to get a slim / FFI-compatible pointer),

  • or directly get a slim pointer abusing the fact that the Rust caller providing the closure will also be providing some unknown yet fixed type for it.

    This second alternative is the more optimized one, but requires a bit more Rust trickery to pull. Luckily, within this forum, we can make sure you get it right :slight_smile:

I can provide examples for the latter, but only when the questions about resource cleanup get answered.

1 Like

A while back I wrote an article on how to implement this correctly. You may find it helpful for understanding how this can be implemented and what is going on under the hood:

2 Likes