< 返回我的博客

爱国的张浩予 发表于 2022-06-12 18:46

Tags:ffi,extern,dll,so,c-abi

【运行时】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链接库。
  • 【重点强调】我已亲测:在.cargo\config.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文件夹内(即,target\debugtarget\release),创建一个指向【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的【产品模式】真会导致什么崩溃线索都找不到。甲方还一口咬定一个文件都没有误删!太恶心了!

    image

    弹个对话框至少还留了一丝与产品经理狡辩的机会:“瞧!是不是,甲方一定是把某个关键的dll给误删了。不是代码的错!”。Nice! 就是这个范儿!

遗憾·待续

运行时【动态链接】是将【依赖项】置于.exe文件之外的。若遇到链接库文件丢失的情况,应用程序就不能正常运行了。

所以,我的下一个目标就是:在编译时,将【静态链接库.a文件】直接编译入.exe可执行文件内,来避免dll文件意外丢失的问题(当然,.exe文件的体积也会更大些)。但是,我正遇到了一个mingw64的编译错误undefined reference to 'BCryptGenRandom'还未搞定。若你对此也有兴趣,请待我的后续更新...

评论区

写评论

还没有评论

1 共 0 条评论, 1 页