141 个版本

0.7.126 2024 年 8 月 15 日
0.7.124 2024 年 6 月 14 日
0.7.120 2024 年 3 月 23 日
0.7.111 2023 年 12 月 17 日
0.7.8 2020 年 11 月 30 日

FFI 中排名 第 25 位

Download history 17007/week @ 2024-05-03 27598/week @ 2024-05-10 20024/week @ 2024-05-17 16137/week @ 2024-05-24 24924/week @ 2024-05-31 23607/week @ 2024-06-07 12970/week @ 2024-06-14 5348/week @ 2024-06-21 3356/week @ 2024-06-28 4039/week @ 2024-07-05 3812/week @ 2024-07-12 3115/week @ 2024-07-19 3742/week @ 2024-07-26 3932/week @ 2024-08-02 3732/week @ 2024-08-09 4351/week @ 2024-08-16

每月下载量 16,271
用于 36 包(直接使用 4 个)

MIT/Apache 协议

320KB
9K 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++客户端的Rust应用程序,该客户端用于大型文件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;
    }
}

现在我们只是提供Rust中extern "Rust"块中所有内容的定义,以及C++中extern "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更低级;你可以把它看作是 extern "C" 签名的替代品,而不是bindgen的替代品。在CXX之上构建一个高级bindgen-like工具是合理的,该工具消耗C++头文件和/或Rust模块(和/或类似Thrift的IDL)作为真相来源,并生成 cxx::bridge,消除重复,同时利用CXX的静态分析安全保证。

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

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

也可以混合使用,使用CXX桥来处理95%的FFI,剩下的几个奇特的签名用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打包,或者可以从本repo的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++中的名称限制
字符串rust::字符串
&strrust::Str
&[T]rust::Slice不能持有不透明的C++类型
&mut [T]rust::Slice不能持有不透明的C++类型
CxxStringstd::字符串不能按值传递
Boxrust::Box不能持有不透明的C++类型
UniquePtrstd::unique_ptr不能持有不透明的Rust类型
SharedPtrstd::shared_ptr不能持有不透明的Rust类型
[T; N]std::array不能持有不透明的C++类型
Vecrust::Vec不能持有不透明的C++类型
CxxVectorstd::vector不能按值传递,不能持有不透明的Rust类型
*mut T, *const TT*, const T*带有原始指针参数的fn必须声明为unsafe才能调用
fn(T, U) -> Vrust::Fn目前只实现了从Rust到C++的传递
Resultthrow/catch仅作为返回类型允许

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

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

Rust中的名称C++中的名称
BTreeMaptbd
HashMaptbd
Arctbd
Optiontbd
tbdstd::map
tbdstd::unordered_map

剩余工作

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

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

最后,我对Rust库设计比C++库设计更了解,所以如果有人提出建议,我会很高兴得到帮助,使本项目的C++ API更符合惯例。


许可证

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

lib.rs:

C++代码构造和编译的CXX代码生成器。

这旨在作为将cxxcrate嵌入到高级代码生成器中的机制。请参阅dtolnay/cxx#235https://github.com/google/autocxx

依赖项

~0.7–8MB
~57K SLoC