You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Rust FFI库中复用Tokio Runtime?——sn_api异步函数的FFI封装优化方案问询

如何在FFI包装器中复用同一个Tokio Runtime来处理异步sn_api调用?

我正在为包含异步函数的sn_api库编写FFI包装器,用于Red语言的单线程非异步代码中。目前我在每个导出函数里用Runtime::new().unwrap().block_on(...),但每次调用都创建新Runtime的开销太大。下面是我的代码:

use std::os::raw::c_char;
use std::ffi::{CString, CStr};
use sn_api::{BootstrapConfig, Safe};
use tokio::runtime::Runtime;

#[no_mangle]
pub extern "C" _safe_connect(ptr: *const Safe, bootstrap_contact: *const c_char) {
    assert!(!ptr.is_null());
    let _safe = unsafe { &*ptr };
    let bootstrap_contact = unsafe { CStr::from_ptr(bootstrap_contact) };
    let mut bootstrap_contacts = BootstrapConfig::default();
    bootstrap_contacts.insert(bootstrap_contact.parse().expect("Invalid bootstrap address"));
    // 如何在其他函数中复用该Runtime?
    Runtime::new().unwrap().block_on(_safe.connect(None, None, Some(bootstrap_contacts)));
}

我想让所有异步函数都运行在同一个共享的Tokio Runtime上,考虑过用单例/全局实例,但我的库是以crate-type = ["cdylib"]编译的,担心全局变量不合适。请问最优的实现方案是什么?


完全同意你的想法——复用同一个Tokio Runtime是解决这个开销问题的核心。针对你的场景,有两种成熟的方案,各有优劣,你可以根据Red端的需求选择:

方案1:全局单例Runtime(最适合单线程Red场景)

虽然编译为cdylib,全局变量其实是完全可行的,只要我们用线程安全的方式初始化它。Tokio的Runtime本身实现了Send和Sync,适合全局共享。我们可以用once_cell或者lazy_static crate来创建一个懒加载的全局Runtime实例,确保它只初始化一次。

示例代码

首先在Cargo.toml中添加依赖:

[dependencies]
once_cell = "1.18"

然后修改你的FFI代码:

use std::os::raw::c_char;
use std::ffi::{CStr};
use sn_api::{BootstrapConfig, Safe};
use tokio::runtime::Runtime;
use once_cell::sync::Lazy;

// 懒加载全局Runtime,第一次使用时初始化,后续复用
static GLOBAL_RUNTIME: Lazy<Runtime> = Lazy::new(|| {
    Runtime::new().expect("Failed to initialize Tokio Runtime")
});

#[no_mangle]
pub extern "C" _safe_connect(ptr: *const Safe, bootstrap_contact: *const c_char) -> i32 {
    // 指针检查不能少,避免未定义行为
    if ptr.is_null() || bootstrap_contact.is_null() {
        return -1; // 返回错误码给Red端处理
    }

    let safe = unsafe { &*ptr };
    let bootstrap_contact = unsafe { CStr::from_ptr(bootstrap_contact) };

    // 替换expect为错误处理,避免panic跨越FFI边界
    let bootstrap_addr = match bootstrap_contact.parse() {
        Ok(addr) => addr,
        Err(_) => return -2, // 解析失败的错误码
    };

    let mut bootstrap_contacts = BootstrapConfig::default();
    bootstrap_contacts.insert(bootstrap_addr);

    // 复用全局Runtime执行异步调用
    match GLOBAL_RUNTIME.block_on(safe.connect(None, None, Some(bootstrap_contacts))) {
        Ok(_) => 0, // 成功返回0
        Err(_) => -3, // 连接失败的错误码
    }
}

这种方案的优点是:

  • Red端代码不需要额外管理Runtime,调用FFI函数即可
  • 完全自动复用同一个Runtime,开销最小
  • 实现简单,几乎不需要修改Red端逻辑

需要注意的点:

  • 不要用expect或panic!,因为FFI边界的panic会导致未定义行为,一定要返回错误码让Red处理
  • 如果Red是单线程的,这个方案完美适配;如果Red后续可能引入多线程,只要确保全局Runtime的线程池配置合理即可

方案2:显式初始化+传递Runtime句柄(更灵活可控)

如果你希望避免全局状态,或者需要在Red端控制Runtime的生命周期,可以提供一个初始化函数创建Runtime,返回其指针给Red,后续所有FFI函数都需要传入这个指针,最后再调用销毁函数释放资源。

示例代码

use std::os::raw::c_char;
use std::ffi::{CStr};
use std::ptr;
use sn_api::{BootstrapConfig, Safe};
use tokio::runtime::Runtime;

// 创建Runtime并返回其指针
#[no_mangle]
pub extern "C" _safe_create_runtime() -> *mut Runtime {
    match Runtime::new() {
        Ok(runtime) => Box::into_raw(Box::new(runtime)), // 将Runtime装箱后转为裸指针
        Err(_) => ptr::null_mut(), // 创建失败返回空指针
    }
}

// 销毁Runtime,释放资源
#[no_mangle]
pub extern "C" _safe_destroy_runtime(runtime_ptr: *mut Runtime) {
    if !runtime_ptr.is_null() {
        unsafe { Box::from_raw(runtime_ptr); } // 从裸指针恢复Box,自动drop
    }
}

#[no_mangle]
pub extern "C" _safe_connect(runtime_ptr: *mut Runtime, ptr: *const Safe, bootstrap_contact: *const c_char) -> i32 {
    if runtime_ptr.is_null() || ptr.is_null() || bootstrap_contact.is_null() {
        return -1;
    }

    let runtime = unsafe { &*runtime_ptr };
    let safe = unsafe { &*ptr };
    let bootstrap_contact = unsafe { CStr::from_ptr(bootstrap_contact) };

    let bootstrap_addr = match bootstrap_contact.parse() {
        Ok(addr) => addr,
        Err(_) => return -2,
    };

    let mut bootstrap_contacts = BootstrapConfig::default();
    bootstrap_contacts.insert(bootstrap_addr);

    match runtime.block_on(safe.connect(None, None, Some(bootstrap_contacts))) {
        Ok(_) => 0,
        Err(_) => -3,
    }
}

这种方案的优点是:

  • 完全无全局状态,更符合模块化设计
  • Red端可以控制Runtime的创建和销毁,甚至可以创建多个Runtime(虽然你的场景可能不需要)
  • 更灵活,适合复杂的集成场景

缺点是Red端需要额外管理Runtime指针,增加了一点复杂度。

总结建议

因为你的Red代码是单线程非异步的,**方案1(全局单例Runtime)**是最优选择——它实现简单,Red端无需额外工作,同时完全解决了重复创建Runtime的开销问题。只要确保错误处理得当(避免panic),这个方案非常稳定可靠。

内容的提问来源于stack exchange,提问作者Maciej Łoziński

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 21:39:08