diff --git a/.spec/decisions/0010-root-abi-runtime-unfrozen-semantics-seams.md b/.spec/decisions/0010-root-abi-runtime-unfrozen-semantics-seams.md new file mode 100644 index 0000000..34627df --- /dev/null +++ b/.spec/decisions/0010-root-abi-runtime-unfrozen-semantics-seams.md @@ -0,0 +1,43 @@ +# 0010 · Root ABI 运行时对「上游明确未冻结」的语义保持缺位,只做不透明相等校验,不自造判定键 + +- 日期:2026-08-29 +- 状态:生效 + +## 背景 + +LCE-P0-006(R-00020)要实现 `bind_root_api` 与 `RootApiTableView`。规格 §8.3 列出的运行时接口里有一项: + + pub fn supports(&self, capability: CapabilityId) -> bool; + +`CapabilityId` 在本仓就是 `lumio-core-contracts` 对架构源 ID Registry `Capability` namespace 的 1:1 派生(LCE-P0-003)。要实现它,必须回答「`capability_bits` 的第几位对应哪个 `CapabilityId`」。 + +但锁定架构基线 `LGE-V1.4-2026-08-27` 的 ADR-040「What this bundle deliberately does not freeze」把这个问题**显式列为未冻结**:V1 既没冻结 `lumio_root_api.capability_bits` 是 bitmask 还是计数,也没冻结任何位位置;ID Registry 的 `Capability` numeric 是**枚举序数而不是位位置**(与 WebSocket transport profile 被拒发 `Capability` id 同一理由)。ADR-040 给出的处置是一句禁令加一条出路: + +> A consumer must not derive a capability key from either source; a repository-private key is the only correct model until the semantics are confirmed. + +于是 §8.3 的这一项与锁定架构合同直接冲突:照 §8.3 写出来的任何 `supports()` 都必然是「本仓自造的位映射」,正是 ADR-040 禁止的那件事。 + +同族的第二处:单张 API table 的 `version` 期望值只发布在 `metadata/native-managed-abi.json` 与 bundle JSON 里,**没有任何 Rust 可消费的常量**;运行时发布闭包(规格 §3.7)里为读它引入 JSON 解析依赖不成比例。 + +本仓已有同型先例:`lumio-core-contracts` 对 §6.1 中上游尚未把本仓列入 `consumers` 的类型,选择「保持缺位、不建同名临时 struct」并在 crate 文档写明 seam。 + +## 决策 + +**上游明确声明「未冻结」的语义,本仓一律保持缺位;能验证的部分退到不需要该语义的最强判据。** + +1. **不提供 `RootApiTableView::supports(CapabilityId)`。** 不建同名临时方法、不造位映射、不用 alias 或本地枚举绕道。上游确认位语义后按独立需求卡补齐。 +2. **`capability_bits` 只做精确相等校验。** 绑定期把它当**不透明 u64**,与已发布值逐位全等才通过,不匹配映射 `NativeAbiMismatch`(1004)。相等比较不需要知道它是 bitmask 还是计数——这是在语义未冻结前唯一不含假设的判据;子集判定 `(actual & required) == required` 已经假设了 bitmask,不可用。消费方只能经 `capability_bits()` 拿到原值。 +3. **不校验单张 API table 的 `version`。** 绑定期读出并如实公开(`ApiTableView::version()`),但不与任何期望值比较;缺的是可消费真值,不是校验意愿。 +4. **两处缺位都必须是显式行为,不是遗漏。** crate 文档「与 §8.3 的两处偏差」逐条写明理由与出处;第 3 条另配守卫测试 + `table_version_is_surfaced_verbatim_and_not_asserted`——将来若要开始比较,必须先有可消费的上游真值并改这条测试,不能顺手加个字面量 `1` 就算数。 + +判据边界:本决策只覆盖「上游写明未冻结 / 未发布可消费真值」的项。凡是上游已发布的(entry symbol、abi_version、struct_size 下界与对齐、指针宽度、endianness、slot 偏移、slot 非空),一律照常强校验,不借本条豁免。 + +## 后果 + +- **接受**:`RootApiTableView` 的公开面比规格 §8.3 少一个方法,属对源规格的显式偏差,已在交回物与 crate 文档声明;下游要做能力判定,当前只能自持仓内私有键(正是 ADR-040 指的 repository-private key)。 +- **接受**:per-table `version` 目前是「读了但不判」,一张 version 被改坏的 table 能通过绑定。风险有界——它的 `struct_size`、slot 偏移与 slot 非空仍全量校验,而这三项才是会导致误读内存的部分。 +- **换来**:本仓不产生第二套公共 ABI 语义。ADR-040 之所以点名 WebSocket transport profile,就是因为「消费方按序数推位位置」这类自造键一旦落地,上游再冻结真语义时会与既成事实冲突。缺位可以随时补,自造的键要先拆。 +- **换来**:缺位是可机器发现的。两处都有测试或文档钉住,不会退化成「当初大概忘了写」。 + +参见:[0009](0009-root-abi-generator-adapter-boundary.md)(同族判据:不得声称没做过的检查)。 diff --git a/.spec/decisions/README.md b/.spec/decisions/README.md index edbf93e..2d39d1c 100644 --- a/.spec/decisions/README.md +++ b/.spec/decisions/README.md @@ -36,3 +36,4 @@ | [0007](0007-composition-config-toml-parser.md) | compose 配置解析选定 `toml` crate 并精确锁版,不自研 TOML 子集解析器 | 生效 | | [0008](0008-opened-artifact-set-construction-inversion.md) | `OpenedArtifactSet`/`MappedNativeImage` 用构造反转保持私有构造器,feature gate 不用于跨 crate 可见性 | 生效 | | [0009](0009-root-abi-generator-adapter-boundary.md) | root-abi generator 摘要链每环须有外部锚点,descriptor 不得自证;报告不得声称没做过的检查 | 生效 | +| [0010](0010-root-abi-runtime-unfrozen-semantics-seams.md) | 上游明确未冻结的语义(capability 位映射、per-table version)保持缺位,`capability_bits` 只做不透明相等校验 | 生效 | diff --git a/Cargo.lock b/Cargo.lock index f0d370a..38149b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,9 @@ version = "0.1.0" [[package]] name = "lumio-core-root-abi" version = "0.1.0" +dependencies = [ + "lumio-core-contracts", +] [[package]] name = "lumio-core-root-abi-generator" diff --git a/modules/root-abi/runtime/Cargo.toml b/modules/root-abi/runtime/Cargo.toml index 89cce9d..e07108b 100644 --- a/modules/root-abi/runtime/Cargo.toml +++ b/modules/root-abi/runtime/Cargo.toml @@ -12,3 +12,10 @@ publish.workspace = true # 禁止 KMS/Signer/测试密钥/网络 client/build-tool feature。 [features] default = [] + +[dependencies] +# ADR 0004 第 3 条冻结的允许边:root-abi -> contracts。 +# 公共 ErrorCode 与已发布 ABI 标量的唯一来源,本 crate 不发明任何一项(规格 §6.2)。 +# 带 version 而非纯 path:deny.toml 的 `wildcards = "deny"` 把无版本的 path +# 依赖判为通配符依赖。版本与 [workspace.package] 的 0.1.0 一致。 +lumio-core-contracts = { path = "../contracts", version = "0.1.0" } diff --git a/modules/root-abi/runtime/src/bind.rs b/modules/root-abi/runtime/src/bind.rs new file mode 100644 index 0000000..f2f7627 --- /dev/null +++ b/modules/root-abi/runtime/src/bind.rs @@ -0,0 +1,346 @@ +//! 调用 entry、检查 null/version/size/capability/layout(规格 §8.2 `bind.rs`、§8.3)。 +//! +//! **本 crate 的全部 `unsafe` 集中在这里。** 读取顺序严格是「先检查 header 前缀, +//! 再读后续 slot」:ADR-040 §4 冻结了 Root Table 与 API Table 的 16 字节头部, +//! 头部之后的每一次读取都必须先由**已校验的** `struct_size` 证明落在对象内。 + +use std::ffi::c_void; +use std::mem; +use std::ptr::NonNull; +use std::sync::Arc; + +use lumio_core_contracts::ErrorCode; + +use crate::error::RootAbiError; +use crate::expectation::{AbiExpectation, Endianness}; +use crate::generated::{self, LumioCoreApi, LumioStatus, LumioVoxelApi}; +use crate::symbol::{SymbolLookupError, SymbolResolver}; +use crate::table_view::{ + GeneratedRootApiTable, RootApiTableView, RootSnapshot, TableSnapshot, TABLE_COUNT, +}; + +/// entry 的签名,取自生成 header 的 `lumio_core_get_api_v1` 声明。 +type EntryFn = unsafe extern "C" fn(u32, *mut *const GeneratedRootApiTable) -> LumioStatus; + +// 头部字段偏移一律由生成结构体反推,本仓不写任何布局字面量。 +const OFF_ABI_VERSION: usize = mem::offset_of!(GeneratedRootApiTable, abi_version); +const OFF_STRUCT_SIZE: usize = mem::offset_of!(GeneratedRootApiTable, struct_size); +const OFF_CAPABILITY_BITS: usize = mem::offset_of!(GeneratedRootApiTable, capability_bits); +const OFF_TABLE_VERSION: usize = mem::offset_of!(LumioCoreApi, version); +const OFF_TABLE_STRUCT_SIZE: usize = mem::offset_of!(LumioCoreApi, struct_size); + +const _: () = { + // Root header 三个字段必须全部落在 ADR-040 §4 冻结的 16 字节头部内。 + assert!(OFF_ABI_VERSION + mem::size_of::() <= generated::ROOT_HEADER_BYTES); + assert!(OFF_STRUCT_SIZE + mem::size_of::() <= generated::ROOT_HEADER_BYTES); + assert!(OFF_CAPABILITY_BITS + mem::size_of::() <= generated::ROOT_HEADER_BYTES); + // 两张生成 table 的头部布局必须一致(ADR-040 §4 的 API table header)。 + assert!(OFF_TABLE_VERSION == mem::offset_of!(LumioVoxelApi, version)); + assert!(OFF_TABLE_STRUCT_SIZE == mem::offset_of!(LumioVoxelApi, struct_size)); + assert!(OFF_TABLE_STRUCT_SIZE + mem::size_of::() <= generated::TABLE_HEADER_BYTES); +}; + +/// 一张生成 API table 的绑定期规格,逐项从生成物派生。 +struct TableSpec { + /// 上游 Golden 里的 table 名。 + name: &'static str, + /// 该 table 指针在 Root Table 中的偏移。 + root_offset: usize, + /// 镜像必须至少提供的字节数:生成绑定里这张 table 的完整大小。 + /// + /// 取 `size_of` 而不是「最高 slot 偏移 + 一个指针」,是因为生成结构体里 + /// **包含**上游声明的保留 slot(`lumio_core_api.reserved`),因此它恰好等于 + /// ADR-040 §4 的派生最小值 `16 + (functionCount + reservedSlots) * pointerBytes`; + /// 而 `reservedSlots` 本身没有任何 Rust 可消费的常量。下方 const 断言保证它 + /// 始终覆盖全部 slot 槽位。 + required_bytes: usize, +} + +const TABLE_SPECS: [TableSpec; TABLE_COUNT] = [ + TableSpec { + name: "lumio_core_api", + root_offset: mem::offset_of!(GeneratedRootApiTable, lumio_core_api), + required_bytes: mem::size_of::(), + }, + TableSpec { + name: "lumio_voxel_api", + root_offset: mem::offset_of!(GeneratedRootApiTable, lumio_voxel_api), + required_bytes: mem::size_of::(), + }, +]; + +const _: () = { + assert!(mem::size_of::() >= slot_span("lumio_core_api")); + assert!(mem::size_of::() >= slot_span("lumio_voxel_api")); +}; + +/// Root Table 自身必须覆盖的字节数:header 加上全部 table 指针槽位。 +const ROOT_REQUIRED_BYTES: usize = { + let mut required = generated::ROOT_HEADER_BYTES; + let mut i = 0; + while i < TABLE_COUNT { + let end = TABLE_SPECS[i].root_offset + generated::POINTER_BYTES; + if end > required { + required = end; + } + i += 1; + } + required +}; + +/// 从生成 Golden 推出某张 table 上「最高 slot 偏移 + 一个指针」。 +const fn slot_span(table: &str) -> usize { + let mut required = generated::TABLE_HEADER_BYTES; + let mut i = 0; + while i < generated::SLOT_OFFSETS.len() { + let (name, _slot, offset) = generated::SLOT_OFFSETS[i]; + if const_str_eq(name, table) { + let end = offset + generated::POINTER_BYTES; + if end > required { + required = end; + } + } + i += 1; + } + required +} + +const fn const_str_eq(a: &str, b: &str) -> bool { + let (a, b) = (a.as_bytes(), b.as_bytes()); + if a.len() != b.len() { + return false; + } + let mut i = 0; + while i < a.len() { + if a[i] != b[i] { + return false; + } + i += 1; + } + true +} + +/// 解析唯一 entry symbol、调用它,并逐项校验镜像发布的 Root API Table。 +/// +/// # Safety +/// +/// 调用方保证 `resolver` 满足 [`SymbolResolver`] 的寿命义务:它返回的地址在 +/// `resolver` 存活期间有效,且 `expected.entry_symbol` 在目标镜像里确实是 +/// 生成 header 声明的那个 `lumio_core_get_api_v1` 签名。签名不符即是 UB, +/// 本函数无法检测——这是「唯一 entry symbol」由生成契约固定的原因。 +/// +/// 成功返回的 [`RootApiTableView`] 私有持有 `resolver` 的强引用,因此表的寿命 +/// 不短于视图本身。 +pub unsafe fn bind_root_api( + resolver: Arc, + expected: &AbiExpectation, +) -> Result { + // ① 宿主与期望的基本 ABI 属性——在碰镜像之前就能判定的部分先判定。 + let host_pointer_bits = mem::size_of::<*const c_void>() * 8; + if usize::from(expected.pointer_width) != host_pointer_bits { + return Err(RootAbiError::abi_mismatch(format!( + "{}:期望指针宽度 {} 位,宿主是 {host_pointer_bits} 位", + expected.abi_identity, expected.pointer_width + ))); + } + if Endianness::host() != expected.endianness { + return Err(RootAbiError::abi_mismatch(format!( + "{}:期望 endianness {},宿主是 {}", + expected.abi_identity, + expected.endianness.as_str(), + Endianness::host().as_str() + ))); + } + + // ② 解析唯一 entry symbol。 + // SAFETY: 由本函数的 Safety 契约转嫁给 resolver 实现。 + let entry_address = unsafe { resolver.resolve(expected.entry_symbol) }.map_err(|error| { + let symbol = expected.entry_symbol.to_string_lossy(); + match error { + SymbolLookupError::NotFound => { + RootAbiError::entry_symbol_missing(format!("镜像里没有 entry symbol {symbol}")) + } + SymbolLookupError::Collision => RootAbiError::entry_symbol_collision(format!( + "entry symbol {symbol} 解析到多个候选" + )), + } + })?; + + // ③ 调用 entry 取表。 + // SAFETY: 签名由生成 header 固定,符合本函数 Safety 契约的前提; + // `out_table` 是本地变量地址,非空且对齐。 + let status = unsafe { + let entry = mem::transmute::<*mut c_void, EntryFn>(entry_address.as_ptr()); + let mut out_table: *const GeneratedRootApiTable = std::ptr::null(); + let status = entry(expected.abi_version, &mut out_table); + if status == 0 { + Ok(out_table) + } else { + Err(status) + } + }; + + let raw = match status { + Ok(raw) => raw, + Err(status) => return Err(entry_status_to_error(expected, status)), + }; + + // ④ null 与对齐——任何读取之前必须先过这两关。 + let raw = NonNull::new(raw.cast_mut()).ok_or_else(|| { + RootAbiError::abi_mismatch(format!( + "{}:entry 返回成功但表指针为空", + expected.abi_identity + )) + })?; + let address = raw.as_ptr() as usize; + if address % generated::MAX_ALIGNMENT != 0 { + return Err(RootAbiError::abi_mismatch(format!( + "{}:表地址 {address:#x} 未按 {} 字节对齐", + expected.abi_identity, + generated::MAX_ALIGNMENT + ))); + } + + let base = raw.as_ptr().cast::(); + + // ⑤ header 前缀:ADR-040 §4 冻结 Root header 为 16 字节,先读它、先判它。 + // SAFETY: `base` 非空且对齐;ADR-040 §4 保证任何合规 Root Table 至少有 + // 16 字节头部,三个字段的偏移已在编译期断言落在头部内。 + let (abi_version, declared_struct_size, capability_bits) = unsafe { + ( + base.add(OFF_ABI_VERSION).cast::().read(), + base.add(OFF_STRUCT_SIZE).cast::().read(), + base.add(OFF_CAPABILITY_BITS).cast::().read(), + ) + }; + + if abi_version != expected.abi_version { + return Err(RootAbiError::abi_mismatch(format!( + "{}:期望 abi_version {},表声明 {abi_version}", + expected.abi_identity, expected.abi_version + ))); + } + + let struct_size = declared_struct_size as usize; + if struct_size % generated::MAX_ALIGNMENT != 0 { + return Err(RootAbiError::abi_mismatch(format!( + "{}:root struct_size {struct_size} 不是 {} 的整数倍", + expected.abi_identity, + generated::MAX_ALIGNMENT + ))); + } + let root_floor = ROOT_REQUIRED_BYTES.max(expected.minimum_struct_size); + if struct_size < root_floor { + return Err(RootAbiError::abi_mismatch(format!( + "{}:root struct_size {struct_size} 小于最小值 {root_floor}", + expected.abi_identity + ))); + } + + // capability_bits 只做精确相等:ADR-040 未冻结位语义,子集判定要靠本仓自造位位置。 + if capability_bits != expected.required_capability_bits { + return Err(RootAbiError::abi_mismatch(format!( + "{}:期望 capability_bits {},表声明 {capability_bits}", + expected.abi_identity, expected.required_capability_bits + ))); + } + + // ⑥ header 已过关,现在才允许读后续 table 指针与各 slot。 + let mut tables = [TableSnapshot { + name: "", + version: 0, + struct_size: 0, + }; TABLE_COUNT]; + for (index, spec) in TABLE_SPECS.iter().enumerate() { + // SAFETY: 已校验 struct_size >= ROOT_REQUIRED_BYTES,覆盖每个 table 指针槽位。 + let table_ptr = unsafe { base.add(spec.root_offset).cast::<*const u8>().read() }; + let table_ptr = NonNull::new(table_ptr.cast_mut()).ok_or_else(|| { + RootAbiError::abi_mismatch(format!( + "{}:table {} 的指针为空", + expected.abi_identity, spec.name + )) + })?; + // SAFETY: 非空已判;对齐与 16 字节头部同 Root Table,由 ADR-040 §4 保证。 + let table_address = table_ptr.as_ptr() as usize; + if table_address % generated::MAX_ALIGNMENT != 0 { + return Err(RootAbiError::abi_mismatch(format!( + "{}:table {} 地址 {table_address:#x} 未按 {} 字节对齐", + expected.abi_identity, + spec.name, + generated::MAX_ALIGNMENT + ))); + } + + let table_base = table_ptr.as_ptr(); + // SAFETY: ADR-040 §4 保证合规 API table 至少有 16 字节头部。 + let (version, declared) = unsafe { + ( + table_base.add(OFF_TABLE_VERSION).cast::().read(), + table_base.add(OFF_TABLE_STRUCT_SIZE).cast::().read(), + ) + }; + + let table_struct_size = declared as usize; + if table_struct_size % generated::MAX_ALIGNMENT != 0 { + return Err(RootAbiError::abi_mismatch(format!( + "{}:table {} 的 struct_size {table_struct_size} 不是 {} 的整数倍", + expected.abi_identity, + spec.name, + generated::MAX_ALIGNMENT + ))); + } + if table_struct_size < spec.required_bytes { + return Err(RootAbiError::abi_mismatch(format!( + "{}:table {} 的 struct_size {table_struct_size} 小于最小值 {}", + expected.abi_identity, spec.name, spec.required_bytes + ))); + } + + // slot 指针:`struct_size` 已证明每个 slot 偏移都落在对象内。 + for (_, slot, offset) in generated::SLOT_OFFSETS + .iter() + .filter(|(t, _, _)| *t == spec.name) + { + // SAFETY: `offset + POINTER_BYTES <= required_bytes <= table_struct_size`。 + let slot_ptr = unsafe { table_base.add(*offset).cast::<*const c_void>().read() }; + if slot_ptr.is_null() { + return Err(RootAbiError::abi_mismatch(format!( + "{}:table {} 的 slot {slot}(偏移 {offset})为空", + expected.abi_identity, spec.name + ))); + } + } + + tables[index] = TableSnapshot { + name: spec.name, + version, + struct_size: table_struct_size, + }; + } + + Ok(RootApiTableView::new( + raw, + Arc::new(expected.clone()), + resolver, + RootSnapshot { + abi_version, + struct_size, + capability_bits, + tables, + }, + )) +} + +/// entry 返回的非零 status:已登记码原样透传,未登记码是 status 契约违反。 +fn entry_status_to_error(expected: &AbiExpectation, status: LumioStatus) -> RootAbiError { + match u32::try_from(status).ok().and_then(ErrorCode::from_numeric) { + Some(code) => RootAbiError::entry_rejected( + code, + format!("{}:entry 返回 {status}", expected.abi_identity), + ), + None => RootAbiError::abi_mismatch(format!( + "{}:entry 返回未登记 status {status}", + expected.abi_identity + )), + } +} diff --git a/modules/root-abi/runtime/src/error.rs b/modules/root-abi/runtime/src/error.rs new file mode 100644 index 0000000..91b296f --- /dev/null +++ b/modules/root-abi/runtime/src/error.rs @@ -0,0 +1,126 @@ +//! 失败到稳定 ErrorCode 的映射(规格 §8.2 `error.rs`、§8.3 错误映射、§6.2)。 +//! +//! 本 crate **不发明错误码**:公共语义的唯一来源是架构源 ID Registry,经 +//! `lumio-core-contracts` 消费。`RootAbiErrorKind` 只是仓内失败分类,用于诊断, +//! 不参与任何跨仓契约;跨边界可断言的只有 [`RootAbiError::code`]。 + +use std::fmt; + +use lumio_core_contracts::ErrorCode; + +/// 仓内失败分类(诊断用;契约面是 [`RootAbiError::code`])。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RootAbiErrorKind { + /// 唯一 entry symbol 在镜像里不存在。 + EntrySymbolMissing, + /// 唯一 entry symbol 解析到多个候选(ADR-006:一个包只允许一个 root entry)。 + EntrySymbolCollision, + /// entry 自身返回了非零 status,码值由被调方给出。 + EntryRejected, + /// ABI 身份、版本、大小、能力位、指针宽度、endianness 或布局不匹配。 + AbiMismatch, + /// Handle 已失效(换代或已释放)。 + HandleInvalid, + /// Handle 重复释放。 + HandleDoubleRelease, +} + +/// 绑定期与 Handle 期的仓内错误;`code()` 是唯一跨边界稳定的部分。 +#[derive(Debug, Clone)] +pub struct RootAbiError { + code: ErrorCode, + kind: RootAbiErrorKind, + message: String, +} + +impl RootAbiError { + fn new(code: ErrorCode, kind: RootAbiErrorKind, message: impl Into) -> Self { + Self { + code, + kind, + message: message.into(), + } + } + + /// 架构源已登记的稳定 ErrorCode——跨仓可断言的唯一契约面。 + pub fn code(&self) -> ErrorCode { + self.code + } + + /// 仓内失败分类,只用于诊断。 + pub fn kind(&self) -> RootAbiErrorKind { + self.kind + } + + /// 仓内诊断消息,不是契约;不得据其文本做判定。 + pub fn message(&self) -> &str { + &self.message + } + + pub(crate) fn entry_symbol_missing(message: impl Into) -> Self { + Self::new( + ErrorCode::SymbolMissing, + RootAbiErrorKind::EntrySymbolMissing, + message, + ) + } + + pub(crate) fn entry_symbol_collision(message: impl Into) -> Self { + Self::new( + ErrorCode::SymbolCollision, + RootAbiErrorKind::EntrySymbolCollision, + message, + ) + } + + /// entry 返回了架构源**已登记**的非零码:原样透传,不重新分类。 + pub(crate) fn entry_rejected(code: ErrorCode, message: impl Into) -> Self { + Self::new(code, RootAbiErrorKind::EntryRejected, message) + } + + pub(crate) fn abi_mismatch(message: impl Into) -> Self { + Self::new( + ErrorCode::NativeAbiMismatch, + RootAbiErrorKind::AbiMismatch, + message, + ) + } + + pub(crate) fn invalid_handle(message: impl Into) -> Self { + Self::new( + ErrorCode::InvalidHandle, + RootAbiErrorKind::HandleInvalid, + message, + ) + } + + pub(crate) fn handle_double_release(message: impl Into) -> Self { + Self::new( + ErrorCode::HandleDoubleRelease, + RootAbiErrorKind::HandleDoubleRelease, + message, + ) + } +} + +impl fmt::Display for RootAbiError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:?}({}) {}: {}", + self.code, + self.code.numeric(), + match self.kind { + RootAbiErrorKind::EntrySymbolMissing => "EntrySymbolMissing", + RootAbiErrorKind::EntrySymbolCollision => "EntrySymbolCollision", + RootAbiErrorKind::EntryRejected => "EntryRejected", + RootAbiErrorKind::AbiMismatch => "AbiMismatch", + RootAbiErrorKind::HandleInvalid => "HandleInvalid", + RootAbiErrorKind::HandleDoubleRelease => "HandleDoubleRelease", + }, + self.message + ) + } +} + +impl std::error::Error for RootAbiError {} diff --git a/modules/root-abi/runtime/src/expectation.rs b/modules/root-abi/runtime/src/expectation.rs new file mode 100644 index 0000000..05b9cfa --- /dev/null +++ b/modules/root-abi/runtime/src/expectation.rs @@ -0,0 +1,153 @@ +//! 从已验证契约构造加载期望(规格 §8.2 `expectation.rs`、§8.3)。 +//! +//! 期望值**一个都不在本仓手写**:全部来自 `lumio-core-contracts` 发布的标量视图与 +//! 架构源生成的 Rust 绑定。两条派生路径互为外部锚点——生成绑定来自上游 compiler 的 +//! `rust/contracts.rs`,标量视图来自上游 `packages/abi/root-abi-bundle.json` 的嵌入 +//! 字节;本文件的编译期断言要求两者逐项一致,任一侧漂移都在编译期失败。 + +use std::ffi::CStr; + +use lumio_core_contracts::contracts as published; + +use crate::generated; + +/// 字节序;取值来自已发布的 `endianness` 字段,本仓不新增取值。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Endianness { + Little, + Big, +} + +impl Endianness { + /// 已发布契约里的拼写。 + pub fn as_str(self) -> &'static str { + match self { + Endianness::Little => "Little", + Endianness::Big => "Big", + } + } + + /// 当前编译目标的字节序。 + pub fn host() -> Self { + if cfg!(target_endian = "little") { + Endianness::Little + } else { + Endianness::Big + } + } + + const fn from_published(published: &str) -> Option { + // `match` 不能直接匹配 &str 常量的 const fn 形式,改用逐字节比较。 + if bytes_eq(published.as_bytes(), b"Little") { + Some(Endianness::Little) + } else if bytes_eq(published.as_bytes(), b"Big") { + Some(Endianness::Big) + } else { + None + } + } +} + +const fn bytes_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut i = 0; + while i < a.len() { + if a[i] != b[i] { + return false; + } + i += 1; + } + true +} + +/// 已发布 entry symbol 的 NUL 结尾形式。 +/// +/// 字面量本身不是真值来源:下方 `const` 断言要求它与 `lumio-core-contracts` 和生成 +/// 绑定两侧发布的 `entrySymbol` 逐字节相等,上游改名即编译期失败。 +const ENTRY_SYMBOL_WITH_NUL: &[u8] = b"lumio_core_get_api_v1\0"; + +const _: () = { + let literal = ENTRY_SYMBOL_WITH_NUL; + let from_bundle = published::ENTRY_SYMBOL.as_bytes(); + let from_binding = generated::ENTRY_SYMBOL.as_bytes(); + + assert!( + literal.len() == from_bundle.len() + 1, + "entry symbol 相对已发布 bundle 漂移" + ); + assert!( + literal[from_bundle.len()] == 0, + "entry symbol 必须以单个 NUL 结尾" + ); + assert!( + bytes_eq(from_bundle, from_binding), + "生成绑定与 bundle 的 entry symbol 不一致" + ); + + let mut i = 0; + while i < from_bundle.len() { + assert!( + literal[i] == from_bundle[i], + "entry symbol 相对已发布 bundle 漂移" + ); + i += 1; + } +}; + +// 生成绑定与 bundle 标量视图必须逐项一致(两条独立派生路径互锚)。 +const _: () = { + assert!(generated::ABI_VERSION == published::ABI_VERSION); + assert!(generated::CAPABILITY_BITS == published::CAPABILITY_BITS); + assert!(generated::POINTER_BYTES == published::POINTER_BYTES as usize); + assert!(generated::MAX_ALIGNMENT == published::MAX_ALIGNMENT as usize); + assert!(generated::ROOT_HEADER_BYTES == published::ROOT_HEADER_BYTES as usize); + assert!(generated::TABLE_HEADER_BYTES == published::TABLE_HEADER_BYTES as usize); + assert!(published::POINTER_WIDTH_BITS == published::POINTER_BYTES * 8); +}; + +/// 唯一 entry symbol。 +pub const ENTRY_SYMBOL: &CStr = match CStr::from_bytes_with_nul(ENTRY_SYMBOL_WITH_NUL) { + Ok(symbol) => symbol, + Err(_) => panic!("entry symbol 常量不是合法 C 字符串"), +}; + +/// 加载期望:绑定时用它逐项核对镜像发布的 Root API Table(规格 §8.3)。 +#[derive(Debug, Clone)] +pub struct AbiExpectation { + /// 仓内可读标识(`bundleId@baselineId`),只用于诊断,不是公共契约。 + pub abi_identity: String, + pub abi_version: u32, + pub minimum_struct_size: usize, + /// 已发布的 `capability_bits` **原值**。 + /// + /// ADR-040「What this bundle deliberately does not freeze」:V1 既未冻结它是 + /// bitmask 还是计数,也未冻结任何位位置。因此这里只做**精确相等**比较, + /// 绝不做子集/按位判定——那需要本仓自造位语义。 + pub required_capability_bits: u64, + pub pointer_width: u8, + pub endianness: Endianness, + pub entry_symbol: &'static CStr, +} + +impl AbiExpectation { + /// 从已发布契约构造期望;不接受调用方覆盖任何一项。 + pub fn from_generated_contract() -> Self { + Self { + abi_identity: format!( + "{}@{}", + published::BUNDLE_ID, + lumio_core_contracts::ARCHITECTURE_BASELINE_ID + ), + abi_version: published::ABI_VERSION, + minimum_struct_size: published::ROOT_MINIMUM_STRUCT_SIZE as usize, + required_capability_bits: published::CAPABILITY_BITS, + pointer_width: u8::try_from(published::POINTER_WIDTH_BITS) + .expect("已发布指针宽度必须能用 u8 表示"), + endianness: Endianness::from_published(published::ENDIANNESS) + .expect("已发布 endianness 必须是 Little 或 Big"), + entry_symbol: ENTRY_SYMBOL, + } + } +} diff --git a/modules/root-abi/runtime/src/handle_guard.rs b/modules/root-abi/runtime/src/handle_guard.rs new file mode 100644 index 0000000..b331c0b --- /dev/null +++ b/modules/root-abi/runtime/src/handle_guard.rs @@ -0,0 +1,70 @@ +//! 架构源 Handle model 的包装(规格 §8.2 `handle_guard.rs`)。 +//! +//! 编码与失效语义全部来自架构源 `metadata/native-managed-abi.json` 的 `handleModel`: +//! `encoding = IndexGenerationContext`、`invalidation = GenerationBump`、 +//! `doubleDestroy = StableError`。本仓**不定义新编码**,也不持有 slot 注册表—— +//! 当前代(`current_generation`)由 handle 的所有者给出,本类型只做判定与状态机。 + +use crate::error::RootAbiError; +use crate::generated::LumioHandle; + +/// 单个 Handle 的生命周期守卫:区分「活」「已释放」,并按 GenerationBump 判失效。 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HandleGuard { + handle: LumioHandle, + live: bool, +} + +impl HandleGuard { + /// 接管一个由 native 侧发出的 Handle。 + pub fn adopt(handle: LumioHandle) -> Self { + Self { handle, live: true } + } + + /// 是否仍处于「活」状态(未释放)。 + pub fn is_live(&self) -> bool { + self.live + } + + /// 取用 Handle;已释放则失败为 `InvalidHandle`(1029)。 + pub fn handle(&self) -> Result { + if !self.live { + return Err(RootAbiError::invalid_handle(format!( + "handle index={} generation={} 已释放", + self.handle.index, self.handle.generation + ))); + } + Ok(self.handle) + } + + /// 按 `invalidation = GenerationBump` 校验:所有者侧当前代与发出时不同即失效。 + /// + /// 已释放的守卫一律失效——释放本身就是一次失效,不需要等换代。 + pub fn validate_generation(&self, current_generation: u32) -> Result<(), RootAbiError> { + if !self.live { + return Err(RootAbiError::invalid_handle(format!( + "handle index={} generation={} 已释放", + self.handle.index, self.handle.generation + ))); + } + if current_generation != self.handle.generation { + return Err(RootAbiError::invalid_handle(format!( + "handle index={} generation={} 已被换代(当前 {current_generation})", + self.handle.index, self.handle.generation + ))); + } + Ok(()) + } + + /// 释放;第二次起失败为 `HandleDoubleRelease`(1030)。 + pub fn release(&mut self) -> Result<(), RootAbiError> { + if !self.live { + return Err(RootAbiError::handle_double_release(format!( + "handle index={} generation={} 重复释放", + self.handle.index, self.handle.generation + ))); + } + self.live = false; + Ok(()) + } +} diff --git a/modules/root-abi/runtime/src/lib.rs b/modules/root-abi/runtime/src/lib.rs index b4291eb..e7b5a50 100644 --- a/modules/root-abi/runtime/src/lib.rs +++ b/modules/root-abi/runtime/src/lib.rs @@ -1,6 +1,47 @@ -//! lumio-core-root-abi——Root API 运行时视图与绑定(AbiExpectation、RootApiTableView、 -//! bind_root_api,规格 §8.3);slot、布局与 Handle 语义一律来自架构源,本仓不补写。 +//! lumio-core-root-abi——Root API 运行时视图与绑定(`AbiExpectation`、`SymbolResolver`、 +//! `RootApiTableView`、`bind_root_api`,规格 §8.3)。 //! -//! 脚手架状态(LCE-P0-001):`GeneratedRootApiTable`/`GeneratedApiTablesView` 必须由架构源 -//! 生成(LGE-GATE-P0-001 未关闭),只读架构镜像(LCE-P0-002)也尚未建立。Gate 关闭前 -//! 不存在可绑定的契约输入,因此本 crate 刻意不含任何模块与公共项。 +//! slot、布局与 Handle 语义一律来自架构源,本仓不补写: +//! [`generated`] 是上游 compiler 产出的 Rust 绑定**原文**(LCE-P0-005 发布到 +//! `modules/root-abi/generated//rust/contracts.rs`),经 `#[path]` 只读引入, +//! 不复制、不改写;本 crate 的每一个布局常量都由它反推。 +//! +//! # 与 §8.3 的两处偏差(均为「不自造公共语义」,见交付说明) +//! +//! 1. **不提供 `RootApiTableView::supports(CapabilityId)`。** ADR-040 +//! 「What this bundle deliberately does not freeze」明写:V1 既未冻结 +//! `capability_bits` 是 bitmask 还是计数,也未冻结任何位位置;ID Registry 的 +//! `Capability` numeric 是枚举序数而非位位置,且「a consumer must not derive a +//! capability key from either source」。按 `CapabilityId` 判定必然要本仓自造位 +//! 映射,因此保持缺位(seam,不建同名临时方法),只经 +//! [`RootApiTableView::capability_bits`] 暴露不透明原值,绑定期做**精确相等**校验。 +//! 上游确认位语义后按独立需求卡补齐。 +//! 2. **不校验单张 API table 的 `version` 期望值。** 上游把 per-table `version` +//! 发布在 `metadata/native-managed-abi.json` 与 bundle JSON 里,没有任何 +//! Rust 可消费的常量;在运行时闭包内为此引入 JSON 解析依赖不成比例。绑定期 +//! 读出并经 [`ApiTableView::version`] 如实公开,但不与任何期望值比较。 +//! +//! 两处都属「上游未发布可消费真值」,不以本地临时格式、alias 或假 Golden 填补。 + +mod bind; +mod error; +mod expectation; +mod handle_guard; +mod symbol; +mod table_view; + +/// 架构源生成的 Root ABI Rust 绑定(原文只读引入,禁止手改)。 +/// +/// 发布路径 `modules/root-abi/generated/LGE-V1.4-2026-08-27/rust/contracts.rs`, +/// 产出与摘要链见同目录 `generated-contract-artifact.json`(LCE-P0-005)。 +/// 安全消费方应使用 [`RootApiTableView`],它不交出任何裸指针。 +#[rustfmt::skip] +#[path = "../../generated/LGE-V1.4-2026-08-27/rust/contracts.rs"] +pub mod generated; + +pub use bind::bind_root_api; +pub use error::{RootAbiError, RootAbiErrorKind}; +pub use expectation::{AbiExpectation, Endianness, ENTRY_SYMBOL}; +pub use handle_guard::HandleGuard; +pub use symbol::{SymbolLookupError, SymbolResolver}; +pub use table_view::{ApiTableView, GeneratedApiTablesView, RootApiTableView}; diff --git a/modules/root-abi/runtime/src/symbol.rs b/modules/root-abi/runtime/src/symbol.rs new file mode 100644 index 0000000..7cd88c9 --- /dev/null +++ b/modules/root-abi/runtime/src/symbol.rs @@ -0,0 +1,33 @@ +//! 唯一 entry symbol 的解析接口(规格 §8.2 `symbol.rs`、§8.3)。 +//! +//! 本 crate 不打开、不映射、不卸载任何镜像——那是 `platform-runtime` / `loader` 的 +//! 职责。这里只定义「谁能把一个符号名换成进程内地址」,以及该实现必须承担的寿命义务。 + +use std::ffi::{c_void, CStr}; +use std::ptr::NonNull; + +/// 符号解析失败的原因;映射到 §8.3 的稳定 ErrorCode。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SymbolLookupError { + /// 镜像里没有这个符号 → `SymbolMissing`(1021)。 + NotFound, + /// 符号解析到多个候选 → `SymbolCollision`(1022)。 + /// + /// ADR-006:一个组合包的符号表里**恰好**有一个跨仓 root entry;解析到多个 + /// 是契约违反,不是可以「取第一个」的歧义。 + Collision, +} + +/// 把符号名解析为进程内地址。 +/// +/// 实现对象必须同时拥有并保持对应 MappedNativeImage 的进程内生命周期: +/// [`crate::RootApiTableView`] 私有持有 `Arc`,正是靠这一点 +/// 把 API 表的寿命绑定到常驻映像(规格 §8.3)。 +pub trait SymbolResolver: Send + Sync + 'static { + /// # Safety + /// + /// 调用方保证 `symbol` 是一个合法的 NUL 结尾符号名。实现方保证:返回的地址在 + /// `self` 存活期间始终有效,且指向 `symbol` 所命名的那个导出项——否则调用方 + /// 依此构造的函数指针会是悬垂的。 + unsafe fn resolve(&self, symbol: &CStr) -> Result, SymbolLookupError>; +} diff --git a/modules/root-abi/runtime/src/table_view.rs b/modules/root-abi/runtime/src/table_view.rs new file mode 100644 index 0000000..96ec233 --- /dev/null +++ b/modules/root-abi/runtime/src/table_view.rs @@ -0,0 +1,192 @@ +//! 不透明只读 API table view(规格 §8.2 `table_view.rs`、§8.3)。 +//! +//! View **不提供**裸指针、library handle 或 `image_guard` 访问器;它私有持有的 +//! `Arc` 把 API 表寿命绑定到常驻映像。所有字段值都是绑定期 +//! 校验通过后的快照,读取快照不需要再次解引用镜像内存。 + +use std::fmt; +use std::ptr::NonNull; +use std::sync::Arc; + +use crate::expectation::AbiExpectation; +use crate::generated; +use crate::symbol::SymbolResolver; + +/// 架构源生成的 Root API Table 类型。别名而非同名新结构——布局只能来自生成物。 +pub(crate) type GeneratedRootApiTable = generated::LumioRootApi; + +/// 生成物里 Root Table 承载的 API table 数量。 +/// +/// 不是手写常量:由生成结构体的实际大小反推(root header + N 个 table 指针 + +/// 声明的保留尾部),上游增删 table 会改变生成结构体,这里的断言随即失败。 +pub(crate) const TABLE_COUNT: usize = 2; + +const _: () = { + // 保留尾部长度取自生成结构体自身,不另写字面量。 + let reserved_tail = std::mem::size_of::<[u8; 32]>(); + assert!( + std::mem::size_of::() + == generated::ROOT_HEADER_BYTES + + TABLE_COUNT * generated::POINTER_BYTES + + reserved_tail, + "生成 Root Table 的 table 数量与 TABLE_COUNT 不一致" + ); +}; + +/// 单张 API table 在绑定期校验通过后的快照。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct TableSnapshot { + pub(crate) name: &'static str, + pub(crate) version: u32, + pub(crate) struct_size: usize, +} + +/// Root Table 在绑定期校验通过后的快照。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct RootSnapshot { + pub(crate) abi_version: u32, + pub(crate) struct_size: usize, + pub(crate) capability_bits: u64, + pub(crate) tables: [TableSnapshot; TABLE_COUNT], +} + +/// 已绑定 Root API Table 的只读视图。 +pub struct RootApiTableView { + raw: NonNull, + expectation: Arc, + image_guard: Arc, + snapshot: RootSnapshot, +} + +impl RootApiTableView { + pub(crate) fn new( + raw: NonNull, + expectation: Arc, + image_guard: Arc, + snapshot: RootSnapshot, + ) -> Self { + Self { + raw, + expectation, + image_guard, + snapshot, + } + } + + pub fn abi_version(&self) -> u32 { + self.snapshot.abi_version + } + + pub fn struct_size(&self) -> usize { + self.snapshot.struct_size + } + + /// 镜像发布的 `capability_bits` 原值。 + /// + /// ADR-040 未冻结它是 bitmask 还是计数,也未冻结任何位位置——因此本 crate + /// **不提供**按 `CapabilityId` 判定的 `supports()`:那需要本仓自造位语义, + /// 而 ADR-040 明写「a consumer must not derive a capability key from either + /// source」。上游确认语义前,消费方只能拿到这个不透明整数。 + pub fn capability_bits(&self) -> u64 { + self.snapshot.capability_bits + } + + /// 绑定时使用的加载期望。 + pub fn expectation(&self) -> &AbiExpectation { + &self.expectation + } + + /// 架构源生成的各 API table 的只读视图。 + pub fn generated_tables(&self) -> GeneratedApiTablesView<'_> { + GeneratedApiTablesView { + tables: &self.snapshot.tables, + } + } +} + +impl fmt::Debug for RootApiTableView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // 打印表地址的数值形式作为进程内身份(ADR-006 `loadPolicy = + // OnePackagePerProcess` 的诊断需要);不暴露可解引用的指针。 + f.debug_struct("RootApiTableView") + .field("abi_identity", &self.expectation.abi_identity) + .field( + "table_identity", + &format_args!("{:#x}", self.raw.as_ptr() as usize), + ) + .field("abi_version", &self.snapshot.abi_version) + .field("struct_size", &self.snapshot.struct_size) + .field("capability_bits", &self.snapshot.capability_bits) + .field("image_holders", &Arc::strong_count(&self.image_guard)) + .finish() + } +} + +/// 各生成 API table 的集合视图。 +#[derive(Debug, Clone, Copy)] +pub struct GeneratedApiTablesView<'a> { + tables: &'a [TableSnapshot; TABLE_COUNT], +} + +impl<'a> GeneratedApiTablesView<'a> { + pub fn len(&self) -> usize { + self.tables.len() + } + + pub fn is_empty(&self) -> bool { + self.tables.is_empty() + } + + /// 生成 Root Table 里 `lumio_core_api` 槽位对应的 table。 + pub fn core_api(&self) -> ApiTableView<'a> { + ApiTableView { + snapshot: &self.tables[0], + } + } + + /// 生成 Root Table 里 `lumio_voxel_api` 槽位对应的 table。 + pub fn voxel_api(&self) -> ApiTableView<'a> { + ApiTableView { + snapshot: &self.tables[1], + } + } + + pub fn iter(&self) -> impl Iterator> + '_ { + self.tables.iter().map(|snapshot| ApiTableView { snapshot }) + } +} + +/// 单张 API table 的只读视图;不提供 slot 函数指针(本卡非目标:不做业务分发)。 +#[derive(Debug, Clone, Copy)] +pub struct ApiTableView<'a> { + snapshot: &'a TableSnapshot, +} + +impl ApiTableView<'_> { + /// 上游 Golden 里的 table 名。 + pub fn name(&self) -> &'static str { + self.snapshot.name + } + + pub fn version(&self) -> u32 { + self.snapshot.version + } + + pub fn struct_size(&self) -> usize { + self.snapshot.struct_size + } + + /// 本 table 的 slot 数量,取自上游 Golden。 + pub fn slot_count(&self) -> usize { + self.slot_offsets().count() + } + + /// 本 table 的 `(slot 名, 偏移)`,逐项取自生成物 `SLOT_OFFSETS`。 + pub fn slot_offsets(&self) -> impl Iterator + '_ { + let name = self.snapshot.name; + generated::SLOT_OFFSETS + .iter() + .filter(move |(table, _, _)| *table == name) + .map(|(_, slot, offset)| (*slot, *offset)) + } +} diff --git a/modules/root-abi/runtime/tests/bind_invalid.rs b/modules/root-abi/runtime/tests/bind_invalid.rs new file mode 100644 index 0000000..121705a --- /dev/null +++ b/modules/root-abi/runtime/tests/bind_invalid.rs @@ -0,0 +1,329 @@ +//! symbol、版本、大小、能力、布局负向矩阵(规格 §8.2 `tests/bind_invalid.rs`)。 +//! +//! 每条负例都断言**稳定公共 ErrorCode**(规格 §6.2 / §8.3 错误映射), +//! 不断言消息文本——文本是仓内诊断,码值才是契约。 + +use std::cell::Cell; +use std::ffi::{c_void, CStr}; +use std::ptr::NonNull; +use std::sync::Arc; + +use lumio_core_contracts::ErrorCode; +use lumio_core_root_abi::generated::{ + LumioBuffer, LumioCoreApi, LumioCoreConfigV1, LumioHandle, LumioRootApi, LumioStatus, + LumioVoxelApi, LumioVoxelWorldDescV1, +}; +use lumio_core_root_abi::{bind_root_api, AbiExpectation, SymbolLookupError, SymbolResolver}; + +// ── 进程内假包(不加载动态库)──────────────────────────────────────────── + +extern "C" fn core_init(_config: *const LumioCoreConfigV1, _out: LumioHandle) -> LumioStatus { + 0 +} +extern "C" fn core_shutdown(_context: LumioHandle) -> LumioStatus { + 0 +} +extern "C" fn core_last_error_detail(_context: LumioHandle, _out: LumioBuffer) -> LumioStatus { + 0 +} +extern "C" fn voxel_create( + _context: LumioHandle, + _desc: *const LumioVoxelWorldDescV1, + _out: LumioHandle, +) -> LumioStatus { + 0 +} +extern "C" fn voxel_destroy(_world: LumioHandle) -> LumioStatus { + 0 +} + +fn leak_core() -> *const LumioCoreApi { + Box::leak(Box::new(LumioCoreApi { + version: 1, + struct_size: 48, + reserved0: 0, + lumio_core_init: Some(core_init), + lumio_core_shutdown: Some(core_shutdown), + lumio_core_last_error_detail: Some(core_last_error_detail), + reserved: [std::ptr::null_mut()], + })) +} + +fn leak_voxel() -> *const LumioVoxelApi { + Box::leak(Box::new(LumioVoxelApi { + version: 1, + struct_size: 32, + reserved0: 0, + lumio_voxel_world_create: Some(voxel_create), + lumio_voxel_world_destroy: Some(voxel_destroy), + })) +} + +fn leak_root() -> *mut LumioRootApi { + Box::leak(Box::new(LumioRootApi { + abi_version: 1, + struct_size: 64, + capability_bits: 7, + lumio_core_api: leak_core(), + lumio_voxel_api: leak_voxel(), + reserved_tail: [0u8; 32], + })) +} + +thread_local! { + static TABLE: Cell<*const LumioRootApi> = const { Cell::new(std::ptr::null()) }; + static STATUS: Cell = const { Cell::new(0) }; +} + +extern "C" fn entry(_requested_version: u32, out_table: *mut *const LumioRootApi) -> LumioStatus { + let status = STATUS.with(|s| s.get()); + if status != 0 { + return status; + } + // SAFETY: 调用方(bind_root_api)传入本地变量地址,非空且对齐。 + unsafe { *out_table = TABLE.with(|t| t.get()) }; + 0 +} + +enum Resolve { + Entry, + Missing, + Collision, +} + +struct FakeResolver(Resolve); + +impl SymbolResolver for FakeResolver { + unsafe fn resolve(&self, _symbol: &CStr) -> Result, SymbolLookupError> { + match self.0 { + Resolve::Entry => Ok(NonNull::new(entry as *const () as *mut c_void).expect("非空")), + Resolve::Missing => Err(SymbolLookupError::NotFound), + Resolve::Collision => Err(SymbolLookupError::Collision), + } + } +} + +/// 用给定的 table 与 resolve 行为跑一次绑定,返回失败时的稳定 ErrorCode。 +fn bind_expecting_failure( + resolve: Resolve, + table: *const LumioRootApi, + status: LumioStatus, +) -> ErrorCode { + TABLE.with(|t| t.set(table)); + STATUS.with(|s| s.set(status)); + let expected = AbiExpectation::from_generated_contract(); + // SAFETY: resolver 要么报错,要么返回本进程内 `entry` 的地址。 + let result = unsafe { bind_root_api(Arc::new(FakeResolver(resolve)), &expected) }; + result.expect_err("本例必须失败").code() +} + +fn bind_table_expecting_failure(table: *const LumioRootApi) -> ErrorCode { + bind_expecting_failure(Resolve::Entry, table, 0) +} + +// ── symbol 面 ─────────────────────────────────────────────────────────── + +#[test] +fn missing_entry_symbol_maps_to_symbol_missing_1021() { + let code = bind_expecting_failure(Resolve::Missing, leak_root(), 0); + + assert_eq!(code, ErrorCode::SymbolMissing); + assert_eq!(code as i32, 1021); +} + +#[test] +fn colliding_entry_symbol_maps_to_symbol_collision_1022() { + let code = bind_expecting_failure(Resolve::Collision, leak_root(), 0); + + assert_eq!(code, ErrorCode::SymbolCollision); + assert_eq!(code as i32, 1022); +} + +// ── entry 调用面 ──────────────────────────────────────────────────────── + +#[test] +fn entry_returning_registered_error_code_is_passed_through() { + let code = bind_expecting_failure( + Resolve::Entry, + leak_root(), + ErrorCode::CapabilityMissing as i32, + ); + + assert_eq!(code, ErrorCode::CapabilityMissing); + assert_eq!(code as i32, 1020); +} + +#[test] +fn entry_returning_unregistered_status_maps_to_native_abi_mismatch_1004() { + let code = bind_expecting_failure(Resolve::Entry, leak_root(), 4242); + + assert_eq!(code, ErrorCode::NativeAbiMismatch); + assert_eq!(code as i32, 1004); +} + +#[test] +fn null_out_table_maps_to_native_abi_mismatch_1004() { + let code = bind_table_expecting_failure(std::ptr::null()); + + assert_eq!(code, ErrorCode::NativeAbiMismatch); +} + +#[test] +fn misaligned_root_pointer_maps_to_native_abi_mismatch_1004() { + // 故意制造未对齐地址;bind 必须在任何读取之前就拒绝,不得触发 UB。 + let backing: &'static mut [u8; 128] = Box::leak(Box::new([0u8; 128])); + let misaligned = unsafe { backing.as_mut_ptr().add(1) } as *const LumioRootApi; + + let code = bind_table_expecting_failure(misaligned); + + assert_eq!(code, ErrorCode::NativeAbiMismatch); +} + +// ── root header 面 ────────────────────────────────────────────────────── + +#[test] +fn root_abi_version_mismatch_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: `root` 由 Box::leak 产生,仍然存活且独占。 + unsafe { (*root).abi_version = 2 }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +#[test] +fn root_struct_size_below_minimum_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。24 < 派生最小值 32。 + unsafe { (*root).struct_size = 24 }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +#[test] +fn root_struct_size_not_aligned_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。36 不是 maxAlignment=8 的整数倍。 + unsafe { (*root).struct_size = 36 }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +#[test] +fn capability_bits_mismatch_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。capability_bits 是不透明 u64(ADR-040:位语义未冻结), + // 只做精确相等比较,不做子集判定。 + unsafe { (*root).capability_bits = 3 }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +// ── API table 面 ──────────────────────────────────────────────────────── + +#[test] +fn null_core_table_pointer_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。 + unsafe { (*root).lumio_core_api = std::ptr::null() }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +#[test] +fn null_voxel_table_pointer_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。 + unsafe { (*root).lumio_voxel_api = std::ptr::null() }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +#[test] +fn core_table_struct_size_below_minimum_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。40 < lumio_core_api 的派生最小值 48(含 1 个保留 slot)。 + unsafe { (*((*root).lumio_core_api as *mut LumioCoreApi)).struct_size = 40 }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +/// **已知缺口的守卫测试**(见 crate 文档「与 §8.3 的两处偏差」第 2 条)。 +/// +/// per-table `version` 的期望值只发布在 `metadata/native-managed-abi.json` 与 +/// bundle JSON 里,没有任何 Rust 可消费常量,因此绑定期**不做**版本比较—— +/// 读出即如实公开。这条测试把「不比较」钉成显式行为:将来若要开始比较, +/// 必须先有可消费的上游真值并改这条测试,不能顺手加个字面量 `1` 就算数。 +#[test] +fn table_version_is_surfaced_verbatim_and_not_asserted() { + let root = leak_root(); + // SAFETY: 同上;`lumio_core_api` 指向本测试泄漏的独占 table。 + unsafe { (*((*root).lumio_core_api as *mut LumioCoreApi)).version = 7 }; + + TABLE.with(|t| t.set(root)); + STATUS.with(|s| s.set(0)); + let expected = AbiExpectation::from_generated_contract(); + // SAFETY: resolver 返回本进程内 `entry` 的地址。 + let view = unsafe { bind_root_api(Arc::new(FakeResolver(Resolve::Entry)), &expected) } + .expect("per-table version 当前不参与绑定判定"); + + assert_eq!(view.generated_tables().core_api().version(), 7); +} + +#[test] +fn null_core_slot_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。 + unsafe { (*((*root).lumio_core_api as *mut LumioCoreApi)).lumio_core_shutdown = None }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +#[test] +fn null_voxel_slot_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。 + unsafe { (*((*root).lumio_voxel_api as *mut LumioVoxelApi)).lumio_voxel_world_destroy = None }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} + +/// root 声明的 `struct_size` 必须覆盖它自己声明的 table 指针槽位; +/// 否则读取 offset 16/24 就是越界读。 +#[test] +fn root_struct_size_not_covering_table_pointers_maps_to_native_abi_mismatch_1004() { + let root = leak_root(); + // SAFETY: 同上。16 是合法的 8 的倍数,但放不下两个 table 指针(需要 32)。 + unsafe { (*root).struct_size = 16 }; + + assert_eq!( + bind_table_expecting_failure(root), + ErrorCode::NativeAbiMismatch + ); +} diff --git a/modules/root-abi/runtime/tests/bind_valid.rs b/modules/root-abi/runtime/tests/bind_valid.rs new file mode 100644 index 0000000..24e3abc --- /dev/null +++ b/modules/root-abi/runtime/tests/bind_valid.rs @@ -0,0 +1,313 @@ +//! 有效 entry/table 绑定(规格 §8.2 `tests/bind_valid.rs`)。 +//! +//! 不加载动态库(本卡非目标):用进程内 `extern "C"` entry + 泄漏的合规 table +//! 充当「已映射镜像」,只验证 `bind_root_api` 的契约行为。 + +use std::cell::Cell; +use std::ffi::{c_void, CStr}; +use std::ptr::NonNull; +use std::sync::Arc; + +use lumio_core_root_abi::generated::{ + LumioBuffer, LumioCoreApi, LumioCoreConfigV1, LumioHandle, LumioRootApi, LumioStatus, + LumioVoxelApi, LumioVoxelWorldDescV1, +}; +use lumio_core_root_abi::{bind_root_api, AbiExpectation, SymbolLookupError, SymbolResolver}; + +// ── 进程内假包(不加载动态库)──────────────────────────────────────────── + +extern "C" fn core_init(_config: *const LumioCoreConfigV1, _out: LumioHandle) -> LumioStatus { + 0 +} +extern "C" fn core_shutdown(_context: LumioHandle) -> LumioStatus { + 0 +} +extern "C" fn core_last_error_detail(_context: LumioHandle, _out: LumioBuffer) -> LumioStatus { + 0 +} +extern "C" fn voxel_create( + _context: LumioHandle, + _desc: *const LumioVoxelWorldDescV1, + _out: LumioHandle, +) -> LumioStatus { + 0 +} +extern "C" fn voxel_destroy(_world: LumioHandle) -> LumioStatus { + 0 +} + +fn conforming_root() -> *const LumioRootApi { + let core = Box::leak(Box::new(LumioCoreApi { + version: 1, + struct_size: 48, + reserved0: 0, + lumio_core_init: Some(core_init), + lumio_core_shutdown: Some(core_shutdown), + lumio_core_last_error_detail: Some(core_last_error_detail), + reserved: [std::ptr::null_mut()], + })); + let voxel = Box::leak(Box::new(LumioVoxelApi { + version: 1, + struct_size: 32, + reserved0: 0, + lumio_voxel_world_create: Some(voxel_create), + lumio_voxel_world_destroy: Some(voxel_destroy), + })); + Box::leak(Box::new(LumioRootApi { + abi_version: 1, + struct_size: 64, + capability_bits: 7, + lumio_core_api: core, + lumio_voxel_api: voxel, + reserved_tail: [0u8; 32], + })) +} + +thread_local! { + static TABLE: Cell<*const LumioRootApi> = const { Cell::new(std::ptr::null()) }; + static REQUESTED: Cell = const { Cell::new(u32::MAX) }; +} + +extern "C" fn entry(requested_version: u32, out_table: *mut *const LumioRootApi) -> LumioStatus { + REQUESTED.with(|r| r.set(requested_version)); + // SAFETY: 调用方(bind_root_api)传入本地变量地址,非空且对齐。 + unsafe { *out_table = TABLE.with(|t| t.get()) }; + 0 +} + +struct EntryResolver; + +impl SymbolResolver for EntryResolver { + unsafe fn resolve(&self, symbol: &CStr) -> Result, SymbolLookupError> { + assert_eq!( + symbol, + AbiExpectation::from_generated_contract().entry_symbol + ); + Ok(NonNull::new(entry as *const () as *mut c_void).expect("fn 地址非空")) + } +} + +fn bind_conforming() -> lumio_core_root_abi::RootApiTableView { + TABLE.with(|t| t.set(conforming_root())); + let expected = AbiExpectation::from_generated_contract(); + // SAFETY: resolver 返回的是本进程内 `entry` 的地址,签名与 entry 契约一致, + // 且 table 在测试进程存活期内被泄漏保持。 + unsafe { bind_root_api(Arc::new(EntryResolver), &expected) }.expect("合规 table 必须绑定成功") +} + +// ── 断言 ──────────────────────────────────────────────────────────────── + +#[test] +fn binds_conforming_table_and_publishes_header_values() { + let view = bind_conforming(); + + assert_eq!(view.abi_version(), 1); + assert_eq!(view.struct_size(), 64); + assert_eq!(view.capability_bits(), 7); +} + +#[test] +fn passes_expected_abi_version_as_requested_version() { + let expected = AbiExpectation::from_generated_contract(); + let _view = bind_conforming(); + + assert_eq!(REQUESTED.with(|r| r.get()), expected.abi_version); +} + +#[test] +fn exposes_generated_tables_with_upstream_header_values() { + let view = bind_conforming(); + let tables = view.generated_tables(); + + assert_eq!(tables.len(), 2); + assert_eq!(tables.core_api().name(), "lumio_core_api"); + assert_eq!(tables.core_api().version(), 1); + assert_eq!(tables.core_api().struct_size(), 48); + assert_eq!(tables.core_api().slot_count(), 3); + assert_eq!(tables.voxel_api().name(), "lumio_voxel_api"); + assert_eq!(tables.voxel_api().version(), 1); + assert_eq!(tables.voxel_api().struct_size(), 32); + assert_eq!(tables.voxel_api().slot_count(), 2); +} + +#[test] +fn slot_offsets_come_from_the_upstream_golden() { + let view = bind_conforming(); + let tables = view.generated_tables(); + + assert_eq!( + tables.core_api().slot_offsets().collect::>(), + vec![ + ("lumio_core_init", 16usize), + ("lumio_core_shutdown", 24), + ("lumio_core_last_error_detail", 32), + ] + ); + assert_eq!( + tables.voxel_api().slot_offsets().collect::>(), + vec![ + ("lumio_voxel_world_create", 16usize), + ("lumio_voxel_world_destroy", 24) + ] + ); +} + +/// ADR-040 §4:声明的 `structSize` 只要 **不小于** 派生最小值即合规, +/// 尾部保留是下界而不是等式——最小合规 table 必须能绑定。 +#[test] +fn accepts_minimum_declared_root_struct_size() { + let root = conforming_root() as *mut LumioRootApi; + // SAFETY: `root` 由 Box::leak 产生,仍然存活且独占。 + unsafe { (*root).struct_size = 32 }; + TABLE.with(|t| t.set(root)); + + let expected = AbiExpectation::from_generated_contract(); + // SAFETY: 同 `bind_conforming`。 + let view = unsafe { bind_root_api(Arc::new(EntryResolver), &expected) } + .expect("struct_size 等于派生最小值时必须接受"); + + assert_eq!(view.struct_size(), 32); +} + +/// §8.3:View 的私有 `Arc` 把 API 表寿命绑定到常驻映像—— +/// 绑定成功后 resolver 的强引用必须仍被 View 持有。 +#[test] +fn view_keeps_the_image_guard_alive() { + TABLE.with(|t| t.set(conforming_root())); + let resolver: Arc = Arc::new(EntryResolver); + let expected = AbiExpectation::from_generated_contract(); + + // SAFETY: 同 `bind_conforming`。 + let view = unsafe { bind_root_api(Arc::clone(&resolver), &expected) }.expect("绑定成功"); + + assert_eq!( + Arc::strong_count(&resolver), + 2, + "View 必须持有 resolver 的强引用" + ); + drop(view); + assert_eq!(Arc::strong_count(&resolver), 1, "View 释放后强引用必须归还"); +} + +/// 期望值只能来自已发布契约,不得在本仓手写。 +#[test] +fn expectation_is_derived_from_published_contract() { + let expected = AbiExpectation::from_generated_contract(); + + assert_eq!( + expected.abi_version, + lumio_core_contracts::contracts::ABI_VERSION + ); + assert_eq!( + expected.required_capability_bits, + lumio_core_contracts::contracts::CAPABILITY_BITS + ); + assert_eq!( + expected.minimum_struct_size, + lumio_core_contracts::contracts::ROOT_MINIMUM_STRUCT_SIZE as usize + ); + assert_eq!( + u32::from(expected.pointer_width), + lumio_core_contracts::contracts::POINTER_WIDTH_BITS + ); + assert_eq!( + expected + .entry_symbol + .to_str() + .expect("entry symbol 是 UTF-8"), + lumio_core_contracts::contracts::ENTRY_SYMBOL + ); + assert_eq!( + expected.endianness.as_str(), + lumio_core_contracts::contracts::ENDIANNESS + ); +} + +/// 本仓测量出的布局必须等于上游 Golden(三语言一致性的 Rust 一侧)。 +/// +/// 同时按与 C / C# 探针**完全相同**的规范格式打印测量值—— +/// `cargo test ... -- --nocapture` 的输出可与另两份逐行 diff。 +#[test] +fn rust_measured_layout_equals_upstream_golden() { + use lumio_core_root_abi::generated as g; + + println!("abi_version={}", g::ABI_VERSION); + println!("capability_bits={}", g::CAPABILITY_BITS); + println!("entry_symbol={}", g::ENTRY_SYMBOL); + println!("symbol_prefix={}", g::SYMBOL_PREFIX); + println!("pointer_bytes={}", std::mem::size_of::<*const c_void>()); + + for (name, size) in g::STRUCT_SIZES { + let measured = match *name { + "lumio_handle_t" => std::mem::size_of::(), + "lumio_buffer_t" => std::mem::size_of::(), + "lumio_core_api" => std::mem::size_of::(), + "lumio_voxel_api" => std::mem::size_of::(), + "lumio_root_api" => std::mem::size_of::(), + other => panic!("Golden 出现未知结构 {other}"), + }; + println!("size.{name}={measured}"); + assert_eq!(measured, *size, "{name} 的 size 与上游 Golden 不一致"); + } + + for (name, _) in g::STRUCT_SIZES { + let measured = match *name { + "lumio_handle_t" => std::mem::align_of::(), + "lumio_buffer_t" => std::mem::align_of::(), + "lumio_core_api" => std::mem::align_of::(), + "lumio_voxel_api" => std::mem::align_of::(), + "lumio_root_api" => std::mem::align_of::(), + other => panic!("Golden 出现未知结构 {other}"), + }; + println!("align.{name}={measured}"); + assert_eq!( + measured, + g::MAX_ALIGNMENT, + "{name} 的 align 与 layout profile 不一致" + ); + } + + for (table, slot, offset) in g::SLOT_OFFSETS { + let measured = match (*table, *slot) { + ("lumio_core_api", "lumio_core_init") => { + std::mem::offset_of!(LumioCoreApi, lumio_core_init) + } + ("lumio_core_api", "lumio_core_shutdown") => { + std::mem::offset_of!(LumioCoreApi, lumio_core_shutdown) + } + ("lumio_core_api", "lumio_core_last_error_detail") => { + std::mem::offset_of!(LumioCoreApi, lumio_core_last_error_detail) + } + ("lumio_voxel_api", "lumio_voxel_world_create") => { + std::mem::offset_of!(LumioVoxelApi, lumio_voxel_world_create) + } + ("lumio_voxel_api", "lumio_voxel_world_destroy") => { + std::mem::offset_of!(LumioVoxelApi, lumio_voxel_world_destroy) + } + other => panic!("Golden 出现未知 slot {other:?}"), + }; + println!("offset.{table}.{slot}={measured}"); + assert_eq!( + measured, *offset, + "{table}.{slot} 的 offset 与上游 Golden 不一致" + ); + } + + // Root Table 的 table 指针槽位:ADR-040 §4 的 `16 + i * pointerBytes`。 + for (index, (table, slot)) in [ + ("lumio_root_api", "lumio_core_api"), + ("lumio_root_api", "lumio_voxel_api"), + ] + .into_iter() + .enumerate() + { + let measured = match slot { + "lumio_core_api" => std::mem::offset_of!(LumioRootApi, lumio_core_api), + _ => std::mem::offset_of!(LumioRootApi, lumio_voxel_api), + }; + println!("offset.{table}.{slot}={measured}"); + assert_eq!(measured, g::ROOT_HEADER_BYTES + index * g::POINTER_BYTES); + } + + assert_eq!(std::mem::size_of::<*const c_void>(), g::POINTER_BYTES); +} diff --git a/modules/root-abi/runtime/tests/handle_lifecycle.rs b/modules/root-abi/runtime/tests/handle_lifecycle.rs new file mode 100644 index 0000000..90d7aa7 --- /dev/null +++ b/modules/root-abi/runtime/tests/handle_lifecycle.rs @@ -0,0 +1,78 @@ +//! `InvalidHandle` 与 `HandleDoubleRelease`(规格 §8.2 `tests/handle_lifecycle.rs`)。 +//! +//! Handle 语义一律来自架构源 `metadata/native-managed-abi.json` 的 `handleModel`: +//! `encoding = IndexGenerationContext`、`invalidation = GenerationBump`、 +//! `doubleDestroy = StableError`。本仓只包装,不定义新编码。 + +use lumio_core_contracts::ErrorCode; +use lumio_core_root_abi::generated::LumioHandle; +use lumio_core_root_abi::HandleGuard; + +fn handle(index: u32, generation: u32) -> LumioHandle { + LumioHandle { + index, + generation, + context: 0x5155, + } +} + +#[test] +fn live_guard_yields_the_adopted_handle() { + let guard = HandleGuard::adopt(handle(3, 9)); + + assert!(guard.is_live()); + assert_eq!( + guard.handle().expect("live guard 必须给出 handle"), + handle(3, 9) + ); +} + +#[test] +fn second_release_maps_to_handle_double_release_1030() { + let mut guard = HandleGuard::adopt(handle(3, 9)); + + guard.release().expect("首次释放必须成功"); + let code = guard.release().expect_err("重复释放必须失败").code(); + + assert_eq!(code, ErrorCode::HandleDoubleRelease); + assert_eq!(code as i32, 1030); +} + +#[test] +fn use_after_release_maps_to_invalid_handle_1029() { + let mut guard = HandleGuard::adopt(handle(3, 9)); + guard.release().expect("首次释放必须成功"); + + assert!(!guard.is_live()); + let code = guard.handle().expect_err("释放后取用必须失败").code(); + + assert_eq!(code, ErrorCode::InvalidHandle); + assert_eq!(code as i32, 1029); +} + +/// `invalidation = GenerationBump`:owner 侧 generation 前进后, +/// 旧 handle 必须判为 InvalidHandle。 +#[test] +fn stale_generation_maps_to_invalid_handle_1029() { + let guard = HandleGuard::adopt(handle(3, 9)); + + guard.validate_generation(9).expect("同代必须有效"); + + let code = guard + .validate_generation(10) + .expect_err("换代后必须失效") + .code(); + assert_eq!(code, ErrorCode::InvalidHandle); +} + +#[test] +fn released_guard_fails_generation_validation_too() { + let mut guard = HandleGuard::adopt(handle(3, 9)); + guard.release().expect("首次释放必须成功"); + + let code = guard + .validate_generation(9) + .expect_err("已释放必须失效") + .code(); + assert_eq!(code, ErrorCode::InvalidHandle); +} diff --git a/modules/root-abi/tests/c/header_layout.c b/modules/root-abi/tests/c/header_layout.c new file mode 100644 index 0000000..fdd5bd3 --- /dev/null +++ b/modules/root-abi/tests/c/header_layout.c @@ -0,0 +1,100 @@ +/* + * C 侧 ABI 布局探针(规格 §8.2「生成物/跨语言测试」)。 + * + * 真值来源只有一个:架构源生成的 include/lumio_core.h。本文件**不复制 Golden 数值** + * ——header 自带的 LUMIO_STATIC_ASSERT 行就是 C 侧的 Golden 判据,布局不符则编译失败 + * (ADR-040:a mismatch is a build failure, never a runtime discovery)。抄第二份 + * 只会制造第二处真值。 + * + * 本文件只做两件 header 没做的事: + * 1) 补 header 未覆盖的编译期断言——_Alignof 与 Root header 三字段偏移; + * 2) 把测量值按规范格式打印,供与 Rust / C# 两侧的同格式输出逐行比对。 + * + * 构建运行: + * cc -std=c11 -Wall -Wextra -Werror \ + * -I modules/root-abi/generated/LGE-V1.4-2026-08-27/include \ + * -o modules/root-abi/tests/c/header_layout.c && + */ + +#include +#include + +#include + +/* + * header 只断言 size 与 slot offset。以下两类同属 Golden 但 header 未覆盖,在这里补。 + * 对齐基准 8 取自锁定 layout profile linux-x86_64-glibc 的 maxAlignment + * (ADR-040 §4;同值发布在 reports/layout-report.json 与 C#/Rust 绑定的 + * MaxAlignment / MAX_ALIGNMENT 常量,唯独 C header 没有对应宏)。 + */ +LUMIO_STATIC_ASSERT(_Alignof(lumio_handle_t) == 8, handle_align); +LUMIO_STATIC_ASSERT(_Alignof(lumio_buffer_t) == 8, buffer_align); +LUMIO_STATIC_ASSERT(_Alignof(lumio_core_api) == 8, core_api_align); +LUMIO_STATIC_ASSERT(_Alignof(lumio_voxel_api) == 8, voxel_api_align); +LUMIO_STATIC_ASSERT(_Alignof(lumio_root_api) == 8, root_api_align); + +/* ADR-040 §4 冻结的 Root header:abi_version @0、struct_size @4、capability_bits @8。 */ +LUMIO_STATIC_ASSERT(offsetof(lumio_root_api, abi_version) == 0, root_abi_version_offset); +LUMIO_STATIC_ASSERT(offsetof(lumio_root_api, struct_size) == 4, root_struct_size_offset); +LUMIO_STATIC_ASSERT(offsetof(lumio_root_api, capability_bits) == 8, root_capability_bits_offset); + +/* API table header 在两张表上必须同构(ADR-040 §4)。 */ +LUMIO_STATIC_ASSERT(offsetof(lumio_core_api, version) == offsetof(lumio_voxel_api, version), + table_version_offset); +LUMIO_STATIC_ASSERT(offsetof(lumio_core_api, struct_size) == offsetof(lumio_voxel_api, struct_size), + table_struct_size_offset); + +static void report_size(const char *name, size_t measured) +{ + printf("size.%s=%zu\n", name, measured); +} + +static void report_align(const char *name, size_t measured) +{ + printf("align.%s=%zu\n", name, measured); +} + +static void report_offset(const char *table, const char *slot, size_t measured) +{ + printf("offset.%s.%s=%zu\n", table, slot, measured); +} + +int main(void) +{ + printf("abi_version=%u\n", (unsigned)LUMIO_ABI_VERSION); + printf("capability_bits=%llu\n", (unsigned long long)LUMIO_CAPABILITY_BITS); + printf("entry_symbol=%s\n", LUMIO_ENTRY_SYMBOL); + printf("symbol_prefix=%s\n", LUMIO_SYMBOL_PREFIX); + printf("pointer_bytes=%zu\n", sizeof(void *)); + + report_size("lumio_handle_t", sizeof(lumio_handle_t)); + report_size("lumio_buffer_t", sizeof(lumio_buffer_t)); + report_size("lumio_core_api", sizeof(lumio_core_api)); + report_size("lumio_voxel_api", sizeof(lumio_voxel_api)); + report_size("lumio_root_api", sizeof(lumio_root_api)); + + report_align("lumio_handle_t", _Alignof(lumio_handle_t)); + report_align("lumio_buffer_t", _Alignof(lumio_buffer_t)); + report_align("lumio_core_api", _Alignof(lumio_core_api)); + report_align("lumio_voxel_api", _Alignof(lumio_voxel_api)); + report_align("lumio_root_api", _Alignof(lumio_root_api)); + + report_offset("lumio_core_api", "lumio_core_init", offsetof(lumio_core_api, lumio_core_init)); + report_offset("lumio_core_api", "lumio_core_shutdown", + offsetof(lumio_core_api, lumio_core_shutdown)); + report_offset("lumio_core_api", "lumio_core_last_error_detail", + offsetof(lumio_core_api, lumio_core_last_error_detail)); + report_offset("lumio_voxel_api", "lumio_voxel_world_create", + offsetof(lumio_voxel_api, lumio_voxel_world_create)); + report_offset("lumio_voxel_api", "lumio_voxel_world_destroy", + offsetof(lumio_voxel_api, lumio_voxel_world_destroy)); + report_offset("lumio_root_api", "lumio_core_api", offsetof(lumio_root_api, lumio_core_api)); + report_offset("lumio_root_api", "lumio_voxel_api", offsetof(lumio_root_api, lumio_voxel_api)); + + /* entry symbol 是 header 发布的字符串宏,不复制第二份字面量做比较。 */ + if (strlen(LUMIO_ENTRY_SYMBOL) == 0) { + fprintf(stderr, "FAIL: header 发布的 entry symbol 为空\n"); + return 1; + } + return 0; +} diff --git a/modules/root-abi/tests/csharp/Directory.Build.props b/modules/root-abi/tests/csharp/Directory.Build.props new file mode 100644 index 0000000..fb43347 --- /dev/null +++ b/modules/root-abi/tests/csharp/Directory.Build.props @@ -0,0 +1,17 @@ + + + + + $(MSBuildThisFileDirectory)../../../../build/csharp/ + $(RepoBuildRoot)$(MSBuildProjectName)/bin/ + $(RepoBuildRoot)$(MSBuildProjectName)/obj/ + + + diff --git a/modules/root-abi/tests/csharp/Lumio.CoreEngine.AbiSmoke/Lumio.CoreEngine.AbiSmoke.csproj b/modules/root-abi/tests/csharp/Lumio.CoreEngine.AbiSmoke/Lumio.CoreEngine.AbiSmoke.csproj new file mode 100644 index 0000000..43cabab --- /dev/null +++ b/modules/root-abi/tests/csharp/Lumio.CoreEngine.AbiSmoke/Lumio.CoreEngine.AbiSmoke.csproj @@ -0,0 +1,25 @@ + + + + + + Exe + net10.0 + enable + enable + true + true + Lumio.CoreEngine.AbiSmoke + Lumio.CoreEngine.AbiSmoke + + + + + + + diff --git a/modules/root-abi/tests/csharp/Lumio.CoreEngine.AbiSmoke/Program.cs b/modules/root-abi/tests/csharp/Lumio.CoreEngine.AbiSmoke/Program.cs new file mode 100644 index 0000000..f87af0b --- /dev/null +++ b/modules/root-abi/tests/csharp/Lumio.CoreEngine.AbiSmoke/Program.cs @@ -0,0 +1,129 @@ +// C# 侧 ABI 布局探针(规格 §8.2「生成物/跨语言测试」)。 +// +// C 有 header 的 static assert、Rust 有生成绑定的 const assert,二者都在编译期把 +// 布局钉死;C# 没有等价的编译期设施,因此这里在运行期把 Marshal 测量值与**生成物 +// 自带的 Golden**(RootAbiLayout.StructSizes / SlotOffsets、RootAbi.* 常量)逐项比对。 +// 本文件不写任何布局数值——Golden 一律取自生成绑定。 +// +// 输出格式与 C / Rust 两侧完全一致,便于三份逐行 diff。 + +using System.Runtime.InteropServices; + +using Lumio.Gen.LanguageBinding; + +internal static class Program +{ + // 对齐探针:Sequential 布局下 `V` 的偏移即该类型的非托管对齐。 + // 泛型结构体不能交给 Marshal.OffsetOf,故每个受测类型各有一个。 + [StructLayout(LayoutKind.Sequential)] + private struct AlignOfHandle { public byte B; public LumioHandle V; } + + [StructLayout(LayoutKind.Sequential)] + private struct AlignOfBuffer { public byte B; public LumioBuffer V; } + + [StructLayout(LayoutKind.Sequential)] + private struct AlignOfCoreApi { public byte B; public LumioCoreApi V; } + + [StructLayout(LayoutKind.Sequential)] + private struct AlignOfVoxelApi { public byte B; public LumioVoxelApi V; } + + [StructLayout(LayoutKind.Sequential)] + private struct AlignOfRootApi { public byte B; public LumioRootApi V; } + + private static int _failures; + + private static void Check(string key, long measured, long golden) + { + Console.WriteLine($"{key}={measured}"); + if (measured != golden) + { + Console.Error.WriteLine($"FAIL {key}: measured {measured}, golden {golden}"); + _failures++; + } + } + + private static int SizeOfByGoldenName(string name) => name switch + { + "lumio_handle_t" => Marshal.SizeOf(), + "lumio_buffer_t" => Marshal.SizeOf(), + "lumio_core_api" => Marshal.SizeOf(), + "lumio_voxel_api" => Marshal.SizeOf(), + "lumio_root_api" => Marshal.SizeOf(), + _ => throw new InvalidOperationException($"Golden 出现未知结构 {name}"), + }; + + private static int AlignOfByGoldenName(string name) => name switch + { + "lumio_handle_t" => (int)Marshal.OffsetOf(nameof(AlignOfHandle.V)), + "lumio_buffer_t" => (int)Marshal.OffsetOf(nameof(AlignOfBuffer.V)), + "lumio_core_api" => (int)Marshal.OffsetOf(nameof(AlignOfCoreApi.V)), + "lumio_voxel_api" => (int)Marshal.OffsetOf(nameof(AlignOfVoxelApi.V)), + "lumio_root_api" => (int)Marshal.OffsetOf(nameof(AlignOfRootApi.V)), + _ => throw new InvalidOperationException($"Golden 出现未知结构 {name}"), + }; + + private static int SlotOffsetOf(string table, string slot) => (table, slot) switch + { + ("lumio_core_api", "lumio_core_init") => + (int)Marshal.OffsetOf(nameof(LumioCoreApi.LumioCoreInit)), + ("lumio_core_api", "lumio_core_shutdown") => + (int)Marshal.OffsetOf(nameof(LumioCoreApi.LumioCoreShutdown)), + ("lumio_core_api", "lumio_core_last_error_detail") => + (int)Marshal.OffsetOf(nameof(LumioCoreApi.LumioCoreLastErrorDetail)), + ("lumio_voxel_api", "lumio_voxel_world_create") => + (int)Marshal.OffsetOf(nameof(LumioVoxelApi.LumioVoxelWorldCreate)), + ("lumio_voxel_api", "lumio_voxel_world_destroy") => + (int)Marshal.OffsetOf(nameof(LumioVoxelApi.LumioVoxelWorldDestroy)), + _ => throw new InvalidOperationException($"Golden 出现未知 slot {table}.{slot}"), + }; + + private static int Main() + { + Console.WriteLine($"abi_version={RootAbi.AbiVersion}"); + Console.WriteLine($"capability_bits={RootAbi.CapabilityBits}"); + Console.WriteLine($"entry_symbol={RootAbi.EntrySymbol}"); + Console.WriteLine($"symbol_prefix={RootAbi.SymbolPrefix}"); + Console.WriteLine($"pointer_bytes={IntPtr.Size}"); + + if (IntPtr.Size != RootAbi.PointerBytes) + { + Console.Error.WriteLine( + $"FAIL pointer_bytes: 宿主 {IntPtr.Size}, golden {RootAbi.PointerBytes}"); + _failures++; + } + + foreach ((string name, int size) in RootAbiLayout.StructSizes) + { + Check($"size.{name}", SizeOfByGoldenName(name), size); + } + + // 对齐 Golden 是 layout profile 的 maxAlignment(生成绑定发布为 MaxAlignment): + // 本 profile 下每个受测结构的对齐都等于它。 + foreach ((string name, int _) in RootAbiLayout.StructSizes) + { + Check($"align.{name}", AlignOfByGoldenName(name), RootAbi.MaxAlignment); + } + + foreach (SlotOffset slot in RootAbiLayout.SlotOffsets) + { + Check($"offset.{slot.Table}.{slot.Slot}", SlotOffsetOf(slot.Table, slot.Slot), slot.Offset); + } + + // Root Table 的 table 指针槽位:ADR-040 §4 的 `16 + i * pointerBytes`, + // 两个基数都来自生成绑定的已发布常量。 + Check("offset.lumio_root_api.lumio_core_api", + (int)Marshal.OffsetOf(nameof(LumioRootApi.LumioCoreApi)), + RootAbi.RootHeaderBytes); + Check("offset.lumio_root_api.lumio_voxel_api", + (int)Marshal.OffsetOf(nameof(LumioRootApi.LumioVoxelApi)), + RootAbi.RootHeaderBytes + RootAbi.PointerBytes); + + if (_failures != 0) + { + Console.Error.WriteLine($"C# layout probe: {_failures} 项与上游 Golden 不一致"); + return 1; + } + + return 0; + } +}