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
ext
contains 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
poll
can be spuriously called at any point, and I must enforce unique access to the Inner struct. - Am I missing
Pin
somewhere? Is my usage of PhantomPinned sufficient to forceObjMutationFutureInner
to be pinned? Do I need to include aBox::pin
to my future creation step? - Would it be more idiomatic to delay the sdk call
ext::schedule_mutation
to the firstpoll
call? - Is there any way to avoid boxing the entire
Inner
structure, and store the state directly in the future object? - Is calling
wake
while the lock is held a bad idea?