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:
JianFeeeee
2026-09-26 09:44:17 +08:00
parent cbe024f6a6
commit 8070844fe9
11 changed files with 754 additions and 1 deletions

View File

@ -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 函数。