看到 rust 可以编译成动态链接库(.so),想到是不是可以用 C 语言链接到这个库呢?答案是肯定的。Rust 提供了 FFI 接口,即 Foreign Function Interface,目的就是和其他语言交互。

废话不多说,开始干。我们要实现三个例子:

  • C 调用 Rust 动态库
  • C 调用 Rust 静态库
  • Rust 调用 C 函数 (不是库)

C 调用 Rust 动态库

Rust 部分

首先是用 cargo new NAME --lib 创建一个新项目,然后编辑 src/lib.rs

#![crate_type = "dylib"]

#[no_mangle]
pub extern fn double_input(input: i32) -> i32 {
    println!("hello --from rust shared library");
    input * 2
}

crate_type = “dylib” 代表编译成动态链接库。

no_mangle 告诉 rust 编译器,不要擅自改变下面这个函数的函数名。一些高级语言比如 c++ 之类,为了防止不同库中的函数名冲突,都会在编译时给每个函数生成独一无二的函数名,比如 func::h485dee

然后编辑 Cargo.toml 文件,在默认的文件基础上加入:

[lib]
name = "double_input"
crate-type = ["dylib"]

最后 cargo build, 就能在 target/debug/ 目录下找到动态链接库 libdouble_input.so

C 部分

然后准备一个 C 文件,也非常简单:

extern int32_t double_input(int32_t input);

int main() {
    int input = 4;
    int output = double_input(input);
    printf("%d * 2 = %d\n", input, output);
    return 0;
}

注意它用 extern 关键字表明函数在外部。

好了,现在有了.so 文件,也有了 C 文件,现在要把他们编译到一起。

gcc test.c -L ./target/debug/ -ldouble_input -o test

其中 -L 选项说明库文件所在的路径,-l 说明库文件的名字。

运行时

通过上面的步骤,我们生成了一个 test 可执行文件,现在运行它。

$ ./test
./test: error while loading shared libraries: libdouble_input.so: 
cannot open shared object file: No such file or directory

系统无法找到库。虽然我们在编译时(compile time)提供了.so文件的位置,但这个信息并没有写入test可执行文件,所以在程序执行期间的动态链接时,默认的搜索路径下找不到这个库。

有两种方法解决这个问题,

1)一个是设置 LD_LIBRARY_PATH 环境变量。比如:

$export LD_LIBRARY_PATH=.

这样,可执行文件执行时,操作系统将先在LD_LIBRARY_PATH下搜索库文件,再到默认路径中搜索。

环境变量的坏处是,它会影响所有的可执行程序。

2)另一个解决方案,即提供 -rpath 选项,将搜索路径信息写入test文件(rpath代表runtime path),就不需要设置环境变量了。

$gcc -g -o test test.c -ldouble_input -L. -Wl,-rpath=.

其中,-Wl 表示 -rpath选项是传递给连接器(linker)。

我们可以用 ldd 命令对比一下前后的结果,注意 libdouble_input.so 的地址。

$ ldd test
    linux-vdso.so.1 (0x00007ffcdb939000)
    libdouble_input.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2d5972a000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f2d59d1d000)


$ ldd test
    linux-vdso.so.1 (0x00007ffea124d000)
    libdouble_input.so => ./target/debug/libdouble_input.so (0x00007fbb2d783000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbb2d392000)

完整的程序代码在这里

C 调用 Rust 静态库

有了上一步动态链接的成功例子,调用静态库也依葫芦画瓢了。需要注意的是:

1)Rust 中的 dylib 要改成 staticlib
2)编译 C,静态链接 .a 库时,命令不一样。

静态链接时 gcc 的参数是这样的:

LDFLAGS := -Wl,-Bstatic -ldouble_input -Wl,-Bdynamic -lpthread -ldl

它的意思是仅对 double_input.so 使用静态链接,其他的库依旧使用动态链接。

因为 rust 或者是 c 在编译时都会加入一些默认的库,而这些库没有静态版本(也就是没有 .a 的版本),所以如果不加后面 dymamic 的部分,编译出错。

我们可以比较一下使用动态链接的 test 文件和静态链接的 test 文件大小,可见静态链接的明显大很多。

$ ls -lh test
-rwxrwxr-x 1 rz rz 11K Dec 16 15:30 test

$ ls -lh test
-rwxrwxr-x 1 rz rz 3.6M Dec 16 15:04 test

Rust 调用 C 函数

rust 语言的官方文档给出了一个非常权威的方式,传送门

所以我们只要依葫芦画瓢就行了。需要注意的几点:

  1. 在主目录下建一个 build.rs
  2. Cargo.toml 中需要指定 build-dependencies
[build-dependencies]
cc = "1.0"

其实 build.rs 的作用就是用 rust cc 库提供的函数把 C 代码编译成 rust 能识别的库,然后直接调用它。

官方文档介绍 build.rs 以及由它生成的部分 output 会影响 Cargo 的编译行为。

一个简单的例子在这里

参考资料