zjchina.net

Drogon · ROS 2 · 独立开发笔记

ADR-00062026-10-01

契约层确立为框架一等公民,跨语言复杂性下沉引擎盖

拍板:zj

背景

官网对外讲述「所有子系统用同一套语言(消息与接口)交流」,但框架内部是多语言实现: 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-core DTO + 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
f64number / format: doubledouble
f32number / format: floatfloat
usizeinteger / format: uintstd::uint64_t
u64integer / format: uint64std::uint64_t
u32integer / format: uint32std::uint32_t
i64integer / format: int64std::int64_t
i32integer / format: int32std::int32_t
Option<T>type:["T","null"] 或 anyOf:[{$ref:T},{type:null}]std::optional<CppT>
Vec<T>array / items: Tstd::vector<CppT>
[f32;N]array / minItems==maxItems==Nstd::array<float,N>
BTreeMap<String,T>object / additionalProperties: Tstd::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 判别分发

三项硬约定

  1. 整数一律走定宽(uint64_t/int64_t…):usize 在 C++ 侧不映射 size_t,跨语言 线协议宽度必须确定,32/64 位平台不漂移。
  2. 有序容器:Rust BTreeMap → C++ std::map,序列化/回放 diff 顺序确定(同 twin 模块契约要点)。
  3. 一模块一真源:全部 26 个 DTO + 其嵌套 definitions 去重后生成单个 C++26 模块 zjbackend_contract(export module zjbackend_contract;,namespace zj::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_json round-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。拍板结论不变。
← 返回决策日志