mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
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 公开接口。
This commit is contained in:
37
internal/agent/api/codec_abimacro_test.go
Normal file
37
internal/agent/api/codec_abimacro_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
//go:build cgo
|
||||
|
||||
package api
|
||||
|
||||
// codec_abimacro_test.go —— 堵住「Go 常量与 C 函数一起错成一样」的盲区。
|
||||
//
|
||||
// TestABIVersionMatches 比的是「Go 常量 vs C 函数返回值」。若有人同时把
|
||||
// Go 常量和 C 函数一起改成 2(而忘了改 ha_abi.h 的宏),那条测试照样通过
|
||||
// —— **两边一起错成一样**是它的盲区。本测试直接问 C 侧宏。
|
||||
//
|
||||
// (C 侧宏的取法在 codec_cgo.go:Go 不允许在 _test.go 里用 cgo,
|
||||
// 故 const 桥接只能写在非测试文件。)
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestABIMacroMatchesRuntimeAndGo(t *testing.T) {
|
||||
macro := codecABIVersionMacroValue()
|
||||
runtime := codecABIVersion()
|
||||
|
||||
if macro != runtime {
|
||||
t.Fatalf("C 侧宏与运行期值不一致:\n"+
|
||||
" HA_CODEC_ABI_VERSION(宏展开)= %d\n"+
|
||||
" ha_codec_abi_version() = %d\n"+
|
||||
" 说明:改了 ha_abi.h 的宏但没同步改 ha_codec_abi_version(),或反之。",
|
||||
macro, runtime)
|
||||
}
|
||||
|
||||
wantGo := codecABIMajorExpected*1000 + codecABIMinorExpected
|
||||
if macro != wantGo {
|
||||
t.Fatalf("C 侧宏与 Go 侧常量不一致:\n"+
|
||||
" C 宏 HA_CODEC_ABI_VERSION = %d (major=%d minor=%d)\n"+
|
||||
" Go 常量期望 = %d (major=%d minor=%d)\n"+
|
||||
" 改法:同步更新 ha_abi.h 与 codec_cgo.go 的 codecABIMajor/MinorExpected。",
|
||||
macro, macro/1000, macro%1000,
|
||||
wantGo, wantGo/1000, wantGo%1000)
|
||||
}
|
||||
}
|
||||
49
internal/agent/api/codec_abiversion_test.go
Normal file
49
internal/agent/api/codec_abiversion_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
@ -63,11 +63,58 @@ package api
|
||||
#cgo CFLAGS: -std=c99
|
||||
#include <stdlib.h>
|
||||
#include "ha_codec.h"
|
||||
|
||||
// 下面这个常量就是 C 侧宏展开后的值,经由 cgo 暴露给 Go。
|
||||
//
|
||||
// ★ 声明成 C 函数(而非 const)才能从 Go 侧读到值:
|
||||
// cgo 生成的 `*_Cvar_*` 变量对 Go 而言**不是常量**(实测报
|
||||
// "is not constant"),所以 Go 侧拿它做不了编译期断言,
|
||||
// 只能在测试期当普通变量比对。编译期的保证由下面那条 C 断言提供。
|
||||
int ha_abi_version_macro(void) { return HA_CODEC_ABI_VERSION; }
|
||||
|
||||
// C 侧自检:宏合成式与主/次版本必须自洽。
|
||||
// 这条断言在**编译 C 时**就生效,而不是等 Go 侧测试跑到。
|
||||
_Static_assert(HA_CODEC_ABI_MAJOR * 1000 + HA_CODEC_ABI_MINOR == HA_CODEC_ABI_VERSION,
|
||||
"ha_abi.h: HA_CODEC_ABI_VERSION 合成式与主/次版本不一致");
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// codecABIVersionExpected 是 C 侧 ha_abi.h 里 HA_CODEC_ABI_VERSION 的 Go 副本。
|
||||
//
|
||||
// ★ 为什么要手工拄一份而不是让 cgo 直接读宏:
|
||||
// cgo 顶部的 C 代码在 cgo 阶段被**预处理并丢弃**,其中的宏在 Go 侧不可见;
|
||||
// 能看到的只有 cgo 生成的文件。用 cgo 的 `const` 桥接(C.ha_codec_abi_version)只能在
|
||||
// **运行期**问到版本,编译期拿不到,无法把「两侧版本不一致」变成构建失败。
|
||||
// 而「注释里说冻结」不是机制。这份 Go 常量 + codec_abiversion_test.go
|
||||
// 把版本漂移变成**测试期断言**,真正对得上才跑得起来。
|
||||
//
|
||||
// 改动规则(与 ha_abi.h 一致):
|
||||
// - C 侧新增函数/枚举值(纯追加)→ 同步把这里 +1,并改 abi_test 的期望
|
||||
// - 改签名/删函数/改结构体布局 → MAJOR+1,**所有调用方必须同步重编**
|
||||
const (
|
||||
codecABIMajorExpected = 1
|
||||
codecABIMinorExpected = 0
|
||||
)
|
||||
|
||||
// codecABIVersionMacroValue 查询 C 侧 HA_CODEC_ABI_VERSION 宏展开后的值。
|
||||
//
|
||||
// ★ 它的存在是为了堵一个盲区:TestABIVersionMatches 比的是
|
||||
// 「Go 常量 vs C 函数返回值」。若有人同时把 Go 常量和 C 函数
|
||||
// 一起改掉(而忘了改 ha_abi.h 的宏),那条测试照样通过 ——
|
||||
// **两边一起错成一样**是它的盲区。这个值直接取自 C 宏,
|
||||
// 由 codec_abimacro_test.go 拿来交叉核对。
|
||||
//
|
||||
// ★ 为何是函数而非 Go 常量:cgo 生成的 `*_Cvar_*` 不是 Go 常量
|
||||
// (实测 "is not constant"),无法在编译期参与断言。
|
||||
// 编译期的保证在 C 侧(codec_cgo.go 里的 _Static_assert)。
|
||||
func codecABIVersionMacroValue() int { return int(C.ha_abi_version_macro()) }
|
||||
|
||||
// codecABIVersion 查询 C 侧自称的 ABI 版本(major*1000 + minor)。
|
||||
func codecABIVersion() int { return int(C.ha_codec_abi_version()) }
|
||||
|
||||
|
||||
// cstr 返回 s 的底层字节首地址与长度,供 C 侧零拷贝读取。
|
||||
//
|
||||
// 空串返回 (nil, 0):调用方不应把 nil 传给会解引用的 C 函数。
|
||||
|
||||
1
internal/agent/api/ha_abi.h
Symbolic link
1
internal/agent/api/ha_abi.h
Symbolic link
@ -0,0 +1 @@
|
||||
../../../csrc/include/ha_abi.h
|
||||
Reference in New Issue
Block a user