Rust 是静态类型语言, 如果有部分代码想要单独编译再加载, 就需要通过 link 来处理,
先把一个模块打包成 dynamic library, 然后运行的时候再来调用.
在 Windows 里边是 *.dll 文件, Linux 里是 *.so 文件, macos 是 *.dylib.
其他还有更小众的操作系统, 可能还有不同的后缀...
我这边对应的系统是 macos.

首先单独编译的部分, Rust 文档给出了比较多的种类, 看文档,
https://doc.rust-lang.org/reference/linkage.html
我的场景用到的是 dylib 或者 cdylib 的方案, Cargo.toml 指定一下配置就好.
我试了一下, 两者对我来说都是可以的, cdylib 是对 C 的支持, 我暂时用不到.

然后加载 dynamic library 的部分我直接用这个库了,
https://docs.rs/libloading/0.7.0/libloading/
但是需要注意的是, 加载的过程传参比较麻烦, 复杂结构会提示 "ffi unsafe".
具体原因没理解, 大概是跟内存布局有关, 我看传递的时候有时候是用的指针,
直接 usize 或者 bool 是可以直接传的, 但是 String 和 &str 传不了,
网上给的方案是用 CString 和 CStr 转换这边有例子,
https://doc.rust-lang.org/std/ffi/struct.CStr.html

此外就是 externs 写法的细节了, 我抄了写例子, 然后修 bug, 最终得到:

#[no_mangle]
pub unsafe extern "C" fn read_file(name_a: *const c_char) -> *mut c_char {
  let name = CStr::from_ptr(name_a).to_str().unwrap();
  let task = fs::read_to_string(&name);
  match task {
    Ok(s) => {
      let a = CString::new(s).unwrap();
      CString::into_raw(a)
    }
    Err(e) => {
      panic!("Failed to read file {:?}: {}", name, e)
    }
  }
}

然后对应调用的部分是:

fn main() {
  let u = call_dynamic();
  println!("Hello, world! {:?}", u);
}

fn call_dynamic() -> Result<String, Box<dyn std::error::Error>> {
  unsafe {
    let lib = libloading::Library::new(
      "/Users/chen/repo/calcit-lang/std/target/release/libcalcit_std.dylib",
    )?;
    let func: libloading::Symbol<unsafe extern "C" fn(name_a: *const c_char) -> *mut c_char> =
      lib.get(b"read_file")?;

    let a = CString::new("/Users/chen/repo/gist/rs-demo/Cargo.toml").expect("should not fail");
    let c_name = a.as_ptr(); // <-- 标记行A

    let ret = CStr::from_ptr(func(c_name)).to_str().unwrap();
    Ok(ret.to_owned())
  }
}

中间遇到一些坑, 导致结果传递参数比较长时间只能得到空字符串,
大致是两个问题吧, 一个是"标记行A"的位置, 需要定义成变量才能正常,
刚开始我写在同一行, 调试始终拿不到内容, 最后参考网上的例子调整, 就好了,
看某个文章提到原因说这是为了给 a 一个单独的引用.(来源丢了)

另一个是从 dynamic library 返回的数据, 我本来用的 *const char,
也是长时间读到空数据, 最后参考某个 FFI 的例子, 改成了 *mut char 随后成功,
https://stackoverflow.com/a/42498913/883571
依然不确定是怎么回事, 依稀翻到一个评论说这样为了让数据停留在堆上,
如果是前者, dynamic library 函数调用完成之后数据请跨了, 就读不到.(来源也丢了)

总之按照上边的写法, 编译后就能得到 macos 上的 *.dylib 文件, 动态加载.

初次使用, 没搞懂的细节还是挺多的, 包括后续怎么分发, 怎么跨平台, 都没有经验.
后续要进展还是提早留笔记.


更新... 发现还有一个 extern "Rust" 的模式, 对应直接使用 Rust ABI.
然后就可以直接用 Rust 当中的各种数据类型了. 省事很多啊.


更新... 用到一个有状态的 dylib, 发现 Linux 当中 Arc 传递出来会丢失, lazy_static 创建的状态也会丢失.
查了一下大致发现是 Linux dl_open dl_close 的行为跟 MacOS 不一致(具体没对应到文档),
GPT 给出的一个建议是用 Arc 保存 libloading 创建出来的 Library 数据, 试了一下是成功的.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者