第 18 章 / 共 51 章
卷三 · 协议开发

JSON 与 xvalue:带所有权的动态值

API 时代的通用语。xvalue 是引用计数的动态值树——本章的核心只有一件事:搞清每个 xvalue* 归谁、何时 Release。

本章导读

站位:xvalue 贯穿全书——Custom 旋钮(ch09)、Swap 交接(ch28)、配置树都是它。本章建立的所有权心智一次投入全书回报。

18.1 引入

C 没有原生 JSON 类型,常见做法是绑定第三方库 + 手拼字符串。XServer 的做法更彻底:xrt 内建一套引用计数的动态值系统(xvalue,116 个 API),JSON 只是它的序列化格式之一(XSON 是另一种)。它同时服务三个场景:配置树(Custom)、运行时数据交换(Swap)、协议序列化(JSON)。

18.2 构造与所有权

构造的真实写法(demo basic.h 节选)实机验证出处:demo-single/route_http/basic.h
static void Handle_JSON(XS_HttpReq* pReq, const RouteParamHTTP* arrParam, uint32 iParamCount)
{
    const XS_HostInfo* pHost = pReq->host;
    xvalue* pObject = xrtValueObject();          /* 你拥有 1 个引用 */

    (void)arrParam; (void)iParamCount;
    xrtValueObjectSetNew(pObject, XRT_STR_LITERAL("server"),
        xrtValueString(XRT_STR_LITERAL("xs3 demo")));
    xrtValueObjectSetNew(pObject, XRT_STR_LITERAL("ok"),
        xrtValueBool(true));
    xrtValueObjectSetNew(pObject, XRT_STR_LITERAL("host"),
        xrtValueString(xrtStrView((pHost->Name ? pHost->Name : "?"))));

    (void)ReplyJSON(pReq, 200, pObject);         /* ReplyJSON 只借用 */
    xrtValueRelease(pObject);                    /* 你释放你的引用 */
}

刚才发生了什么——所有权规则一目了然:

  • 构造函数(Object/Array/String/Int/Bool)返回你持有的引用
  • xrtValueObjectSetNew 的 New 后缀:把值引用转交容器——你不再需要(也不能)Release 被放入的值
  • Release 减引用计数;归零销毁整棵子树(子值随容器走)
  • XRT_STR_LITERAL 用于字面量视图(编译期长度);运行时字符串用 xrtStrView 指长度
实机输出
$ curl -s http://127.0.0.1:9080/json
{"server":"xs3 demo","ok":true,"host":"app"}

18.3 解析与访问

解析请求 JSON(demo http.h 模式)出处:demo-single/modules/http.h(模式节选)
/* 从请求体视图解析(配合 ch16 的 body 读取)*/
xvalue* pRoot = xrtJsonParse(tBodyView);
if ( pRoot == NULL ) {
    (void)ReplyText(pReq, 400, "bad json");
    return XS_OK;
}

/* 访问:Get 返回借用(不增引用,读完不用 Release)*/
xvalue* pName = xrtValueObjectGet(pRoot, "name");
if ( pName != NULL ) {
    /* 取标量值(类型不符返回默认)*/
    int64 iId = xrtValueInt64(xrtValueObjectGet(pRoot, "id"));
}

/* 序列化:Stringify 返回 str(调用方释放)*/
str sOut = xrtJsonStringify(pRoot, ...);
xrtValueRelease(pRoot);        /* 解析者释放树 */
/* ... 使用 sOut ... */

注意 Get 是借用、Parse 是持有——两件事最容易混淆:Get 到的指针不用 Release,但也不能活得比树久。

18.4 ReplyJSON 模式

demo 的 ReplyJSON 注释只有一句:"只借用 pObject,调用方仍负责 xrtValueRelease"——这就是模块边界的所有权声明(第 5 章三要素)。序列化在 Reply 内部完成(Stringify → ReplyRaw),handler 保持"构造 → 交给 Reply → 释放"的三段式,永远直白。

18.5 契约

  • 构造线:构造返回持有引用;SetNew 转交;Release 配对。
  • 访问线:ObjectGet/ArrayGet 返回借用——不 Release、不超树生命周期。
  • 序列化线:Stringify 返回 str 由调用方释放;Parse 返回树由解析者释放。

常见错误

Release 了 Get 的结果
症状
随机的树损坏 / 崩溃,通常在并发下才显形。
原因
Get 是借用(不增引用)——对它 Release 是在销毁不属于自己的引用。口诀:谁 Create/Parse 谁 Release,Get 永不 Release。
SetNew 之后又 Release 子值
症状
提前释放导致容器内悬空。
原因
New 后缀 = 所有权转交容器。之后子值归树管——你手里的引用已经交出去了。
✗ 双重管理
xvalue* pV = xrtValueInt(1);
xrtValueObjectSetNew(pObj, "k", pV);
xrtValueRelease(pV);   /* 交了又放 */
✓ 正确
xrtValueObjectSetNew(pObj, "k",
    xrtValueInt(1));   /* 内联最常见 */

编程练习

基础嵌套构造

构造 {"a":1,"list":[{"x":true},...]} 三层嵌套并 ReplyJSON 返回。

验收:输出与 jq 解析一致;无泄漏(内存报告核对)。
进阶POST 校验器

实现 /api/echo-json:解析 body,校验必填字段(缺失 400 + 错误清单数组),通过则回显。

验收:缺两个字段时返回两个错误项;畸形 JSON 返回 400。
挑战所有权压力测试

写一个 1000 次的构造-解析-释放循环,配合 xrt 内存报告(xrt 教程 ch06)验证引用计数归零。

验收:循环前后活动分配数一致。

小结

要点结论
xvalue引用计数动态值树;JSON 只是序列化格式之一
所有权口诀谁 Create/Parse 谁 Release;Get 借用不 Release
SetNew所有权转交容器,之后归树管
ReplyJSON只借用——模块边界要写所有权注释
xrt 延伸阅读