2 个不稳定版本

使用旧的 Rust 2015

0.5.3 2017年1月11日
0.2.0 2017年6月12日
0.0.0 2017年1月11日

#2524 in Rust 模式


被用于 qt_generator

MIT 许可证

620KB
14K SLoC

cpp_to_rust_generator

此crate包含生成器实现的主体部分。

有关项目其他部分的信息,请参阅仓库根目录下的README

使用方法

参考 qt_generator crate 的README,了解如何运行 Qt crate 的生成器。

如果您想为另一个 C++ 库生成 crate,请创建一个新的二进制 crate,并使用其 API 调用生成器。

在线文档

请参阅 full_run.rsqt_generator 的示例。

依赖项

所有 crate 都需要稳定的 Rust ≥ 1.15 和由 cargo 自动提供的某些依赖项。

生成器还需要

  • 目标 C++ 库(包含和库文件),与所使用的 Rust 工具链兼容。
  • cmake ≥ 3.0。
  • 一个与所使用的 Rust 工具链兼容的 C++ 构建工具链
    • 在 Linux 上:make 和一个 C++ 编译器;
    • 在 Windows 上:MSVC 或 MinGW 环境;
    • 在 OS X 上:命令行开发工具(不需要完整安装 Xcode)。
  • libclang-dev ≥ 3.5(CI 使用 3.8 和 3.9)。

以下环境变量可能对于 clang 解析器正确工作是必需的

  • LLVM_CONFIG_PATH(指向 llvm-config 二进制文件的路径)
  • CLANG_SYSTEM_INCLUDE_PATH(例如,对于 clang 3.8.0:$CLANG_DIR/lib/clang/3.8.0/include)。

生成的 crates 需要

  • 目标 C++ 库;
  • cmake;
  • 一个 C++ 构建工具链。

环境变量

cpp_to_rust_generator 可能需要将 CLANG_SYSTEM_INCLUDE_PATH 环境变量设置为指向 clang 系统头文件的路径,例如 /usr/lib/llvm-3.8/lib/clang/3.8.0/include。如果没有它,解析可能会因为类似这样的错误而终止

fatal error: 'stddef.h' file not found

CPP_TO_RUST_TEMP_TEST_DIR 变量可用于指定测试使用的临时目录位置。如果目录在测试运行之间保留,测试将运行得更快。

生成的crates的构建脚本接受环境变量 CPP_TO_RUST_LIB_PATHSCPP_TO_RUST_FRAMEWORK_PATHSCPP_TO_RUST_INCLUDE_PATHS。它们可以用来覆盖构建脚本(如果有的话)选择的路径。如果需要指定多个路径,可以像在目标平台上的 PATH 变量中分隔一样分隔它们。

C++构建工具和链接器也可能读取其他环境变量,包括 LIBPATHLIBRARY_PATHLD_LIBRARY_PATHDYLD_FRAMEWORK_PATH。生成器提供了指定库路径的API,在构建C++包装库时将其传递给cmake,并在构建脚本的输出中报告路径,但这可能不足以让链接器找到库,因此您可能需要手动设置它们。

生成器工作流程

生成器本身(cpp_to_rust_generator)是一个库,它公开了用于配置过程不同方面的API。为了运行生成器并生成输出crate,必须使用二进制crate(如qt_generator)并使用其API启动生成器。

生成器按照以下步骤运行

  1. 如果目标库有任何已经处理并转换为Rust crates的依赖项,则在其生成过程中收集的信息将从缓存目录中加载并用于进一步处理。
  2. 执行clang C++解析器以从库的头文件中提取有关库的类型和方法的信息。
  3. 生成一个具有C兼容接口的C++包装库。库通过包装函数公开找到的每个方法。
  4. 为crate生成Rust代码。C++包装库中的函数通过Rust的FFI支持在crate中可用。Rust代码还包含所有找到的C++枚举、结构体和类的enumstruct(包括模板类的实例化)。
  5. 使用C++库文档(如果有的话)和cpp_to_rust的处理数据生成crate的完整功能文档(示例)。
  6. 将Rust代码保存到输出目录,包括调用者提供的任何额外文件(测试、示例等)。还附带了构建crate所需的构建脚本。
  7. 生成器的内部信息写入缓存目录,并在处理库的依赖项时使用。

可以使用cargo构建生成的crate并将其作为依赖项包含到其他项目中,就像任何其他crate一样。

C++/Rust功能覆盖

许多内容直接从C++转换为Rust

  • 基本类型映射到Rust的基本类型(如bool)和lib crate提供的类型(如libc::c_int)。
  • 固定大小的数值类型(例如 int8_tqint8)映射到Rust的固定大小类型(例如 i8)。
  • 指针、引用和值映射到Rust的相应类型。
  • C++命名空间映射到Rust子模块。
  • C++类和结构体映射到Rust结构体。这也适用于库API中遇到的模板类的所有实例化,包括依赖项的模板类。
  • 自由函数映射到自由函数。
  • 类方法映射到结构体的实现。
  • 析构函数映射到DropCppDeletable实现。
  • 函数指针类型映射到Rust的等效表示。不支持具有引用或类值的函数指针。
  • static_castdynamic_cast通过相应的特质在Rust中可用。

Rust标识符的名称根据Rust的命名约定进行修改。

当无法直接翻译时

  • C++库的每个包含文件的 内容都放置到一个单独的子模块中。
  • 使用将参数包装在元组中并创建描述每个方法接受的元组的特质来模拟方法重载。具有默认参数的方法以相同的方式处理。
  • 单继承映射到DerefDerefMut实现,允许在派生对象上调用基类方法。当解引用转换不足时,应使用static_cast将派生类转换为基类。
  • 为每个公开类字段创建获取器和设置器方法。

尚未实现但计划中

  • 将C++ typedef转换为Rust类型别名。
  • 根据C++操作方法实现结构体的操作符特质(问题)。操作符目前以带有op_前缀的常规函数的形式公开。
  • 如果C++端存在适用方法,为结构体实现Debug和Display特质。
  • 为集合实现迭代器特质。
  • 子类化API(问题)。
  • 提供对类的公共变量的访问(问题)。
  • 提供枚举到int及其反向转换(用于Qt API)。
  • 支持嵌套在模板类型中的C++类型,如Class1<T>::Class2

不打算支持

  • 高级模板使用,例如具有整数模板参数的类型。
  • 模板部分特殊化。
  • 模板方法和函数。

Qt特定功能覆盖

已实现

  • QFlags<Enum>类型被转换为位于qt_core::flags的Rust的类似实现。
  • qt_core::connection实现了一种使用信号和槽的方法。可以使用内置Qt类的信号和槽,并从Rust代码创建绑定到任意闭包的槽。在编译时检查参数类型兼容性。

尚未实现但计划中

  • 从Rust代码创建自定义信号。

平台支持

支持Linux、OS X和Windows。使用Travis和Appveyor在以下平台和目标上持续测试cpp_to_rust和Qt crates

  • Ubuntu Trusty x64(稳定-x86_64-unknown-linux-gnu);
  • OS X 10.9.5(稳定-x86_64-apple-darwin);
  • Windows Server 2012 R2 Windows 7 x64 with MSVC 14(稳定-x86_64-pc-windows-msvc)。

Windows MinGW工具链部分支持。

备注

表达库依赖关系

cpp_to_rust利用Rust的crate系统。如果C++库依赖于另一个C++库,生成的Rust crate也将依赖于依赖项的crate并重用它类型。

文档生成

文档非常重要!cpp_to_rust 生成带有对应 C++ 类型和方法的 rustdoc 注释。重载方法有详细的文档,列出了所有可用的变体。Qt 文档集成在 rustdoc 注释中。

在堆栈和堆上分配 C++ 对象

cpp_to_rust 支持 C++ 对象的两种分配位置模式。合适的模式会自动分别针对每种类型选择,并且可以在生成器配置中覆盖。分配位置模式仅影响返回类值的方法(不是引用、指针或原始类型)和构造函数。

Box 模式

  1. 在 C++ 端使用 new 在堆上放置返回的对象。构造函数调用方式为 new MyClass(args),而返回对象值的功能调用方式为 new MyClass(function(args))
  2. new 返回的指针通过 FFI 传递到 Rust 包装器和 cpp_utils::CppBox::new。返回给调用者的是 CppBox<T>
  3. CppBox 被丢弃时,它调用删除器函数,该函数在 C++ 端调用 delete object
  4. 原始指针可以移出 CppBox 并传递给另一个可以拥有对象的所有权的函数。

结构体模式

  1. 在堆栈上创建一个未初始化的 Rust 结构体。结构体的尺寸与 C++ 对象的尺寸相同。(当前平台上的对象尺寸由构建脚本确定)。
  2. 将结构体的指针传递给使用定位 new 的 C++ 包装函数。构造函数调用方式为 new(buf) MyClass(args),其中 buf 是指向在 Rust 中创建的结构体的指针。结构体被填充为有效的数据。
  3. 调用者保留结构体的所有权,可以使用 BoxVec 将其移动到堆上,传递到另一个位置并获取其引用和指针。
  4. 当结构体被丢弃时,使用 C++ 包装函数调用 C++ 析构函数。结构体本身的内存由 Rust 管理。

Box 模式是在 Rust 中存储 C++ 对象的更通用和更安全的方式,但它可能在处理多个小对象时产生不必要的开销。

结构体模式更有限且危险。如果对象的指针被存储在某个地方,则无法使用它,因为 Rust 可以在内存中移动结构体,指针可能变得无效。有时不清楚它们是否被存储。还禁止将此类结构体传递给接受所有权的函数,因为它们会尝试删除它并释放由 Rust 管理的内存。但是,结构体模式允许避免堆分配,并可用于小型简单结构和类。

生成器自动为将指针传递给其他函数的类型选择 Box 模式,因为这些函数可能拥有对象的所有权。此外,对于具有任何虚拟方法的类型,Box 模式是默认的。所有其他类型默认使用结构体模式。

指针、引用和原始类型(在大多数情况下)以原始形式通过FFI边界传递,因此分配位置模式对它们没有影响。如果使用类值作为参数,它将在Rust端转换为const引用,以便无论所有权和内存中的位置如何都可以传递该值。

跨平台兼容性

生成器目前假设C++库的API在所有支持的平台上是一致的。任何特定平台的(例如仅适用于Windows)类和方法都应该在生成器的配置中被列入黑名单。只要这样做,生成的crate应该可以在生成器支持的平台上成功运行。生成的crate的构建脚本将使用当前可用的工具链构建C++包装库,并在必要时确定实际的struct大小。

在处理特定平台的API时,可以在每个目标平台上运行生成器,并使用该平台上的结果。

如果使用与不同版本的C++库一起生成的crate,则该版本需要与用于生成的版本源兼容(但不要求二进制兼容)。对于Qt来说,这意味着较旧和较新的补丁版本和较新的次版本应该与较旧的crate兼容。

API稳定性

生成器处于积极开发中,其自身API尚未稳定。这不应该是一个大问题,因为这只是个开发工具,使用生成器API的代码量应该相对较小。

大问题是生成器无法提供生成crate的API稳定性。当切换到生成器的较新版本时,生成的API可能会发生重大变化(这目前是非常可能的)。

当切换到另一个版本的C++库时,生成器还可能引入破坏性变更,即使这些版本在C++侧是兼容的。例如

  • 引入新的枚举变体会改变Rust中先前存在的枚举的名称。
  • 引入新的方法重载可能会改变Rust中相应方法的名称。

解决这些问题需要两个步骤

  1. 稳定生成器的行为。目前尚不清楚最佳生成API的方式,但最终变化应该是最小的。
  2. 实现冻结crate API的能力,并强制生成器在处理较新版本时创建向后兼容的API。

安全性

无法自动将Rust的安全性带到C++ API中,因此大多数生成的API都非常不安全,需要以C++术语进行思考。在Rust中,生成原始指针是完全安全的,但取消引用它是危险的。生成器将接受原始指针的所有包装函数标记为不安全,因为原始指针不保证有效,函数几乎肯定将尝试取消引用指针(如果幸运的话,它将检查空指针)。

可以引入一个生成器API来标记方法为安全,并/或将它们的签名更改为安全(例如,将原始指针转换为引用、Option或任何更合适的东西),但这尚未实现。

FFI类型

C++包装函数的签名中只能包含与C兼容的类型,因此引用和类值被替换为指针,包装函数执行必要的转换到和从原始C++类型。在Rust代码中,这些类型被转换回引用和值。

可执行文件大小

如果Rust crate和C++包装库都是静态构建的,则链接器只对最终的可执行文件运行一次,该可执行文件使用这些crate。它应该能够删除所有未使用的包装函数,并生成一个合理小的文件,该文件仅依赖于原始C++库。

依赖项

~15MB
~289K SLoC