7 个版本

0.0.7 2022年12月2日
0.0.6 2020年11月22日
0.0.4 2020年9月2日

#102FFI

每月 30 次下载

MIT/Apache

310KB
4.5K SLoC

Rust 3.5K SLoC // 0.0% comments C++ 708 SLoC // 0.0% comments Bazel 231 SLoC // 0.0% comments JavaScript 93 SLoC Shell 10 SLoC Handlebars 6 SLoC

CXX — Rust 和 C++ 之间安全的 FFI

github crates.io docs.rs build status

此库提供了一个 安全的 机制来从 Rust 调用 C++ 代码,并从 C++ 调用 Rust 代码,不受使用 bindgen 或 cbindgen 生成不安全的 C 风格绑定时可能出现的问题的影响。

但这并不意味着 100% 的 C++ 代码都是不安全的。在审计项目时,你将负责审计所有的 Rust 不安全代码和 所有的 C++ 代码。在此新模型下的核心安全主张是,仅审计 C++ 一侧就足以捕获所有问题,即 Rust 一侧可以 100% 安全。

[dependencies]
cxx = "1.0"

[build-dependencies]
cxx-build = "1.0"

编译器支持:需要 rustc 1.63+ 和 c++11 或更高版本
发行说明


指南

请参阅 https://cxx.rs 以获取教程、参考资料和示例代码。


概述

我们的想法是将我们 FFI 边界两边的签名定义在一起嵌入到一个 Rust 模块中(下一节将展示一个示例)。基于此,CXX 可以获得关于边界的完整视图,以对类型和函数签名进行静态分析,以维护 Rust 和 C++ 的不变性和要求。

如果一切检查无误,则 CXX 使用一对代码生成器,在两边的相关 extern "C" 签名上同时发出,以及在任何必要的静态断言,以在构建过程中的后续阶段验证正确性。在 Rust 一侧,此代码生成器只是一个属性过程宏。在 C++ 一侧,如果您的构建由 Cargo 管理,则可以是一个小的 Cargo 构建脚本,或者对于 Bazel 或 Buck 等其他构建系统,我们提供了一个命令行工具,该工具生成头文件和源文件,应该易于集成。

结果 FFI 桥接在零或可忽略的开销下运行,即无需复制、序列化、内存分配或运行时检查。

FFI签名可以使用任一端的本地类型,例如Rust的String或C++的std::string,Rust的Box或C++的std::unique_ptr,Rust的Vec或C++的std::vector等,以任何组合方式。CXX确保一个ABI兼容的签名,双方都理解,基于对关键标准库类型的内置绑定来暴露给另一语言的惯用API。例如,当从Rust操作C++字符串时,其len()方法变为调用C++定义的size()成员函数;当从C++操作Rust字符串时,其size()成员函数调用Rust的len()


示例

在这个例子中,我们编写了一个Rust应用程序,该程序希望利用现有的大文件blobstore服务的C++客户端。Blobstore支持对不连续缓冲区上传的put操作。例如,我们可能会上传循环缓冲区的快照,这些快照通常由2个块组成,或者由于某些原因在内存中分散的文件片段。

此示例的可执行版本位于此存储库的demo目录下。要尝试运行,请在该目录中运行cargo run

#[cxx::bridge]
mod ffi {
    // Any shared structs, whose fields will be visible to both languages.
    struct BlobMetadata {
        size: usize,
        tags: Vec<String>,
    }

    extern "Rust" {
        // Zero or more opaque types which both languages can pass around but
        // only Rust can see the fields.
        type MultiBuf;

        // Functions implemented in Rust.
        fn next_chunk(buf: &mut MultiBuf) -> &[u8];
    }

    unsafe extern "C++" {
        // One or more headers with the matching C++ declarations. Our code
        // generators don't read it but it gets #include'd and used in static
        // assertions to ensure our picture of the FFI boundary is accurate.
        include!("demo/include/blobstore.h");

        // Zero or more opaque types which both languages can pass around but
        // only C++ can see the fields.
        type BlobstoreClient;

        // Functions implemented in C++.
        fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
        fn put(&self, parts: &mut MultiBuf) -> u64;
        fn tag(&self, blobid: u64, tag: &str);
        fn metadata(&self, blobid: u64) -> BlobMetadata;
    }
}

现在我们只需提供extern "Rust"块中所有内容的Rust定义和extern "C++"块中所有内容的C++定义,并安全地进行相互调用。

以下是涉及此演示的完整源文件集合的链接

查看由CXX代码生成器在两种语言中为示例生成的代码的详细信息

   # run Rust code generator and print to stdout
   # (requires https://github.com/dtolnay/cargo-expand)
$ cargo expand --manifest-path demo/Cargo.toml

   # run C++ code generator and print to stdout
$ cargo run --manifest-path gen/cmd/Cargo.toml -- demo/src/main.rs

细节

如示例所示,FFI边界涉及3种类型的项

  • 共享结构 — 其字段对两种语言都可见。在cxx::bridge中编写的定义是单一事实来源。

  • 不可见类型 — 其字段对另一语言保密。这些类型不能通过值传递给FFI,而只能通过间接方式传递,例如引用&、Rust的BoxUniquePtr。可以是类型别名,用于任意复杂度的泛型语言特定类型,具体取决于您的使用情况。

  • 函数 — 在任一语言中实现,可从另一语言调用。

在CXX桥接的extern "Rust"部分,我们列出了Rust作为真实来源的类型和函数。这些类型和函数都隐式地引用了CXX桥接的父模块super。你可以将上面例子中的两个条目看作是类似于use super::MultiBufuse super::next_chunk,但它们被重新导出到C++。父模块将直接包含简单事物的定义,或者包含将它们引入作用域的相关use语句。

extern "C++"部分中,我们列出了C++作为真实来源的类型和函数,以及声明这些API的头文件。在未来,这个部分可能会像bindgen一样从头文件中生成,但到目前为止,我们需要编写签名;静态断言将验证它们是否准确。

你的函数实现,无论是C++还是Rust,不需要定义为extern "C" ABI或no_mangle。CXX将在必要时插入正确的shim来确保一切正常工作。


与bindgen和cbindgen的比较

请注意,在CXX中,所有函数签名都会重复:它们在实现定义的地方(在C++或Rust中)输入一次,然后在cxx::bridge模块中再次输入。尽管编译时断言保证了这些输入保持同步,但这与bindgencbindgen不同,在那里函数签名由人工输入一次,工具在一个语言中消耗它们,在另一个语言中输出。

这是因为在某种程度上,CXX扮演了一个不同的角色。它比bindgen或cbindgen更底层;你可以将其视为我们已知的外部C签名概念的一种替代,而不是bindgen的替代。在CXX之上构建一个更高层次的bindgen-like工具是合理的,该工具消耗C++头文件和/或Rust模块(和/或类似于Thrift的IDL)作为真实来源,并生成cxx::bridge,消除重复,同时利用CXX的静态分析安全保证。

但请注意,在其他方面,CXX比bindgens更高层次,具有对常见标准库类型的丰富支持。通常,当我们在处理惯用的C++ API时,我们会手动将该API包装在C风格的原始指针函数中,应用bindgen以获取不安全的原始指针Rust函数,并再次复制API以在Rust中以惯用方式公开。这是一种更糟糕的重复形式,因为它是整个过程中都不安全的。

通过使用CXX桥接作为语言之间的共同理解,而不是作为共享理解的C风格的外部签名,常见的FFI用例可以使用100%安全代码表达。

也可以合理地混合使用,对于您95%的FFI使用CXX桥接器,对于一些原因CXX的静态限制阻碍了,剩下的几个奇特的签名以传统方式使用bindgen和cbindgen进行。如果您最终采用这种方法,请提交一个问题,这样我们就可以知道哪些方法对使工具更具表现力是有价值的。


基于Cargo的设置

对于由Cargo编排的构建,您将使用一个构建脚本,该脚本运行CXX的C++代码生成器,并编译生成的C++代码以及您的crate中的任何其他C++代码。

标准的构建脚本如下。指定的行返回一个cc::Build实例(来自广泛使用的cccrate),您可以在其中设置任何额外的源文件和编译器标志。

# Cargo.toml

[build-dependencies]
cxx-build = "1.0"
// build.rs

fn main() {
    cxx_build::bridge("src/main.rs")  // returns a cc::Build
        .file("src/demo.cc")
        .std("c++11")
        .compile("cxxbridge-demo");

    println!("cargo:rerun-if-changed=src/main.rs");
    println!("cargo:rerun-if-changed=src/demo.cc");
    println!("cargo:rerun-if-changed=include/demo.h");
}

非Cargo设置

对于Bazel或Buck等非Cargo构建,CXX提供了一个作为独立命令行工具调用C++代码生成器的替代方法。该工具作为cxxbridge-cmdcrate打包在crates.io上,也可以从本仓库的gen/cmd目录构建。

$ cargo install cxxbridge-cmd

$ cxxbridge src/main.rs --header > path/to/mybridge.h
$ cxxbridge src/main.rs > path/to/mybridge.cc

安全性

请注意,这个库的设计是有意限制和有观点的!我们的目标不是足够强大以处理任意签名,而是在今天我们可以做出有用的安全保证的合理功能集合,并可能随着时间的推移而扩展。您可能会发现,有效地使用CXX桥接器需要一些练习,因为它不会以您习惯的方式工作。

确保安全性的考虑因素包括

  • 设计上,我们的成对代码生成器协同工作,控制FFI边界的两端。在Rust中,编写自己的extern "C"块通常是不安全的,因为Rust编译器不知道您编写的签名是否与另一语言中实现的签名匹配。有了CXX,我们达到了这种可见性并知道另一边是什么。

  • 我们的静态分析可以检测并防止传递不应该以值传递的方式传递的类型,例如,因为它们可能包含Rust的移动行为会破坏的内部指针。

  • 对于许多人来说,一个Rust中的结构体和一个C++中的结构体可以有完全相同的布局/字段/对齐/一切,但在值传递时仍然不相同的ABI。这是一个长期的bindgen错误,会导致看似完全正确的代码中的段错误(rust-lang/rust-bindgen#778)。CXX知道这一点,并在需要的地方透明地插入必要的零成本解决方案,因此您可以放心地以值传递方式传递结构体。这是通过拥有边界两侧而不是一侧来实现的。

  • 模板实例化:例如,为了在Rust中公开由真实的C++ unique_ptr支持的UniquePtr类型,我们有一种使用Rust特质将行为连接到另一语言执行的模板实例化的方式。


内置类型

除了所有原始类型(i32 <=> int32_t)之外,以下常见类型可以用于共享结构体的字段以及函数的参数和返回值。

Rust中的名称C++中的名称限制
Stringrust::String
&strrust::Str
&[T]rust::Slice<const T>不能包含不透明的C++类型
&mut [T]rust::Slice<T>不能包含不透明的C++类型
CxxStringstd::string不能通过值传递
Box<T>rust::Box<T>不能包含不透明的C++类型
UniquePtr<T>std::unique_ptr<T>不能持有不透明的Rust类型
SharedPtr<T>std::shared_ptr<T>不能持有不透明的Rust类型
[T; N]std::array<T, N>不能包含不透明的C++类型
Vec<T>rust::Vec<T>不能包含不透明的C++类型
CxxVector<T>std::vector<T>不能通过值传递,不能持有不透明的Rust类型
*mut T, *const TT*, const T*使用原始指针参数的函数必须声明为unsafe才能调用
fn(T, U) -> Vrust::Fn<V(T, U)>目前只实现了从Rust传递到C++
Result<T>throw/catch仅允许作为返回类型

本仓库中rust命名空间的C++ API由include/cxx.h文件定义。当您使用这些类型时,需要在您的C++代码中包含此头文件。

以下类型预计“很快”就会支持,但尚未实现。我不认为这些实现会很难,但这取决于为每种类型在其非原生语言中设计一个良好的API。

Rust中的名称C++中的名称
BTreeMap<K, V>tbd
HashMap<K, V>tbd
Arc<T>tbd
Option<T>tbd
tbdstd::map<K, V>
tbdstd::unordered_map<K, V>

剩余工作

CXX还处于早期阶段;我将其作为最小可行产品发布,以收集关于方向的反馈并邀请协作者。请检查开放问题。

特别是,如果您在构建或链接这些内容时遇到问题,请特别报告这些问题。我相信有办法使构建方面更友好或更健壮。

最后,我比C++库设计更了解Rust库设计,因此如果您有任何建议,我会很乐意得到帮助,使本项目的C++ API更具风格。


许可证

根据您的选择,受Apache License, Version 2.0或MIT许可协议的许可。
除非您明确声明,否则根据Apache-2.0许可协议定义的您提交给本项目的任何有意贡献,都将按上述方式双重许可,而无需任何附加条款或条件。

依赖关系

~0.3–1.8MB
~26K SLoC