在Rust中向动态库暴露主程序公共API的技术方案问询
可行方案与实现思路
1. 抽离公共API Crate(最优解)
这是Rust生态中处理动态库交互最成熟的方案,完全解决结构体共享、函数调用的问题,且类型安全:
- 新建独立Rust crate(比如
game_api),仅存放结构体定义、Trait、函数签名、常量,不包含具体实现:// game_api/src/lib.rs #[derive(Debug, Clone)] pub struct WorldPosition { pub x: f32, pub y: f32, } // 主程序需实现的核心API trait pub trait GameApi { fn spawn_entity(&self, pos: WorldPosition, entity_type: &str); fn show_gui_message(&self, msg: &str); // 其他需要暴露的功能 } // 模组初始化函数签名,主程序会调用该函数传递API实例 pub type ModInitFn = extern "C" fn(api: &dyn GameApi) -> Result<(), String>; - 主程序依赖
game_apicrate,实现GameApitrait,启动时加载模组并传递API实例:// 主程序中的GameApi实现 struct GameCore; impl GameApi for GameCore { fn spawn_entity(&self, pos: WorldPosition, entity_type: &str) { // 主程序实体生成逻辑 } fn show_gui_message(&self, msg: &str) { // 主程序GUI显示逻辑 } } // 加载模组的代码 fn load_mod(path: &str) { let lib = libloading::Library::new(path).unwrap(); let init_fn: libloading::Symbol<ModInitFn> = lib.get(b"mod_init").unwrap(); // 传递主程序API实例给模组 init_fn(&GameCore).unwrap(); } - 模组同样依赖
game_apicrate,实现初始化函数并导出:// 模组代码 #[no_mangle] pub extern "C" fn mod_init(api: &dyn GameApi) -> Result<(), String> { // 调用主程序API api.show_gui_message("模组加载成功!"); api.spawn_entity(WorldPosition { x: 100.0, y: 200.0 }, "custom_monster"); Ok(()) }
该方案完全类型安全,结构体与API定义仅需编写一次,主程序与模组各自编译,无代码重复。
2. 主程序导出全局符号(适合简单场景)
若不想抽离单独crate,可让主程序直接导出全局函数(借助#[no_mangle]和extern "C"),模组通过libloading加载这些符号。但结构体定义需在主程序与模组中保持一致,建议通过公共头文件或共享crate维护:
- 主程序导出函数:
use std::os::raw::c_char; use std::ffi::{CStr, CString}; #[no_mangle] pub extern "C" fn game_spawn_entity(x: f32, y: f32, entity_type: *const c_char) { let entity_type = unsafe { CStr::from_ptr(entity_type).to_str().unwrap() }; // 主程序逻辑 } #[no_mangle] pub extern "C" fn game_show_gui_message(msg: *const c_char) { let msg = unsafe { CStr::from_ptr(msg).to_str().unwrap() }; // 主程序GUI逻辑 } - 模组加载并调用:
use std::os::raw::c_char; use std::ffi::CString; fn call_main_api() { let lib = libloading::Library::new("../target/release/game.exe").unwrap(); let spawn_entity: libloading::Symbol<extern "C" fn(f32, f32, *const c_char)> = lib.get(b"game_spawn_entity").unwrap(); let msg = CString::new("模组调用主程序API").unwrap(); spawn_entity(100.0, 200.0, msg.as_ptr()); }
这种方式需手动处理C字符串转换,结构体传递时必须保证内存布局一致,容易出错,适合功能较少的场景。
3. 回调上下文封装(优化回调思路)
若不想用公共crate,可将主程序所有API函数指针封装成上下文结构体,主程序将该结构体传递给模组初始化函数,模组通过结构体指针调用主程序功能:
- 主程序定义上下文结构体(需用
#[repr(C)]保证内存布局稳定):use std::os::raw::c_char; use std::ffi::{CStr, CString}; #[repr(C)] pub struct GameApiContext { pub spawn_entity: extern "C" fn(x: f32, y: f32, entity_type: *const c_char), pub show_gui_message: extern "C" fn(msg: *const c_char), } // 主程序实现API函数 #[no_mangle] extern "C" fn spawn_entity_impl(x: f32, y: f32, entity_type: *const c_char) { let entity_type = unsafe { CStr::from_ptr(entity_type).to_str().unwrap() }; // 逻辑实现 } #[no_mangle] extern "C" fn show_gui_message_impl(msg: *const c_char) { let msg = unsafe { CStr::from_ptr(msg).to_str().unwrap() }; // 逻辑实现 } // 加载模组时传递上下文 fn load_mod(path: &str) { let api_ctx = GameApiContext { spawn_entity: spawn_entity_impl, show_gui_message: show_gui_message_impl, }; let lib = libloading::Library::new(path).unwrap(); let init_fn: libloading::Symbol<extern "C" fn(api_ctx: *const GameApiContext)> = lib.get(b"mod_init").unwrap(); unsafe { init_fn(&api_ctx) }; } - 模组中定义完全一致的
GameApiContext结构体(必须包含#[repr(C)]),然后调用:use std::os::raw::c_char; use std::ffi::CString; #[repr(C)] pub struct GameApiContext { pub spawn_entity: extern "C" fn(x: f32, y: f32, entity_type: *const c_char), pub show_gui_message: extern "C" fn(msg: *const c_char), } #[no_mangle] pub extern "C" fn mod_init(api_ctx: *const GameApiContext) { let api_ctx = unsafe { &*api_ctx }; let msg = CString::new("通过上下文调用API").unwrap(); (api_ctx.show_gui_message)(msg.as_ptr()); }
该方案无需公共crate,但需手动维护结构体一致性,易因定义不一致引发内存错误,适合快速原型开发。
方案对比
| 方案 | 类型安全 | 维护成本 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 公共API Crate | ✅ | 低 | 中 | 中大型游戏模组系统 |
| 主程序导出全局符号 | ❌ | 中 | 低 | 小型游戏/简单模组功能 |
| 回调上下文封装 | ❌ | 高 | 中 | 快速原型/特殊场景 |
内容的提问来源于stack exchange,提问作者64_Tesseract
相关产品推荐
相关产品推荐

