RUST和大型C++项目的相互配合

背景 RUST目前最大的价值在于内存安全,因此很多大型C/C++项目都在尝试使用RUST重写核心组件。 刚好最近有幸参与其中,为一家国内巨头企业探索RUST在C/C++项目的应用,因此写一下笔记。 场景 因为是探索阶段,所以本次重新的是项目中一个较小组件,代码量5K左右,但是提供了较多外部API,并

背景

RUST目前最大的价值在于内存安全,因此很多大型C/C++项目都在尝试使用RUST重写核心组件。
刚好最近有幸参与其中,为一家国内巨头企业探索RUST在C/C++项目的应用,因此写一下笔记。

场景

因为是探索阶段,所以本次重新的是项目中一个较小组件,代码量5K左右,但是提供了较多外部API,并且需要进行线程保护,算是比较典型的C/C++和RUST混用场景。

遇到的问题记录

函数指针

其实就是定义函数原型,这点重写起来没有什么难度。

C语言样例

// 函数指针
int (*func)(int, void*);

// 类型定义
typedef int (*DefinedFunc)(int, void*);

// 类型使用
void main(DefinedFunc func) {
	func(0, NULL);
}

RUST语言重写

// 函数指针
fn (i32, i32) -> i32;

// 类型定义
pub type DefinedFunc = fn (i32, i32) -> i32;

// 类型使用
fn main(func: DefinedFunc) {
	func(1, 2);
}

到这里定义没有问题,但是使用的地方还是有差异。
C语言是允许传入空指针的,由使用者就行判断使用。
RUST语言,对于RUST内部的接口,可以使用Option<T>进行包裹,对于和C交互的接口,则只能使用裸指针了,要注意做好保护。
下面会有裸指针交互的问题分析。

结构体公共成员

C语言代码中有些结构体存在公共成员,这些成员是使用宏的方式定义的,且不说这种写法是否合理或者值得提倡,确实是大量存在的,切均是外部API,修改影响极大,因此原先的项目中一直保留。

#define MSG_HEADER			uint_32 sender;   \
							uint_32 receiver; \
							uint_32 length;

typedef struct {
	MSG_HEADER
	uint8 value[4];
}

RUST语言中是没有结构体继承的说法的,因此考虑使用结构体嵌套的方式。

pub struct MsgHeader {
	pub sender: c_uint,
	pub receiver: c_uint,
	pub length: c_uint
}

struct DemoMsg {
	pub header: MsgHeader,
	pub value: [c_uchar; 4]
}

检索到有过程宏的方式,来标注结构体,让其平铺新增的成员,但是貌似不好控制成员顺序,因此没有继续研究了。

STD库的路径问题

场景:

  1. RUST使用libc箱,定义dylib类型,C语言调用时要去找rust-std。

  2. RUST使用libc箱,定义cdylib类型,RUST其他crate无法使用其类型定义等信息。

  3. RUST使用libc箱,定义staticlib类型,RUST其他crate无法使用其类型定义等信息。

C调用RUST传入裸指针

场景:
C/C++函数项目中,原先是这么使用的

// caller.c
#include "callee.h"

void caller(void) {
	int id = 3;
	IdPara para = { 0, 0 };
	(void)callee(id, ¶);
}

// callee.h
typedef struct {
	int a;
	int b;
} IdPara;

int callee(int id, IdPara* para);

// callee.c
int callee(int id, IdPara* para) {
	if (para == NULL) {
		return -1;
	}

	// do sth. with para
	return 0;
}

当我们尝试将callee模块使用RUST重写后,就会遇到裸指针的问题。

C/C++项目中try/catch共用异常分支

场景:
在大型嵌入式项目中,通常会由一个异常组件提供try/catch机制,达到以下几个目的:

  1. 提供统一的卫语句风格

  2. 共用分支缩减软件成本

  3. 共用日志点号

实例代码如下:

int DemoFunc(int id, char* ptr) {
	DEMO_TRY {
		DEMO_EXPECT(id < 0, ERRCODE_INVALID_ID);
		DEMO_EXPECT(ptr == NULL, ERRCODE_NULL_PTR);
		// use ptr to do sth.
		return 0;
	} DEMO_CATCH {
		DEMO_LOG(LOG_ID(1000), DEMO_ERR);
		return DEMO_ERR;
	}
}

上面的代码主要演示了函数对入参的检验,如果检验不通过,则跳转到catch分支,记录日志并返回错误码。

使用rust重写上面的代码会遇到以下几个问题:

  1. rust本身并不推荐使用try/catch来传递错误,而是独立的match语句

  2. match语句如何能区分错误码,并且共用异常分支来控制软件成本大小

C语言变长结构体

C语言样例

typedef struct {
	unsigned int name;
	unsigned int length;
	unsigned char value[0];
} Demo;

这个用法在C语言中是非常常见的,特别是嵌入式环境是大量使用的,有利于内存管理和性能提升。比如一条消息,会添加自己的协议头,比如上面的Demo,定义了name和length两个成员后,其实真正的消息内容是通过零长数组value可挂在后面的,使用length去控制内存读取的长度。

对于这种情况,RUST看起来只能通过unsafe代码块来操作裸指针,将零长数组后面的内容解析到下一层的结构中,然后进行使用,或者直接裸指针一路解引用到底。

可变全局变量

静态全局常量没什么好讨论的,C和RUST都差不多。

比如C语言中定义一个参数列表指针,初始并不分配空间,在使用时根据场景来分配或释放。

int* g_valueList = NULL;

先看下常规方法:
对于rust是无法直接定义得到的,因为rust直接定义的全局变量都必须保持静态。
这个有点难搞,一开始使用lazy_static来定义,这也是网上检索到的方法,但是实际上RUST会上报贬义错误,因为裸指针是无法被Mutex包裹的,会上报以下错误

*mut c_void connot be sent between threads safely

当然如果不使用裸指针,就可以了,比如上面的例子,可以将int的参数列表实例化成Vec。
但是这里毕竟是例子,实际工程中没有这么简单,这片内存是使用另一个C语言实现的功能来分配内存的,如果用rust将其实现,这就偏离了重写目标了。因此需要用裸指针来保存这个地址。

看来又要使用不安全代码块了,用万能的unsafe,有了unsafe什么都搞定了:)

static mut VALUE_LIST: *const c_void = std::ptr::null();

fn main() {
	...
	unsafe {
		// 更换变量绑定的裸指针地址
		VALUE_LIST = std::ptr::null();
	}
	...
}

函数指针作为入参传入

C语言调用函数原型

// demo.h
typedef int (*TestFunc)(int, int);

int RegHook(int user, TestFunc hook);

使用rust写成下面的样子

type TestFunc(i32, i32) -> i32;

pub extern "C" fn RegHook(user: i32, hook: TestFunc) -> i32 {
	......
}

这种写法在TestFunc的位置上报warning:not FFI-safe
看起来像是不允许extern中定义fn(i32, i32) -> i32

解决方案:
使用Box<TestFunc>套住
PS: 貌似不符合FFI规范,待测试

FFI编程

LICENSED UNDER CC BY-NC-SA 4.0
评论