1 个不稳定版本

0.0.0 2019 年 1 月 7 日

#33 in #generate-documentation


用于 qt_ritual

MIT 许可证

550KB
13K SLoC

cpp_to_rust_generator

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

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

如何使用

请参阅 qt_generator 包的 README,了解如何运行 Qt 包的生成器。

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

在线文档

请参阅 full_run.rsqt_generator 中的示例。

依赖关系

所有包都需要稳定的 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_PATHllvm-config 二进制的路径)
  • CLANG_SYSTEM_INCLUDE_PATH(例如,对于 clang 3.8.0,为 $CLANG_DIR/lib/clang/3.8.0/include)。

生成的包需要

  • 目标 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 变量可以用于指定测试使用的临时目录的位置。如果目录在测试运行之间保留,测试将运行得更快。

生成的crate的构建脚本接受环境变量 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 crate,则在它们生成过程中收集的信息将从缓存目录中加载并用于进一步处理。
  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. 生成器的内部信息写入缓存目录,并在处理库的依赖项时使用。

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

C++/Rust功能覆盖率

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

  • 原始类型映射到Rust的原始类型(如 bool)和libc 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> 类型转换为 Rust 自身的相似实现,位于 qt_core::flags)。
  • qt_core::connection 实现了使用信号和槽的方式。可以使用内置 Qt 类的信号和槽,并从 Rust 代码中创建绑定到任意闭包的槽。在编译时检查参数类型兼容性。

尚未实现但计划中

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

平台支持

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

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

Windows MinGW 工具链部分受支持。

备注

表达库依赖

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

文档生成

文档很重要!cpp_to_rust 生成 rustdoc 注释,包含关于对应 C++ 类型和方法的信息。重载方法具有详细文档,列出了所有可用变体。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 模式是一种更通用且更安全的方式,可以将 C++ 对象存储在 Rust 中,但它在处理多个小型对象时可能会产生不必要的开销。

结构体模式更加有限且危险。如果对象的指针存储在某个地方,则不能使用它,因为 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中相应方法的名称。

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

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

安全性

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

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

FFI类型

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

可执行文件大小

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

依赖关系

~6–8MB
~148K SLoC