Ritual

ritual运行使用来自Rust的C++库。它分析库的C++API并生成功能齐全的crate,提供对该API的方便(但仍然不安全)访问。

这个项目的主要动机是提供从Rust对Qt的访问。Ritual提供大量自动化,支持增量运行,并实现兼容API转换。这主要是由Qt提供的巨大API和Qt版本之间的API差异决定的。然而,ritual被设计为通用的,也可以用来轻松的为其他C++库创建绑定。

关于ritual和转换

C ++ / Rust特性覆盖

支持的功能:

  • 原始类型映射到Rust的原始类型(如bool)和FFI类型(如c_int)。
  • 固定大小的数字类型(例如int8_tqint8)映射到 Rust 的固定大小类型(例如i8)。
  • 指针、引用和值映射到由cpp_core crate提供的特殊智能指针类型( RefPtrCppBox等) 。
  • C++ 命名空间映射到 Rust 模块。
  • C++ 类、结构和枚举映射到 Rust 结构。这也适用于库 API 中遇到的模板类的所有实例化,包括依赖项的模板类。
  • 自由函数映射到自由函数。
  • 类方法映射到结构的实现。
  • 析构函数映射到CppDeletable实现,并且可以由CppBox自动调用。
  • 函数指针类型映射到 Rust 的等效表示。不支持具有引用或类值的函数指针。
  • static_castdynamic_cast在 Rust 中通过相应的特征可用。
  • 从基类继承的方法可通过Deref实现获得(如果类有多个基类,则只有第一个基类的方法可直接使用)。
  • 为每个公共类字段创建 Getter 和 setter 方法。
  • 尽可能将运算符转换为 Rust 的运算符 trait 实现。
  • C++ STL 风格的迭代器可以通过适配器从 Rust 访问。

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

文档很重要!ritual生成rustdoc注释,其中包含有关相应 C++ 类型和方法的信息。Qt 文档集成在rustdoc注释中。

尚未实施,但可以在未来实施:

  • 将 C++ typedef s 翻译成 Rust 类型别名。
  • 如果适用的方法存在于 C++ 端,则为结构实现DebugDisplay特征。
  • (实现子类化 API )。

未计划支持:

  • 高级模板使用,例如带有整数模板参数的类型。
  • 模板部分专业化。

Qt 特定的功能覆盖率

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

API 稳定性和版本控制

Ritual 可以分析多个不同版本的 C++ 库并生成支持所有这些版本的 crate。跨版本通用的 API 部分保证也具有相同的 Rust API。对于不总是可用的 API 部分,Rust 绑定将具有仅当 C++ 库的当前本地版本具有它们时才启用它们的特性属性。尝试使用已安装的 C++ 库版本中不可用的功能将导致编译时错误。

当新版本的 C++ 库发布时,ritual可以在生成的 crate 中保留所有现有的 API,并在功能标志下添加新引入的 API 项。这允许对生成的 crate 进行 semver 兼容的更改,以支持 C++ 库的所有可用版本。

管理依赖

与大多数语言一样,C++ 允许库在其公共 API 中使用来自其他库的类型。当生成 Rust 绑定时,它们应该理想地重用公共依赖项,而不是在每个 crate 中生成包装器的副本。Ritual 支持从已处理的依赖项中导出类型并在公共 API 中使用它们。

如果基于ritual的 crate 发布在crates.io上,并且您想在生成自己的绑定时将其用作依赖项,则ritual也可以从中导出信息。这允许独立开发人员基于彼此的工作而不是重复工作。

除了 Qt crates,ritual项目还提供了cpp_std crate,它提供了对 C++ 标准库类型的访问。在处理 API 中使用 STL 类型的库时应该使用它。但是,cpp_std仍处于早期开发阶段,仅提供对一小部分标准库的访问。

平台支持

支持 Linux、macOS 和 Windows。ritual和 Qt crates 在 Travis 上不断测试

安全

将 Rust 的安全性自动带入 C++ API 是不可能的,因此大多数生成的 API 使用起来不安全,需要用 C++ 术语进行思考。大多数生成的函数都是不安全的,因为不能保证原始指针是有效的,并且大多数函数会取消引用一些指针。

ritual的预期用途之一是生成一个低级接口,然后在其上编写一个安全接口(只能手动完成)。对于像 Qt 这样的大型库,当为整个库设计一个安全的功能齐全的 API 不可行时,建议在模块中包含不安全的用法,并为项目所需的 API 部分实现安全接口。

可执行文件大小

默认情况下,Rust crates 和 C++ 包装器库是静态构建的,链接器只为使用 crates 的最终可执行文件运行一次。它应该能够消除所有未使用的包装函数并生成一个仅依赖于原始 C++ 库的相当小的文件。

它是如何工作的

生成器按以下步骤运行:

  1. 如果目标库有任何已经处理并转换为 Rust crates 的依赖项,则在它们生成期间收集的信息将从工作空间目录或从crates.io 加载并用于进一步处理。
  2. clang C++ 解析器被执行以从其头文件中提取有关库的类型和方法的信息。
  3. 对检测到的方法进行检查,过滤掉无效的解析结果。
  4. 生成具有C兼容接口的C++封装库。该库使用包装函数公开每个找到的方法。
  5. 为 crate 生成一个 Rust 代码。使用 Rust 的 FFI 支持在 crate 中提供了来自 C++ 包装器库的函数。Rust 代码还包含所有找到的 C++ 枚举、结构和类(包括模板类的实例化)的struct
  6. C++ 库文档(如果可用)和ritual的处理数据用于为 crate 生成功能齐全的文档(示例 )。
  7. Rust 代码与调用者提供的任何额外文件(测试、示例等)一起保存到输出目录。还附上了构建板条箱所需的构建脚本。
  8. 生成器的内部信息写入数据库文件,可以在处理库的依赖时使用。

使用 docker(推荐)

为保证解析结果的一致性和可复现性,建议使用可复现的环境,例如docker提供的环境。

Ritual 提供了Dockerfile包含它的依赖:

  • docker.builder.dockerfile是适用于 C++ 标准库的基础镜像。在处理其他 C++ 库时,它也应该用作基础映像。
  • docker.qt.dockerfile是用于生成 Qt crates 的图像。

您可以使用以下命令构建映像:

1
2
3
4
5
6
cd ritual
# for any libraries
docker build . -f docker.builder.dockerfile -t ritual_builder
# only for Qt
docker build . -f docker.qt.dockerfile --target qt_downloader -t ritual_qt_downloader
docker build . -f docker.qt.dockerfile -t ritual_qt

请注意,图像仅包含环境。不包括预建的ritual物。这允许您编辑生成器的源代码并重新运行它,而无需重建 docker 映像的缓慢过程。您可以使用cargo来运行生成器,就像您通常在主机系统上执行的操作一样。

运行容器时,将/build挂载到主机系统上的持久目录。该目录将包含所有临时构建文件,因此使其持久化将允许您重新创建容器,而无需从头开始重新编译所有内容。

除了 build 目录之外,您还应该挂载一个或多个目录,其中包含生成器的源代码和ritual工作区目录(见下文),以使其在容器中可用。这些目录的路径可以是任意的。

这是在容器中运行 shell 的命令示例:

1
2
3
4
5
6
7
8
9
docker run \
--mount type=bind,source=~/ritual/repo,destination=/repo \
--mount type=bind,source=~/ritual/qt_workspace,destination=/qt_workspace \
--mount type=bind,source=~/ritual/tmp,destination=/build \
--name ritual_qt \
--hostname ritual_qt \
-it \
ritual_qt \
bash

使用cargo在容器内运行生成器,就像在主机系统中一样。

没有docker

如果您不想或不能使用docker,您可以在主机系统上安装所有必需的依赖项并使用cargo本地运行生成器,就像任何 Rust 项目一样。

“ritual”需要:

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

请注意,C++ 工具链、Rust 工具链和 Qt 构建必须兼容。例如,Windows 上的 MSVC 和 MinGW 目标不兼容。

clang解析器可能需要以下环境变量才能正常工作:

  • LLVM_CONFIG_PATHllvm-config二进制文件的路径)
  • CLANG_SYSTEM_INCLUDE_PATH(例如$CLANG_DIR/lib/clang/3.8.0/include用于clang 3.8.0)。

如果在运行生成器时产生类似“fatal error: ‘stddef.h’ file not found”的解析错误,请检查“CLANG_SYSTEM_INCLUDE_PATH”变量是否设置正确。

如果libsqlite3未在系统范围内安装,则可能需要设置SQLITE3_LIB_DIR环境变量。

运行cargo test以确保正确设置依赖项。

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

ritual在生成的 crate 上运行cargo时, RITUAL_WORKSPACE_TARGET_DIR变量会覆盖cargo的目标目录。

生成的 crates 的构建脚本接受RITUAL_LIBRARY_PATHRITUAL_FRAMEWORK_PATHRITUAL_INCLUDE_PATH环境变量。它们可用于覆盖构建脚本(如果有)选择的路径。如果需要指定多个路径,请以与您的平台上分隔PATH变量相同的方式分隔它们。此外,RITUAL_CMAKE_ARGS允许您在构建 C++ 胶水库时指定传递给cmake的附加参数。

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

运行生成器

注意:如上所述,建议使用 docker 来创建合适的环境。

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

Qt crates 可以这样生成:

1
2
cd ritual
cargo run --release --bin qt_ritual -- /path/to/workspace -c qt_core -o main

工作区目录将用于存储数据库、临时文件和生成的 crate。对所有 Qt crate 使用相同的工作区目录,以确保ritual可以使用以前生成的 crate 中的类型。

类似地,这是如何生成cpp_std的:

1
cargo run --release --bin std_ritual -- /path/to/workspace -c cpp_std -o main

Rust + Qt 指南

一般情况下用户关注的是以下内容,之间使用ritual生成的QT crate(翻译叫板条)

Qt crate是用ritual生成的。该项目维护了以下 Qt crates(将来可能会添加更多 crates):

Crate Docs
qt_core
qt_gui
qt_widgets
qt_ui_tools
qt_3d_core
qt_3d_render
qt_3d_input
qt_3d_logic
qt_3d_extras
qt_charts
qt_qml

支持的环境:64 位 Linux、64 位 Windows(msvc 工具链)、64 位 macOS。
支持的 Qt 版本:从 5.9 到 5.14。

配置

除了Rust自己的构建工具,您还需要设置C ++编译器、Qt和CMake 。

C++ 编译器

在 Linux 上,从存储库安装gcc

在 Windows 上,安装 Visual Studio(例如 Visual Studio Community 2017 )。确保在安装 Visual Studio 时启用 C++ 应用程序开发组件。

Visual Studio 将创建一个启动菜单选项(例如x64 Native Tools Command Prompt for VS 2017),用于在设置了环境变量的情况下启动命令提示符。您需要将它用于所有构建操作,以便 VS 编译器和链接器可用。

在 macOS 上,安装 Xcode 命令行工具。可以使用xcode-select --install命令启动安装。您不需要完整的 Xcode 安装。

Qt安装与配置

您可以使用 官方安装程序在任何操作系统上安装 Qt 。安装程序允许您选择多个可用版本和构建之一。确保选择“桌面”构建,而不是移动操作系统构建。在 Windows 上,还要确保选择与您的 Visual Studio 版本相对应的构建(例如MSVC 2017),而不是 MinGW 构建。选择 64 位版本,而不是 32 位版本。

在 Linux 和 macOS 上,您还可以从存储库(或brew)安装 Qt 开发包。

如果 Qt 未在系统范围内安装,则需要设置PATH以指向存储qmake可执行文件的目录。您可能还需要设置允许动态链接器在运行时查找 Qt 库的变量:

这里是注意点~ 用于指定QT库

然后这里就会有一个蛋疼的问题,交叉编译。这个问题单独写一篇文章。rust和QT分开都可以交叉编译。当混合在一起就涉及到Qt部分如何传递交叉编译参数。

在 Linux 上:

1
2
export PATH="~/Qt/5.14.0/gcc_64/bin:$PATH"
export LD_LIBRARY_PATH="~/Qt/5.14.0/gcc_64/lib:$PATH"

在 macOS 上:

1
2
3
4
5
export PATH="~/Qt/5.14.0/gcc_64/bin:$PATH"
#如果 Qt 构建为库:
export DYLD_LIBRARY_PATH=~/Qt/5.14.0/clang_64/lib:$DYLD_LIBRARY_PATH
#如果 Qt 作为框架构建:
export DYLD_FRAMEWORK_PATH=~/Qt/5.14.0/clang_64/lib:$DYLD_FRAMEWORK_PATH

在 Windows 上(在 VS 命令提示符中):

1
set PATH=C:\Qt\5.13.0\msvc2017_64\bin;%PATH%

CMake

你还需要cmake。在 Linux 和 macOS 上,从存储库(或brew)安装它。

在 Windows 上,在 官方网站 下载 CMake 安装程序。在安装过程中,选择将cmake添加到系统PATH中。您需要重新打开命令提示符或注销以应用更改。或者,在提示符中将其安装目录添加到“PATH”。

运行cmake --version以验证cmake是否可用。

验证安装

要检查所有设置是否正确,请尝试在您的环境中构建 C++/Qt 项目。如果您通过官方安装程序安装了 Qt,它将在 Qt 安装的“Examples”目录中存储示例。您还可以在 Qt git 存储库 中找到它们。

在 Linux 上:

1
2
3
4
5
6
cd /tmp
mkdir build
cd build
qmake ~/Qt/Examples/Qt-5.13.0/widgets/dialogs/standarddialogs
make
./standarddialogs

在 macOS 上:

1
2
3
4
5
6
cd /tmp
mkdir build
cd build
qmake ~/Qt/Examples/Qt-5.13.0/widgets/dialogs/standarddialogs
make
open standarddialogs.app

在 Windows 上(在 VS 命令提示符中):

1
2
3
4
5
6
cd C:\tmp
mkdir build
cd build
qmake C:\Qt\Examples\Qt-5.13.0\widgets\dialogs\standarddialogs
nmake
release\standarddialogs.exe

最后,您可以尝试构建提供的示例:

1
2
3
git clone https://github.com/rust-qt/examples
cd examples
cargo run --bin basic_form

如果使用 MSYS2 出现如下错误:“The procedure entry point inflateValidate could not be located in the dynamic link library C:\msys64\mingw64\bin\libpng16-16.dll”通常是由于PATH 上的其他一些 zlib1.dll。您可以使用以下命令从 MSYS2 shell 验证这一点:

1
which zlib1.dll

要解决此问题,您需要修复 PATH 或将 zlib1.dll 从C:\msys64\mingw64\bin目录复制到target\debugtarget \release目录。

入门

要使用 Rust 中的 Qt,请将 crates 作为依赖项添加到您Cargo.toml,例如:

1
cargo add qt_widgets

或者手动
ini [dependencies] qt_widgets = "0.5"

每个 crate 都会重新导出其依赖项,因此,例如,您可以将qt_core作为qt_widgets::qt_core访问,而无需添加显式依赖项。为方便起见,您还可以将它们添加为直接依赖项,但请确保使用兼容的版本。

请参阅 rust-qt/examples 存储库以了解如何使用 Qt crates 提供的 API。

大多数Qt API都尽可能按原样翻译成Rust 。标识符根据Rust的命名约定进行修改。重载的方法(接受多组参数类型的方法)被包装为不同的Rust方法,其后缀可以区分他们。

在许多情况下,您可以处理Qt文档并将其中的示例几乎直接转换为Rust代码。Crate文档(可在[ docs.rs ] ( https://docs.rs/qt_core/ )或通过cargo doc获得)功能嵌入式Qt文档。

此外,Rust crates提供了一些帮助来改善人体工程学:

Qt应用程序对象(QApplicationQGuiApplicationQCoreApplication)需要存在argcargv,而这些在Rust中不能直接使用。使用init帮助器正确初始化应用程序:

1
2
3
4
5
fn main() {
QApplication::init(|app| unsafe {
//...
})
}

qt_core提供了方便使用信号和槽的 API。您可以将内置信号连接到内置插槽,如下所示:

1
2
let mut timer = QTimer::new_0a();
timer.timeout().connect(app.slot_quit());

您还可以将信号连接到 Rust 闭包(参见 basic_form 示例

1
2
let button_clicked = SlotNoArgs::new(NullPtr, move || { ... });
button.clicked().connect(&button_clicked);

您还可以创建和发出信号:

1
2
3
4
5
use qt_core::{QTimer, SignalOfInt};
let signal = SignalOfInt::new();
let timer = QTimer::new_0a();
signal.connect(timer.slot_start());
signal.emit(100);

在编译时检查信号和槽参数的兼容性。

请注意,每组参数类型都需要一个单独的 Rust 类型的信号或槽(例如SlotNoArgsSlotOfInt等)。其他 crate 也可能提供新的信号和槽对象(例如qt_widgets::SlotOfQTreeWidgetItem)。

QString::from_std_strQString::to_std_stringQByteArray::from_sliceimpl<'a> From<&'a QString> for String提供了从 Qt 类型到 Rust 类型的转换。

QFlags泛型类型模仿 C++ 的QFlags类的功能。

qdbg函数 fromqt_core将可打印的(带有QDebug)Qt 对象包装到实现 Rust 的 shim 对象中fmt::Debug

智能指针

cpp_core crate提供了智能指针,以使使用 Rust 中的 C++ 对象更容易:

  • CppBox: 拥有,非空(对应于通过值传递的 C++ 对象或语义上拥有该对象的指针)
  • Ptr: 可能拥有,可能为 null(对应于 C++ 指针)
  • Ref: 不拥有,非空(对应于 C++ 引用)

与 Rust 引用不同,这些指针可以自由复制,生成指向同一对象的多个可变指针,这在使用 C++ 库时通常是必需的。

qt_core为使用提供额外的特殊指针QObject:

  • QPtr: 不拥有,可自动为空
  • QBox: 拥有,除非有父级,可自动为空

所有这些智能指针都有用于转换 ( static_upcast, static_downcast, dynamic_cast) 和创建迭代器 ( iter, iter_mut) 的方法。它们还通过转发到相应的 C++ 运算符来实现运算符特征。

不安全

将 Rust 的安全性自动带入 C++ API 是不可能的,因此大多数生成的 API 使用起来不安全,需要用 C++ 术语进行思考。大多数生成的函数都是不安全的,因为不能保证原始指针是有效的,并且大多数函数会取消引用一些指针。

建议在模块中包含不安全的用法,并为项目所需的 API 部分实现安全接口。

使用 Qt 对象时应该小心。Qt 有自己的所有权系统,必须得到尊重。如果您保留指向另一个对象拥有的对象的指针,则可以将其删除,并且在尝试访问已删除的对象时可能会产生未定义的行为。

另一方面,C++ 并不要求可变访问是独占的,因此有时允许在有其他可变指针指向它时改变对象。ritual提供的智能指针类型让您可以方便地做到这一点。

部署

官方安装程序、Linux 存储库和 brew 中可用的所有(或大多数)Qt 构建都是共享库或框架。这意味着使用这些库构建的任何可执行文件都将依赖于 Qt,并且如果最终用户的系统上不存在 Qt,则不会运行。

可以静态构建 Qt,这样您就可以构建独立的可执行文件,但这是一个更复杂的过程。在 Windows上消除对vc-redist动态库的依赖也很难做到。创建包含所有必需文件的目录的使用macdeployqt和工具要容易得多。windeployqtRust-Qt crates 不支持链接到静态 Qt 构建。

Rust-Qt 生成的可执行文件与 C++ 编译器生成的普通可执行文件非常相似,因此部署过程与部署 C++/Qt 应用程序没有区别。您可以使用官方 Qt 部署指南:

对于 Windows,基本思想是将可执行文件复制到新目录并运行windeployqt以使用 Qt 所需的所有文件填充它。请注意,Visual Studio 生成的可执行文件依赖于 Visual C++ Redistributable。windeployqt会将安装程序复制vc_redist.x64.exe到您的目标目录,并且您的安装程序应运行该目录以确保该库的正确版本在最终用户的系统上可用。

Linux 上的一种常见方法是声明您的包依赖于 Qt 库,并且只在包中包含您的可执行文件。系统的包管理器将确保安装 Qt 包。有关详细说明,请参阅目标 Linux 发行版的文档。

脚本进行QT导出依赖库

这里对linux多说一点,使用脚本进行QT导出依赖库。

创建导出依赖库的脚本,如lddd.sh

1
2
3
4
5
#!/bin/sh  
exe=$1
des=$2
deplist=$(ldd $exe | awk '{if (match($3,"/")){ printf("%s "),$3 } }')
cp $deplist $des

exe 表示执行文件的名称
des 导出依赖库的目标路径

1
sudo ./lddd.sh Hello "/home/landv/QT/qtProject/"