< 返回版块

Mike Tang 发表于 2020-07-16 16:11

Tags:rust,ffi,

从前面的章节,我们可以看到,C与Rust/Rust与C的交互,核心就是指针的操作。两边的代码使用的是同一个程序栈,栈上的指针能放心地传递,而不用担心被错误释放的问题(栈上内存被调用规则自动管理,C和Rust中都是如此)。两边的代码可能使用不同的堆分配器,因此,堆上的指针的传递需要严格注意,需要各自管理各自的资源,谁创建谁释放。指针传递过程中,需要分析所有权问题。有了这种基本思维模型后,我们用 Rust 进行 FFI 编程,就会心中有数,知道什么时候该做什么,不再是一团浆糊了。

从本篇开始,我们进入新的领域:在 C 代码中调用 Rust 的功能。

我们先来看最简单的例子:C 中向 Rust 函数中,传入两个数,相加,并打印。

调用加法函数,并打印

Rust 代码:

// 在 Cargo.toml 中,加入如下两行

[lib]
crate-type = ["cdylib"]

要让 Rust 导出动态共享库,需要在 Cargo.toml 中这样设置的,必须。

// src/lib.rs

#[no_mangle]
pub extern "C" fn addtwo0(a: u32, b: u32) {
    let c = a + b;
    println!("print in rust, sum is: {}", c);
}

执行 cargo build 编译。会在 target/debug/ 下生成 lib{cratename}.so (我们这里为 librustffi3.so)这个动态链接库文件。

接下来看 C 代码:

#include <stdio.h>
#include <stdint.h>

extern void addtwo0(uint32_t, uint32_t);

int main(void) {
  addtwo0(1, 2);
}

编译:

gcc -o ./ccode01  ./csrc/ccode01.c -L ./  -lrustffi3

会在当前目录下生成 ccode01 二进制文件(我已把 librustffi3.so 文件拷贝至当前目录)。运行

LD_LIBRARY_PATH=. ./ccode01

输出下面结果:

print in rust, sum is: 3

在 C 中处理返回值

上个示例,Rust 中计算的值,并没有返回给 C 这边。我们看看怎么返回回来。修改上述示例如下。

Rust代码:

// src/lib.rs

#[no_mangle]
pub extern "C" fn addtwo1(a: u32, b: u32) -> u32 {
    let c = a + b;
    println!("print in rust, sum is: {}", c);
    c
}

C 代码:

#include <stdio.h>
#include <stdint.h>

extern uint32_t addtwo1(uint32_t, uint32_t);

int main(void) {
  uint32_t sum = addtwo1(10, 20);
  printf("print in c, sum is: %d\n", sum);
}

运行生成结果如下:

print in rust, sum is: 30
print in c, sum is: 30

可以看到,直接给 Rust 函数和 C 导入的函数签名添加返回值类型就可以了,两边的类型要保持一致。

C 向 Rust 传入一个数组计算元素的和并返回

前面两个例子是最简单的整型类型的参数传递,能说明 Rust 导出共享库的基本样板操作。但在函数参数这块儿,能说明的问题有限。下面,我们设计一个新的例子:C 向 Rust 传入一个数组计算元素的和并返回。

先来看 C 代码:

// csrc/ccode03.c

#include <stdio.h>
#include <stdint.h>

extern uint32_t sum_of_array(const uint32_t *numbers, size_t length);

int main(void) {
  uint32_t numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  unsigned int length = sizeof(numbers) / sizeof(*numbers);
  uint32_t sum = sum_of_array(numbers, length);
  printf("print in c, sum is: %d\n", sum);
}

配套的 Rust 代码:

// src/lib.rs

use std::slice;

#[no_mangle]
pub extern "C" fn sum_of_array(array: *const u32, len: usize) -> u32 {
    let array = unsafe {
        assert!(!array.is_null());

        slice::from_raw_parts(array, len)
    };

    array.iter().sum()
}

编译和运行代码:

编译 rust so:cargo build
编译 c binary:gcc -o ./ccode03 ./ccode03.c -L ./ -lrustffi3
运行:LD_LIBRARY_PATH=. ./ccode03

输出:

print in c, sum is: 55

分析代码后,可以看到。数组的传递,实际是剖分成两个要素传递:

  1. 数组的地址,或首元素指针(这两个本质是一样的),数组的指针的类型就是指向数组首元素的指针的类型;
  2. 数组长度。数组的长度不是数组所占字节的长度,而是元素个数。

可以看到,这个例子中,C 中的数组是分配在栈上的,并且在分配时直接初始化了。

Rust 代码中,参数中的 *const u32 就对应 C 中的 const uint32_t *

对于外界传入 Rust 的指针,Rust 这边,总是要先检查一下指针有效性的(确保不为空):

assert!(!array.is_null());

Rust 拿到 C 传递过来的指针后,标准的规范是:

  1. 尽早转换为 Rust 的安全类型进行操作。也就是说,保证不安全(unsafe块中的)的代码尽量少,并且直接使用这个指针的代码尽可能的少,转换成 Rust 中的标准类型再用。
  2. 尽量保证 zero cost。避免不必要的内存 copy 操作,影响性能。

为满足第一条规则,在转换前,我们的代码没有任何业务代码。

为满足第二条规则,这里使用了 slice 类型,而不是 Vec 类型:

    let array = unsafe {
        slice::from_raw_parts(array, len)
    };

注意 from_raw_parts() 操作是 unsafe 的,因此需要包在 unsafe {} 中执行。

总结

本篇,我们研究了 Rust 导出动态链接库给 C 用的基本形式和规范。下一篇,我们会探讨字符串作为函数参数和返回值传递的细节。

本文所有代码在这里找到:https://github.com/daogangtang/learn-rust/tree/master/10rustffi3

评论区

写评论
作者 Mike Tang 2020-07-17 11:58

空了我也去研究一下

gwy15 2020-07-16 23:12

写得很好,我前几天在 qt 里调用 rust 编译的动态库基本上也是这个流程。

有一个问题想请教楼主:如何在 Windows 下实现 c/c++ 调用 rust 的静态链接库?

g++ ... -Wl,--gc-sections -lpthread -ldl 可以在 linux 下正常链接,但是 windows 下不太行 :(

1 共 2 条评论, 1 页