1. 简介
“FFI"是” Foreign Function Interface"的缩写,大意为不同编程语言所写程序间的相互调用。鉴于C语言事实上是编程语言界的万国通,世界通用语。
对于其它语言(比如python/go),如果将 Rust 库的公共接口转换为应用程序二进制接口( C ABI),则在其它编程语言(比如python/go)中可以相对容易地使用它们。
2. rust导出clib库
其它语言调用 Rust 代码的一般模式或步骤:
- 针对 Rust 代码中需要公开的 API,为其编写对应的 C API;
- 通过cbindgen 工具生成 C API 的头文件或手动添加 C API 函数定义;
- 在其它语言中,使用其支持调用 C API 的 FFI 模块或库,完成对 Rust 代码的调用。
在其它语言中调用 Rust 导出库时,关键是处理好 Rust 中的常见数据类型,包括数值,字符串,数组,结构体等。
2.1 整数与字符串
整数在 Rust,C 中都有对应的转换,通常很容易通过 FFI 边界。
字符串则比较复杂,Rust 中的字符串,是一组 u8 组成的 UTF-8 编码的字节序列,字符串内部允许 NUL 字节;但在 C 中,字符串只是指向一个 char 的指针,用一个 NUL 字节作为终止。
我们需要做一些特殊的转换,在 Rust FFI 中使用 std::ffi::CStr,它表示一个 NUL 字节作为终止的字节数组,可以通过 UTF-8 验证转换成 Rust 中的 &str。
#[no_mangle]
pub extern "C" fn count_char(s: *const c_char) -> c_uint {
let c_str = unsafe {
assert!(!s.is_null());
CStr::from_ptr(s)
};
let r_str = c_str.to_str().unwrap();
r_str.chars().count() as u32
}
2.2 数组
在 Rust 和 C 中,数组均表示相同类型元素的集合,但在 C 中,其不会对数组执行边界检查,而 Rust 会在运行时检查数组边界。同时在 Rust 中有切片的概念,它包含一个指针和一组元素的数据。
在 Rust FFI 中使用 from_raw_parts 将指针和长度,转换为一个 Rust 中的切片。
#[no_mangle]
pub extern "C" fn sum_of_even(ptr: *const c_int, len: size_t) -> c_int {
let slice = unsafe {
assert!(!ptr.is_null());
slice::from_raw_parts(ptr, len as usize)
};
let sum = slice.iter()
.filter(|&&num| num % 2 == 0)
.fold(0, |sum, &num| sum + num);
sum as c_int
}
3. go 调用 c
Golang 自带的 CGO 可以支持与 C 语言接口的互通。
在 golang 代码中加入 import “C” 语句就可以启动 CGO 特性。这样在进行 go build 命令时,就会在编译和连接阶段启动 gcc 编译器。
3.1 简单示例
将 C 代码进行抽象,放到相同目录下的 C 语言源文件 hello.c 中
// demo/hello.c
#include <stdio.h>
int SayHello() {
puts("Hello World");
return 0;
}
在 Go 代码中,声明 SayHello() 函数,再引用 hello.c 源文件,就可以调起外部 C 源文件中的函数了。同理也可以将C 源码编译打包为静态库或动态库进行使用。
// demo/test5.go
package main
/*
#include "hello.c"
int SayHello();
*/
import "C"
import (
"fmt"
)
func main() {
ret := C.SayHello()
fmt.Println(ret)
}
3.2 字符串传递
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// static void myprint(char* s) {
// printf("%s\n", s);
// }
import "C"
import "unsafe"
func main() {
cs := C.CString("Hello from stdio")
C.myprint(cs)
C.free(unsafe.Pointer(cs)) // yoko注,去除这行将发生内存泄漏
}
从上面例子可以看到,Go 代码中的cs变量在传递给 c 代码使用完成之后,需要调用C.free进行释放。
3.3 其它类型转换
另外值得说明的是,以下几种类型转换,都会发生内存拷贝
// Go string to C string
func C.CString(string) *C.char {}
// Go []byte slice to C array
// 这个和C.CString一样,也需要手动释放申请的内存
func C.CBytes([]byte) unsafe.Pointer {}
// C string to Go string
func C.GoString(*C.char) string {}
// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string {}
// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte {}