背景
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库的路径问题
场景:
RUST使用libc箱,定义dylib类型,C语言调用时要去找rust-std。
RUST使用libc箱,定义cdylib类型,RUST其他crate无法使用其类型定义等信息。
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机制,达到以下几个目的:
提供统一的卫语句风格
共用分支缩减软件成本
共用日志点号
实例代码如下:
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重写上面的代码会遇到以下几个问题:
rust本身并不推荐使用try/catch来传递错误,而是独立的match语句
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规范,待测试