-
Notifications
You must be signed in to change notification settings - Fork 0
Unified Token Stream
NextJson 的解码器只有一套解码原语,但背后有两种输入源。这是"内部/邻接标签
枚举、Value 往返"等路径没有出现第二套实现的根本原因。本页用一个具体例子把
这条机制走一遍。
enum DecoderInput<'de> {
Bytes(&'de [u8]), // 惰性单 token 前瞻词法
Tree(Vec<Token<'de>>, ...), // 内容重放
}| 输入源 | 用途 | 特点 |
|---|---|---|
Bytes |
普通 JSON / 各格式字节流 | 逐个 token 惰性词法;未转义字符串 Cow::Borrowed 零分配借用;整数手写解析带溢出检测 |
Tree |
内部标签 / 邻接标签枚举、Value 驱动的解码 |
先校验整棵树再重放;字符串是 owned 的 |
两种源暴露完全相同的 FormatDecoder 方法,所以宏生成的代码不需要知道自己在
哪条路径上。
pub enum Token<'de> {
Null,
Bool(bool),
Number(Number),
Str(Cow<'de, str>),
BeginObject,
EndObject,
BeginArray,
EndArray,
}Token 是词法层与重放层共享的最小单位:Bytes 源逐 token 产出,Tree 源把
预存的 Vec<Token> 逐个吐出。
let v: MyStruct = nextjson::nextdecode(br#"{"a":1}"#)?;Decoder 的输入源是 Bytes(&b"{\"a\":1}"[..])。注意它没有预先扫描全文,
而是用"一次只 lex 一个 token"的方式工作:
- 类型问
begin_object→ 词法器跳过空白,看到{,返回对象开始; - 类型问
object_key→ 词法器切出"a"。因为"a"里没有转义符,它直接 切输入切片(Cow::Borrowed("a")),零分配; - 类型问
u64→ 词法器看到1,用手写整数解析(带溢出检测)得出1u64; - 类型问
object_entry_sep→ 看到},返回"没有更多条目"; - 类型问
end_object→ 消费}。
整个过程中,只有"被转义的字符串"才需要分配新内存(Cow::Owned)。这是
"零拷贝边界"的实现基础:能借就借,不能借才拷贝,且文档明确不把必要拷贝
伪装成零拷贝。
现在假设同一段数据先被收集成了一棵 token 树:
let toks: Vec<Token> = vec![
Token::BeginObject,
Token::Str(Cow::Borrowed("a")),
Token::Number(Number::U64(1)),
Token::EndObject,
];Decoder 的输入源是 Tree(toks)。类型问同样的问题,Tree 源从 Vec 里
逐个吐出预存的 token:
-
begin_object→ 弹出Token::BeginObject; -
object_key→ 弹出Token::Str; -
u64→ 弹出Token::Number(Number::U64(1)); - ......
类型代码一个字都不用改——它看到的接口和 Bytes 源一模一样。这就是"统一
Token 流"的含义:一旦 Bytes 路径正确,Tree 路径在结构上不可能偏离,因为它
只是把词法结果换成了预存 token。
序列化库最容易出现"第二套实现"的地方:
-
内部标签枚举(
#[njson(tag = "type")]):解码时先读 tag 决定变体,然后 继续用同一解码器消费剩余字段——中间需要把"已读出的内容"重新放回去或重放; -
邻接标签枚举(
tag + content):内容与标签分离,需要把内容"重放"到 变体解码器; -
untagged 枚举:需要
save/restore回溯; -
Value解码:把整棵 JSON 树读进内存再按需解码; -
文档式格式(toml / yaml / bson):解析器先把整份文档收集成树,再通过
TreeDecoder重放成同一套原语。
若每种场景都写一套读取逻辑,正确性风险随枚举形态数量线性增长。统一 Token 流 让这些路径共享同一引擎。
- 解析器一次只 lex 一个 token,不预先扫描全文;
-
peek_token只做一个 token 的前瞻(存进 lookahead 字段); - 未转义字符串直接切输入切片(
Cow::Borrowed),零分配; - 转义字符串(
\n、\uXXXX、surrogate 对)才物化新的 UTF-8 字节。
性能注记:
Bytes路径对常见标量(数字 / 未转义字符串 / 布尔 / null)走的是 字节直达快路径(Phase 9 起),不再构造Token再 match——只有当类型不匹配、 需要回退保证错误消息一致时才走 token 路径。数字/字符串原语因此各快约 20%。
-
Bytes语义对文本格式是"天然"的; - 计数式二进制格式(msgpack / postcard)用
array_entry_sep/object_entry_sep递减计数; - 文档式格式(toml / yaml / bson)经
TreeDecoder把收集好的树重放成同一套原语 ——再次复用统一引擎。
- 编码侧对应事件流:Core Contracts / Multi-Format Engine
- 借用边界:Decode Slot / Streaming(流式解码必须 owned,因为无法借用流)