上篇讲了全链路总览(Excel 矩阵 → merged.json → FIDL/FDEPL → codegen → Stub/Client → 编译运行)。这篇把两个最磨人的环节拆开:环节 A:Excel 矩阵 → FIDL/FDEPL 导出,和 环节 B:codegen + Stub/Client 生成。7 个服务、83 个接口能一次跑通,靠的不是运气,是下面这几页排坑笔记。
如果按"人肉工作量"和"出错概率"给全链路每个环节打分,A 和 B 毫无悬念地霸榜:
环节 A 是"领域语义翻译":一张 116 行 × 35 列的矩阵,合并单元格、类型引用、十六进制 ID 混在一起,要翻译成 Franca 语法(FIDL/FDEPL)。语法记不全?抄错一个字母?一错错一片。
环节 B 是"工具链玄学":官方生成器的参数差异、CommonAPI 运行时的几个"静默失败"陷阱——代码能编译、能运行,就是不干活。
甲方给的"服务矩阵"是一份 5 个 Sheet 的 Excel。转换工具第一件事不是写代码,是让 AI 先把每个 Sheet 的列语义读出来——这一步决定了后面所有代码怎么写:
| Sheet | 装什么 | 关键列 |
|---|---|---|
| 1 服务定义 | 每个服务的全部接口(Method/Event/Getter/Notifier)、参数、类型引用 | 服务名、接口名、接口 ID、参数类型、发送策略 |
| 2 数据定义 | 所有参数的类型定义(枚举、结构体、数组) | 类型名、类别、离散值、成员、基础类型 |
| 3 SOME/IP 通信 | IP、VLAN、端口、通信方式 | 源/目的地址、端口、UDP/TCP |
| 4 SD 全局 | 服务发现全局参数 | 组播地址、端口、TTL |
| 5 SD ECU | 单 ECU 的服务发现配置 | ECU 名、服务实例 |
有个容易忽略的点:Getter 和 Notifier 在 Excel 里是独立行,不是方法的附属。矩阵里 Field 的 Getter 有独立的 Method ID(0x4001 段)、Notifier 有独立 Event ID(0x9001 段)和独立事件组。建模时要把它们拆成独立的 method + broadcast,FDEPL 里再显式分配原始 ID——否则序列化 ID 和矩阵对不上,联调直接抓瞎。
用 AI 写转换脚本,最容易犯的错是"让 AI 直接读 Excel 吐 FIDL"。正确做法是中间加一层权威格式:
Excel(5 Sheet) → parse/check/merge → merged.json(权威中间格式)
→ export_full_design.py → FIDL/FDEPL
merged.json 是纯文本、可 diff、可评审的"唯一真相"。接口改一个参数?改 Excel 重跑一遍,10 秒出全套文件;想审查 AI 生成得对不对?直接 diff JSON,不用对着 Excel 数格子。这一步 AI 干的是生成器代码——生成器写对一次,后面永远不出语法错误。
这是环节 A 最大的坑。转换工具跑起来之后,检查生成的 FIDL,发现接口里引用的类型名,在类型定义表里根本不存在。不是个例,是成片出现:
还有更隐蔽的:FgaTimeoutAlarm Notifier 引用了 AvailTime_Struct,实际应该引用 TimeoutAlarm_Struct(事件和结构体对不上,订阅了也解析不了数据);FLTempInfo Getter 一个单元格里塞了两个类型 TempParame_Enum/TempParame_Float(复合引用写在一格,要拆成两行)。
为什么必须修? 因为错误会传染:Excel 引用错 → FIDL 引用了未定义类型 → codegen 要么报错、要么生成语义错误的代码 → 联调时行为完全不对,但代码"看起来没问题"。到那时排错成本是 Excel 里的 100 倍。
怎么修的? 写了一个 fix_excel_refs.py:先备份原表 → 全局类型名替换 + 定向修正 + 行拆分 → 最后跑一遍残留检查确认零错误引用。核心是一句原则:
现在生成器里也内置了同类防御:递归收集类型时遇到未定义类型直接抛错;枚举离散值不是 0 起连续也会告警——因为 CommonAPI 会自动编号,编号和矩阵对不上就是隐性炸弹。
这个坑是查 COVESA 生成器源码才定位的,纯知识盲区。需求很朴素:矩阵里枚举都是 uint8,序列化当然应该占 1 字节。FDEPL 里给枚举配宽度,直觉会写:
enumeration ACResponse_Enum {
SomeIpEnumWidth = 8 // ❌ 错误写法
}
结果生成出来一看:枚举序列化占 8 字节(64 位),跟 uint8 差 8 倍,线上数据直接对不上。真相:SomeIpEnumWidth 的单位是字节,不是位。写 8 = 64 位。要表达 uint8 得用:
enumeration ACResponse_Enum {
SomeIpEnumBitWidth = 8 // ✅ 8 位 = 1 字节
}
还有个更细的分支:枚举嵌在结构体里时,成员级宽度要用 SomeIpStructEnumBitWidth = 8,不是上面那个。三件套记牢:
| 场景 | 配置项 | 值 |
|---|---|---|
| 顶层枚举(typeCollection 里) | SomeIpEnumBitWidth | 8 |
| 结构体里的枚举成员 | SomeIpStructEnumBitWidth | 8 |
生成的 FIDL 实际长这样(真实文件节选):
interface ACBlowCtrlSrv {
version { major 1 minor 0 }
method BlowModeSet {
// method_id: 0x0001
in {
ACBlowCtrlSrvTypes.ACZoneInstID_Enum acZoneInstID
ACBlowCtrlSrvTypes.ACClntID_Enum acClntID
ACBlowCtrlSrvTypes.BlowMode_Enum blowMode
}
out {
ACBlowCtrlSrvTypes.ACResponse_Enum acResponse
}
}
broadcast BlowLeveInfoNotifier {
// event_id: 0x9001 / eventgroup_id: 0x0001
out {
ACBlowCtrlSrvTypes.LevelCommand_Enumm value
}
}
}
这些坑有一个共同特点:语法手册里都有,但没人会去背。AI 在这里的价值不是"知道语法"(大模型确实知道),而是把语法规则固化进生成器——人只改 Excel,语法永远由生成器保证。这才是"改表即出代码"的底气。
FIDL/FDEPL 就绪,进入官方生成器和业务代码环节。如果说环节 A 的坑是"明坑"(报错、编译失败),环节 B 的坑全是暗坑:能编译、能运行、就是不干活,或者悄悄干错。
官方生成器跑一遍:
commonapi-core-generator -sk ACBlowCtrlSrv_interface_V1.0.fidl
commonapi-someip-generator ACBlowCtrlSrv_interface_V1.0.fdepl
输出 src/gen 下的全套类型定义、Proxy、Stub 骨架。看起来一切正常,直到对照甲方给的参考工程(用的 core 3.2.0)时发现差异:
查证结果:core 生成器 3.2.0 默认生成 StubDefault,3.2.15 默认不生成,必须显式加 -sk(skeleton)参数。别小看这个差异——没有 StubDefault,StubImpl 就得手写 initStubAdapter / getInterfaceVersion 一整套生命周期代码,而且手写版本很容易踩下一个坑(B.2)。现在所有生成脚本都固定带 -sk,一行参数省掉整个类别的坑。
这是整个项目最阴险的坑,没有之一。连官方 Demo 都一直有这个 bug。
现象:客户端订阅了事件,服务端 fire 也调用了,返回值正常,但事件就是到不了客户端。没有报错、没有异常、日志一切正常。
fire 是靠 stubAdapter_ 发事件的,不存 = 发射器是空的 = 事件静默丢弃。而且这是运行时行为,编译不报错、运行不报错,只有事件不到达。解决:继承 StubDefault 后(B.1 的 -sk),initStubAdapter 自动完成存储,这个坑从根上消失。
场景:写完 C API 封装层(纯 C 接口,供非 C++ 模块和测试调用),独立编译一个纯 C 测试程序,运行时发现:
auto proxy = runtime->buildProxy(...); if (proxy == nullptr) { // ← 一直走进来 // 报错退出 }
代码逻辑完全正确、gen 代码也编译进来了,为什么 proxy 建不出来?查 CommonAPI 源码才明白:Proxy 工厂是静态注册的——每个生成的 XxxProxy.cpp 里有一个 INITIALIZER 宏,在 main() 执行之前把该 proxy 类型注册进全局工厂。只要生成代码没有被链接进最终可执行文件,工厂里就没有这个类型,buildProxy 就返回 nullptr。
我们当时踩的正是这个:独立 C 程序只链了自己的 .c/.cpp,没链 GEN_SOURCES(生成代码)——proxy 工厂空荡荡。解法(工程化):把纯 C 测试做成一个 CMake target,显式链接 GEN_SOURCES:
add_executable(capi_test src/client/capi_test.c src/client/be12_capi.cpp)
target_link_libraries(capi_test PRIVATE ${GEN_SOURCES} ...)
vsomeip 的事件上报,CommonAPI 固定走可靠通道(内部固定 notify(..., true))。就算你的服务是 UDP 通信(我们 7 个服务全 UDP),vsomeip-server.json 里也必须配 reliable 端口,否则事件通道建不起来。这是"服务定义 UDP 很合理、事件却悄悄走另一个通道"的认知冲突,配 JSON 时最容易漏。
列完坑,说句公道话——这些坑没有一个是大模型"替我们避开的",但 AI 把踩坑成本降了一个数量级:
| 环节 | 人肉做 | 带 AI 做 |
|---|---|---|
| 5 Sheet 语义建模 | 读半天表,理解靠经验 | AI 先读模板结构,输出列语义,人来确认 |
| 引用一致性检查 | 肉眼对表,15 处错误漏一半 | 脚本全量扫,零残留 |
| FIDL 语法 | 背手册 + 反复试错 | 语法固化进生成器,永不复发 |
| 生成器版本差异(-sk) | 翻 changelog 对比 | 对照参考工程 diff 生成产物,一眼定位 |
| 静默失效类坑 | 从现象反推源码,论天算 | 给 AI 现象 + 日志,它直接指"查 stubAdapter_" |
AI 真正值钱的地方:把"踩坑"变成"一次性的",再把"知识"沉淀成"可复用的"。每个坑解决后,要么固化进生成器(语法、宽度、ID 分配),要么写进 SOP(-sk、可靠端口、GEN_SOURCES 链接)。下次做新项目,这些坑直接不存在。
比坑更值钱的是"怎么让 AI 帮你少踩坑":
1. 先把方案讲清楚再动手。让 AI 写转换工具前,先让它输出"5 个 Sheet 怎么读、中间格式长什么样、检查规则有哪些",你确认了再写代码——方案阶段纠错成本是代码阶段的 1/10。
2. 一次只改一件事。修 Excel 引用错误时,一个脚本只干一类修正;AI 同时改五处的时候,错了你都分不清是哪处引入的。
3. 验收标准先行。生成器写完后先定"通过标准"(引用零残留、枚举宽度正确、ID 与矩阵一致),再跑全量——AI 按标准自查,比人肉抽查可靠。
两个最难环节拆完了。下一篇(完结篇)讲剩下的"把工程变产品"的部分:
*(本文所有代码示例均来自真实生成产物(已脱敏泛化);数字:7 个服务、83 个接口、48 个 R&R、32 个 Getter、140 条事件,全部经真实编译运行验证。前篇:全链路总览。)