ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CXX 共享类型(Shared types)完全指南:让 Rust 与 C++ 双方共享结构体与枚举的定义

CXX 共享类型(Shared types)完全指南:让 Rust 与 C++ 双方共享结构体与枚举的定义 开发工具【免费下载链接】cxxSafe interop between Rust and C项目地址https://gitcode.com/gh_mirrors/cx/cxx点击查看免费下载本文是 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或 Cunique_ptr等间接方式传递。共享类型与不透明类型最关键的差异体现在两个层面内部可见性只有共享类型能让双方都看到字段。文档原文的定义是Shared types enablebothlanguages to have visibility into the internals of a type.按值传递能力不透明类型不能按值跨边界传递而FFI bridge 允许共享类型按值传入和返回。例如函数可以写成fn deck() - VecPlayingCard其中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, // A1, J11, Q12, K13 } enum Suit { Clubs, Diamonds, Hearts, Spades, } unsafe extern C { fn deck() - VecPlayingCard; fn sort(cards: mut VecPlayingCard); } }需要注意的一个限制对于枚举目前只支持 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 Borroweda { flags: a [a str], } }// generated header struct Borrowed final { rust::Sliceconst rust::Str flags; };a [a str]在 C 侧变成了rust::Sliceconst rust::Str切片对应rust::Slicestr对应rust::Str这两个类型由 include/cxx.h 提供。由于生命周期在 C 侧被擦除C 代码在处理借用数据时需要像往常一样自行保证借用关系的安全C code will need care when working with borrowed data, as usual in C。枚举判别值discriminants显式指定、自动推断与repr覆盖显式判别值你可以为部分或全部变体提供显式判别值这些数值会被原样传播到生成的 Cenum 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 个候选类型候选底层类型范围u80 .. 255i8-128 .. 127u160 .. 65535i16-32768 .. 32767u320 .. 2³²-1i32-2³¹ .. 2³¹-1u640 .. 2⁶⁴-1i64-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::hashT的模板特化template struct std::hashT使该类型可被用于std::unordered_map等哈希容器PartialEq→ 生成operator和operator!PartialOrd→ 生成operator、operator、operator、operatorBitAnd→ 生成operatorBitOr→ 生成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 typerepr(align)非 2 的幂或大于 2¹³报错invalid repr(align) attribute: ...结构体字段按值使用未定长类型如不透明类型报错using ... by value is not supportedBitAnd/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 侧枚举以底层整数 透明包装的形态同时满足 Cenum class的非穷尽语义与 Rust 的类型安全判别值的自动最小类型推断、repr覆盖、extern enum 的静态断言、双语言 derive 与对齐控制则共同把这一机制打造成一套既可表达、又被编译器严格校验的安全方案。若想继续深入官方文档 共享类型 是权威起点配套的 核心概念、attributes 页面以及 syntax 目录下的解析与检查源码、tests/ui 目录下每个.rs.stderr配对的反例测试都是值得反复对照的学习材料。赞分享开发工具【免费下载链接】cxxSafe interop between Rust and C项目地址https://gitcode.com/gh_mirrors/cx/cxx点击查看免费下载相关推荐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 互操作枚举声明与代码生成原理 共享枚举文档教程上一篇网页视频下载难开源资源嗅探扩展实战攻略四步上手M3U8流媒体也能拿下下一篇三步实现网盘免客户端高速下载网盘直链下载助手油猴脚本完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表