mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 12:53:35 +00:00
C 化第二刀:为协议编解码层铺 JSON 底座。**本刀只交付库 + 验收,
未改 Go 生产路径**(接线是独立一步,库先验完再换产线)。
为什么是它:SSE 单块解析(parseOpenAICompatibleStreamChunkFull)是每个流式
chunk 都要跑的最热路径,实测 1937ns/13allocs(content 块)、3122ns/21allocs
(toolcall 块),而纯字节扫描理论下限 133ns/1alloc —— 差距 15~23×。
一次 1 万块的会话 = 1~2 万次堆分配,正是 GC 抖动的来源。
为什么不复用 SDK 的 remotedevice/ha_json.c(实测三缺陷,不可直接复用):
① 无 \u 解码:\u4f60\u597d → ?0?d?d?0(非 ASCII 全靠转义时内容直接损坏)
② 只有 _get_int 无浮点:temperature:0.7 静默变 0
③ null 与「键缺失」不可区分
外加它是 DOM + malloc,与本层「不 malloc / 零拷贝 / 纯函数」正交。
设计:scan(结构,零分配零解码)+ extract(取值,按需解码)两段分离。
content 可能是很大的多模态数组,而 stringifyContent 只需要 text 字段拼起来;
若 scan 就解码并分配缓冲,等于把成本付给不需要它的调用方。
★ 被测试抓出 7 个真实缺陷(写 C 时同一逻辑我读三遍都认为正确):
1 代理对合成成功后未跳过 unconditionally 的 U+FFFD 发射(😀 → 两个 FFFD)
2 过长编码检查用了只含首字节位的 cp(「你」→ 6 个 FFFD)
3 members_next 只报值起点不消费值 → 游标停在值前(模糊测试第一轮抓到)
4 扫描阶段不校验转义字符合法性({"a":"\q"} C 判合法、json.Valid=false)
5 扫描阶段不校验 \u 后四位十六进制(同上)
6 get_int 接受前导零(007 / 00)
7 cgo 桥接把 C 结构体声明为 Go 局部变量 → 运行时 panic
(cgo argument has Go pointer to unpinned Go pointer)
其中 4 个是「静默分叉」——不崩、不报错,生产里表现为「内容少一个字符」
或「某些块被静默丢弃」,极难归因。这正是黄金对照不可省的理由。
★ 另纠正我自己两次错误的「真值」(比代码 bug 更危险,会变成错误规格):
第一版真值表里 content:{} 的花括号少了一层,把「我写错 JSON」误读成
「Go 对 content 严格」。修正后实测发现一对方向相反的语义:
content 走 interface{} 宽松({}→"{}"、true→"true"),
reasoning_content/usage/finish_reason 强类型严格(123 ⇒ 整块作废)。
照错误表写 C 会产出「比 Go 更严格」的实现,静默丢弃本该生效的块。
两个由缺陷倒逼的设计决定:
- members_next 返回**完整值 span** 并内部跳过 ⇒ 「返回 1」蕴含「成员良构」。
要求调用方自己推进游标的 API 是错的:忘一次就解析到上一个值且不报错。
- members_complete() 区分「正常扫到 }」与「输入畸形」,否则无法复刻 Go 严格性。
同时修两个基础设施目标对「多源文件/多测试」的适配:
- csrc-sanitize:每个契约测试各自链接(多个 main 合链会 multiple definition,
而报错被吞后会被误报成「本机无 sanitizer」——一个假的 SKIP)
- csrc-cross:多源文件改用 -fsyntax-only 逐文件(gcc 不支持多源单 -o)
实测(全部当场可复现):
- C 契约测试 119 项断言全过;黄金对照 5 组全过(语法/成员/解码/整数/随机字节)
- libFuzzer 4948 万次运行零崩溃(121s)
- ASan+UBSan PASS(两个契约测试各跑);gcc+clang 零告警;arm64 交叉编译 0 告警
- 全量 go test -count=1 ./... 0 FAIL;make build-linux-arm64 → ELF aarch64
- 纪律检查 SDK 公开接口 diff = 0 行(未触碰 SDK)
决策关闭(jianf 本轮裁决):C 实现留主仓 csrc/(它本就是替换内核 Go 实现,
SDK 从未被触碰,跨端复用才需进 SDK 而它们不调用本层);ha_json.c 不复用;
下一刀即协议编解码层。
126 lines
5.3 KiB
CMake
126 lines
5.3 KiB
CMake
cmake_minimum_required(VERSION 3.10)
|
||
project(ha_codec VERSION 0.1.0 LANGUAGES C)
|
||
|
||
# ============================================================
|
||
# ha_codec — HomeAgent 内核编解码层(C 实现)
|
||
#
|
||
# 零外部依赖,纯 C99。产出静态库供 homed 经 cgo 链接,
|
||
# 同时可独立用于其他端(鸿蒙 / 嵌入式 / C SDK)。
|
||
#
|
||
# 使用方式:
|
||
# add_subdirectory(path/to/csrc)
|
||
# target_link_libraries(my_app ha_codec)
|
||
# target_include_directories(my_app PRIVATE ${HA_CODEC_INCLUDE_DIR})
|
||
# ============================================================
|
||
|
||
option(BUILD_SHARED_LIBS "Build ha_codec as shared library" OFF)
|
||
option(BUILD_TESTS "Build ha_codec tests" OFF)
|
||
option(BUILD_FUZZ "Build libFuzzer targets" OFF)
|
||
option(BUILD_BENCH "Build micro benchmarks" OFF)
|
||
|
||
# ★ C99 而非编译器默认档(clang 默认 gnu17)。
|
||
# 理由:内核 C 侧的编译契约是 C99(cgo CFLAGS 与这里必须一致),
|
||
# 在更新的默认档下编译会**静默**通过,而 Go 侧用 -std=c99 编不过
|
||
# —— 两边同时构建、行为却分叉,是最难查的一类问题。
|
||
# 实测:本轮 -Wpedantic 门禁就当场抓到过 C11 特性(_Static_assert)。
|
||
# C90/C95 不支持:ha_abi.h 用了 // 注释与 stdint。
|
||
# C11 开关保留给想验证「未来切到 C11 也不坏」的人。
|
||
set(CMAKE_C_STANDARD 99)
|
||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||
set(CMAKE_C_EXTENSIONS OFF) # 禁用 gnu99 扩展,严格 -std=c99
|
||
|
||
set(HA_CODEC_SRC
|
||
src/ha_codec.c
|
||
src/ha_json_scan.c
|
||
)
|
||
|
||
if(BUILD_SHARED_LIBS)
|
||
add_library(ha_codec SHARED ${HA_CODEC_SRC})
|
||
if(WIN32)
|
||
set_target_properties(ha_codec PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||
endif()
|
||
else()
|
||
add_library(ha_codec STATIC ${HA_CODEC_SRC})
|
||
endif()
|
||
|
||
set(HA_CODEC_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||
target_include_directories(ha_codec PUBLIC ${HA_CODEC_INCLUDE})
|
||
|
||
# 告警门禁:零告警才允许通过(与 Makefile 的 csrc-lint 同一标准)。
|
||
# C 侧没有 Go 的 vet 等价物,告警是唯一的静态信号。
|
||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||
add_compile_options(-Wall -Wextra -Wpedantic -Wshadow -Wconversion)
|
||
endif()
|
||
|
||
# 不链接任何外部库 —— 保持与 ha_remotedevice 同一克制标准
|
||
target_link_libraries(ha_codec PRIVATE)
|
||
|
||
set(HA_CODEC_INCLUDE_DIR ${HA_CODEC_INCLUDE} CACHE INTERNAL "ha_codec include directories")
|
||
|
||
install(TARGETS ha_codec
|
||
EXPORT ha_codec-targets
|
||
LIBRARY DESTINATION lib
|
||
ARCHIVE DESTINATION lib
|
||
RUNTIME DESTINATION bin
|
||
INCLUDES DESTINATION include
|
||
)
|
||
install(DIRECTORY include/ DESTINATION include)
|
||
|
||
# ============================================================
|
||
# 测试
|
||
# ============================================================
|
||
if(BUILD_TESTS)
|
||
add_executable(ha_codec_test test/test_ha_codec.c)
|
||
target_link_libraries(ha_codec_test PRIVATE ha_codec)
|
||
add_executable(ha_json_scan_test test/test_ha_json_scan.c)
|
||
target_link_libraries(ha_json_scan_test PRIVATE ha_codec)
|
||
enable_testing()
|
||
add_test(NAME ha_codec_test COMMAND ha_codec_test)
|
||
add_test(NAME ha_json_scan_test COMMAND ha_json_scan_test)
|
||
endif()
|
||
|
||
# ============================================================
|
||
# 模糊测试:编码语义与内存安全的持续检验
|
||
#
|
||
# 动机:ha_codec 声称**逐值等价于 Go 参考实现**,其中最关键的一条是
|
||
# 「对畸形 UTF-8 的解码边界与 Go 的 utf8.DecodeRuneInString 一致」。
|
||
# 该行为在正常输入下永远测不到 —— 只有随机字节才能覆盖
|
||
# 截断序列 / 过长编码 / 代理对 / 超 U+10FFFF / 嵌入 NUL。
|
||
# Go 侧已有 TestGolden_InvalidUTF8(3000 组随机字节)做等价钉死;
|
||
# C 侧则需要独立验证两件事:
|
||
# 1. 任何输入都不崩、不越界(内存安全)
|
||
# 2. 返回值不违反头文件声明的不变式(0 <= keep <= len 等)
|
||
# 两者在 libFuzzer 上是持续的,而不是等下一次手写用例。
|
||
#
|
||
# 需 clang + -fsanitize=fuzzer;无则明确跳过(门禁不能假装通过)。
|
||
# ============================================================
|
||
if(BUILD_FUZZ)
|
||
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||
foreach(fz IN ITEMS test_fuzz_ha_codec test_fuzz_ha_json_scan)
|
||
add_executable(${fz} test/${fz}.c)
|
||
target_link_libraries(${fz} PRIVATE ha_codec)
|
||
target_compile_options(${fz} PRIVATE
|
||
-fsanitize=fuzzer,address,undefined
|
||
-fno-omit-frame-pointer)
|
||
target_link_options(${fz} PRIVATE
|
||
-fsanitize=fuzzer,address,undefined)
|
||
endforeach()
|
||
else()
|
||
message(WARNING
|
||
"BUILD_FUZZ=ON 需要 clang(libFuzzer);当前编译器是 "
|
||
"${CMAKE_C_COMPILER_ID},已跳过。")
|
||
endif()
|
||
endif()
|
||
|
||
# ============================================================
|
||
# 微基准:C 侧自身的开销(与 Go 侧 codec_bench_test.go 对照)
|
||
#
|
||
# 为什么 C 侧也要基准:Go 侧基准里,「C 实现省下的时间」与「cgo 边界成本」
|
||
# 是混在一起的。若 C 侧本身在某场景很慢,改 Go 绑定无济于事;
|
||
# 必须在能隔离处(纯 C、无边界)测出函数体成本,才知道该优化谁。
|
||
# ============================================================
|
||
if(BUILD_BENCH)
|
||
add_executable(ha_codec_bench bench/bench_ha_codec.c)
|
||
target_link_libraries(ha_codec_bench PRIVATE ha_codec)
|
||
endif()
|