Rust FFI 编程--理解不同语言的数据类型转换

1. 简介

“FFI"是” Foreign Function Interface"的缩写,大意为不同编程语言所写程序间的相互调用。鉴于C语言事实上是编程语言界的万国通,世界通用语。

对于其它语言(比如python/go),如果将 Rust 库的公共接口转换为应用程序二进制接口( C ABI),则在其它编程语言(比如python/go)中可以相对容易地使用它们。

2. rust导出clib库

其它语言调用 Rust 代码的一般模式或步骤:

  1. 针对 Rust 代码中需要公开的 API,为其编写对应的 C API;
  2. 通过cbindgen 工具生成 C API 的头文件或手动添加 C API 函数定义;
  3. 在其它语言中,使用其支持调用 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 {}

参考

Rust FFI 编程 - 其它语言调用 Rust 代码 - Python

Go 与 C 的桥梁:cgo 入门,剖析与实践

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值