Files
HomeAgent/csrc/include/ha_codec.h
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

98 lines
4.2 KiB
C
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.

#ifndef HA_CODEC_H
#define HA_CODEC_H
/*
* ha_codec — HomeAgent 内核编解码层(C 实现)
*
* ============================ 接口冻结声明 ============================
* 本头文件是对外契约。函数签名、语义、返回值一经发布即为冻结接口,
* 修改必须走大版本流程(与 third_party/homeagent-sdk 同一冻结标准)。
*
* 设计约束(见 docs/zh/c-core/llm-orchestration-c.md §四):
* 1. 只吃 const char* + **显式长度**,出数值/字节偏移 —— 不回调 Go、
* 不传 Go 指针、不要求 NUL 结尾
* 2. **不 malloc**:不需要出参缓冲区,需要「结果」时返回字节偏移/长度,
* 由调用方在自己的缓冲上切片(零拷贝)
* 3. 无状态、纯函数、线程安全(不写全局可变状态)
*
* 当前覆盖:L1 协议编解码层中的纯计算部分(第一个最小切片)。
*
* ============================ 为什么签名带长度 ============================
* 初版签名用 `const char*` 隐含「NUL 结尾」,于是每次调用都要:
* Go `C.CString` 分配+拷贝一遍 → C `strlen` 再扫一遍。
* 实测这部分开销占单次调用的 80% 以上(cgo 边界本身仅 ~30ns,
* 而初版 ModelContextWindow 实测 175ns)。
* 改为「指针 + 长度」后,Go 侧用 unsafe.StringData 直接传底层数组,
* 零分配零拷贝。这是设计约束第 1 条的字面要求。
*/
#include <stddef.h>
#include "ha_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ==================== ABI 自述(供 Go 侧与日志核对) ==================== */
/* 返回 HA_CODEC_ABI_VERSION(major*1000 + minor)。
*
* 存在的意义:Go 侧不该靠 `#include` 宏做版本断言(cgo 头文件里的宏在
* 预处理后不可见),而要**运行期/测试期**能问 C 侧「你自称什么版本」。
* 由 codec_cgo.go 绑定、codec_abiversion_test.go 与 C 侧宏三方比对。 */
int ha_codec_abi_version(void);
/* ==================== 模型上下文窗口推断 ==================== */
/* 无法从模型名推断时的哨兵值(与 Go 侧一致)。
*
* 为什么返回哨兵而不是直接给兜底值:调用方需要区分「真推断出了」与
* 「推断不出、只能兜底」——后者要打一行日志(窗口被低估必须可见),
* 并提示部署方用 per-source context_window 显式声明。
* 若 C 侧直接返回兜底值,调用方就永远分不清这两种情况。 */
#define HA_CODEC_CONTEXT_WINDOW_UNKNOWN (-1)
/* 由模型名推断最大上下文窗口(token 数);推断不出返回
* HA_CODEC_CONTEXT_WINDOW_UNKNOWN。
*
* model 为 UTF-8 字节序列,**不需要 NUL 结尾**;model_len 是字节数。
* model 为 NULL 或 model_len 为 0 时返回 UNKNOWN。
*
* 匹配大小写不敏感(仅对 ASCII 字母做折叠;非 ASCII 字节按原样比较,
* 与 Go 侧对模型名的实际输入一致)。
*
* 语义必须与 Go 侧 modelContextWindowPure 逐值一致(黄金对照测试钉死)。 */
int ha_codec_model_context_window(const char *model, size_t model_len);
/* ==================== token 估算与截断 ==================== */
/* 粗略估算 token 数。
*
* 规则(与 Go 侧 EstimateTokens 一致):保守取 max(1, runeCount * 2)。
* 按 UTF-8 **字符数**(rune)计,不是字节数。
* text 为 NULL 或 text_len 为 0 返回 0。
*
* 非法 UTF-8 序列按 Go 的 utf8 解码语义处理(每字节一个 rune),
* 保证与 Go 侧逐值一致。 */
int ha_codec_estimate_tokens(const char *text, size_t text_len);
/* 按 token 预算计算「应保留的字节数」。
*
* ★ 返回的是**字节数**而非字符串:截断结果必然是输入的前缀,
* 调用方直接在自己的缓冲上切片即可(零拷贝、无出参缓冲区、无 malloc)。
*
* 语义与 Go 侧 TruncateByTokens 一致:从开头保留 maxTokens/2 个 rune;
* 未超预算时返回 text_len(即整串)。
* max_tokens <= 0 或 text 为 NULL/text_len 为 0 时返回 0。
*
* 返回值保证 <= text_len。 */
size_t ha_codec_truncate_by_tokens(const char *text, size_t text_len,
int max_tokens);
#ifdef __cplusplus
}
#endif
#endif /* HA_CODEC_H */