Rust 调用 C/Rust 生成的动态库

在始终是 C/C++ 有着更优越性能的情况下,因而之前介绍过多种 其他不同的语言如何加载使用 C/C++ 写的动态库,有 Go, Python, Java 和 C#。在学习 Rust 之时也有类似的需求。本文的做法是要用到第三方库 libloading,这里将参考官方的例子。


先来创建一个动态库,使用和 Go 调用 C 写的动态库完整例子(Linux版) 一文中相同的例子,add.c 代码内容如下

 1#include <string.h>
 2#include <stdio.h>
 3#include <stdlib.h>
 4char* add(char* src, int n)
 5{
 6    char str[20];
 7    sprintf(str, "%d", n);
 8    char *result = malloc(strlen(src)+strlen(str)+1);
 9    strcpy(result, src);
10    strcat(result, str);
11    return result;
12}
在 Linux 中使用如下命令编译出 libadd.so 动态库文件
$ gcc -fPIC -shared -o libadd.so add.c
在 Windows 或 Mac OS X 平台可启动一个 Docker 容器,然后在容器中执行上面的命令生成 Linux 下用的 libadd.so 文件。做法是
$ docker run -it -v $(pwd):/work -w /work rust:1.78-buster bash            # 然后在容器中执行
root@a208ba393783:/work# gcc -fPIC -shared -o libadd.so add.c
其实本文完全可以在 Mac OS X 下进行,只要 Mac 下也安装了 gcc,不过在 Mac 下动态库的扩展名是  *.dylib。上面选择 Docker 镜像 rust:1.78 有一举两得之效,它既有 Rust 环境,又有 gcc 8.3.0 编译器。

接下来是用 cargo 创建一个 Rust 项目,命令是
$ cargo new shared-library
然后添加依赖 libloading, 运行命令
$ cd shared-library
$ cargo add libloading
假如希望生成的执行文件更精致,那么要在 Cargo.toml 文件中加上 strip = true, 如此最终的 Cargo.toml 文件内容如下
1[profile.release]
2strip = true
3[package]
4name = "shared-library"
5version = "0.1.0"
6edition = "2021"
7[dependencies]
8libloading = "0.8.3"
我们直接编辑 Rust 项目 shared-library 的 main.rs 程序代码,内容如下
 1use std::env::args;
 2use std::error::Error;
 3use libloading::{Library, Symbol};
 4use std::ffi::{CStr, CString};
 5use std::os::raw::c_char;
 6type AddFunction = unsafe fn(*const c_char, i32) -> *mut c_char;
 7fn main() {
 8    let args: Vec<String> = args().collect();
 9    let lib_path = &args[1];
10    let result = call_dynamic(lib_path).unwrap();
11    println!("Result: {}", result);
12}
13fn call_dynamic(lib_path: &amp;String) -> Result<String, Box<dyn Error>> {
14    unsafe {
15        let lib = Library::new(lib_path)?;
16        let add: Symbol<AddFunction> = lib.get(b"add")?;
17        let src = CString::new("example string ")?;
18        let result_ptr = add(src.as_ptr(), 5);
19        let result_cstr = CStr::from_ptr(result_ptr);
20        let result_str = result_cstr.to_str()?.to_owned();
21        Ok(result_str)
22    }
23}
留意上面代码中如何加载动态库,映射动态库中的函数,Rust 与 C 之间类型的转换,并且所有与动态库的交互要放到 unsafe 块中

现在再次回到 Docker 容器
$ docker run -it -v $(pwd):/work -w /work rust:1.78-buster bash            # 然后在容器中执行
root@b122a91ea255:/work# cargo build --release                  # 会生成 target/release/shared-library 可执行文件
root@b122a91ea255:/work# target/release/shared-library ./libadd.so  # 假设  libadd.so 在 /work 目录
Result: example string 5
如果执行 target/release/shared-library 时指定的 libadd.so 是错误的,或者是在当前目录中但未写成 ./libadd.so 的格式,会报告找不到 libadd.so 的错误,比如在容器中执行的是
root@b122a91ea255:/work# target/release/shared-library libadd.so   # 下面是报错信息
thread 'main' panicked at src/main.rs:12:41:
called `Result::unwrap()` on an `Err` value: DlOpen { desc: "libadd.so: cannot open shared object file: No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Rust 加载调用 Rust 写的动态库

前面描述的是如何在 Rust 中加载调用 C/C++ 写的动态库,如果是用 Rust 写的动态库,然后用 Rust 加载调用就更简单了。看起来有些多此一举,实际也有可能出现这样的类似于出口转内销的操作。

创建一个动态库项目
$ cargo new add --lib
然后编译加生成的 src/lib.rs 文件,内容如下
1#[no_mangle]
2pub fn add(str: String, n: i32) -> String {
3    format!("{} {}", str, n)
4}
在生成的 add/Cargo.toml 文件中加入
1[lib]
2crate-type = ["dylib"]
指示 cargo build 要生成动态库文件,这里的值永远写成 "dylib",会在不同的平台下生成不同扩展名的动态库文件,如 Windows 的  *.dll, Linux 下是 *.so, Mac OS X 中是 *.dylib。

接着编译
$ cargo build    # 将会生成 target/debug/libadd.so 文件
由于是用 Rust 写成的动态库,所以在 Rust 中加载该动态库调用其中的方法时类型就好处理多了。我们还是重用之前的 shared-library 项目,把 main.rs 的内容修改为
 1use std::env::args;
 2use std::error::Error;
 3use libloading::{Library, Symbol};
 4type AddFunction = unsafe fn(String, i32) -> String;
 5fn main() {
 6    let args: Vec<String> = args().collect();
 7    let lib_path = &args[1];
 8    let result = call_dynamic(lib_path).unwrap();
 9    println!("Result: {}", result);
10}
11fn call_dynamic(lib_path: &String) -> Result<String, Box<dyn Error>> {
12    unsafe {
13        let lib = Library::new(lib_path)?;
14        let add: Symbol<AddFunction> = lib.get(b"add")?;
15        let src = String::from("example string ");
16        let result_str = add(src, 5);
17        Ok(result_str)
18    }
19}
进到 shared-library 项目所在的目录,编译
$ cargo build    # 会生成 target/debug/shared-library
最后执行
$ /work/target/debug/shared-library /work/add/debug/libadd.so
$ Result: example string 5
动态库与调用方有着一致的类型,所以无需进行类型的转换,方便许多。