我们已经了解了,Rust语言是多泛式(混合泛式)的语言,它可以做命令式(过程式)编程,也可以做面向对象编程,也可以做函数式编程。把Rust简单地归类为某种泛式的编程语言,都不太合适。Rust就是Rust。C语言是比较传统的过程式编程语言,因此,从Rust到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 在 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 能让两边的类型名,看起来更一致。
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位数布局
指定C布局
指定C布局,具体的每一个变体占用多少内存,是由当前平台的C编译器来决定的。也就是说Rust这边与对手方的C编译器的约定保持一致(比如,4个字节),可能不同的平台,不同的C编译器,会有所不同。组合指定
组合指定只能用在带负载枚举上(但是带负载枚举在实际场合中,跨FFI边界的场景并不多,如果有必要,后面开专题说明)。
而 Fieldless enum 只能指定 int 位数布局和 C 布局中的一种,不能组合指定。如:
#[repr(C)] enum SomeEnum { A, B, C, }
转换到C中,可以把 A 与整数进行比较(从0开始递增,此处A=0,B=1,C=2)。其它后续的就是 C 中枚举的知识了,此不赘述。