bigywater.tech
Rust · 122

FFI 调用 C 函数

Rust 调 C 函数需要什么?字符串怎么传?结构体怎么传?回调怎么写?声明写错会怎样?这篇用真实的 libc 和一个自己编译的 C 静态库一次验证清楚。

17 次
qsort 回调 Rust 比较函数的次数(7 个元素)
0 个警告
把 extern 声明写错(返回类型/参数个数)时 rustc 给出的警告
-11
少声明一个参数时程序的退出码(段错误)

调用 libc

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
      strlen("héllo") = 6(C 数的是字节:é 占 2 字节)  Rust 的 len() = 6
      abs(-42) = 42  toupper('a') = A
      getpid() == std::process::id(): true
      CString::new("a\0b") = Err("nul byte found in provided data at position: 1")(字符串中间不能有 \0)
      c"…" 字面量(以 \0 结尾的 &CStr):"字面量" 字节数 9

可空指针的 FFI 表示见Option<&T> 的空指针优化;repr(C) 见#[repr(C)] 与内存布局。

回调

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
      qsort 用 Rust 写的 extern "C" 比较函数降序排序:[9, 8, 7, 5, 3, 2, 1];回调被 C 调用了 17 次

链接自己的 C 库

实测 · 本机执行输出
  clang -c vec2.c → vec2.o(退出码 0);ar rcs libvec2.a(退出码 0)
  rustc 链接静态库: 成功
    vec2_add(按值传递、按值返回结构体) = Vec2 { x: 11.0, y: 22.0 }
    vec2_len2(&r)(按指针传递)= 605
    vec2_scale_all 原地修改 Rust 的数组:[Vec2 { x: 10.0, y: 10.0 }, Vec2 { x: 20.0, y: 30.0 }]
    fill_greeting 往 Rust 的缓冲区写了 12 字节:"hello from C"
    缓冲区太小(4 字节):C 返回 0(约定:0 表示放不下)
    sum_i32(Rust 数组) = 15

用 clang 与 ar 手工构建静态库,没有使用 cc crate 与 build.rs(见Build Script build.rs)。

声明写错

实测 · 本机执行输出
  right        编译通过(没有任何警告);运行退出码 0,输出 '6'
  ret_f64      编译通过(没有任何警告);运行退出码 0,输出 '0.0000000000…(很小的数,共 326 个字符)'
  no_n         编译通过(没有任何警告);运行退出码 -11,输出 ''
  n_wrong_ptr  编译通过(没有任何警告);运行退出码 0,输出 '288628513'
  编译器没有机会核对 C 头文件:声明与定义是否一致完全靠人;bindgen / cbindgen 就是用来自动生成声明的(本文没有使用)

输出里的垃圾值每次运行可能不同。

分步演示

FFI 调用 C
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。C 代码用系统 clang 编译
    • 没有验证:bindgen / cbindgen 自动生成、C 回调里 panic 穿过 FFI 边界(extern "C" 里的 panic 会 abort)、变长参数函数(printf)的调用、动态库的 link 与 rpath、C++ 的名字改编、跨平台 ABI 差异(Windows x64 等)
    京ICP备2026067502号-1京公网安备11010502063487号