在代码的江湖中,Rust 以其坚不可摧的安全性著称,而 C++则以其灵活多变的内功闻名。但在这个由 0 和 1 构成的数字世界里,总有一些侠客渴望打破界限,探索未知。Zngur,就是这样一位跨界的侠客,它携带着一把能够连接 Rust 与 C++的神奇钥匙。
想象一下,你是一位编程界的少侠,站在 C++的山巅之上,眺望着 Rust 的辽阔原野。你心中充满了向往,想要将 Rust 的绝世武功带入你的 C++江湖。但那道看似不可逾越的界限,却让你望而却步。直到 Zngur 出现,它轻轻一挥手中的钥匙,那界限便如烟雾般消散。
一个 Zngur 项目由三部分组成:
一个名为 main.zng
的接口定义语言(IDL)文件。一个 Rust crate(可以是任何类型,二进制文件、rlib、静态库、cdy-lib 等)。 一个 C++ 项目。
开始之前,安装 Zngur:
cargo install zngur-cli
然后使用 cargo init
生成一个新 staticlib crate 并在 Cargo.toml
中添加以下内容:
[lib]
crate-type = ["staticlib"]
创建一个空的 main.cpp
和 main.zng
文件。你的目录结构应该如下所示:
Basic structure of main.zng
设想我们想在 C++ 中使用这个库存:
struct Item {
name: String,
size: u32,
}
struct Inventory {
items: Vec<Item>,
remaining_space: u32,
}
impl Inventory {
fn new_empty(space: u32) -> Self {
Self {
items: vec![],
remaining_space: space,
}
}
// ...
}
将其复制到 src/lib.rs
。现在我们需要在 main.zng
文件中声明我们需要在 C++ 中访问的内容:
Zngur 需要知道桥接内类型的大小和对齐方式。你可以使用 rust-analyzer(通过悬停在结构体或类型别名上来查看)来找出这些信息,或者先填入一些随机数字,然后根据编译器的错误来修正。
注意:理想情况下 main.zng
文件应该是自动生成的,但我们还未能实现这一点。另外,Zngur 可以在没有显式大小和对齐的情况下工作(有一些限制),详见布局策略以获取更多详细信息。
现在,运行 zngur g ./main.zng
来生成 C++ 和 Rust 粘合文件。它将生成一个 ./generated.h
C++ 头文件和一个 ./src/generated.rs
文件。在 lib.rs
文件中添加 mod generated;
来包含生成的 Rust 文件。然后用以下内容填充 main.cpp
文件:
Zngur 将在 C++ 端将每个 Rust 项目及其完整路径添加到 rust
命名空间中,例如 String
将变为 rust::std::string::String
。
要构建它,你需要先使用 cargo build
构建 Rust 代码,这将在 ./target/debug
文件夹中生成 libyourcrate.a
,然后你可以通过链接到它来构建你的 C++ 代码:
为了确保一切正常工作,让我们给 Inventory
添加 #[derive(Debug)]
并使用 zngur_dbg
来查看它:
#include "./generated.h"
int main() {
auto inventory = rust::crate::Inventory::new_empty(1000);
}
clang++ main.cpp -g -L ./target/debug/ -l your_crate
如果一切顺利,执行程序后你应该看到类似这样的输出:
现在让我们向其中添加更多的方法:
impl Inventory {
fn add_item(&mut self, item: Item) {
self.remaining_space -= item.size;
self.items.push(item);
}
fn add_banana(&mut self, count: u32) {
for _ in 0..count {
self.add_item(Item {
name: "banana".to_owned(),
size: 7,
});
}
}
// ...
}
注意 add_banana
的返回类型是 ()
类型,所以我们也需要添加它。现在我们可以在 C++ 文件中使用它了:
#include "./generated.h"
int main() {
auto inventory = rust::crate::Inventory::new_empty(1000);
inventory.add_banana(3);
zngur_dbg(inventory);
}
[main.cpp:6] inventory = Inventory { items: [ Item { name: "banana", size: 7, }, Item { name: "banana", size: 7, }, Item { name: "banana", size: 7, }, ], remaining_space: 979, }
// ...
type crate::Item { #layout(size = 32, align = 8); } type crate::Inventory { // ... fn add_item(&mut self, crate::Item); }
但是仅仅这样我们还不能使用 add_item
,因为在 C++ 端没有办法获得 rust::crate::Item
。为了解决这个问题,我们需要为 Item
类型添加构造函数:
type crate::Item {
#layout(size = 32, align = 8);
constructor { name: ::std::string::String, size: u32 };
}
但这并没有解决问题,因为我们不能创建 String
,所以我们不能调用构造函数。为了创建 String
,我们声明了原始类型 str
及其 to_owned
方法:
这里有一些新东西。首先,由于 str
是原始类型,它不需要完整路径。然后是 wellknown_traits(?Sized)
而不是 #layout(size = X, align = Y)
,这告诉 Zngur 这个类型是未定大小的,它应该将其引用视为胖指针,并防止按值存储它。
现在你可能会想知道我们如何获得 &str
来从中创建 String
?幸运的是,Zngur 对原始类型有一些特殊支持,它有一个 rust::Str::from_char_star
函数,可以从以零结尾的有效 UTF8 char*
创建 &str
,与相同的生命周期。如果 Zngur 没有这个,我们可以通过导出其 from_raw_parts
然后将其转换为 &str
来创建 [u8]
,from_char_star
仅仅为了便利而存在。
所以现在我们终于可以使用 add_item
方法了:
type ::std::string::String {
#layout(size = 24, align = 8);
}
type crate::Item {
#layout(size = 32, align = 8);
constructor { name: ::std::string::String, size: u32 };
}
int main() {
auto inventory = rust::crate::Inventory::new_empty(1000);
inventory.add_banana(3);
rust::Ref<rust::Str> name = rust::Str::from_char_star("apple");
inventory.add_item(rust::crate::Item(name.to_owned(), 5));
zngur_dbg(inventory);
}
[main.cpp:8] inventory = Inventory { items: [ // ... ], remaining_space: 974, }
泛型类型
让我们尝试添加并桥接 into_items
方法:
Vec
是一个泛型类型,但使用它的语法并没有不同:
注意这只会带来 Vec<Item>
,对于使用 Vec<i32>
或 Vec<String>
或 Vec<SomethingElse>
你需要将它们各自单独添加。
现在你可以在 C++ 中使用 into_items
方法了:
impl Inventory {
fn into_items(self) -> Vec<Item> { self.items }
}
type ::std::vec::Vec<crate::Item> {
#layout(size = 24, align = 8);
wellknown_traits(Debug);
}
type crate::Inventory {
// ... fn into_items(self) -> ::std::vec::Vec<crate::Item>;
}
你可以在 examples/tutorial
查看完整代码。
从 Rust 调用 C++ C++/Rust 互操作有两个方面,没有互操作工具能不支持双方。这里,我们将执行上述任务的反向操作,交换 Rust 和 C++ 的规则。假设我们有这段 C++ 代码:
rust::std::vec::Vec<rust::crate::Item> v = inventory.into_items();
zngur_dbg(v);
[main.cpp:11] v = [ Item { name: "banana", size: 7, }, Item { name: "banana", size: 7, }, Item { name: "banana", size: 7, }, Item { name: "apple", size: 5, }, ]
#include <string>
#include <vector>
namespace cpp_inventory {
struct Item {
std::string name;
uint32_t size;
};
struct Inventory {
std::vector<Item> items;
uint32_t remaining_space;
Inventory(uint32_t space) : items(), remaining_space(space) {}
void add_item(Item item) {
remaining_space -= item.size;
items.push_back(std::move(item));
}
// ...
};
} // namespace cpp_inventory
创建一个新的 cargo 项目,这次是一个二进制项目,因为我们想要编写主函数让它在 Rust 中。将上述代码复制到 inventory.h
文件中。然后在 main.zng
文件中添加以下内容:
并在 main.rs
文件中添加这些:
这次我们将在 cargo 构建脚本中使用 Zngur 生成器。我们仍然可以使用 zngur-cli
,但在 cargo 是主导的项目中,使用构建脚本更好。将 zngur
和 cc
添加到你的构建依赖中:
[build-dependencies]
cc = "1.0"
zngur = "latest-version"
然后填写 build.rs
文件:
use std::{env, path::PathBuf};
use zngur::Zngur;
fn main() {
build::rerun_if_changed("main.zng");
build::rerun_if_changed("impls.cpp");
let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
Zngur::from_zng_file(crate_dir.join("main.zng"))
.with_cpp_file(out_dir.join("generated.cpp"))
.with_h_file(out_dir.join("generated.h"))
.with_rs_file(out_dir.join("generated.rs"))
.generate();
let my_build = &mut cc::Build::new();
let my_build = my_build.cpp(true).compiler("g++").include(&crate_dir).include(&out_dir);
let my_build = || my_build.clone();
my_build().file(out_dir.join("generated.cpp")).compile("zngur_generated");
my_build().file("impls.cpp").compile("impls");
}
现在我们有 crate::Inventory
和 crate::Item
,它们可以包含它们的 C++ 对应物。但是在 Rust 中没有办法使用它们。在 Zngur 中,Rust 端不能访问 C++ 不透明对象。所以要使这些类型在 Rust 中有用,我们可以在 C++ 中为这些类型添加 impl 块。在 main.zng
中添加这个:
并在 impls.cpp
中添加这段代码:
#include "generated.h"
#include <string>
using namespace rust::crate;
Inventory rust::Impl<Inventory>::new_empty(uint32_t space) {
return Inventory(rust::ZngurCppOpaqueOwnedObject::build<cpp_inventory::Inventory>(space));
}
rust::Unit rust::Impl<Inventory>::add_banana(rust::RefMut<Inventory> self, uint32_t count) {
self.cpp().add_banana(count);
return {};
}
rust::Unit rust::Impl<Inventory>::add_item(rust::RefMut<Inventory> self, Item item) {
self.cpp().add_item(item.cpp());
return {};
}
Item rust::Impl<Item>::new_(rust::Ref<rust::Str> name, uint32_t size) {
return Item(rust::ZngurCppOpaqueOwnedObject::build<cpp_inventory::Item>(cpp_inventory::Item{
.name = ::std::string(reinterpret_cast<const char*>(name.as_ptr()), name.len()),
.size = size
}));
}
这些函数看起来像是一些不必要的样板代码,但是编写它们有一些好处:
我们可以在这些函数中将 C++ 类型转换为 Rust 等效类型。例如,在 Item::new
中将指针和长度转换为切片,或将 &str
转换为 std::string
。我们可以将异常转换为 Rust 的 Result
或 Option
。我们可以控制方法的签名,并为引用使用适当的生命周期和可变性。在可变性的情况下,Rust 的可变性意味着独占性,这可能太限制了,我们可能想考虑 C++ 类型的内部可变性。我们也可以添加可空性 Option
或使函数 unsafe
。我们可以选择 Rusty 风格的函数名(比如 new
和 len
)或者将功能放在适当的 trait 中(例如实现 Iterator
trait 而不是公开 .begin
和 .end
函数)。
即使在支持直接调用 C++ 函数的工具中,人们经常最终会为这些原因编写 Rust 包装器。在 Zngur 中,那段代码就是包装器,它位于 C++ 中,所以它可以做任何 C++ 做的事情。
在 Rust 到 C++ 方面,我们使用了 zngur_dbg
宏来查看结果。我们将在这里做同样的事情,使用 dbg!
宏。要做到这一点,我们需要为 crate::Inventory
实现 Debug
trait。在 main.zng
中添加这个:
并在 impls.cpp
中添加这段代码:
// ...
type ::std::fmt::Result {
#layout(size = 1, align = 1);
constructor Ok(());
}
type ::std::fmt::Formatter {
#layout(size = 64, align = 8);
fn write_str(&mut self, &str) -> ::std::fmt::Result;
}
extern "C++" {
// ...
impl std::fmt::Debug for crate::Inventory {
fn fmt(&self, &mut ::std::fmt::Formatter) -> ::std::fmt::Result;
}
}
rust::std::fmt::Result rust::Impl<Inventory, rust::std::fmt::Debug>::fmt(rust::Ref<::rust::crate::Inventory> self, rust::RefMut<::rust::std::fmt::Formatter> f) {
::std::string result = "Inventory { remaining_space: ";
result += ::std::to_string(self.cpp().remaining_space);
result += ", items: [";
bool is_first = true;
for (const auto &item : self.cpp().items) {
if (!is_first) {
result += ", ";
} else {
is_first = false;
}
result += "Item { name: \"";
result += item.name;
result += "\", size: ";
result += ::std::to_string(item.size);
result += " }";
}
result += "] }";
return f.write_str(rust::Str::from_char_star(result.c_str()));
}
现在我们可以编写主函数:
并运行它:
fn main() {
let mut inventory = Inventory::new_empty(1000);
inventory.add_banana(3);
inventory.add_item(Item::new("apple", 5));
dbg!(inventory);
}
[examples/tutorial_cpp/src/main.rs:12] inventory = Inventory { remaining_space: 974, items: [ // ... ] }
你可以在 examples/tutorial_cpp
查看完整代码。
github 地址:
https://github.com/HKalbasi/zngur?tab=readme-ov-file#zngur