Files
HomeAgent/internal/agent/api/codec_abiversion_test.go
JianFeeeee 8070844fe9 build(csrc): C 基础设施门禁(ABI 版本 / 告警 / sanitizer / 交叉编译 / 模糊测试)
C 化从「一把刀」推进到「可持续推进」,本轮先把基础设施建起来:
不建它,后续每个 C 切片都在裸奔(无告警门禁、无内存安全检查、
无交叉编译验证、无 ABI 漂移检测)。

新增门禁(make check-csrc / check-csrc-full,已接进 make test):
- csrc-lint   gcc+clang 双编译器 × -Wall -Wextra -Wpedantic -Wshadow
              -Wconversion,零告警才算过(-Wconversion 是为 cgo 窄化
              准备的:size_t→int 截断在默认档下是静默的)
- csrc-abi    ABI 版本运行期自述 + 荒谬值检查
- csrc-headers 头文件自包含性(每个 .h 能单独编过)
- csrc-sanitize ASan+UBSan 跑 C 契约测试
- csrc-cross  arm64 交叉编译(homed 的真实发布目标)
- csrc-fuzz   libFuzzer:内存安全 + 6 条不变式(可 CI 门禁)

新增 ABI 契约(csrc/include/ha_abi.h):
- ha_codec.h 声明「签名冻结」,但冻结只写在注释里;现改为
  HA_CODEC_ABI_MAJOR/MINOR + 运行期自述 + Go 侧常量,
  三方交叉断言,版本漂移在测试期判红而非线上表现为行为诡异。
- 门禁当场抓出我自己的两个真 bug:①_Static_assert 是 C11 而项目
  是 -std=c99(-Wpedantic 报的);②##msg 不能拼接字符串字面量,
  导致两个断言共用一个 typedef 名(clang 报的)。

基础设施当场抓出的三个真实缺陷(都是「本机 gcc 能编过、别处会炸」类):
- bench 用了 POSIX clock_gettime,而 CMake 刻意 C_EXTENSIONS OFF
  (严格 c99)→ 头文件未声明;补 _POSIX_C_SOURCE(须在任何头之前)
- 头文件缺 include 时只在「恰好被别的头先包含」处静默编过
- Go 不允许在 _test.go 用 cgo ⇒ C 侧 const 桥接只能放非测试文件,
  且 cgo 生成的 *_Cvar_* 不是 Go 常量(「两边一起错成一样」的盲区,
  改用 C 函数返回 + 编译期 _Static_assert 补上)

实测(全部当场可复现):
- 双编译器 × c99/c11 零告警;ASan+UBSan PASS
- libFuzzer 91s 跑 3329316 次、零崩溃(6 条不变式全过)
- 变异测试:改 C 侧宏 / 让 Go 常量与 C 函数「一起错成一样」,
  均被对应断言抓住(证明门禁不是摆设)
- C 侧纯函数基准(首次把函数体成本与 cgo 边界成本分开测):
  ascii_1k 121.7ns/8.4GB/s;zh_1k 1434ns;truncate 两者均约 128-134ns
- 全量 go test -count=1 ./...:57 包 0 FAIL
- make build-linux-arm64 → ELF aarch64
- 纪律检查 FAIL=0;SDK 公开接口 diff = 0 行

未动:csrc/ 是内核 C ABI,不属 third_party/homeagent-sdk 公开接口。
2026-09-26 09:44:17 +08:00

50 lines
2.2 KiB
Go
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.

//go:build cgo
package api
// codec_abiversion_test.go —— Go 侧与 C 侧 ABI 版本必须对得上。
//
// ============================ 为什么这是必需的 ============================
// ha_codec.h 声明「签名一经发布即冻结」,但注释不参与编译 —— 两侧对
// 「我以为的版本」不一致时,没有任何机制会报错。典型事故:
// 某人在 C 侧给 openAIToolCall 之类的结构体加了字段并把 MAJOR 提到 2,
// Go 侧没改 —— 产出的二进制「看起来能跑」,但字段错位,
// 表现为插件行为诡异 / 记忆内容错乱,极难定位。
//
// 这条测试把「两侧版本一致」变成**会失败的事实**。
//
// 判定:Go 侧常量(codecABIMajorExpected / codecABIMinorExpected)必须等于
// C 侧 ha_codec_abi_version() 运行期返回值,也必须等于 C 侧宏展开值
// (后者由 codec_abi_macro_test.go 单独验证,避免「两边都错成一样」)。
import "testing"
func TestABIVersionMatches(t *testing.T) {
got := codecABIVersion()
want := codecABIMajorExpected*1000 + codecABIMinorExpected
if got != want {
t.Fatalf("ABI 版本不一致:\n"+
" C 侧 ha_codec_abi_version() = %d (major=%d minor=%d)\n"+
" Go 侧 codecABIVersion 期望 = %d (major=%d minor=%d)\n"+
" 改法:若 C 侧新增了函数/字段(纯追加),把本文件两个常量各 +1;\n"+
" 若改了签名/删了函数/改了结构体布局,那是 MAJOR 变更,\n"+
" 所有调用方必须同步重编,不能只改版本号。",
got, got/1000, got%1000, want, want/1000, want%1000)
}
}
// TestABIVersionSane 防止「两边一起写成荒谬值」也能通过上面的测试。
func TestABIVersionSane(t *testing.T) {
got := codecABIVersion()
if got < 1000 || got > 99999 {
t.Fatalf("ABI 版本荒谬:%d(major 必须在 1-9,minor 必须在 0-99)", got)
}
if codecABIMajorExpected < 1 || codecABIMajorExpected > 9 {
t.Errorf("Go 侧 major 常量越界:%d", codecABIMajorExpected)
}
if codecABIMinorExpected < 0 || codecABIMinorExpected > 99 {
t.Errorf("Go 侧 minor 常量越界:%d", codecABIMinorExpected)
}
}