11.0. FFI
原文链接:doc.rust-lang.org/nomicon/ffi.html
外部函数接口 (FFI)#
介绍#
这个教程会使用 snappy 压缩 / 解压缩库来介绍外部代码绑定的编写方法。Rust 目前还不能直接调用 C++ 的库,但是 snappy 有 C 的接口(文档在 snappy-c.h
中)。
关于 libc 的说明#
接下来很多的例子会使用 libc
crate,它为我们提供了很多 C 类型的定义。如果你要亲自尝试一下这些例子的话,你需要把 libc
添加到你的 Cargo.toml
:
[dependencies]
libc = "0.2.0"
然后在你的 crate 的根文件插入一句 extern crate libc;
调用外部函数#
下面是一个调用外部函数的小例子,安装了 snappy 才能编译成功。
extern crate libc;
use libc::size_t;
#[link(name = "snappy")]
extern {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("max compressed length of a 100 byte buffer: {}", x);
}
extern
代码块中是外部库的函数签名的列表,这个例子中使用的是平台相关的 C 的 ABI。#[link(...)]
属性用来构建一个链接 snappy 库的链接器,以便解析库中的符号 (symbol)。
外部函数都被认为是不安全的,所以对它们的调用必须包装在 unsafe {}
中,也就是向编译器承诺块中的代码都是安全的。C 的库经常暴露非线程安全的接口,而且几乎所有的接受指针参数的函数都是不合法的,因为指针可能是悬垂指针,而裸指针不符合 Rust 的内存安全模型。
在声明外部函数的参数类型时,Rust 编译器不能检查声明的正确性,所以我们需要自己保证它是正确的,这也是运行期正确绑定的条件之一。
extern
块还可以继续扩展,包含所有的 snappy API:
extern crate libc;
use libc::{c_int, size_t};
#[link(name = "snappy")]
extern {
fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t) -> c_int;
fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t) -> c_int;
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t) -> c_int;
fn snappy_validate_compressed_buffer(compressed: *const u8,
compressed_length: size_t) -> c_int;
}
创建安全接口#
原生的 C API 进行封装,以保证内存安全,还有使用 vector 等高级概念。库可以选择只暴露安全的、高级的接口,并隐藏非安全的内部细节。
我们使用 slice::raw
模块封装接受内存块的函数,这个模块会把 Rust 的 vector 转换为内存的指针。Rust 的 vector 是一块连续的内存。它的长度是当前包含的元素的数量,容量是分配内存可存储的元素的总数。长度是小于等于容量的。
pub fn validate_compressed_buffer(src: &[u8]) -> bool {
unsafe {
snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
}
}
上方的 validate_compressed_buffer
包装器用到了 unsafe
代码块,但是函数签名里没有 unsafe
关键字,这说明它保证函数调用对所有的输入都是安全的。
snappy_compress
和 snappy_uncompress
函数更复杂一些,因为它们需要分配一块空间储存输出的结果。
snappy_max_compressed_length
函数可以用来分配一段最大容积内的 vector,以保存输出的结果。这个 vector 可以传递给 snappy_compress
函数作为输出参数。还会传递一个输出参数获取压缩后的真实长度,以便设置返回值的长度。
pub fn compress(src: &[u8]) -> Vec<u8> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen = snappy_max_compressed_length(srclen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
snappy_compress(psrc, srclen, pdst, &mut dstlen);
dst.set_len(dstlen as usize);
dst
}
}
解压缩也是类似的,因为 snappy 的压缩格式中保存了未压缩时的大小,函数 snappy_uncompressed_length
可以获取需要的缓存区的尺寸。
pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen: size_t = 0;
snappy_uncompressed_length(psrc, srclen, &mut dstlen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {
dst.set_len(dstlen as usize);
Some(dst)
} else {
None // SNAPPY_INVALID_INPUT
}
}
}
接下来,我们添加一些测试用例来展示如何使用它们。
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid() {
let d = vec![0xde, 0xad, 0xd0, 0x0d];
let c: &[u8] = &compress(&d);
assert!(validate_compressed_buffer(c));
assert!(uncompress(c) == Some(d));
}
#[test]
fn invalid() {
let d = vec![0, 0, 0, 0];
assert!(!validate_compressed_buffer(&d));
assert!(uncompress(&d).is_none());
}
#[test]
fn empty() {
let d = vec![];
assert!(!validate_compressed_buffer(&d));
assert!(uncompress(&d).is_none());
let c = compress(&d);
assert!(validate_compressed_buffer(&c));
assert!(uncompress(&c) == Some(d));
}
}
析构函数#
外部库经常把资源的所有权返还给调用代码。如果是这样,我们必须用 Rust 的析构函数保证所有的资源都被释放了(特别是在 panic 的情况下)。
更多关于析构函数的内容,请见 Drop trait。
C 代码到 Rust 函数的回调#
一些外部库需要用到回调向调用者报告当前状态或者中间数据。我们是可以把 Rust 写的函数传递给外部库的。要求是回调函数必须标为 extern
并遵守正确的调用规范,以保证 C 代码可以调用它。
然后回调函数会通过注册调用传递给 C 的库,并在外部库中被触发。
下面是一个简单的例子。
Rust 代码:
extern fn callback(a: i32) {
println!("I'm called from C with value {0}", a);
}
#[link(name = "extlib")]
extern {
fn register_callback(cb: extern fn(i32)) -> i32;
fn trigger_callback();
}
fn main() {
unsafe {
register_callback(callback);
trigger_callback(); // 触发回调
}
}
C 代码:
typedef void (*rust_callback)(int32_t);
rust_callback cb;
int32_t register_callback(rust_callback callback) {
cb = callback;
return 1;
}
void trigger_callback() {
cb(7); // Will call callback(7) in Rust.
}
这个例子中,Rust 的 main()
要调用 C 的 trigger_callback()
,而这个函数会反过来调用 Rust 中的 callback()
。
将 Rust 对象作为回调#
之前的例子演示了 C 代码如何调用全局函数。但是很多情况下回调也可能是一个 Rust 对象,比如说封装了某个 C 的结构体的 Rust 对象。
要实现这一点,我们可以传递一个指向这个对象的裸指针给 C 的库。C 的库接下来可以将指针转换为 Rust 的对象。这样回调函数就可以非安全地访问相应的 Rust 对象了。
#[repr(C)]
struct RustObject {
a: i32,
// 其他成员……
}
extern "C" fn callback(target: *mut RustObject, a: i32) {
println!("I'm called from C with value {0}", a);
unsafe {
// 用回调函数接收的值更新RustObject的值:
(*target).a = a;
}
}
#[link(name = "extlib")]
extern {
fn register_callback(target: *mut RustObject,
cb: extern fn(*mut RustObject, i32)) -> i32;
fn trigger_callback();
}
fn main() {
// 创建回调用到的对象:
let mut rust_object = Box::new(RustObject { a: 5 });
unsafe {
register_callback(&mut *rust_object, callback);
trigger_callback();
}
}
C 代码:
typedef void (*rust_callback)(void*, int32_t);
void* cb_target;
rust_callback cb;
int32_t register_callback(void* callback_target, rust_callback callback) {
cb_target = callback_target;
cb = callback;
return 1;
}
void trigger_callback() {
cb(cb_target, 7); // 调用Rust的callback(&rustObject, 7)
}
异步回调#
上面给出的例子里,回调都是外部 C 库的直接的函数调用。当前线程的控制权从 Rust 转移到 C 再转移回 Rust,不过最终回调都是在调用触发回调的函数的线程里执行的。
如果外部库启动了自己的线程,并在那个线程里调用回调函数,情况就变得复杂了。这时再访问回调中的 Rust 数据结构是非常不安全的,必须使用正常地同步机制。除了 Mutex 等传统的同步机制,还有另一个选项就是使用 channel(在 std:
中)将数据从触发回调的 C 线程传送给一个 Rust 线程。:mpsc
如果一个异步回调使用了一个 Rust 地址空间里的对象,一定要注意,在这个对象销毁之后 C 的库不能再调用任何的回调。我们可以在对象的析构函数里注销回调,并且重新设计库确保毁掉注销后就不会被调用了。
链接#
extern
代码块上的 link
属性用于指导 rustc 如何链接到一个本地的库。现在 link
属性有两种可用的形式:
#[link(name = "foo")]
#[link(name = "foo", kind = "bar")]
两种形式中,foo
都是我们要链接的本地库的名字。而第二种形式中的 bar
是要链接的本地库的类型。目前有三种已知的本地库类型:
- 动态 -
#[link(name = "readline")]
- 静态 -
#[link(name = "my_build_dependency", kind = "static")]
- 框架 -
#[link(name = "CoreFundation", kind = "framework")]
注意,框架只适用于 MacOS 平台。
不同的 kind
表明本地库以不同的方式参与链接。从链接器的角度看,Rust 编译器产生两种输出结果:部分结果 (rlib/staticlib) 和最终结果 (dylib/binary)。本地动态库和框架依赖可以被最终结果使用,而静态库则不会,因为静态库是直接集成在接下来的输出里的。
举几个这个模型用法的例子:
本地构建依赖。有时候编写 Rust 代码需要一些 C/C++ 作为补充,但是把 C/C++ 代码以一个库的形式发布却不容易。这种情况下,代码应该包装在
libfoo.a
中,然后 Rust 的 crate 会声明一个依赖#[link(name = "foo", kind = "static")]
。
不管 crate 最终以哪种形式输出,本地静态库都会被包含在输出中,这表明发布静态库并不必要。普通动态库。通用的系统库(比如
readline
)在许多系统中都支持,而我们经常遇到找不到库的本地备份的的情况。如果这样的依赖被包含在 Rust 的 crate 中,部分结果(比如 rlib)不会链接到这个库中。但是如果 rlib 被最终结果包含了,本地库也会被链接。
在 MacOS 中,框架和动态库具有相同的语义。
非安全代码块#
有一些操作,比如解引用裸指针、或者调用被标为 unsafe 的函数,它们只能存在于非安全代码块中。非安全代码块隔离了非安全性,并向编译器承诺非安全性不会影响到块以外的代码。
非安全函数则不同,它们声明非安全性一定会影响到函数之外。一个非安全函数写法如下:
unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }
这个函数只能在 unsafe
代码块或者另外一个 unsafe
函数里被调用。
访问外部全局变量#
外部 API 经常暴露一些全局变量,用于记录全局状态等。为了访问这些变量,你需要在 extern
块中用 static
关键字声明它们:
extern crate libc;
#[link(name = "readline")]
extern {
static rl_readline_version: libc::c_int;
}
fn main() {
println!("You have readline version {} installed.",
unsafe { rl_readline_version as i32 });
}
有时也可能需要通过外部的接口修改全局状态。如果要这么做,静态变量还要添加 mut
,让我们可以修改它们。
extern crate libc;
use std::ffi::CString;
use std::ptr;
#[link(name = "readline")]
extern {
static mut rl_prompt: *const libc::c_char;
}
fn main() {
let prompt = CString::new("[my-awesome-shell] $").unwrap();
unsafe {
rl_prompt = prompt.as_ptr();
println!("{:?}", rl_prompt);
rl_prompt = ptr::null();
}
}
注意,所有和 static mut
的操作都是非安全的,不管是读还是写。处理全局可变状态的时候一定要格外的小心。
外部调用规范#
大多数外部代码都暴露 C 的 ABI,而 Rust 默认根据平台相关的 C 的调用规范调用外部函数。还有一些外部函数使用其他的规范,最典型的就是 WindowsAPI。Rust 也有方法告诉编译器使用哪种规范:
extern crate libc;
#[cfg(all(target_os = "win32", target_arch = "x86"))]
#[link(name = "kernel32")]
#[allow(non_snake_case)]
extern "stdcall" {
fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> libc::c_int;
}
这段代码作用于整个 extern
代码块。支持的 ABI 包括:
stdcall
appcs
cdecl
fastcall
vectorcall
这个目前被abi_vectorcall
隐藏着,不允许修改。Rust
rust-intrinsic
system
C
win64
sysv64
列表中所有的 abi 都是自解释的,但是 system
可能会显得有些奇怪。它的意思是选择一个合适的与目标库通信的 ABI。比如,在 win32 的 x86 架构上,它实际使用的是 stdcall
。而在 x86_64 上,Windows 使用 C
调用规范,所以它实际使用的是 C
。这意味着在我们之前的例子中,我们可以使用 extern "system" { ... }
为所有的 Windows 系统定义块,而不仅仅是 x86 的平台。
与外部代码互用性#
只有给一个结构体指定了#[repr(C)]
,Rust 才保证结构体的布局与平台的 C 的表示方法相兼容。#[repr(C, packed)]
可以让结构体成员之间无填充。#[repr(C)]
也可以作用于枚举类型。
Rust 的 Box<T>
用一个非空的指针指向它包含的对象。但是,这些指针不能手工创建,而是要由内部分配器去管理。引用可以安全地等同于非空指针。不过,违背借用检查和可变性规则就不能保证是安全的了,所以在需要使用指针的地方我们尽量使用裸指针,因为编译器不会对它做过多的限制。
Vector 和 String 拥有相同的内存布局,而且 vec
和 str
模块里也有一些与 C API 相关的工具。但是,字符串不是以 \0
结尾的。如果你想要一个与 C 兼容的 Null 结尾的字符串,你应该使用 std::ffi
模块中的 CString
类型。
[crate.io 的 libc
crate](https://crates.io/crates/libc)在
libc 模块中包含了C标准库的类型别名和函数定义,而Rust默认链接
libc 和
libm`。
可变函数#
在 C 中,函数可以是 “可变的”,也就是说可以接收可变数量的参数。在 Rust 中可以在外部函数声明的参数类表中插入...
实现这一点:
extern {
fn foo(x: i32, ...);
}
fn main() {
unsafe {
foo(10, 20, 30, 40, 50);
}
}
普通的 Rust 函数不能是可变的:
// 这段不能通过编译
fn foo(x: i32, ...) { }
空指针优化#
一些 Rust 类型被定义为永不为 null
,包括引用(&T
、&mut T
)、Box<T>
、以及函数指针(extern "abi" fn()
)。可是在使用 C 的接口时,指针是经常可能为 null
的。看起来似乎需要用到 transmute
或者非安全代码来处理各种混乱的类型转换。但是,Rust 其实提供了另外的方法。
一些特殊情况中,enum
很适合做空指针优化,只要它包含两个变量,其中一个不包含数据,而另外一个包含一个非空类型的成员。这样就不需要额外的空间做判断了:给那个包含非空成员的变量传递一个 null
,用它来表示另外那个空的变量。这种行为虽然被叫做 “优化”,但是和其他的优化不同,它只适用于合适的类型。
最常见的受益于空指针优化的类型是 Option<T>
,其中 None
可以用 null
表示。所以 Option<extern "C" fn(c_int) - > c_int>
就很适合表示一个使用 C ABI 的可为空的函数指针(对应于 C 的 int (*)(int)
)。
下面是一个刻意造出来的例子。假设一些 C 的库提供了注册回调的方法,然后在特定的条件下调用回调。回调接受一个函数指针和一个整数,然后用这个整数作为参数调用指针指向的函数。所以我们会向 FFI 边界的两侧都传递函数指针。
extern crate libc;
use libc::c_int;
extern "C" {
// 注册回调。
fn register(cb: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>, c_int) -> c_int>);
}
// 这个函数其实没什么实际的用处。它从C代码接受一个函数指针和一个整数,
// 用整数做参数调用指针指向的函数,并返回函数的返回值。
// 如果没有指定函数,那默认就返回整数的平方。
extern "C" fn apply(process: Option<extern "C" fn(c_int) -> c_int>, int: c_int) -> c_int {
match process {
Some(f) => f(int),
None => int * int
}
}
fn main() {
unsafe {
register(Some(apply));
}
}
C 的代码是像这样的:
void register(void (*f)(void (*)(int), int)) {
...
}
看,并不需要 transmute
!
C 调用 Rust#
你可能想要用某种方式编译 Rust,让 C 可以直接调用它。这件事很简单,只需要做少数的处理:
#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
"Hello, world!\0".as_ptr()
}
extern
让它对应的函数符合 C 的调用规范,在上面的外部调用规范一节有详细讨论。no_mangle
属性关闭 Rust 的 name mangling,让它更方便被链接。
FFI 和 panic#
使用 FFI 的时候要格外注意 panic!
。跨越 FFI 边界的 panic!
属于未定义行为。如果你写的代码可能会 panic,你应该使用 catch_unwind
在一个闭包里执行它:
use std::panic::catch_unwind;
#[no_mangle]
pub extern fn oh_no() -> i32 {
let result = catch_unwind(|| {
panic!("Oops!");
});
match result {
Ok(_) => 0,
Err(_) => 1,
}
}
fn main() {}
请注意,catch_unwind
只能捕获可展开的 panic,不能捕获 abort。更多的信息请参考 catch_unwind
的文档。
表示不透明结构体#
有时候,C 的库要提供一个指针指向某个东西,但又不想让你知道那个东西的内部细节。最简单的方式是使用 void *
:
void foo(void *arg);
void bar(void *arg);
在 Rust 中我们可以用 c_void
类型表示它:
extern crate libc;
extern "C" {
pub fn foo(arg: *mut libc::c_void);
pub fn bar(arg: *mut libc::c_void);
}
这是一个完全合法的方法。不过,我们其实还可以做得更好。要解决这个问题,一些 C 库可能会创建一个结构体,可结构体的细节和内存布局是私有的。这样提高了类型的安全性。这种结构体被称为” 不透明 “的。下面是一个 C 的例子:
struct Foo; /* Foo是一个接口,但它的内容不属于公共接口 */
struct Bar;
void foo(struct Foo *arg);
void bar(struct Bar *arg);
在 Rust 中,我们可以使用枚举来创建我们自己的不透明类型:
#[repr(C)] pub struct Foo { _private: [u8; 0] }
#[repr(C)] pub struct Bar { _private: [u8; 0] }
extern "C" {
pub fn foo(arg: *mut Foo);
pub fn bar(arg: *mut Bar);
}
# fn main() {}
给结构体一个私有成员而不给它构造函数,这样我们就创建了一个不透明的类型,而且我们不能在模块之外实例化它。(没有成员的结构体可以在任何地方实例化)因为我们希望在 FFI 中使用这个类型,我们必须加上#[repr(C)]
。还为了避免在 FFI 中使用 ()
的时候出现警告,我们用了一个空数组。空数组和空类型的行为一致,同时它还是 FFI 兼容的。
但因为 Foo
和 Bar
是不同的类型,我们需要保证两者之间的类型安全性,所以我们不能把 Foo
的指针传递给 bar()
。
注意,用空枚举作为 FFI 类型是一个很不好的设计。编译器将空枚举视为不可达的空类型,所以使用 &Empty
类型的值是很危险的,这可能导致很多程序中的问题(触发未定义行为)。