如何为Rust编写的可执行程序提供支持Mod的友好API?
实现Rust程序的Mod API方案
核心思路:动态加载共享库
你的可执行程序需要通过动态加载共享库(对应平台的.so/.dll/.dylib文件)实现Mod加载,这是跨语言Mod支持的标准方案。Rust中可以借助libloading crate完成动态库加载,同时通过规范的C绑定接口,让Rust和C编写的Mod都能兼容调用开放功能。
步骤1:定义稳定的C兼容API接口
先将需要开放的功能封装为C兼容函数——C是跨语言交互的通用中间层,Rust Mod也能通过extern "C"语法调用这些接口。
示例:提供给C/C++开发者的API头文件
创建my_app_api.h:
#ifndef MY_APP_API_H #define MY_APP_API_H // 通用错误码定义 typedef enum { MY_APP_OK = 0, MY_APP_ERR_INVALID_ARG, MY_APP_ERR_NOT_FOUND } MyAppResult; // 程序核心上下文指针(内部由主程序管理,Mod仅做传递) typedef struct MyAppContext MyAppContext; // 开放API:获取应用版本号 const char* my_app_get_version(MyAppContext* ctx); // 开放API:注册Mod初始化回调(Mod通过该回调接收程序启动事件) MyAppResult my_app_register_init_callback(MyAppContext* ctx, void (*init_fn)(MyAppContext*)); #endif
Rust端实现API导出
在主程序中用#[no_mangle]和extern "C"标记导出函数,确保名称不被编译混淆:
use std::ffi::CString; use std::sync::Mutex; // 主程序上下文结构体,存储状态和Mod回调列表 pub struct MyAppContext { version: &'static str, init_callbacks: Mutex<Vec<extern "C" fn(*mut MyAppContext)>>, } #[no_mangle] pub extern "C" fn my_app_get_version(ctx: *mut MyAppContext) -> *const u8 { let ctx = unsafe { &*ctx }; CString::new(ctx.version).unwrap().into_raw() } #[no_mangle] pub extern "C" fn my_app_register_init_callback( ctx: *mut MyAppContext, init_fn: extern "C" fn(*mut MyAppContext), ) -> i32 { let ctx = unsafe { &mut *ctx }; ctx.init_callbacks.lock().unwrap().push(init_fn); 0 // 返回MY_APP_OK } // 程序启动时触发所有Mod的初始化回调 pub fn trigger_mod_inits(ctx: *mut MyAppContext) { let ctx = unsafe { &mut *ctx }; for callback in ctx.init_callbacks.lock().unwrap().iter() { callback(ctx); } }
注意:所有导出函数必须遵循C调用约定,且#[no_mangle]是必须的,否则编译后函数名会被混淆,动态加载时无法找到。
步骤2:实现Mod自动加载逻辑
使用libloading和glob crate扫描mods文件夹,自动加载其中的共享库。
1. 添加依赖到Cargo.toml
[dependencies] libloading = "0.8" glob = "0.3" # 用于递归扫描文件夹中的共享库文件
2. 编写加载逻辑代码
use glob::glob; use libloading::{Library, Symbol}; use std::path::Path; // 定义Mod入口函数签名:所有Mod必须导出`my_app_mod_init`函数 type ModInitFn = extern "C" fn(*mut MyAppContext) -> i32; pub fn load_mods(ctx: *mut MyAppContext, mods_dir: &str) -> Result<Vec<Library>, Box<dyn std::error::Error>> { let mut loaded_libs = Vec::new(); // 根据平台匹配共享库后缀 let pattern = match std::env::consts::OS { "windows" => format!("{}/**/*.dll", mods_dir), "macos" => format!("{}/**/*.dylib", mods_dir), _ => format!("{}/**/*.so", mods_dir), }; for entry in glob(&pattern)? { let path = entry?; if !path.is_file() { continue; } // 加载动态库 let lib = Library::new(&path)?; println!("Loaded mod: {}", path.display()); // 获取Mod的初始化入口函数 let init_fn: Symbol<ModInitFn> = unsafe { lib.get(b"my_app_mod_init")? }; // 调用Mod初始化函数,传入主程序上下文 let result = init_fn(ctx); if result != 0 { eprintln!("Mod {} initialization failed (error code: {})", path.display(), result); continue; } loaded_libs.push(lib); } Ok(loaded_libs) }
步骤3:为Rust Mod开发者提供封装工具
针对Rust开发者,你可以发布一个my_app_mod crate,封装C绑定的细节,降低Mod开发门槛。
示例:my_app_mod crate的核心代码
use std::ffi::CStr; // 导入主程序的C绑定 #[link(name = "my_app")] extern "C" { pub fn my_app_get_version(ctx: *mut MyAppContext) -> *const u8; pub fn my_app_register_init_callback( ctx: *mut MyAppContext, init_fn: extern "C" fn(*mut MyAppContext), ) -> i32; } #[repr(C)] pub struct MyAppContext; // 封装为Rust友好的函数 pub fn get_version(ctx: &mut MyAppContext) -> &str { unsafe { let c_str = CStr::from_ptr(my_app_get_version(ctx as *mut _)); c_str.to_str().unwrap() } } // 提供宏,简化Mod初始化函数的注册 #[macro_export] macro_rules! my_app_mod_init { ($init_fn:expr) => { #[no_mangle] pub extern "C" fn my_app_mod_init(ctx: *mut $crate::MyAppContext) -> i32 { let ctx = unsafe { &mut *ctx }; $init_fn(ctx); 0 } }; }
Rust Mod开发示例
Rust开发者只需依赖my_app_mod,即可快速编写Mod:
use my_app_mod::{get_version, my_app_mod_init, MyAppContext}; fn init_mod(ctx: &mut MyAppContext) { println!("My Rust Mod initialized! App version: {}", get_version(ctx)); // 此处可调用更多API实现交互逻辑 } my_app_mod_init!(init_mod);
编译Mod时需指定为动态库:
cargo build --release --lib --crate-type cdylib
步骤4:处理生命周期与安全问题
- 动态库生命周期:
loaded_libs需在程序运行全程保持存活,否则动态库会被卸载,引发悬垂指针问题。 - 线程安全:若主程序为多线程架构,API函数必须保证线程安全,例如使用
Mutex或RwLock保护共享状态。 - 内存安全:避免在API中传递Rust特有类型(如
String、Vec),改用C兼容类型(如*const u8、size_t),并明确内存所有权规则(例如谁负责释放内存)。 - 版本兼容性:API接口发布后尽量不要修改函数签名,否则旧Mod会失效。可在上下文结构体中添加版本字段,让Mod能判断当前API版本并做兼容处理。
总结
- 用
#[no_mangle]和extern "C"导出C兼容的API函数,确保跨语言可调用。 - 借助
libloading动态扫描并加载mods文件夹中的共享库。 - 为Rust开发者提供封装好的crate,降低开发复杂度。
- 严格遵守C交互的内存、线程安全规则,保证API的稳定性与兼容性。
内容的提问来源于stack exchange,提问作者Alexis
相关产品推荐
相关产品推荐

