Rust 调用 C/Rust 生成的动态库
在始终是 C/C++ 有着更优越性能的情况下,因而之前介绍过多种 其他不同的语言如何加载使用 C/C++ 写的动态库,有 Go, Python, Java 和 C#。在学习 Rust 之时也有类似的需求。本文的做法是要用到第三方库 libloading,这里将参考官方的例子。
先来创建一个动态库,使用和 Go 调用 C 写的动态库完整例子(Linux版) 一文中相同的例子,add.c 代码内容如下
在 Linux 中使用如下命令编译出 libadd.so 动态库文件
接下来是用 cargo 创建一个 Rust 项目,命令是
我们直接编辑 Rust 项目 shared-library 的 main.rs 程序代码,内容如下
留意上面代码中如何加载动态库,映射动态库中的函数,Rust 与 C 之间类型的转换,并且所有与动态库的交互要放到 unsafe 块中
现在再次回到 Docker 容器
创建一个动态库项目
在生成的 add/Cargo.toml 文件中加入
指示 cargo build 要生成动态库文件,这里的值永远写成 "dylib",会在不同的平台下生成不同扩展名的动态库文件,如 Windows 的 *.dll, Linux 下是 *.so, Mac OS X 中是 *.dylib。
接着编译
进到 shared-library 项目所在的目录,编译
先来创建一个动态库,使用和 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}$ 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 # 然后在容器中执行其实本文完全可以在 Mac OS X 下进行,只要 Mac 下也安装了 gcc,不过在 Mac 下动态库的扩展名是 *.dylib。上面选择 Docker 镜像 rust:1.78 有一举两得之效,它既有 Rust 环境,又有 gcc 8.3.0 编译器。
root@a208ba393783:/work# gcc -fPIC -shared -o libadd.so add.c
接下来是用 cargo 创建一个 Rust 项目,命令是
$ cargo new shared-library然后添加依赖 libloading, 运行命令
$ cd shared-library假如希望生成的执行文件更精致,那么要在 Cargo.toml 文件中加上
$ cargo add libloading
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" 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: &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}现在再次回到 Docker 容器
$ docker run -it -v $(pwd):/work -w /work rust:1.78-buster bash # 然后在容器中执行如果执行 target/release/shared-library 时指定的 libadd.so 是错误的,或者是在当前目录中但未写成
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
./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}1[lib]
2crate-type = ["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}$ cargo build # 会生成 target/debug/shared-library最后执行
$ /work/target/debug/shared-library /work/add/debug/libadd.so动态库与调用方有着一致的类型,所以无需进行类型的转换,方便许多。
$ Result: example string 5