- 开发工具
【免费下载链接】cxx
Safe interop between Rust and C++
本文是 CXX 官方文档 book/src/shared.md 的深度解读与源码级扩展。共享类型(shared types)是 CXX 安全 FFI 体系中最核心的机制之一:它允许一个数据结构同时被 Rust 与 C++ 双方看到内部字段,并且可以按值跨越语言边界传递。读完本文,你将掌握共享结构体与枚举的完整写法、生成代码的形态、判别值(discriminant)推断规则、extern enum 静态断言机制、derive 行为以及对齐控制,并结合 syntax 目录下的编译器实现源码理解每条规则背后的校验逻辑。
什么是共享类型:与不透明类型的本质区别
在 CXX 中,FFI 边界上的语言是"共享"的还是"不透明"的,决定了类型的可见性(详见 核心概念):
- 共享结构体 / 共享枚举(shared structs & enums):字段对双方语言可见,定义通常以
cxx::bridge模块中的 Rust 声明为唯一事实来源(single source of truth)。 - 不透明 Rust 类型 / 不透明 C++ 类型(opaque types):字段对另一方保密,只能通过引用
&、RustBox或 C++unique_ptr等间接方式传递。
共享类型与不透明类型最关键的差异体现在两个层面:
- 内部可见性:只有共享类型能让双方都看到字段。文档原文的定义是:"Shared types enablebothlanguages to have visibility into the internals of a type."
- 按值传递能力:不透明类型不能按值跨边界传递,而FFI bridge 允许共享类型按值传入和返回。例如函数可以写成
fn deck() -> Vec<PlayingCard>,其中PlayingCard作为共享结构体可以整体按值返回。
另外有一个对使用体验影响很大的设计点:共享类型在 bridge 模块中的书写顺序不重要。C++ 是顺序敏感的语言,但 CXX 会对类型做拓扑排序(topological sort)并自动前向声明(forward-declare)所需类型。这一能力在源码层面由语法分析后的排序逻辑支撑(参见 syntax/toposort.rs),使用者无需像手写头文件那样小心翼翼地安排声明顺序。
声明共享结构体与枚举
在#[cxx::bridge]模块中直接书写struct和enum,即得到一个共享类型。以下取自官方文档的示例演示了一个扑克牌数据结构:PlayingCard结构体包含一个Suit枚举字段和一个u8数值字段,随后在unsafe extern "C++"中声明两个操作它的函数:
#[cxx::bridge] mod ffi { struct PlayingCard { suit: Suit, value: u8, // A=1, J=11, Q=12, K=13 } enum Suit { Clubs, Diamonds, Hearts, Spades, } unsafe extern "C++" { fn deck() -> Vec<PlayingCard>; fn sort(cards: &mut Vec<PlayingCard>); } }需要注意的一个限制:对于枚举,目前只支持 C 风格(即单元变体 unit variants)。带字段的数据枚举(data enum)在 CXX 中是不被允许的,这一点在 UI 测试用例 tests/ui/data_enums.rs 中有专门验证——它会产生编译错误并输出对应的.stderr诊断信息(tests/ui/data_enums.stderr)。
生成的 C++ 与 Rust 数据结构形态
C++ 侧:聚合初始化兼容的结构体
共享结构体会编译成一个与聚合初始化(aggregate initialization)兼容的 C++ 结构体。也就是说,你可以用PlayingCard card = {Suit::Hearts, 12};这样的花括号初始化语法直接构造它。以上面的定义为例,生成的 C++ 头文件大致为:
// generated header struct PlayingCard final { Suit suit; uint8_t value; }; enum class Suit : uint8_t { Clubs = 0, Diamonds = 1, Hearts = 2, Spades = 3, };观察两点:struct被标记为final,字段按声明顺序排列;Suit变成一个enum class,其底层整数类型由 CXX 自动选择一个"足够大"的类型(此处为uint8_t,推断规则见下文"枚举判别值"一节)。
Rust 侧:#[repr(transparent)]包装枚举
C++ 标准允许enum class持有不属于任何已列变体的值(这不是未定义行为)。为了与这一语义兼容,CXX 在 Rust 侧并不生成原生enum,而是生成一个透明的包装结构体,把底层整数放在公开的repr字段中:
#[derive(Copy, Clone, PartialEq, Eq)] #[repr(transparent)] pub struct Suit { pub repr: u8, } #[allow(non_upper_case_globals)] impl Suit { pub const Clubs: Self = Suit { repr: 0 }; pub const Diamonds: Self = Suit { repr: 1 }; pub const Hearts: Self = Suit { repr: 2 }; pub const Spades: Self = Suit { repr: 3 }; }这意味着:
- 每个变体被生成为
pub const关联常量,数值与 C++ 侧enum class的判别值完全一致; - 在 Rust 代码中你可以自由地把枚举当作整数使用——通过公开的
repr字段读取或构造任意值; match模式匹配仍然可用,但必须书写通配符分支(_)来处理"值不属于任何已列变体"的情况:
fn main() { let suit: Suit = /*...*/; match suit { Suit::Clubs => ..., Suit::Diamonds => ..., Suit::Hearts => ..., Suit::Spades => ..., _ => ..., // fallback arm } }这一点与原生 Rust 枚举的穷尽性检查完全不同,是从 C++ 侧传来的任意值在 Rust 侧必须面对的现实,务必在代码审查时注意。
带生命周期的共享结构体:生命周期在 C++ 侧被擦除
如果共享结构体带有泛型生命周期参数,这些生命周期不会在 C++ 侧有任何表示。C++ 侧得到的只是一个普通的、持有借用数据的结构体:
#[cxx::bridge] mod ffi { struct Borrowed<'a> { flags: &'a [&'a str], } }// generated header struct Borrowed final { rust::Slice<const rust::Str> flags; };&'a [&'a str]在 C++ 侧变成了rust::Slice<const rust::Str>:切片对应rust::Slice,&str对应rust::Str(这两个类型由 include/cxx.h 提供)。由于生命周期在 C++ 侧被擦除,C++ 代码在处理借用数据时需要像往常一样自行保证借用关系的安全("C++ code will need care when working with borrowed data, as usual in C++")。
枚举判别值(discriminants):显式指定、自动推断与repr覆盖
显式判别值
你可以为部分或全部变体提供显式判别值,这些数值会被原样传播到生成的 C++enum class中:
#[cxx::bridge] mod ffi { enum SmallPrime { Two = 2, Three = 3, Five = 5, Seven = 7, } }隐式判别值的规则
- 未显式指定判别值的变体,被赋值为"前一个判别值 + 1";
- 如果第一个变体没有显式判别值,它被赋值为 0。
这两条规则在编译器源码 syntax/discriminant.rs 的insert_next中实现:previous为None时返回Discriminant::zero(),否则在Sign::Positive(正数)分支中执行magnitude += 1;对负数判别值则从负方向递减(magnitude -= 1,到零后翻转为正号)。同样的文件中还定义了判别值溢出检查:当增量越过u64::MAX时会报告"discriminant overflow on value after ..."错误。
默认底层类型的自动推断
默认情况下,CXX 会为枚举选择能容纳所有判别值(无论显式还是隐式)的最小整数类型。推断逻辑位于 syntax/discriminant.rs 的inferred_repr:把所有已收集的判别值取最小值和最大值,然后遍历一张按范围从小到大排列的表LIMITS,找到第一个能同时装下min与max的类型。这张表共 8 个候选类型:
| 候选底层类型 | 范围 |
|---|---|
u8 | 0 ..= 255 |
i8 | -128 ..= 127 |
u16 | 0 ..= 65535 |
i16 | -32768 ..= 32767 |
u32 | 0 ..= 2³²-1 |
i32 | -2³¹ ..= 2³¹-1 |
u64 | 0 ..= 2⁶⁴-1 |
i64 | -2⁶³ ..= 2⁶³-1 |
可见候选类型覆盖了从u8到i64的全部有符号/无符号组合。如果判别值(如负数)导致任何候选类型都装不下,inferred_repr会报错"these discriminant values do not fit in any supported enum repr type"。此外,在收集过程中若发现某个显式判别值超出已推断类型的范围(例如先写了5u8之后又出现一个更大值),insert 也会立即报出"discriminant value ... is outside the limits of ..."错误。
用#[repr(...)]覆盖底层类型
如果你出于 ABI 对齐、与既有 C 头文件一致等原因需要不同的底层表示,可以显式提供#[repr(...)]属性(支持u8/i8/u16/i16/u32/i32/u64/i64/usize/isize,见 syntax/repr.rs 与 syntax/atom.rs 的解析逻辑):
#[cxx::bridge] mod ffi { #[repr(i32)] enum Enum { Zero, One, Five = 5, Six, } }// generated header enum class Enum : int32_t { Zero = 0, One = 1, Five = 5, Six = 6, };这里Five = 5之后的Six被隐式赋值为 6(前一个判别值加 1),底层类型被强制指定为int32_t。
值得注意的是,在 syntax/discriminant.rs 的expr_to_discriminant中,判别值还支持带整数后缀的写法(如Two = 2u8),后缀会被解析为对应的Atom并参与底层类型的推断;而不支持非整数字面量表达式——此时报错"enums with non-integer literal discriminants are not supported yet"(对应 UI 测试 tests/ui/non_integer_discriminant_enum.rs)。另外在 syntax/check.rs 的check_api_enum中还有一条规则:没有任何变体且未显式提供#[repr(...)]的枚举是不允许的(报错"explicit #[repr(...)] is required for enum without any variants")。
Extern enums:以既有 C++ 定义为准的枚举
如果你需要互操作一个已经存在、以既有 C++ 定义为事实来源的枚举,做法是:先让那个 C++ 定义通过某个include!进入 bridge,然后把这个枚举额外声明为 extern C++ 类型:
#[cxx::bridge] mod ffi { enum Enum { Yes, No, } extern "C++" { include!("path/to/the/header.h"); type Enum; } }CXX 能识别这种模式(同一名称既在 bridge 内声明为共享枚举、又在extern "C++"中被声明为类型),其行为会发生质的改变:
- 不再生成该枚举的 C++ 定义(因为定义已经存在于
header.h中); - 取而代之,生成C++ 静态断言(static assertions),逐一校验你在 Rust 侧写的变体名、判别值和整数表示与既有 C++ 枚举定义完全一致。
也就是说,Rust 侧的声明变成了对 C++ 事实的一份"对照清单",任何不一致都会在编译期被静态断言捕获,而不是在运行期静默出错。这与文档 核心概念 中强调的"静态断言验证签名准确性"哲学一脉相承。
Extern enums 支持普通共享枚举的全部特性(显式判别值、repr),同样会被静态断言校验。运行时这两个定义在 ABI 上是同一份数据,因此可以安全地按值传递。
Derives:一份derive同时作用于两种语言
在 CXX bridge 模块内,derive(...)支持以下标准 trait(完整支持清单见 syntax/derive.rs 的Trait枚举):
CloneCopyDebugDefaultEqHashOrdPartialEqPartialOrdBitAnd(仅枚举)BitOr(仅枚举)BitXor(仅枚举)
特别提醒:共享枚举会自动获得Copy、Clone、Eq、PartialEq的实现(因为生成的 Rust 表示是#[derive(Copy, Clone, PartialEq, Eq)]的透明结构体),所以你在枚举上完全可以省略这四个 derive。
#[cxx::bridge] mod ffi { #[derive(Clone, Debug, Hash)] struct ExampleStruct { x: u32, s: String, } #[derive(Hash, Ord, PartialOrd)] enum ExampleEnum { Yes, No, } }这些 derive天然同时作用于 Rust 数据类型和对应的 C++ 数据类型,在 C++ 侧的具体映射如下:
Hash→ 在 C++ 中生成std::hash<T>的模板特化(template <> struct std::hash<T>),使该类型可被用于std::unordered_map等哈希容器;PartialEq→ 生成operator==和operator!=;PartialOrd→ 生成operator<、operator<=、operator>、operator>=;BitAnd→ 生成operator&;BitOr→ 生成operator|;BitXor→ 生成operator^。
在 syntax/check.rs 的类型检查阶段,derive 的合法性也被严格把关:
BitAnd/BitOr/BitXor用在结构体上会报错("derive(...) is currently only supported on enums, not structs");- 枚举上的
derive(Default)要求恰好有一个变体被标记为#[default],否则报错; derive(ExternType)不允许用在共享结构体/枚举上。
(注:Trait枚举中还包含Serialize、Deserialize、ExternType等成员,它们服务于 serde 派生与不透明类型的其他场景,不属于共享类型的标准文档范围,此处仅作提示。)
使用示例:
#[cxx::bridge] mod ffi { #[derive(Clone, Debug, Hash)] struct ExampleStruct { x: u32, s: String, } #[derive(Hash, Ord, PartialOrd)] enum ExampleEnum { Yes, No, } }Alignment:用repr(align(...))控制对齐
属性repr(align(…))为共享结构体设置最小所需对齐(minimum required alignment)。对齐值必须是2 的幂,且范围在 2⁰(=1)到 2¹³(=8192)之间。在 C++ 侧,这会变成一个alignas说明符。
对齐值的合法性校验在 syntax/repr.rs 的Repr::parse中完成:非 2 的幂报"invalid repr(align) attribute: not a power of two";大于 2¹³ 报"invalid repr(align) attribute: larger than 2^13";且不接受算术表达式(如repr(align(2 + 2))),只接受整数字面量(报错"an arithmetic expression is not supported")。
#[cxx::bridge] mod ffi { #[repr(align(4))] struct ExampleStruct { b: [u8; 4], } }这一能力对于与 SIMD 数据、内存池或外部硬件缓冲区的对齐约束对接非常实用。相关 UI 测试可参考 tests/ui/struct_align.rs 与 tests/ui/repr_align_suffixed.rs。
实践要点:哪些写法会被编译器拒绝
综合 syntax/check.rs 与各 UI 测试,编写共享类型时最容易踩的坑如下:
| 写法 | 编译结果 |
|---|---|
| 共享结构体没有任何字段 | 报错"structs without any fields are not supported" |
枚举无任何变体且无#[repr(...)] | 报错"explicit #[repr(...)] is required for enum without any variants" |
| 枚举带非整数字面量判别值 | 报错"enums with non-integer literal discriminants are not supported yet" |
判别值超出已指定repr的范围 | 报错"discriminant value ... is outside the limits of ..." |
| 判别值整体超出 8 种候选类型范围 | 报错"these discriminant values do not fit in any supported enum repr type" |
repr(align)非 2 的幂或大于 2¹³ | 报错"invalid repr(align) attribute: ..." |
| 结构体字段按值使用未定长类型(如不透明类型) | 报错"using ... by value is not supported" |
BitAnd/BitOr/BitXor用于结构体 | 报错"derive(...) is currently only supported on enums, not structs" |
另外,共享类型(包括结构体与枚举)不允许使用Box、UniquePtr、Vec、str等保留名,也不允许与i32这类原子类型同名(check_reserved_name的逻辑见 syntax/check.rs)。命名的实际约束还有 UI 测试 tests/ui/reserved_name.rs 佐证。
结语
共享类型是 CXX 在"让两种语言看到同一份数据"这一目标上的核心答案:结构体以聚合初始化兼容的形态出现在 C++ 侧,枚举以"底层整数 + 透明包装"的形态同时满足 C++enum class的非穷尽语义与 Rust 的类型安全;判别值的自动最小类型推断、repr覆盖、extern enum 的静态断言、双语言 derive 与对齐控制,则共同把这一机制打造成一套既可表达、又被编译器严格校验的安全方案。若想继续深入,官方文档 共享类型 是权威起点,配套的 核心概念、attributes 页面以及 syntax 目录下的解析与检查源码、tests/ui 目录下每个.rs+.stderr配对的反例测试,都是值得反复对照的学习材料。
- 开发工具
【免费下载链接】cxx
Safe interop between Rust and C++
相关推荐
CXX 共享类型(Shared Types)实战指南:在 Rust 与 C++ 之间复用结构体与枚举
CXX 共享类型(Shared Types)实战指南:在 Rust 与 C++ 之间复用结构体与枚举 本指南以 comprehensive rust 课程 An
文档教程mediasoup-types 完全指南:解读 mediasoup Rust crate 的类型定义与共享数据结构
mediasoup types 完全指南:解读 mediasoup Rust crate 的类型定义与共享数据结构 mediasoup types 是 medi
后端音视频comprehensive-rust 教程:CXX 桥接中的共享枚举(Shared Enums)——Rust 与 C++ 互操作枚举声明与代码生成原理
comprehensive rust 教程:CXX 桥接中的共享枚举(Shared Enums)——Rust 与 C++ 互操作枚举声明与代码生成原理 共享枚举
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考