#ifndef HA_SSE_H #define HA_SSE_H /* * ha_sse — LLM 流式协议(SSE 分块)的「结构导航」辅助层 * * ============================ 定位 ============================ * 本层不是 JSON 库(那是 ha_json_scan),而是把 ha_json_scan 原语组合成 * **协议层需要的几次定位**,供内核 parseOpenAICompatibleStreamChunkFull 使用。 * * ★ 为什么这些函数放在 csrc/ 而不是内联在 Go 的 cgo 前言里: * 放在 cgo 前言里的 C 代码**逃出了全部 C 门禁**(告警 / ASan+UBSan / * 交叉编译 / 模糊测试),而它恰恰是本刀最容易出错的位置。 * 移进 csrc/ 后,同一个 -Wall -Wextra -Wpedantic -Wconversion 门禁 * 与 sanitizer 都覆盖到它 —— 这是一次真实的结构调整,不是形式主义。 * * ============================ 为什么只做「导航」 ============================ * 实测两条 wire 语义(docs/zh/c-core/sse-codec-c.md §5)使「全量 C 化」不成立: * §5.1 重复键是**字段级合并**(json.Unmarshal 的 SetIndex 叠加语义) * §5.2 stringifyContent 的 default 分支是 json.Marshal(键排序 / 浮点 * 最短往返 / HTML 转义 / int 舍入) * 二者都只在**取值**阶段需要,故本层只回答「值在哪里、它的热分支结果是什么」, * 需要重新序列化的形态交回 Go(由 encoding/json 保证语义)。 * * ============================ 键匹配:大小写敏感 ============================ * 本层是 **map key** 语义(`m["text"]`)⇒ 大小写敏感。 * 实测 `{"TEXT":"up"}` 取不到 `text`、`{"text":"low"}` 可以(§5.4-1)。 * * ⚠️ 与 ha_json_key_eq(大小写**不**敏感,用于 struct 字段名)语义相反。 * 两者用途不同、都必要,**不要「统一」掉**。 * struct 字段那一跳由 encoding/json 负责,天然正确。 */ #include #include "ha_abi.h" #include "ha_json_scan.h" #ifdef __cplusplus extern "C" { #endif #define HA_SSE_ABI_MAJOR 1 #define HA_SSE_ABI_MINOR 0 #define HA_SSE_ABI_VERSION (HA_SSE_ABI_MAJOR * 1000 + HA_SSE_ABI_MINOR) HA_STATIC_ASSERT(HA_SSE_ABI_MAJOR >= 1 && HA_SSE_ABI_MAJOR <= 9, ha_sse_abi_major_in_range); HA_STATIC_ASSERT(HA_SSE_ABI_MINOR >= 0 && HA_SSE_ABI_MINOR <= 99, ha_sse_abi_minor_in_range); int ha_sse_abi_version(void); /* * 在对象里按**大小写敏感**的键定位值。 * * 返回: 1 = 找到(*out 已写);0 = 未找到(对象良构);-1 = 对象畸形。 * *dup 在发现**重复键**时置 1(后者胜已写入 *out)—— * 调用方据此整体回退到 encoding/json,因为重复键的字段级合并语义 * 见 sse-codec-c.md §5.1,本层不实现。 */ int ha_sse_obj_find(const ha_span *obj, const char *key, size_t keylen, ha_span *out, int *dup); /* * 在对象里按**大小写不敏感**的键定位值(struct 字段语义)。 * * ★ 为什么必须与 ha_sse_obj_find 并存(两个函数,语义相反): * - Go 的 `raw struct{ Choices ... \`json:"choices"\` }` 是 **struct 字段**, * encoding/json 对字段名做**大小写不敏感**匹配 ⇒ 实测 * `{"CHOICES":[{"DELTA":{"CONTENT":"ci"}}]}` 能取到 content="ci"。 * - 而 `content` 是 `interface{}` → `map[string]interface{}`,取 `m["text"]` * 是 **map key** 语义 ⇒ 大小写**敏感**(实测 `{"TEXT":"up"}` 取不到)。 * 跳错层就会静默漏掉字段(或取到不该取的),故两个函数都必要, * 调用方必须按「这一跳在 Go 里是 struct 还是 map」来选择。 * * 返回与 ha_sse_obj_find 相同:1=找到 0=未找到 -1=畸形。 * 对 *dup:大小写不敏感语义下,`{"CHOICES":..,"choices":..}` 两次都会命中 * 同一个 Go 字段(后者胜),故同样置 dup 让调用方回退。 */ int ha_sse_obj_find_ci(const ha_span *obj, const char *key, size_t keylen, ha_span *out, int *dup); /* * 校验 doc 是「**恰好一个**良构 JSON 对象」(尾部只允许空白)。 * * 返回 1 = 是;0 = 否。 * * ★ 为什么必须单独校验尾部:ha_sse_obj_find 用 members_complete 只保证 * 对象本身闭合,**不检查尾部残留** —— 而 Go 的 json.Unmarshal 会拒绝 * `{"a":1}{"b":2}`(trailing garbage)。少了这一步,快速路径会比 Go 宽松, * 把一个 Go 判为失败的块判为成功 ⇒ 静默接受垃圾块。 */ int ha_sse_root_object(const ha_span *doc); /* * 取数组**第一个元素**的 span。 * * 返回: 1 = 有元素;0 = 空数组;-1 = 非数组或畸形。 * * 为什么只要第一个:`choices[0]` 是协议约定(Go 侧也只读 resp.Choices[0]), * 本层据此避免为后续元素做无用功。 */ int ha_sse_arr_first(const ha_span *arr, ha_span *out); /* * stringifyContent 的 **C 可判定分支**: * - 字符串值 → 反转义后原样输出 * - 数组值 → 逐元素取对象的 "text" 字段(精确键)拼接 * * 返回: 1 = 已写入(*outlen 为字节数);0 = 需回退 Go。 * 回退的两种情形: * a) 缓冲不足(调用方应给 >= val->len*3+4 的 cap) * b) 值类型是对象 / 数字 / 字面量 —— 那些要走 json.Marshal(§5.2) * * 数组元素的规则(§5.4-2/3 实测): * · 非对象元素 **静默跳过**(`["a",{"text":"b"}]` → "b") * · 非对象的 "text"(如 text:123)**静默跳过** * · 元素里出现重复的 "text" 键 ⇒ 整体回退 Go(合并语义) */ int ha_sse_stringify(const ha_span *val, char *out, size_t cap, size_t *outlen); /* * arguments 为**字符串**时,取出其解码结果(省掉 interface{} 与二次解析)。 * * 返回 1 = 已写入;0 = 不是字符串或失败(调用方按既有路径处理)。 * 非字符串 arguments(对象/数组/数字)**必须**回退 Go:那里的 * `rawArgsString` 会 `json.Marshal` 重新编码,而这个**重新编码的键序 * 可能与原文不同**(实测 {"b":2,"a":1} → {"a":1,"b":2})—— * 逐值一致要求由 encoding/json 来做。 */ int ha_sse_arg_string(const ha_span *val, char *out, size_t cap, size_t *outlen); #ifdef __cplusplus } #endif #endif /* HA_SSE_H */