Getting a library-user-defined Rust struct out of a C callback while giving access to other state

I'm writing bindings to a C graphics library. It has a very C-ish idiom where you store state in a user-defined "handle" struct, attach it to a library-defined opaque "view" struct via a void pointer, then fish your handle back out of the view in a callback. Within the callback the user is assumed to have access to as much of the state of the running program as they want, which is generally all accessed via the handle. I'm fairly new to Rust and struggling to wrap this in a nice API.

This is what the signature of the callback function has to look like, and the way to attach a callback to a view once you've defined it, from the low-level bindings:

#[repr(C)]
#[derive(Debug, Copy, Clone)]
// an opaque struct containing data about the view that received the event
pub struct View {
    _unused: [u8; 0],
}

pub type Event = u32;
pub type Status = u32;

type EventCallback = unsafe extern "C" fn(
    view: *mut View,
    event: *const Event
) -> Status;

pub fn setEventCallback(view: *mut View, eventCallback: EventCallback) -> Status;

The bound library also includes functions to attach a void pointer to a view and get it back out again:

pub type Handle = *mut ::std::os::raw::c_void;

pub fn setHandle(view: *mut View, handle: Handle);
pub fn getHandle(view: *mut View) -> Handle;

You can see the intended pattern. You declare a struct that keeps track of whatever data you need across event processing runs and attach a instance of it to the relevant view. Then, you write an event callback for the view that gets the instance back out again, casts it to the right type, and works with it. The handle can hold whatever you want, including pointers to the world itself, other views associated with the world, etc., which is how the library's own examples work. This is all fine for C, but not very Rust-y.

My first instinct was to make a World struct that holds View structs, which themelves hold user-defined handle structs and are generic over their handle's type. That way, the user could define whatever handle they liked, pass it in when creating the view, and everything would be fine. The trouble is that a pointer to the view needs to get passed back and forth across the FFI, and sooner or later it needs to get cast back to an actual View reference. The function where that happens needs to be generic to work with any possible View, and thus any function that calls it needs to be generic, on and on until you hit the FFI. Since the names of generic functions have to be mangled, they can't be exposed via the FFI, so they can't hook into the bound library's callback routine. No amount of indirection seemed to save me from this, although maybe there's an approach I didn't hit on.

Next I decided to try using trait objects, with an empty Handle trait. The trouble there, as I discovered, is that a trait object does not expose the concrete object's fields. Same thing with a boxed trait. In both cases, the compiler only knows what's defined on the trait, so the user can't actually make use of what they've defined on their handle struct once they've stored a pointer to it in their View. Trying to flesh out the Handle trait to have some behavior that exposes the interface of the concrete type doesn't work so well either, because of the stuff around Self and object safety.

What's more, the callback functions are specific to each view, but I need to be able to take the view pointer that gets passed in to the callback and work back to the current world, set the current view on the world, and then pass the world into the user's callback so that they have access to everything they need. I'm trying to have the user only use the world's interface and hide everything behind it, so I don't want them to have to store references to the world and other views and stuff in their handle (if Rust could even be made to allow such a thing). I've run into a lot of trouble with the borrow checker trying to deal with this—avoiding having two mutable references to the same thing is very hard with that design.

I mocked up a very simple ideal usage example that goes as follows:

struct MainHandle {
    count: u64
}

impl MainHandle {
    fn new() -> Self {
        Self { count: 0 }
    }

    fn increment(&mut self) -> u64 {
        if self.count != std::u64::MAX { self.count += 1; }
        self.count
    }
}

fn main() {
    // automatically creates the first view behind the scenes
    // and associates this handle with it
    let app = World::new(MainHandle::new());

    // app's current view is set to the automatically-created
    // view in the context of this callback
    app.set_event_callback(|app, event| {
        println!("Event: {}", event);
        // app can be derefed to the current view's handle
        println!("# of events: {}", app.increment());
        if event == Event::Close { app.quit(); }
        Status::Success
    });

    app.set_window_name("Event Printer");
    app.start()
}

I would love to be able to make an API along these lines into a reality. I know this might be too good to be true, but I don't want to put users too close to the low-level bindings or there's not much point in writing high level ones. Is there a way to get what I want?

It sounds like it can be done. You would probably have to make the world be non-Send to simplify the thread safety. I can take a closer look once I get home from work, but I would make a WorldInner type that the world has an Rc to, and I would make some sort of CallbackData struct that contains a box to the closure along with an Rc to the world. Then the actual callback you give to setEventCallback is always the same function which just calls the boxed closure in the CallbackData type.

Can you expand a bit on how you create a new view?

Sure! Thanks for your help, by the way—I haven't yet tried the approach you described so I'm excited to give it a shot. I've gotten pulled away into other stuff for just a bit but I should be able to get back to this shortly.

In the underlying C library, making a view is simple. You first make a world, then make a view that is associated with it:

pub fn newWorld() -> *mut World;
pub fn newView(world: *mut World) -> *mut View;

The view represents a drawing context; it can be attached to an OS-level window or nested inside a parent view. Many of the functions in the C library for drawing and event handling and the like take a view as an argument. Views are strongly associated with their world; multiple worlds can coexist in one process but must be kept very separate. The main motivation behind the library is supporting UIs for audio plugins; the degree of separation between multiple plugins in a DAW is a good reference point for how separate worlds need to be. The instantiation of a world is not thread-safe, for instance, even if the worlds are kept to separate threads.

So far, I've been holding pointers to views inside a hash table in the World struct and freeing them when the world is dropped. Views in the table are indexed by str. When a new world is instantiated, a new view is instantiated at the same time and associated with the key "main". Users can make more views through the world's interface, passing in a new str to identify the view by. My idea is that users will call methods on the world that sometimes may talk to the view behind the scenes, and that methods on the world that would take a view str as an argument will have forms that just use "main", to easily support applications that have only one view (since that seems to be quite common). I'm open to changing this design if needed, though.

Would you have to create a new view for every event callback? It seems like each view can store exactly one piece of information? Can you create multiple views for each world?

Your event callback can be generic, as long as you give it a type parameter when you pass it to C.

extern "C" fn my_event_callback_wrap<V: View>(view: *mut V, ...) {
    // convert pointers to references, call into user-provided code
}

fn set_callback<V: View>(v: Box<V>) {
    unsafe {
        setEventCallback(v.into_raw(), my_event_callback_wrap::<V>);
    }
}

@alice An event callback is attached to a view, yeah, and a pointer to the view is one of the things passed into the callback. The same callback could be attached to multiple views, but a view can only have one callback, and only one view is passed into the callback when it's called. Here's the signature of an event callback and the function used to attach one to a view, just once more for reference:

type EventCallback = unsafe extern "C" fn(
    view: *mut View,
    event: *const Event
) -> Status;

pub fn setEventCallback(view: *mut View, eventCallback: EventCallback) -> Status;

As a side note, events are things like user input that come in from outside—clicks and keypresses and such. You poll the world for events in every run of your main loop, and it then dispatches the events to each relevant view by calling their callbacks.

Views are an opaque struct in the underlying library, but there are functions that allow you to get their user-defined handle, world, visibility, dimensions, native window, etc. So they store a lot of information, but the only information they contain that you as a user are completely free to structure is their handle.

@kennethuil Unfortunately that hasn't helped in this case, because the holder of the handle itself is what needs to be generic, and the genericism of the functions that take it as a parameter are an outcome of that. The view and/or world need to be generic over all the possible user handles in that design. Here's a brief example pulled from one of my earlier attempts at figuring this out, although it would need to be restructured to support different handles for each view (which is part of the reason why the generic approach is probably not ideal for this case anyway):

pub struct World<'a, T: 'a> {
    handle: T,
    callback: Box<dyn Fn(&mut Self, Event) -> Status + 'a>,
    // etc...
}

// gives "functions generic over types or consts must be mangled"
#[no_mangle]
unsafe extern "C" fn world_callback<T>(
    view: *mut View,
    event: *const Event
) -> PuglStatus {
    let world: &World<T> = &*(getHandle(view) as *const World<T>);
    // etc...
}

Wherever you put the handle inside has to be generic in that design, and if you can't reach that handle from the view pointer that gets passed to the callback it won't do you any good in the first place. So, the genericism seems more-or-less inescapable to me if you stick to that approach. I don't think it's optimal anyhow in the long run, because every view can have a different handle and there can be theoretically limitless views, which that design doesn't support and isn't a good fit for genericism in general.

Please consider this example: playground. The safety of this example relies on the following assumptions:

  1. The C library will not do anything involving multiple threads.
  2. Even though the World contains a Rc, it does not implement Clone.
  3. The C code will never call a callback unless the original World object is currently inside the start function.

There may still be some challenges regarding std::mem::swap inside event callbacks... Perhaps a &mut T is better than a &mut World when passing a reference to the callbacks.

Oh thanks!! I'll have to play around with it—some of the stuff you've used I haven't really touched yet (Rc, PhantomData, Cell, UnsafeCell). Looking over the docs I do have the feeling they're exactly what I've been needing.

The C library doesn't do any multithreading as far as I can see, and parts of it aren't threadsafe to begin with. All the computationally intensive stuff you would do with it is on the GPU, and that's under the user's control anyway—the library only gives a context for it. A project making use of it would almost certainly have only one World in existence for the duration of its run so there's no need for World to implement Clone—I have a feeling the C library would segfault if a World was handled that way naively. And, fortunately, it's up to the user when the callbacks get called—the C library exposes functions for requesting the world to dispatch events to the views. Usually you would call one during your main loop, which I'm calling start in this case. That's another place the user needs to be able to pass in a closure or the like, I just haven't gotten that far yet.

I can lay out the details behind the types you mentioned:

Reference counting (docs)
An Rc is a reference counted handle to heap allocated data. The allocation is freed once the last Rc is dropped. I use it to share the WorldInner between the World given to the user and the callback handlers.

Since you can have several Rcs to the same element, a &mut Rc<T> is not enough to obtain a &mut T.

Note that since our callback handler is never dropped with the current setup, we have a memory leak.

PhantomData (docs)
At compile-time, PhantomData<T> acts exactly like T. At runtime PhantomData<T> acts as if there's nothing there. I just used it because I wanted to ensure that WorldInner didn't implement Send, although I've since realized. Note that the use of Cell was a mistake — it should have been PhantomData<Rc<()>> because Cell is actually Send (it's just not Sync). That said i believe it turns out not to be needed at all due to the mutable pointer making it not send anyways.

Various types of cells (Cell, RefCell, UnsafeCell)
Cells are tools that allow mutation of values through immutable references. They avoid threading issues by being single-threaded. Cell ensures safety by only being usable with Copy types. RefCell ensures safety by checking borrowing rules at runtime, and UnsafeCell gives the job to you.

The reason it's ok to have an UnsafeCell around the state in my example is because of my three assumptions. By not allowing the user to clone the world, they can only access the world through:

  1. The world they created originally, or
  2. a reference given to an event handler.

So when not inside a callback, the user only has that one World, so unique access is guaranteed. When inside a callback, the original world object is stuck being mutably borrowed by the call to start, so the only access the user has to the World is through the mutable reference we just passed to the event handler.

Note my worry regarding std::mem::swap. The user could create a new world and swap it with the one in the reference. Now they have a world they could keep around. This is why I suggest just giving an &mut T instead. Swapping out the state is fine.

1 Like

If you are defining your own unsafe extern "C" fn to feed it as a C callback you do not need the #[no_mangle] since you don't need the function to be pub-accessible from C (C does not need to access your function by name since it can do it by address, the one you are providing).


Also, even though I have not looked in detail into your problem, you should know that if you can choose one type to point to with a type-erased void * pointer, while wanting some polymorphism over trait Behavior, then you can point to a Box<dyn Behavior + 'static>, meaning that you'll end up with a Box<Box<dyn Behavior + 'static>> (inner Box + dyn handles polymorphism "without generics", the outer Box matches the void * ABI)

Afterwards, if you do not like the double indirection, you may be able to optimize the layout and properties of this very general Box<Box<dyn ...>> pattern by replacing its usage with function pointer types derived from the different monomorphisations of a custom generic function that casts an opaque type according to its type parameter, like @kennethuil showed.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.