Rust FFI 编程 - Rust导出共享库03

这次,我们来关注一下Rust语言的基本特性到C的映射。

我们已经了解了,Rust语言是多泛式(混合泛式)的语言,它可以做命令式(过程式)编程,也可以做面向对象编程,也可以做函数式编程。把Rust简单地归类为某种泛式的编程语言,都不太合适。Rust就是Rust。C语言是比较传统的过程式编程语言,因此,从Rust到C的转换,就会有一些无法直接对标的东西。于是,做这种映射工作就需要一些额外的规范或约定。本文我们来关注:

  1. 结构体的方法的处理
  2. 泛型的处理
  3. Type alias
  4. 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 中枚举的知识了,此不赘述。


上一篇:CocoaPods 安装


下一篇:Rust FFI 编程 - 其它语言调用 Rust 代码 - Python