【运行时】FFI
链接C ABI
动态链接库(实操分享)
不需要依赖任何第三方crate
就可达成·运行时·链接的功能要求。至于使用第三方crate
所带来的好处,我将在文章末尾给出解释与列举。
"干货"步骤
首先,在rs
代码里,使用extern { ... }
块导入外部函数。代码模板如下:
#[link(name = "actual_lib_name_without_extname")] extern {
#[link_name = "actual_external_function_name"] // 支持对`FFI`函数重命名
fn external_function_local_alias(_: *const c_char) -> *const c_char; // 原始指针`*const c_char`是对`FFI`安全的。
...
}
上述【代码模板】解释:
actual_lib_name_without_extname
需要被替换为【链接库文件名(不含扩展名与lib
前缀)】actual_external_function_name
需要被替换为【外部函数真实名字】external_function_local_alias
需要被替换为【外部函数的本地别名】。即,根据本地命名规范,对外部函数·重命名。
然后,设置环境变量$RUSTFLAGS
export RUSTFLAGS=-L native=<链接库搜索目录>
更多解释:
- 被依赖的【
C ABI
动态链接库(文件)】必须被预置于此<链接库搜索目录>下。- 否则,在编译过程中,会出现“找不到链接库”的错误
= note: ld.exe: cannot find -l<链接库文件名>
。
- 否则,在编译过程中,会出现“找不到链接库”的错误
- 环境变量
$RUSTFLAGS
会将【编译器配置指令-L
】传递给rustc
核心和向Library Search Path
清单临时添加一个新检索目录。- <链接库搜索目录>支持以
Cargo Package
根目录为起点的【相对路径】。 native=
前缀表示:在该<链接库搜索目录>下预存都是C ABI
链接库,而不是Rust ABI
链接库。
- <链接库搜索目录>支持以
- 【重点强调】我已亲测:在
.cargoconfig.toml [build] rustflags = "***"
配置项内,设置此-L
编译器参数不管用 — 原因不详且和Cargo Book
文档描述不符。
接着,若你的目标仅只是cargo build
编译出一个.exe
可执行文件,那么到这就可以打住了。
再续,若你的目标是cargo run
既编译源码又运行可执行文件,那么还有一步需要被完成。即,使【C ABI
动态链接库】对编译输出的.exe
文件可见。否则,在应用程序启动过程中,会遇到(exit code: 0xc0000135, STATUS_DLL_NOT_FOUND)
的错误和程序崩溃退出。其支持两种作法:
- 要么,徒手·复制·【
C ABI
动态链接库(文件)】至【编译输出.exe
文件】所在文件夹内。 - 要么,在
Cargo Package
根目录下,编写一个简单的build.rs
构建脚本- 【功能】指派
cargo
,在编译过程中,在$OUT_DIR
文件夹内(即,targetdebug
或targetrelease
),创建一个指向【C ABI
动态链接库(文件)】的【符号链接】。 - 【例程】至于如何编写该
build.rs
程序,可参考: use ::std::{env, fs, os, path::{Path, PathBuf}};fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let work_dir = vec!["../../..", "../../../deps"]; work_dir.iter().for_each(|dir_path| symbolic_link_dll(&Path::new(&out_dir[..]).join(dir_path).canonicalize().unwrap())); }#[cfg(windows)]fn symbolic_link_dll(exe_dir: &PathBuf) { const DLL_FILE: &str = "auxiliaries_native.dll"; let mut dll_origin = env::current_dir().unwrap(); dll_origin.push("assets"); dll_origin.push(DLL_FILE); if dll_origin.exists() { let dll_symbol = exe_dir.join(DLL_FILE); if dll_symbol.exists() { fs::remove_file(dll_symbol.clone()).unwrap(); } os::windows::fs::symlink_file(dll_origin.clone(), dll_symbol.clone()).unwrap(); } }#[cfg(not(windows))]fn symbolic_link_dll(exe_dir: &PathBuf) { unreachable!("算是家庭作业,自己实现看看。其实,和`win32`的差不多!"); }
- 【功能】指派
最后,执行cargo run
命令,完成:
- 编译源码
- 启动
.exe
可执行文件。 - 在程序初始化过程中,寻找【
C ABI
动态链接库】文件和链接之。- 若出于某些原因
dll
丢了、找不到了,程序直接崩溃退出 —— 连写日志的机会都没有。 - 超恶心!既没日志,也没
GUI
错误提示框。啥都没有,难死我了!
- 若出于某些原因
- 显示出
GUI
主界面。
在我的业务场景下,该应用程序是一个Win32 GUI App
— 体积绝对碾压electron
(比性能,算我欺负你)。
第三方crate
可带来的好处
相比于直接写extern {...}
块的简单粗暴,使用第三方crate
(比如,dlopen
)可带来的优势有两点:
- 延后【懒】链接【
C ABI
动态链接库】。这样,应用程序的启动与初始化延时会更短些。 - 若被依赖的【动态链接库(文件)】不能被找到或载入失败,那么你的应用程序至少还有机会弹出一个友好的【提示框】问询用户:“您是否误删了哪个
.dll
后缀文件?”,而不是没头没脑地直接崩溃退出 — 特别是,禁用了console
的【产品模式】真会导致什么崩溃线索都找不到。甲方还一口咬定一个文件都没有误删!太恶心了! 弹个对话框至少还留了一丝与产品经理狡辩的机会:“瞧!是不是,甲方一定是把某个关键的dll
给误删了。不是代码的错!”。Nice! 就是这个范儿!
遗憾·待续
运行时【动态链接】是将【依赖项】置于.exe
文件之外的。若遇到链接库文件丢失的情况,应用程序就不能正常运行了。
所以,我的下一个目标就是:在编译时,将【静态链接库.a
文件】直接编译入.exe
可执行文件内,来避免dll
文件意外丢失的问题(当然,.exe
文件的体积也会更大些)。但是,我正遇到了一个mingw64
的编译错误undefined reference to 'BCryptGenRandom'
还未搞定。若你对此也有兴趣,请待我的后续更新...