mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-26 20:33:15 +00:00
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 公开接口。
98 lines
4.2 KiB
C
98 lines
4.2 KiB
C
#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 */
|