通过 FFI 删除 Tokio 运行时会出现“无法在不允许阻塞的上下文中删除运行时”的恐慌。

Dropping a Tokio runtime through FFI panics with `Cannot drop a runtime in a context where blocking is not allowed.`

我正在使用一个 tokio 运行time,它被装箱并由 FFI 函数的指针返回。 运行time 用于 运行 futures,并且包装在 C# Task 中。 我可以自由地创建和使用 运行time,但是当我再次尝试从 FFI 函数中删除它时,出现以下错误: Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.

发生恐慌的 close_connection 函数不是从任何 Rust 异步上下文调用的,但它可能是从与创建它的线程不同的 C# 线程调用的。 AFAIS 在 运行 时间产生的所有期货都在 运行 时间被删除之前完成。

处理运行时间的正确方法是什么?

防锈代码:

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#代码:

[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;
}

问题是因为在 C# 中我在 await 之后调用了 CloseConnectionFfi,它是从内部 tokio 运行时线程调用的。

var result = await  GetFfiAsync("checkfoo32", connection);
CloseConnectionFfi(connection);

一旦我将关闭委托给一个单独的线程,问题就消失了。

await Task.Run(() =>  CloseConnectionFfi(connection));