#cpp #api-bindings

sys no-std cxx

Rust和C++之间的安全互操作

169个版本 (稳定版)

1.0.126 2024年8月15日
1.0.124 2024年6月14日
1.0.120 2024年3月23日
1.0.111 2023年12月17日
0.0.0 2019年12月28日

#4FFI

Download history 262494/week @ 2024-05-03 274943/week @ 2024-05-10 259167/week @ 2024-05-17 242583/week @ 2024-05-24 269604/week @ 2024-05-31 240689/week @ 2024-06-07 242117/week @ 2024-06-14 246033/week @ 2024-06-21 220132/week @ 2024-06-28 217174/week @ 2024-07-05 254751/week @ 2024-07-12 257938/week @ 2024-07-19 228266/week @ 2024-07-26 214058/week @ 2024-08-02 230038/week @ 2024-08-09 175314/week @ 2024-08-16

每月894,041次下载
342 个crates中(178个直接使用)

MIT/Apache

300KB
4K SLoC

Rust 3K SLoC // 0.0% comments C++ 661 SLoC // 0.0% comments Bazel 200 SLoC // 0.0% comments JavaScript 94 SLoC Shell 11 SLoC Handlebars 7 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.67+和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()


示例

在这个示例中,我们编写了一个希望利用现有C++客户端的大文件blobstore服务的Rust应用程序。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为事实来源。这些都隐式地引用了super模块,即CXX桥接的父模块。您可以将上面示例中列出的两个条目视为类似于use super::MultiBufuse super::next_chunk,除了它们被重新导出到C++。父模块将直接包含简单事物的定义,或者包含将它们引入作用域的相关use语句。

extern "C++"部分,我们列出了类型和函数,这些类型和函数以C++为事实来源,以及声明这些API的头文件。将来,这个部分可能通过bindgen风格从头文件生成,但到目前为止,我们需要写出签名;静态断言将验证它们的准确性。

您的函数实现本身,无论是C++还是Rust,不需要被定义为extern "C" ABI或no_mangle。CXX将在必要时添加正确的垫片,以确保一切正常工作。


与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桥接,因为它简单直接,而对于剩下的少量奇特签名,则使用传统的bindgen和cbindgen方式,如果CXX的静态限制成为障碍。如果您采用这种方法,请提交一个问题,以便我们知道哪些改进会使工具更具表现力。


基于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++代码生成器的替代方法。该工具打包为crates.io上的cxxbridge-cmdcrate,也可以从该存储库的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,我们实现了这种可见性,并知道另一侧是什么。

  • 我们的静态分析可以检测并防止将不应按值传递的类型从C++传递到Rust,例如,因为它们可能包含会被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]const T 的 rust::Slice不能持有不透明的 C++ 类型
&mut [T]T 的 rust::Slice不能持有不透明的 C++ 类型
CxxStringstd::string不能按值传递
Box<T>T 的 rust::Box不能持有不透明的 C++ 类型
UniquePtr<T>std::unique_ptr<T>不能持有不透明的 Rust 类型
SharedPtr<T>std::shared_ptr<T>不能持有不透明的 Rust 类型
[T; N]std::array<T, N>不能持有不透明的 C++ 类型
Vec<T>T 的 rust::Vec不能持有不透明的 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仅作为返回类型允许

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

以下类型“很快”将得到支持,但尚未实现。我不认为这些类型有任何难以实现的问题,但为每种类型设计一个良好的 API 是一个非原生语言的问题。

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

剩余工作

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

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

最后,我比 C++ 库设计更了解 Rust 库设计,因此我会很高兴得到任何人在此项目的 C++ API 中的建议。


许可

根据您的选择,此项目受 Apache License,版本 2.0 或 MIT 许可证的许可。
除非您明确表示,否则您根据 Apache-2.0 许可证定义的任何旨在包含在此项目中的贡献,都将如上所述双重许可,不附加任何额外条款或条件。

依赖关系

~0.3–0.9MB
~20K SLoC