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 不复用;
下一刀即协议编解码层。
This commit is contained in:
JianFeeeee
2026-09-26 10:07:27 +08:00
parent f877dff95f
commit 2b78a9288e
13 changed files with 2456 additions and 16 deletions

View File

@ -126,19 +126,34 @@ csrc-abi:
# C 侧没有 Go 的 -race 等价物,sanitizer 就是这里的关等物。
# 若本机无 libasan/libubsan(交叉工具链常见),明确 SKIP 而非静默跳过。
.PHONY: csrc-sanitize
# ★ 每个测试文件**各自**链接成独立二进制:契约测试每个都带 main,
# 合在一起会「multiple definition of main」——而报错被 2>/dev/null
# 吞掉后会被误报成「本机无 sanitizer」,是个假的 SKIP。
# 故这里逐个构建、逐个跑,任何一个失败都判红。
csrc-sanitize:
@echo "== C 侧 ASan+UBSan =="
@tmp=$$(mktemp -d); \
if ! $(CC) $(CSRC_CFLAGS) -fsanitize=address,undefined -fno-omit-frame-pointer \
-o $$tmp/san_test $(CSRC_SRCS) $(CSRC_DIR)/test/*.c 2>/dev/null; then \
echo " [SKIP] 本机无 ASan/UBSan 运行库(交叉工具链常见),已跳过"; rm -rf $$tmp; exit 0; \
fi; \
if ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
$$tmp/san_test > $$tmp/out.txt 2>&1; then \
echo " ASan+UBSan 契约测试: PASS"; rm -rf $$tmp; \
else \
echo " [FAIL] sanitizer 报告:"; cat $$tmp/out.txt | head -30; rm -rf $$tmp; exit 1; \
fi
built=0; \
for t in $(CSRC_DIR)/test/test_*.c; do \
case "$$t" in *fuzz*) continue ;; esac; \
base=$$(basename $$t .c); \
if ! $(CC) $(CSRC_CFLAGS) -fsanitize=address,undefined -fno-omit-frame-pointer \
-o $$tmp/$$base $(CSRC_SRCS) $$t 2>$$tmp/build.log; then \
if grep -qi 'sanitize\|asan\|ubsan' $$tmp/build.log; then \
echo " [SKIP] 本机无 ASan/UBSan 运行库,已跳过"; rm -rf $$tmp; exit 0; \
fi; \
echo " [FAIL] 构建失败 ($$base):" ; head -10 $$tmp/build.log; rm -rf $$tmp; exit 1; \
fi; \
built=1; \
if ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
$$tmp/$$base > $$tmp/$$base.out 2>&1; then \
echo " ASan+UBSan $$base: PASS"; \
else \
echo " [FAIL] sanitizer 报告 ($$base):"; head -30 $$tmp/$$base.out; rm -rf $$tmp; exit 1; \
fi; \
done; \
rm -rf $$tmp; \
if [ "$$built" = "0" ]; then echo " [FAIL] 没找到任何契约测试"; exit 1; fi
# csrc-headers:头文件自包含性(每个 .h 都能单独编过)
#
@ -207,12 +222,16 @@ csrc-cross:
if ! command -v $$CC_ARM64 >/dev/null 2>&1; then \
echo " [SKIP] $$CC_ARM64 不存在(未装交叉工具链)"; exit 0; \
fi; \
if $$CC_ARM64 -std=$(CSRC_STD) $(CSRC_WARN_FLAGS) -Werror -I$(CSRC_DIR)/include \
-c $(CSRC_SRCS) -o /dev/null 2>/dev/null; then \
echo " $$CC_ARM64: 0 告警、编译通过 ✓"; \
ok=1; \
for src in $(CSRC_SRCS); do \
if ! $$CC_ARM64 $(CSRC_CFLAGS) -Werror -fsyntax-only $$src 2>&1 | head -20; then \
ok=0; \
fi; \
done; \
if [ "$$ok" = "1" ]; then \
echo " $$CC_ARM64: 0 告警、编译通过 ✓($(words $(CSRC_SRCS)) 个源文件)"; \
else \
echo " [FAIL] arm64 交叉编译失败(把 .o 汇成单个输出是 gcc 的已知限制,改用逐文件)"; \
$$CC_ARM64 $(CSRC_CFLAGS) -Werror -fsyntax-only $(CSRC_SRCS) 2>&1 | head -20; exit 1; \
echo " [FAIL] arm64 交叉编译失败"; exit 1; \
fi
# check-csrc:C 侧全部门禁的聚合入口(接进 make test 与 CI)