127 个稳定版本
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.5.10 | 2020 年 11 月 16 日 |
71 在 编程语言 中
919,538 每月下载量
在 346 个库中使用(通过 cxx)
6KB
CXX — Rust 和 C++ 之间的安全 FFI
此库提供了一种从 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 以获取教程、参考资料和示例代码。
概述
我们的想法是在一个 Rust 模块中定义 FFI 边界两边的签名,这些签名被嵌入在一起(下一节将展示一个示例)。基于此,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应用程序,该程序希望利用现有的C++客户端来利用大型文件blobstore服务。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的Box
或UniquePtr
。可以是类型别名,用于任意复杂的通用语言特定类型,具体取决于您的用例。 -
函数 — 在任一语言中实现,可从另一语言调用。
在CXX桥接的extern "Rust"
部分,我们列出了Rust作为真实来源的类型和函数。这些都隐式地引用了super
模块,即CXX桥接的父模块。你可以把上面列出的两个示例看作是use super::MultiBuf
和use 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模块中再次输入。尽管编译时断言保证了它们保持同步,但这与bindgen和cbindgen不同,其中函数签名由人工输入一次,工具在一个语言中消费它们,并在另一个语言中输出。
这是因为CXX扮演着稍微不同的角色。在某种程度上,它比bindgen或cbindgen更低级;你可以把它看作是我们所知的extern "C"
签名的替代品,而不是bindgen的替代品。在CXX之上构建一个高级的bindgen-like工具是合理的,该工具消费C++头文件和/或Rust模块(和/或类似于Thrift的IDL)作为真实来源,并生成cxx::bridge,消除重复,同时利用CXX的静态分析安全保证。
但请注意,在其他方面,CXX比bindgens更高级,它对常见的标准库类型提供了丰富的支持。经常在使用bindgen处理惯用的C++ API时,我们最终会手动将API包装在C-style原始指针函数中,应用bindgen以获取不安全的原始指针Rust函数,并再次复制API以在Rust中以惯用方式公开这些。这是一种更糟糕的重复形式,因为它是全程不安全的。
通过使用CXX桥接作为两种语言之间的共同理解,而不是使用extern "C"
C-style签名作为共同理解,常见的FFI用例可以使用100%安全的代码表达。
也可以混合使用,对于95%的FFI使用CXX桥接,因为这部分比较简单,而对于剩下的少量奇特的签名,可以按照老方法使用bindgen和cbindgen进行,如果CXX的静态限制阻碍了某些原因。如果您最终采取这种方法,请提交一个问题,以便我们知道如何使工具更具表现力。
基于Cargo的设置
对于由Cargo编排的构建,您将使用一个运行CXX的C++代码生成器的构建脚本,并编译生成的C++代码以及您的crate中的任何其他C++代码。
标准的构建脚本如下。指示的行返回一个cc::Build
实例(来自常用广泛的cc
crate),您可以在其上设置任何额外的源文件和编译器标志,就像平常一样。
# 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-cmd
crate打包,也可以从本仓库的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的一个长期存在的bug,会导致看起来完全正确的代码出现段错误(rust-lang/rust-bindgen#778)。CXX知道这一点,可以在需要的地方透明地插入必要的零成本解决方案,所以您可以放心地按值传递结构体。这是通过拥有边界两侧而不是只有一侧来实现的。
-
模板实例化:例如,为了在Rust中暴露由真实的C++ unique_ptr支持的UniquePtr<T>类型,我们有一种使用Rust特连接到另一语言执行的模板实例化的方法。
内置类型
除了所有原始类型(i32 <=> int32_t)外,以下常用类型可以用在共享结构体的字段以及函数的参数和返回值中。
Rust中的名称 | C++中的名称 | 限制 |
---|---|---|
String | rust::String | |
&str | rust::Str | |
&[T] | rust::Slice<const T> | 无法持有不透明的C++类型 |
&mut [T] | rust::Slice<T> | 无法持有不透明的C++类型 |
CxxString | std::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 T | T*,const T* | 使用原始指针参数的fn函数必须声明为unsafe才能调用 |
fn(T, U) -> V | rust::Fn<V(T, U)> | 目前只实现了从Rust到C++的传递 |
Result<T> | throw/catch | 仅作为返回类型允许 |
本仓库中的include/cxx.h文件定义了rust命名空间的C++ API。当与这些类型一起工作时,您需要在C++代码中包含此头文件。
以下类型预计“很快”将得到支持,但尚未实现。我不认为这些会有难题,但为每种类型设计一个合适的API是一个问题。
Rust中的名称 | C++中的名称 |
---|---|
BTreeMap<K, V> | tbd |
HashMap<K, V> | tbd |
Arc<T> | tbd |
Option<T> | tbd |
tbd | std::map<K, V> |
tbd | std::unordered_map<K, V> |
剩余工作
CXX还处于早期阶段;我将其作为最小可行产品发布,以收集关于方向的意见并邀请合作者。请检查开放问题。
特别是,如果您在构建或链接这些内容时遇到问题,请特别报告问题。我相信有方法可以使构建方面更加友好或更健壮。
最后,我对Rust库设计的了解比对C++库设计的了解更多,因此如果您有任何建议,我将非常欢迎帮助使本项目的C++ API更加地道。
许可证
根据您的选择,受Apache License,Version 2.0或MIT许可证的许可。有关更多信息,请参阅Apache License, Version 2.0或MIT许可证。除非您明确声明,否则根据Apache-2.0许可证定义,您有意提交给本项目并由您提供的任何贡献将双重许可如上所述,不附加任何额外的条款或条件。
lib.rs
:
此crate是cxx和cxx-build crate的实现细节,不公开任何公共API。