I have a third party SDK that I must integrate with. Bundled within this SDK is a threadpool that asynchronously completes tasks. It exposes an interface like this:
/// Opaque context type
typedef struct ctx ctx_t;
/// Opaque object type
typedef struct my_obj my_obj_t;
my_obj_t * allocate_obj();
/// Callback type
typedef void (*done_callback)(void *udata, my_obj_t *obj, int error_code);
/// Schedule obj for modification. This will be pushed into a threadpool and eventually executed.
/// When execution is complete, done_callback will be called
void schedule_mutation(ctx_t * ctx, my_obj_t *obj, done_callback *callback, void * udata);
I am trying to implement schedule_mutation as an async function, inside of Rust. Below is a rough skeleton of an implementation:
- Assume module
extcontains bindgen-like bindings to the external api - Ignore the following mistakes, omitted due to
lazinessbrevity:- Potentially spurious waker clones
- Never freeing shared future struct
I only wish to ask if this is the right direction.
use std::{
cell::RefCell,
ffi::c_void,
future::Future,
marker::PhantomPinned,
pin::Pin,
ptr::NonNull,
sync::Mutex,
task::{self, Poll, Waker},
};
pub struct MyLibrary(NonNull<ext::ctx_t>);
impl MyLibrary {
pub unsafe fn schedule_mutation(&mut self) -> ObjMutationFuture {
let obj = ext::allocate_obj();
let future = ObjMutationFuture::new();
ext::schedule_mutation(
self.0.as_ptr(),
obj,
Some(obj_callback),
future.inner_ptr() as *mut c_void,
);
future
}
}
pub extern "C" fn obj_callback(inner: *mut c_void, obj: *mut ext::my_obj_t, error_code: i32) {
unsafe {
let inner: &Mutex<ObjMutationFutureInner> =
&*(inner as *const Mutex<ObjMutationFutureInner>);
let mut inner = inner.lock().unwrap();
inner.obj = obj;
inner.error_code = error_code;
inner.done = true;
if let Some(waker) = inner.waker.take() {
waker.wake();
}
}
}
struct ObjMutationFutureInner {
/// The waker to fulfill
waker: Option<Waker>,
/// The actual result
obj: *mut ext::my_obj_t,
error_code: i32,
done: bool,
/// Make extra-sure it's pinned
_pin: PhantomPinned,
}
pub struct ObjMutationFuture(*const Mutex<ObjMutationFutureInner>);
impl ObjMutationFuture {
fn new() -> ObjMutationFuture {
ObjMutationFuture(Box::into_raw(Box::new(Mutex::new(
ObjMutationFutureInner {
waker: None,
obj: std::ptr::null_mut(),
error_code: 0,
done: false,
_pin: PhantomPinned,
},
))))
}
unsafe fn inner_ptr(&self) -> *const Mutex<ObjMutationFutureInner> {
self.0
}
}
impl Future for ObjMutationFuture {
type Output = Result<*mut ext::my_obj_t, i32>;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
let mutex = unsafe { &*self.0 };
let mut inner = mutex.lock().unwrap();
if inner.done {
// TODO: destroy inner future object
if inner.error_code == 0 {
Poll::Ready(Ok(inner.obj))
} else {
Poll::Ready(Err(inner.error_code))
}
} else {
// TODO: add would_wake checks to avoid spurious clones
inner.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
In short, I Box::into_raw(Box::new(...)), my way into a state object pointer, give one pointer to the SDK, and another pointer to the future. Future polls lock the underlying object, check if it's done, and update the waiter. The SDK callback locks the object, stores the result, and calls wake.
I have a few questions:
- Is this the right approach? Have I missed some general important concept?
- Is there any way to avoid the mutex? I assume no, since
pollcan be spuriously called at any point, and I must enforce unique access to the Inner struct. - Am I missing
Pinsomewhere? Is my usage of PhantomPinned sufficient to forceObjMutationFutureInnerto be pinned? Do I need to include aBox::pinto my future creation step? - Would it be more idiomatic to delay the sdk call
ext::schedule_mutationto the firstpollcall? - Is there any way to avoid boxing the entire
Innerstructure, and store the state directly in the future object? - Is calling
wakewhile the lock is held a bad idea?