#bindings #code-generation #c

bin+lib citrus

C到Rust语法转换器

2 个不稳定版本

0.10.2 2023年10月22日
0.8.0 2017年9月11日
0.6.3 2017年9月3日
0.5.0 2017年8月19日

#40 in FFI

每月 25 次下载

GPL-3.0+ AND BSD-3-Clause

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

典型的工作流程是

  1. 清理C代码 — 坏的C代码会使Rust代码更糟。
  2. 将C语法转换为Rust语法(这是由Citrus自动完成的!)。
  3. 不断将C特性转换为Rust特性,直到编译成功(Citrus也会完成一些简单的转换)。
  4. 重构为Rust风格代码。

C从Rust的角度来看非常奇怪。生成的代码将非常不符合Rust风格。请不要根据这个来判断Rust :)。

准备C代码以进行转换

  • 对于所有长度和数组索引使用 size_t(如果需要负值,则使用 ssize_tptrdiff_t)。Rust对此非常挑剔。

  • 尽可能多地改为const:变量、函数参数、全局变量。在Rust中,默认情况下事物是不可变的。

    • 不要重用变量,例如,不要在一个函数中重复使用一个i,而是为每个循环重新定义它。
    • 如果您收到“在嵌套指针类型中丢弃限定符”错误,这不是您的错,这是C语言中的“错误”
  • 尽量减少宏的使用。非平凡宏在转换过程中展开,并且它们的高级语法会丢失。

    • 将函数宏改为inline函数。对于转换,您甚至可能需要取消定义assertMAXoffsetof
    • 如果您使用宏来生成函数的几个版本(如func_intfunc_float),只保留一个具有唯一typedef的类型版本。您可以在以后用泛型参数替换typedef的名称。
  • intlong替换为特定大小的类型,例如<stdint.h>int32_tint64_tsize_t

    • 使用来自<stdbool.h>bool
    • 只有当您确实想用它来表示负数时,才使用signed char
    • 可以保留unsigned charshortfloatdoublelong 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
  • 将固定大小的数组替换为切片或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