背景
官网对外讲述「所有子系统用同一套语言(消息与接口)交流」,但框架内部是多语言实现: C++26 Drogon 后端、Rust SSR/wasm、浏览器 JS/DOM。多语言各取所长,但带来三条边界的 契约同步、跨语言调试、边界测试三份持续成本,是未来开发的痛点。
现状的「契约」散落为手写 serde DTO + schemars 导出 + schema_drift 漂移锁——机制已经 做了一半(单一真源 + 漂移锁),但没有被当作一条显式的框架层对待:它仍是 zjchina-core 的实现细节,开发者仍要关心语言边界与序列化,而不是只关心布局与功能。
这与 ROS 2 形成对照:ROS 2 的「统一语言」不是编程语言,而是 IDL 契约层——.msg
一次定义,rclcpp/rclpy 各语言绑定自动生成、编译期锁死,多语言绑定恰是其原生设计。
我们抄到了「单一真源 + 漂移锁」,还没抄到「契约层作为一等公民」这后半句。
选项
- 维持现状(契约是 L2 的实现细节)。 零迁移成本,但多语言痛点原样存在;未来 AI 框架(MCP/Agent)会再造一套契约,边界随规模持续增长。
- 契约层独立成一个新 crate / 服务。 边界最清晰,但引入新的节点与网络边界, 违背「契约是数据、不是服务」的本质,且会与 zjchina-core 的线协议 DTO 重复。
- 契约层定为框架一等公民(采纳)。 物理载体仍是 zjchina-core,但把 「单一真源 DTO → Schema 导出 → 跨语言侧生成 → 漂移锁」确立为一条横切 L2/L3 的 正交公共语言,像 ROS 2 的 IDL。上层开发者只面对组件 DSL 与页面功能。
证据
- ROS 2 的「统一语言」实质是 IDL 代码生成:消息一次定义 → 多语言绑定自动生成、
编译期锁死。我们的
zjchina-coreDTO +export-schemas+schema_drift_tests已经是这条路径的雏形,缺的是「契约层一等公民」的正交定位与「跨语言侧自动生成」。 - 现有事实:
zjchina-core::all_schemas()已用 schemars 导出 JSON Schema 落盘docs/schemas/,漂移锁已拦截手动漏跑(改 DTO 必须重跑 export-schemas 并提交 diff); 但 C++ 后端侧的对应结构仍是手写 JSON 拼装,没有从契约自动生成。 - AGENTS.md 依赖准入的三条边界(线协议 DTO / 内容管线 / HTTP API)已隐含「契约是 横切关注点」,本决策只是把隐式前提显性化。L0-L3 分层里 L3 即「MCP/Agent 契约空位」, 与契约层天然复用同一套生成机制。
拍板
拍板者 zj:契约层确立为框架一等公民——它是横切 L2(数据 DTO)与 L3(AI/Agent
契约)的公共语言,物理载体为 zjchina-core,治理 = 单一真源 DTO + Schema 导出 +
跨语言侧生成 + 漂移锁(类 ROS 2 IDL)。目标:把跨语言复杂性下沉引擎盖,开发者只写
页面布局与页面功能。AI 协作者(TRAE)参与概念推演与架构图绘制,拍板权属作者。
日期:2026-10-01。
类型映射与生成规范(C++ 侧生成)
契约层一等公民的落地方向是「单一真源 DTO → Schema 导出 → 跨语言侧生成 → 漂移锁」。
Rust 侧(zjchina-core serde DTO)是唯一真源,C++ 侧强类型结构由 export-cpp 从
schemars 的 draft-07 JSON Schema 派生,不手写、不二次维护。
线型映射表
schemars 输出(format 由 struct 字段类型决定)到 C++26 的固定映射,生成器与 C++ 测试
共用这本表,禁止就地 ad-hoc:
| Rust 字段类型 | JSON Schema 形状 | C++ 类型 |
|---|---|---|
String | {"type":"string"} | std::string |
bool | {"type":"boolean"} | bool |
f64 | number / format: double | double |
f32 | number / format: float | float |
usize | integer / format: uint | std::uint64_t |
u64 | integer / format: uint64 | std::uint64_t |
u32 | integer / format: uint32 | std::uint32_t |
i64 | integer / format: int64 | std::int64_t |
i32 | integer / format: int32 | std::int32_t |
Option<T> | type:["T","null"] 或 anyOf:[{$ref:T},{type:null}] | std::optional<CppT> |
Vec<T> | array / items: T | std::vector<CppT> |
[f32;N] | array / minItems==maxItems==N | std::array<float,N> |
BTreeMap<String,T> | object / additionalProperties: T | std::map<std::string,CppT>(有序,对齐 BTreeMap 确定性) |
普通枚举(rename_all=snake_case) | definitions 内 {enum:[...]} | enum class + to_string/from_string |
tagged union(#[serde(tag="type")]) | anyOf 的 {required:[type,value]} 变体 | std::variant<…>,type 判别分发 |
三项硬约定
- 整数一律走定宽(
uint64_t/int64_t…):usize在 C++ 侧不映射size_t,跨语言 线协议宽度必须确定,32/64 位平台不漂移。 - 有序容器:Rust
BTreeMap→ C++std::map,序列化/回放 diff 顺序确定(同twin模块契约要点)。 - 一模块一真源:全部 26 个 DTO + 其嵌套
definitions去重后生成单个 C++26 模块zjbackend_contract(export module zjbackend_contract;,namespacezj::contract),每个类型恰好声明一次,按$ref依赖拓扑排序,杜绝多 Schema 内联导致的重复定义。每个类型配套to_json(const T&)->Json::Value与from_json(const Json::Value&, T&)->bool。
漂移锁
- Rust 侧:
generated_cpp_is_up_to_date测试(镜像schema_drift_tests)——重跑 生成器比对backend/src/zjbackend_contract.cppm,漂移即红; - C++ 侧:模块纳入
zjbackend/backend_test编译,类型错误在构建期现形;测试 用例锁代表性结构(含 tagged union)的to_json→from_jsonround-trip。
修订记录(Amendments)
- 2026-10-01(修订:厘清 RPC 与 HTTP2 的边界):前瞻「Drogon v2 支持 HTTP2 后 能否实现 RPC(类 Dioxus server functions)无缝通信」。结论:HTTP2 是传输层 优化(多路复用/头压缩/连接复用),RPC 的关键在契约层(IDL → 生成 stub),二者 正交;HTTP2 落地不改变「做不做 RPC」的答案。 与 RPC 的无缝感来自「前后端同一 Rust 二进制 + 编译期接线」,而本框架后端是 C++,跨语言鸿沟常在——Drogon 约束下 「无缝」的上限即本决策的契约生成路径(Rust 侧生成类型化 fetch 封装 + C++ 侧生成 handler 签名)。据此:不为 RPC 而 RPC,精力收敛在本决策的契约生成推进上;HTTP2 落地后当传输层红利直接享受(HMI 遥测多短请求、server push 预推壳资源),不再 引入 gRPC/自研二进制 RPC。拍板结论不变。