I'm using a tokio runtime which is boxed and returned by pointer from an FFI function. The runtime is used in order to run futures, and the is wrapped in a C# Task.
I can create and use the runtime freely, but when I try to drop it, again from an FFI function, I get the following error:
Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context
.
The close_connection
function, in which the panic happens, isn't called from any Rust asynchronous context, but it might be called from a different C# thread than the one it was created on. AFAIS all of the futures spawned on the runtime have completed before the runtime was dropped.
What's the correct way to dispose of the runtime?
Rust code:
pub struct MyConnection {
connection: MultiplexedConnection,
runtime: Runtime
}
#[no_mangle]
pub extern "C" fn create_connection() -> *const c_void {
let runtime = Runtime::new().unwrap();
let _runtime_handle = runtime.enter();
let connection = runtime.block_on(get_multiplexed_async_connection()).unwrap();
let connection_object = MyConnection {
connection, runtime
};
let connection_box = Box::new(connection_object);
Box::into_raw(connection_box) as *const c_void
}
#[no_mangle]
pub extern "C" fn close_connection(connection_ptr: *const c_void) {
let connection_box = unsafe {Box::from_raw(connection_ptr as * mut MyConnection)};
let _runtime_handle = connection_box.runtime.enter(); // I'm not sure this line is required. I get the same error regardless of whether it's here/
drop(connection_box); // here I get the error.
}
#[no_mangle]
pub extern "C" fn get(key: *const c_char, connection_ptr: *const c_void, callback: unsafe extern "C" fn(*const c_char) -> ()) {
let connection_box_cast = connection_ptr as usize;
let key_cstring = unsafe {CString::from_raw(key as *mut c_char)};
let key_cstring_clone = key_cstring.clone();
let connection_box = unsafe {Box::from_raw(connection_ptr as * mut MyConnection)};
let _runtime_handle = connection_box.runtime.enter();
connection_box.runtime.spawn(async move {
let key_bytes = key_cstring_clone.as_bytes();
let mut connection_box_internal = unsafe { Box::from_raw(connection_box_cast as * mut MyConnection) };
let result = connection_box_internal.connection.get(key_bytes).await.unwrap_or("".to_string());
std::mem::forget(connection_box_internal);
let result_c = CString::new(result).unwrap();
let ptr = result_c.as_ptr();
std::mem::forget(result_c);
unsafe { callback(ptr) };
});
std::mem::forget(key_cstring);
std::mem::forget(connection_box);
}
C# code:
[DllImport("redis_rs_benchmark", CallingConvention = CallingConvention.Cdecl, EntryPoint = "create_connection")]
public static extern IntPtr CreateConnectionFfi();
[DllImport("redis_rs_benchmark", CallingConvention = CallingConvention.Cdecl, EntryPoint = "close_connection")]
public static extern void CloseConnectionFfi(IntPtr connection);
public delegate void StringAction(string arg);
[DllImport("test", CallingConvention = CallingConvention.Cdecl, EntryPoint = "get")]
public static extern void GetFfi(string key, IntPtr connection, StringAction callback);
static Task<string> GetFfiAsync(string key,IntPtr connection) {
var tsc = new TaskCompletionSource<String>();
GetFfi(key, connection, (result) => {
tsc.SetResult(result);
});
return tsc.Task;
}
async string test() {
var connection = CreateConnectionFfi();
var result = await GetFfiAsync("checkfoo32", connection);
CloseConnectionFfi(connection);
return result;
}
The issue was that since in C# I called CloseConnectionFfi
after await
, it was called from the internal tokio runtime thread.
var result = await GetFfiAsync("checkfoo32", connection);
CloseConnectionFfi(connection);
Once I delegated the closing to a separate thread, the issue was gone.
await Task.Run(() => CloseConnectionFfi(connection));