# 前言
C2Rust 是一个将符合 C99 标准的代码迁移到 Rust 的转换器,本项目需要基于 C2Rust 将 C 预处理阶段的宏转换为 Rust 编译阶段的宏。因此,需要对 C 宏进行分析,并利用 Rust 宏的性质来寻找它们之间可能的映射关系和转换方案。
# 研究对象
根据 C99 标准,C 宏大体上可以分为替换文本宏(Replacing Text Macros)和条件包含(Conditional Inclusion)两种,而我们要根据这两类宏设计与之对应的 Rust 宏的转换规则。
替换文本宏的句法如下:
#define identifier replacement-list(optional) // Object-like Macros | |
#define identifier( parameters ) replacement-list // Function-like Macros | |
#define identifier** **( parameters, ... ) replacement-list // Function-like Macros | |
#define identifier** **( ... ) replacement-list // Function-like Macros | |
#undef identifier // #undef directive |
条件包含的句法如下:
#if expression | |
#ifdef identifier | |
#ifndef identifier | |
#elif expression | |
#else | |
#endif |
# 具体转换方案
# define 宏指令的转换
C 中 define 指令定义标识符为宏,即它们指示编译器将所有标识符的后继出现替换为替换列表,可以可选地附加地处理。我们此阶段 define 指令宏的转换规则主要适用于以下这两种常用的语法形式:
#define identifier replacement-list(optional) (1) | |
#define identifier( parameters ) replacement-list (2) | |
#define identifier( parameters, ... ) replacement-list (3) | |
#define identifier( ... ) replacement-list (4) |
这里将第一类 define 宏指令归为常量宏,第二、三、四类 define 宏指令归为语句宏或函数宏,下面针对这两种形式的 define 语法给出具体的转化规则。
考虑到为了在 rust 中能够设计一个通用的声明宏来对应 C 中 define 定义的不同变量类型的宏指令,这里将上诉 C 种的常量宏具体使用形式大致归为以下四种:
#define MAX_SIZE 1024 | |
#define MY_STR "root/api/" | |
#define MYTY int | |
#define MYNIL |
针对这四种形式的常量宏,设计如下 rust 对应的声明宏:
my_def!(MAX_SIZE: i32, 1024); | |
my_def!(MY_STR: &'static str, "root/api/"); | |
my_def!(MYTY, i32); | |
my_def!(MYNIL: bool, true); |
上述四种形式的 define 指令的常量宏包含了基础类型的定义、String 非基础类型的定义、取类型别名和无替换文本的情况。据此,设计如下声明宏 my_def:
macro_rules! my_def { | |
($name: ident : $ty: ty, $value: expr) => { | |
#[allow(non_upper_case_globals)] | |
pub const $name: $ty = $value; | |
}; | |
($name: ident, $ty: ty) => { | |
#[allow(non_camel_case_types)] | |
pub type $name = *mut $ty; | |
}; | |
} |
匹配规则中的 ty 标识为类型,expr 标识为表达式,通过定义一个公开的 const 类型变量名为传入的 name、值为传入的 value 来实现 C 中的常量宏的定义。使用方式如下:
my_def!(MAX_SIZE: i32, 1024); | |
my_def!(MY_STR: &'static str, "root/api/"); | |
my_def!(MYTY, i32); | |
my_def!(MYNIL: bool, true) |
功能与上述 C 中的 define 的四条宏指令功能一致。
对于语句宏或函数宏,类似于 C 中的函数调用,替换文本 replacement-list 一般也是函数调用或者函数的函数体语句块。针对此种用法,设计如下与之功能对应的 rust 声明宏:
macro_rules! function_macro { | |
( | |
$(#[$attr:meta]) | |
$vis:vis fn $name:ident ( $( $arg:ident : $t:ty ),* ) -> $ret:ty { | |
$($body:tt) | |
} | |
) => { | |
$(#[$attr]) | |
$vis fn $name ( $( $arg : $t ),* ) -> $ret { | |
$($body) | |
} | |
}; | |
} |
标识 vis 标明函数的可见性,$( $arg:ident : t:ty ),\***和**( $arg : $t ),* 重复零次或者多次,因此参数的个数可以不受限制,而函数体的标识符为 tt 标识,是 Token 树,可以被匹配的定界符 (
、 []
或 {}
中的单个或多个 token,因此该声明宏也能完成声明宏的嵌套,而对于函数体 body 部分可以借用 c2rust 进行代码转换。
C 中 define 语句:
#define add(a, b) ((a) + (b)) | |
#define myprint(a, b) | |
{ | |
printf("%d\n", (a) * (b)); | |
(a) * (b); | |
} | |
#define mytotal(a, b) | |
({ | |
int sum = 0; | |
for (int i = (a); i <= (b); i++) { | |
sum += i; | |
} | |
sum; | |
}) | |
#define factorial(n) | |
({ | |
unsigned long long result; | |
if ((n) <= 1) { | |
result = 1; | |
} else { | |
result = (n) * factorial((n) - 1); | |
} | |
result; | |
}) |
使用设计的声明宏模板,则对应 rust 以下形式:
function_macro!( | |
pub fn add(a: i32, b: i32) -> i32 { | |
a + b | |
} | |
); | |
function_macro!( | |
pub fn myprint(a: i32, b: i32) -> i32 { | |
println!("{}", a*b); | |
a*b | |
} | |
); | |
function_macro!( | |
pub fn mytotal(a: i32, b: i32) -> i32 { | |
let mut sum = 0; | |
for i in a..=b { | |
sum += i; | |
} | |
sum | |
} | |
); | |
function_macro! { | |
pub fn factorial(n: u64) -> u64 { | |
if n <= 1 { | |
1 | |
} else { | |
n * factorial(n - 1) | |
} | |
} | |
} |
# 条件编译宏指令
C 中的条件编译相关的宏指令常用于各类 C 相关项目中,本课题也需考虑此类宏指令的对应规则。C 中条件编译指令可分为以下几类:
- #ifdef 和 #ifndef 用于检查某个宏标识是否被定义,但只处理其后的第一个标识符,如 #ifdef ABCD && ABCD == 1,只要 #define 了 ABCD,其后 ABCD == 1 无论为 true 或者 false 都将被丢弃,C 中编译器只会对此写法提示 warning,不会阻止编译。
- #if 、#elif 和 #else 用于条件编译判断
- #if defined () 和 #if !defined () 功能上等价于 #ifdef 和 #ifndef,但其后的 && 或者 || 接的标识符仍旧有效。
- #if 条件表达式,用于更为复杂的条件编译判断
为了便于统一处理,当处理 #ifdef 和 #ifndef 时统一转换成 #if defined () 和 #if !defined (),并且抛弃 #ifdef 和 #ifndef 后面的 && 或者 || 的内容。进一步处理类似 #if defined (ABCD) 和 #if !defined (DCBA) 时可以等价转化于 #if (F_ABCD == true) 和 #if !(F_DCBA == true) 给 ABCD 或 DCBA 加上前缀 F_进行判断。考虑到在实际 C 编程中用 #if 进行条件编译判断时可能出现未 #define AAA 的标识出现,所以在进行 rust 代码条件编译整个过程之前,需进行如下操作:
- 利用我们的 shell 文件提取 C 中所有用于条件编译的符号,加上前缀 F_保存到用户指定目标文件 symbols.txt 中,指令如下:./extract_symbols.sh -c scan_condition.c -o symbols.txt
- 然后提取 C 文件中所有 #define 的符号,保存到用户的指定目标文件 defsyb.txt 中,指令如下:./extract_def.sh -c scan_condition.c -o defsyb.txt
- 再通过将 define 过的符号设置 true,未定义的符号设置为 false,将这些符号作为 bool 变量保存起来,用于后续条件编译的判断,指令如下:. /codename symbols.txt defsyb.txt
至此,C 中条件编译皆可转化为 #if #elif #else 此类形式的指令了,对于此种形式指令我们设计了如下 rust 声明宏进行替换:
macro_rules! c_conditional { | |
(if $condition:expr => { $($if_block:tt)* } $(else if $elif_condition:expr => { $($elif_block:tt)* }) $(else => { $($else_block:tt)* })?) | |
=> { | |
if $condition { | |
$($if_block) | |
} | |
$(else if $elif_condition { | |
$($elif_block) | |
}) | |
$(else { | |
$($else_block) | |
})? | |
}; | |
} |
调用方法如下:
c_conditional!( | |
if !(F_ABCD == true) => { // some code | |
println!("F_ABCD is false"); | |
} | |
else if F_ABCD == true && ABCD == 3 => { // some code | |
println!("F_ABCD is true and ABCD is 3"); | |
} | |
else => { // some code | |
println!("F_ABCD is true and ABCD is not 3"); | |
} | |
); |
同时支持条件编译的嵌套:
c_conditional!( | |
if !(F_ABCD == true) => { // some code | |
c_conditional!( | |
if !(F_ABCD == true) => { // some code | |
println!("F_ABCD is false"); | |
} | |
else if F_ABCD == true && ABCD == 3 => { // some code | |
println!("F_ABCD is true and ABCD is 3"); | |
} | |
else => { // some code | |
println!("F_ABCD is true and ABCD is not 3"); | |
} | |
); | |
} | |
else if F_ABCD == true && ABCD == 3 => { // some code | |
println!("F_ABCD is true and ABCD is 3"); | |
} | |
else => { // some code | |
println!("111"); | |
c_conditional!( | |
if !(F_ABCD == true) => { // some code | |
println!("F_ABCD is false"); | |
} | |
else if F_ABCD == true && ABCD == 3 => { // some code | |
println!("F_ABCD is true and ABCD is 3"); | |
} | |
else => { // some code | |
println!("000000 F_ABCD is true and ABCD is not 3"); | |
} | |
); | |
} | |
); |
关于 C 中的条件编译,常常使用一些操作系统或者其他一些环境相关的预定义宏作为条件编译判断的部分,如以下形式:
#ifdef __LINUX__ | |
#include "a.h" | |
// some code | |
#elif __WINDOWS__ | |
#include "b.h" | |
// some code | |
#else | |
// some code |
关于此类情景,通过收集当前系统执行环境的环境变量,在进行整个 C 宏转化为 Rust 宏之前,就用 define 中的常量宏进行定义设置为 true,后续再正常进行相关宏的替换操作。
# 总结
在本文中我们提出了一种 C 语言自动转换为 Rust 语言的对应规则。该规则设计的关键点在于采用 Rust 声明宏完成 C 的替换文本宏和条件编译宏的对应,当编译器在编译代码时遇到使用声明宏的地方,它会根据声明宏的规则进行模式匹配,并将匹配的代码进行相应的替换,也即是声明宏的替换是在编译时进行的,而不是在运行时。通过对设计的相关声明宏模板进行单元测试,测试结果证明我们设计的对应规则是有效可行的。