Files
HomeAgent/csrc/CMakeLists.txt
JianFeeeee 4d3962a845 feat(csrc): 第二刀 —— 零分配 JSON 扫描/取值层 ha_json_scan(含黄金对照)
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 不复用;
下一刀即协议编解码层。
2026-09-26 10:07:27 +08:00

126 lines
5.3 KiB
CMake
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()