Zngur 指南:Rust 与 C++的跨界桥梁

文摘   2024-08-18 00:42   江苏  

在代码的江湖中,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.cppmain.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 是主导的项目中,使用构建脚本更好。将 zngurcc 添加到你的构建依赖中:

[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::Inventorycrate::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 的 ResultOption。我们可以控制方法的签名,并为引用使用适当的生命周期和可变性。在可变性的情况下,Rust 的可变性意味着独占性,这可能太限制了,我们可能想考虑 C++ 类型的内部可变性。我们也可以添加可空性 Option 或使函数 unsafe。我们可以选择 Rusty 风格的函数名(比如 newlen)或者将功能放在适当的 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


编程悟道
自制软件研发、软件商店,全栈,ARTS 、架构,模型,原生系统,后端(Node、React)以及跨平台技术(Flutter、RN).vue.js react.js next.js express koa hapi uniapp Astro
 最新文章