4.8.外部语言接口
最后更新于:2022-04-01 00:43:18
## 介绍
本教程会使用[snappy](https://github.com/google/snappy)压缩/解压缩库来作为一个Rust编写外部语言代码绑定的介绍。目前Rust还不能直接调用C++库,不过snappy库包含一个C接口(记录在[snappy-c.h](https://github.com/google/snappy/blob/master/snappy-c.h)中)。
下面是一个最简单的调用其它语言函数的例子,如果你安装了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库来解析符号。
外部函数被假定为不安全的所以调用它们需要包装在`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需要需要封装才能提供内存安全性和利用像向量这样的高级内容。一个库可以选择只暴露出安全的,高级的接口并隐藏不安全的底层细节。
包装用到了缓冲区的函数涉及使用`slice::raw`模块来将Rust向量作为内存指针来操作。Rust的向量确保是一个连续的内存块。它的长度是当前包含的元素个数,而容量则是分配内存的大小。长度小于或等于容量。
~~~
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`函数可以用来分配一个所需最大容量的向量来存放压缩的输出。接着这个向量可以作为一个输出参数传递给`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
}
}
}
~~~
作为一个参考,我们在这里使用的例子可以在[GitHub的这个库](https://github.com/thestinger/rust-snappy)中找到。
## 析构函数
外部库经常把资源的所有权传递给调用函数。当这发生时,我们必须使用Rust析构函数累提供安全性和确保释放了这些资源(特别是在恐慌的时候)。
## 在Rust函数中处理C回调(Callbacks from C code to Rust functions)
一些外部库要求使用回调来向调用者反馈它们的当前状态或者即时数据。可以传递在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(); // Triggers the 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对象上使用回调(Targeting callbacks to Rust objects)
之前的例子展示了一个全局函数是如何在C代码中被调用的。然而我们经常希望回调是针对一个特殊Rust对象的。这个对象可能代表对应C语言中的封装。
这可以通过向C库传递这个对象的不安全指针来做到。C库则可以根据这个这个通知中的指针来取得Rust对象。这允许回调不安全的访问被引用的Rust对象。
Rust代码:
~~~
#[repr(C)]
struct RustObject {
a: i32,
// other members
}
extern "C" fn callback(target: *mut RustObject, a: i32) {
println!("I'm called from C with value {0}", a);
unsafe {
// Update the value in RustObject with the value received from the callback
(*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() {
// Create the object that will be referenced in the callback
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); // Will call callback(&rustObject, 7) in Rust
}
~~~
## 异步回调
在之前给出的例子中回调在一个外部C库的函数调用后直接就执行了。在回调的执行过程中当前线程控制权从Rust传到了C又传到了Rust,不过最终回调和和触发它的函数都在一个线程中执行。
当外部库生成了自己的线程并触发回调时情况就变得复杂了。在这种情况下回调中对Rust数据结构的访问时特别不安全的并必须有合适的同步机制。除了想互斥量这种经典同步机制外,另一种可能就是使用通道(在`std::comm`中)来从触发回调的C线程转发数据到Rust线程。如果一个异步回调指定了一个在Rust地址空间的特殊Rust对象,那么在确保在对应Rust对象被销毁后不会再有回调被C库触发就格外重要了。这一点可以通过在对象的析构函数中注销回调和设计库使其确保在回调被注销后不会再被触发来取得。
## 链接
在`extern`上的`link`属性提供了基本的构建块来指示`rustc`如何连接到原生库。现在有两种被接受的链接属性形式:
* `#[link(name = "foo")]`
* `#[link(name = "foo", kind = "bar")]`
在这两种形式中,`foo`是我们链接的原生库的名字,而在第二个形式中`bar`是编译器要链接的原生库的类型。目前有3种已知的原生库类型:
* 动态 - `#[link(name = "readline")]`
* 静态 - `#[link(name = "my_build_dependency", kind = "static")]`
* 框架 - `#[link(name = "CoreFoundation", kind = "framework")]`
注意框架只支持OSX平台。
不同`kind`的值意味着链接过程中不同原生库的参与方式。从链接的角度看,rust编译器创建了两种组件:部分的(rlib/staticlib)和最终的(dylib/binary)。原生动态库和框架会从扩展到最终组件部分,而静态库则完全不会扩展。
一些关于这些模型如何使用的例子:
* 一个原生构建依赖。有时编写部分Rust代码时需要一些C/C++代码,另外使用发行为库格式的C/C++代码只是一个负担。在这种情况下,代码会被归档为`libfoo.a`然后rust包装箱可以通过`#[link(name = "foo", kind = "static")]`声明一个依赖。
不管包装箱输出为何种形式,原生静态库将会包含在输出中,这意味着分配一个原生静态库是没有必要的。
* 一个正常动态库依赖。通用系统库(像`readline`)在大量系统上可用,通常你找不到这类库的静态拷贝。当这种依赖被添加到包装箱里时,部分目标(比如rlibs)将不会链接这些库,但是当rlib被包含进最终目标(比如二进制文件)时,原生库将被链接。
在OSX上,框架与动态库有相同的语义。
## 不安全块
一些操作,像解引用不安全的指针或者被标记为不安全的函数只允许在unsafe块中使用。unsafe块隔离的不安全性并向编译器保证不安全代码不会泄露到块之外。
不安全函数,另一方面,将它公布于众。一个不安全的函数这样写:
~~~
unsafe fn kaboom(ptr: *const int) -> int { *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.",
rl_readline_version as int);
}
~~~
另外,你可能想修改外部结接口提供的全局状态。为了做到这一点,声明为`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的调用约定来调用外部函数。一些外部函数,尤其是大部分Windows API,使用其它的调用约定。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`
* `aapcs`
* `cdecl`
* `fastcall`
* `Rust`
* `rust-intrinsic`
* `system`
* `C`
* `win64`
列表中大部分ABI都是自解释的,不过`system`ABI可能看起来有点奇怪。这个约束会选择任何能和目标库正确交互的ABI。例如,在x86构架的win32,这意味着会使用`stdcall`ABI。在x86_64上,然而,windows使用`C`调用约定,所以`C`会被使用。这意味在我们之前的例子中,我们可以使用`extern "system" { ... }`定义一个适用于所有windows系统的块,而不仅仅是x86系统。
## 外部代码交互性(Interoperability with foreign code)
只有当`#[repr(C)]`属性被用于结构体时Rust能确保`struct`的布局兼容平台的C的表现。`#[repr(C, packed)]`可以用来不对齐的排列结构体成员。`#[repr(C)]`也可以被用于一个枚举。
Rust拥有的装箱(`Box`)使用非空指针作为指向他包含的对象的句柄。然而,它们不应该手动创建因为它们由内部分分配器托管。引用可以被安全的假设为直接指向数据的非空指针。然而,打破借用检查和可变性规则并不能保证安全,所以倾向于只在需要时使用裸指针(`*`)因为编译器不能为它们做更多假设。
向量和字符串共享同样基础的内存布局,`vec`和`str`模块中可用的功能可以操作C API。然而,字符串不是`\0`结尾的。如果你需要一个NUL结尾的字符串来与C交互,你需要使用`std::ffi`模块中的`CString`类型。
标准库中的`libc`模块包含类型别名和C标准库中的函数定义,Rust默认链接`libc`和`libm`。
## “可空指针优化”(The "nullable pointer optimization")
特定类型被定义为不为`nulll`。这包括引用(`&T`,`&mut T`),装箱(`Box`),和函数指针(`extern "abi" fn()`)。当使用C接口时,可能为空的指针经常被使用。作为一个特殊的例子,一个泛化的`enum`包含两个变体,其中一个没有数据,而另一个包含一个单独的字段,非常适合“可空指针优化”。当这么一个枚举被用一个非空指针类型实例化时,它表现为一个指针,而无数据的变体表现为一个空指针。那么`Option c_int>`可以用来表现一个C ABI中的可空函数指针。
## 在C中调用Rust代码
你可能会希望这么编译Rust代码以便可以在C中调用。这是很简单的,不过需要一些东西:
~~~
#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
"Hello, world!\0".as_ptr()
}
~~~
`extern`使这个函数遵循C调用约定,就像之前讨论[外部调用约定](http://kaisery.gitbooks.io/rust-book-chinese/content/content/4.8.Foreign%20Function%20Interface%20%E5%A4%96%E9%83%A8%E8%AF%AD%E8%A8%80%E6%8E%A5%E5%8F%A3.md)时一样。`no_mangle`属性关闭Rust的命名改编,这样它更容易链接。