这次,我们来关注一下Rust语言的基本特性到C的映射。
我们已经了解了,Rust语言是多泛式(混合泛式)的语言,它可以做命令式(过程式)编程,也可以做面向对象编程,也可以做函数式编程。把Rust简单地归类为某种泛式的编程语言,都不太合适。Rust就是Rust。
C语言是比较传统的过程式编程语言,因此,从Rust到C的转换,就会有一些无法直接对标的东西。于是,做这种映射工作就需要一些额外的规范或约定。
本文我们来关注:
- 结构体的方法的处理
- 泛型的处理
- Type alias
- Enum 到 C 的映射
结构体的方法的处理
我们知道,Rust中,可以对结构体(或 enum 等)添加方法。这是属于面向对象的特性,而纯C是不支持这种特性的。于是,我们必须将这些方法单独实现为一批函数,在这批函数名前面加上统一的前缀,看下面代码:
rust 代码
// rust
#[repr(C)]
struct Foo {
a: isize,
b: isize
}
impl Foo {
pub fn method1() {
...
}
pub fn method2(x: isize) -> isize {
...
}
pub fn method3(x: isize, y: isize) -> isize {
...
}
}
这段代码翻译成C的时候,对应的大概会是下面这个样子:
struct Foo {
int a;
int b;
}
void foo_method1(Foo* foo);
int foo_method2(Foo* foo, int x);
int foo_method1(Foo* foo, int x, int y);
然而,这种映射是不能自动转换的(毕竟只是我们自己的约定),需要手动写出来。于是我们需要实现接口层的Rust代码:
// We have struct Foo now
#[no_mangle]
unsafe extern "C" fn foo_method1(foo: *const Foo) {
let foo = &*foo;
foo.method1();
}
#[no_mangle]
unsafe extern "C" fn foo_method2(foo: *const Foo, x: isize) -> isize {
let foo = &*foo;
foo.method2(x)
}
#[no_mangle]
unsafe extern "C" fn foo_method3(foo: *const Foo, x: isize, y: isize) -> isize {
let foo = &*foo;
foo.method3(x, y)
}
然后,用这个接口层代码编译出动态链接库,C那边使用就行了。
泛型的处理
泛型的处理稍微复杂一些。但实际原理也不难。在Rust中,泛型,我们指的是静态分派,另外还有一种,使用 trait object,实现动态分派。在这里,我们专注于静态分派的分析。
静态分派的意思是,编译器在编译时,根据你对泛型的具体化类型,进行特化展开处理。具体类型有几种,就复制几份不同的特化实现(因此增大了代码量)。这样,在调用时,就直接调用的特化后的函数/方法,而不再需要指针跳转一次了。所以,静态分派相对于动态分派,实际是用空间换时间,效率要高一些。
因此,我们在向C导出含泛型的方法时,也用静态分派的思维实现一个接口层就行了。
下面来看实际代码。比如,我们现在有如下Rust结构体:
#[repr(C)]
struct Buffer<T> {
data: [T; 8],
len: usize,
}
并且实现了方法:
impl<T> Buffer<T> {
pub fn print(&self) {
...
}
}
假如我们在实际中,用到了 i32 和 f32 两种类型。那么,我们实现 FFI 层的时候,需要这样写:
#[no_mangle]
extern "C" fn buffer_print_i32(buf: Buffer<i32>) { ... }
#[no_mangle]
extern "C" fn buffer_print_f32(buf: Buffer<f32>) { ... }
然后,对应的 C 这边的代码就是类似下面的:
struct Buffer_i32 {
int32_t data[8];
size_t len;
};
struct Buffer_f32 {
float data[8];
size_t len;
};
void buffer_print_i32(Buffer_i32 buf);
void buffer_print_f32(Buffer_f32 buf);
可见,我们在 FFI 的 rust 方面,把方法名具体化了。在 C 这边,除了具体化的方法名,还把类型具体化了。就这样,适应了 C 这边无泛型的困扰。
细节的读者可能会发现,如果有M个方法,N种类型,最后分出来的函数有:M x N 个。
Type alias
Type alias 在 Rust 中,就使用 type
关键字,正好在 C 中,有 typedef 这个关键字,起到类似的功能。
比如,在 Rust 这边,有如下代码:
// type.rs
#[repr(C)]
struct Buffer<T> {
data: [T; 8],
len: usize,
}
type IntBuffer = Buffer<i32>;
#[no_mangle]
extern "C" fn buffer_print_int(buf: IntBuffer) { }
对应的 C 代码,会类似下面这个样子:
struct Buffer_i32 {
int32_t data[8];
size_t len;
};
typedef Buffer_i32 IntBuffer;
void buffer_print_int(IntBuffer buf);
Type Alias 能让两边的类型名,看起来更一致。
枚举到 C 的映射
Rust 中,枚举分三大类:空枚举(Empty Enum),无字段枚举(Fieldless Enum)和带负载枚举(Data-carrying enum) 。
空枚举指的是:enum Foo;
这种形式。空枚举没有变体,是一个空类型,等于 !
。
无字段枚举,就是我们通常所说的 C-like 枚举。它的变体中不带有额外数据/字段。
enum SomeEnum {
A,
B,
C,
}
enum SomeEnum {
Variant22 = 22,
Variant44 = 44,
Variant45,
}
带负载枚举是 Rust 的特色,就是变体中还带数据负载的枚举,类似下面这种:
enum Foo {
Bar(String),
Baz,
}
既然此处我们是要研究与C的对应关系,其实真正Rust要导出共享库给C使用的场景,涉及到的枚举(基本)都是 Fieldless Enum。所以我们这里只限于说明 Fieldless Enum 到 C 枚举布局上的一些细节。
Rust 的枚举上,可以标注其内存布局,像下面这样:
#[repr(C)]
enum SomeEnum {
A,
B,
C,
}
Rust 的枚举可以标注的布局种类有如下一些:
指定int位数布局
- #[repr(u8)] 每个变体占用一个字节内存,以下类推
- #[repr(u16)]
- #[repr(u32)]
- #[repr(u64)]
- #[repr(i8)]
- #[repr(i16)]
- #[repr(i32)]
- #[repr(i64)]
指定C布局
- #[repr(C)]
指定C布局,具体的每一个变体占用多少内存,是由当前平台的C编译器来决定的。也就是说Rust这边与对手方的C编译器的约定保持一致(比如,4个字节),可能不同的平台,不同的C编译器,会有所不同。
组合指定
- #[repr(C, u8)]
- #[repr(C, u16)]
组合指定只能用在带负载枚举上(但是带负载枚举在实际场合中,跨FFI边界的场景并不多,如果有必要,后面开专题说明)。
而 Fieldless enum 只能指定 int 位数布局和 C 布局中的一种,不能组合指定。如:
#[repr(C)]
enum SomeEnum {
A,
B,
C,
}
转换到C中,可以把 A 与整数进行比较(从0开始递增,此处A=0,B=1,C=2)。其它后续的就是 C 中枚举的知识了,此不赘述。
重要参考
以下链接,都值得一读。
- https://blog.eqrion.net/announcing-cbindgen/
- https://s3.amazonaws.com/temp.michaelfbryan.com/objects/index.html
- https://rust-lang.github.io/unsafe-code-guidelines/layout/enums.html