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?