2 个不稳定版本
0.10.2 | 2023年10月22日 |
---|---|
0.8.0 | 2017年9月11日 |
0.6.3 |
|
0.5.0 |
|
#40 in FFI
每月 25 次下载
180KB
4.5K SLoC
Citrus: 将C转换为Rust
这是一个帮助将C程序转换为Rust程序的工具。它将C语法转换为Rust语法,但主要忽略C语义的细节。
生成的程序可能无法运行,甚至可能无法编译。然而,该工具产生了可读的源代码,可以手动重构为Rust程序。
示例
void gz_compress(FILE *in, gzFile out) {
char buf[BUFLEN];
int len;
int err;
for (;;) {
len = fread(buf, 1, sizeof(buf), in);
if (ferror(in)) {
perror("fread");
exit(1);
}
if (len == 0) break;
if (gzwrite(out, buf, (unsigned)len) != len) error(gzerror(out, &err));
}
fclose(in);
if (gzclose(out) != Z_OK) error("failed gzclose");
}
#[no_mangle]
pub unsafe extern "C" fn gz_compress(mut in_: *mut FILE, mut out: gzFile) {
let mut buf: [i8; 16384];
let mut len;
let mut err;
loop {
len = fread(buf, 1, std::mem::size_of_val(&buf), in_);
if ferror(in_) != 0 { perror("fread"); exit(1); }
if len == 0 { break ; }
if gzwrite(out, buf, len as c_uint) != len {
error(gzerror(out, &mut err));
};
}
fclose(in_);
if gzclose(out) != Z_OK { error("failed gzclose"); };
}
安装
需要安装 clang
。在macOS上需要Xcode。
用法
一次转换一个文件,打印到stdout
citrus[<citrus 选项…>] <文件.c> [<编译器参数…>]
选项包括
--api=rust
— 允许函数参数中仅使用Rust类型,并且不将函数导出到C。如果您正在一次迁移所有代码,请使用此选项。--api=c
— 为C互操作性生成所有函数参数。如果您正在逐个迁移代码,请使用此选项。
编译器参数是编译C文件所需的标准标志,例如 -I<include dir>
和 -D<macro>
。
citrus program.c -I./include
citrus --api=rust program.c > program.rs
典型的工作流程是
- 清理C代码 — 坏的C代码会使Rust代码更糟。
- 将C语法转换为Rust语法(这是由Citrus自动完成的!)。
- 不断将C特性转换为Rust特性,直到编译成功(Citrus也会完成一些简单的转换)。
- 重构为Rust风格代码。
C从Rust的角度来看非常奇怪。生成的代码将非常不符合Rust风格。请不要根据这个来判断Rust :)。
准备C代码以进行转换
-
对于所有长度和数组索引使用
size_t
(如果需要负值,则使用ssize_t
或ptrdiff_t
)。Rust对此非常挑剔。 -
尽可能多地改为
const
:变量、函数参数、全局变量。在Rust中,默认情况下事物是不可变的。- 不要重用变量,例如,不要在一个函数中重复使用一个
i
,而是为每个循环重新定义它。 - 如果您收到“在嵌套指针类型中丢弃限定符”错误,这不是您的错,这是C语言中的“错误”。
- 不要重用变量,例如,不要在一个函数中重复使用一个
-
尽量减少宏的使用。非平凡宏在转换过程中展开,并且它们的高级语法会丢失。
- 将函数宏改为
inline
函数。对于转换,您甚至可能需要取消定义assert
、MAX
和offsetof
。 - 如果您使用宏来生成函数的几个版本(如
func_int
、func_float
),只保留一个具有唯一typedef的类型版本。您可以在以后用泛型参数替换typedef的名称。
- 将函数宏改为
-
将
int
和long
替换为特定大小的类型,例如<stdint.h>
的int32_t
、int64_t
或size_t
。- 使用来自
<stdbool.h>
的bool
。 - 只有当您确实想用它来表示负数时,才使用
signed char
。 - 可以保留
unsigned char
、short
、float
、double
和long long
不变。
- 使用来自
-
在函数参数中使用
arr[]
表示数组,并使用*ptr
表示恰好一个元素。- 优先使用数组索引而不是指针算术(是的,
arr[i],不,
ptr+i
)。 - 如果您可以使用
f(size_t length, arr[static length])
(是的,这是一个有效的C语法)则获得加分。
- 优先使用数组索引而不是指针算术(是的,
-
向不应使用
NULL
参数调用的函数添加__attribute__((nonnull))
。 -
将
for
循环改为格式for(size_t i = start; i < end; i++)
。- 如果不能这样做,则使用
while
代替(但避免do..while
)。
- 如果不能这样做,则使用
-
不要在表达式中使用
var++
。使用++var
或将其放在单独的一行上。Rust只允许var += 1;
-
删除所有
goto
及其标签。 -
删除“聪明”的微优化。它们在转换过程中非常痛苦,并且大多数最终都变得不适用。
- 避免使用指针、联合、类型转换或在不同目的下重复使用同一块内存的技巧。
- 考虑使用值返回而不是输入输出指针,例如,如果您需要返回多个项目,请通过值返回一个小型结构。
- 清晰地使用malloc/free。不要在不同类型的对象之间重复/移动指针,因为在转换过程中,其中一些可能最终使用libc malloc,而另一些则使用Rust的
Vec
。
-
测试非常有助于很多。不仅单元测试,还包括高级测试,例如已知良好的整个程序输出。
转换后的清理
- 添加以下代码:
use std::os::raw::*; use std::slice; use std::ptr
; - 逐行验证代码,以确保没有遗漏或翻译错误。
- 注意隐式转换为int和环绕算术。
- 如果您打算逐个函数地移植代码
- 将所有C函数设置为extern(即删除
static
)并使用bindgen
生成用于回调C的绑定。 - 注意哪些函数实际上应该是公共的,因为在过渡期间,所有内容最终都变为
pub
。
- 将所有C函数设置为extern(即删除
- 将固定大小的数组替换为切片或
Vec
。Rust的固定大小数组很麻烦。 - 使用
let foo = slice::from_raw_parts(foo_ptr, number_of_elements_not_bytes)
修复数组索引。
从头开始构建Citrus工具
因为如果所需的C3依赖项需要精确的LLVM 5.0以及相应的静态Clang库(libclang.a
+ 头文件)。您可能需要为此从源代码构建Clang(抱歉)。Clang的稳定C API不足以完成此任务,因此Citrus必须使用脆弱的C++ Clang API,这不能保证与任何东西兼容。
从源代码构建LLVM 5和静态Clang。有关更详细的构建说明,请参阅此处。设置LLVM和Clang的变量。
# Must have 'libclang.a'
export LIBCLANG_STATIC_PATH=…/clang/build/lib/
# Path straight to the 'llvm-config' executable
export LLVM_CONFIG_PATH=…/llvm/bin/llvm-config
# Should contain 'clang' and 'clang-c' sub-directories
export LIBCLANG_INCLUDE_PATH=…/clang/include
cargo build
依赖关系
~5.5MB
~115K SLoC