一个用C编写的快速、零拷贝Transit格式读写器
摘要
一个快速、零拷贝的C11库,用于读写Transit数据格式,具备SIMD加速,通过一个与编解码器无关的单一引擎支持JSON、JSON-Verbose和MessagePack线格式。
查看缓存全文
缓存时间: 2026/06/08 11:17
DotFox/transit.c 源码:https://github.com/DotFox/transit.c
Transit.C
一个快速、零拷贝的 Transit (https://github.com/cognitect/transit-format) 读取器和写入器,使用 C11 编写,并带有 SIMD 加速。一个编解码无关的引擎支持全部三种 Transit 有线格式——JSON、JSON-Verbose 和 MessagePack——并直接解码到单个竞技场 (arena) 中,字符串有效载荷采用借用(从不拷贝)方式。
CI (https://github.com/DotFox/transit.c/actions/workflows/ci.yml)
许可证:MIT (https://opensource.org/licenses/MIT)
太长不看版 - 什么是 Transit?
Transit (https://github.com/cognitect/transit-format) 是一种格式和一组库,用于在不同语言编写的应用程序之间传递值。它基于 JSON 和 MessagePack 构建,因此您可以使用它们的工具链和速度,但拥有更丰富的类型系统和内建的有效载荷压缩。可以把它看作“能正确往返真实类型的 JSON”:
- 宿主格式的基础类型:映射、数组、字符串、数字、布尔值、null
- JSON 缺少的扩展类型:关键字
:foo、符号、瞬间(时间戳)、UUID、URI、大整数/大十进制数、字符、字节数组、集合和列表 - 内建压缩(缓存):重复的映射键、关键字、符号和标签只写一次,之后通过简短的
^N代码引用,因此键较多的冗长有效载荷会被大幅压缩 - 通过标记值扩展:使用标签加表示形式将您自己的类型通过线路传输,并用自定义处理器解码——不再需要
{"__type": "Date", "value": "..."}之类的技巧 - 语言无关且自描述:最初来自 Cognitect/Clojure,现已有多种语言的实现
为什么选择 Transit 而不是纯 JSON?
真实的类型系统(关键字保持为关键字,瞬间保持为瞬间)、通过键缓存实现更小的有效载荷、以及通过标签提供一流的可扩展性——同时仍然使用您已有的 JSON/MessagePack 基础设施。
了解更多: 官方 Transit 规范 (https://github.com/cognitect/transit-format)
特性
- 🚀 快速:SIMD 加速的字符串扫描(x86-64 上的 SSE2,arm64 上的 NEON)、Grisu2 最短双精度格式化、以及无
memset的标记生成器,内联整数解析 - 💾 零拷贝:无需转换的字符串、字节数组和键直接从输入缓冲区 借用 并编入结果树——从不复制
- 🧩 单一引擎,三种格式:一个编解码无关的读取器状态机和一个写入器遍历逻辑,即可处理 JSON、JSON-Verbose 和 MessagePack;格式只是位于一个
transit_codec_t背后的标记读取器/写入器 - 📡 流式发射器:一个推送 API(
transit_emit_*),可以增量生成 Transit,而无需构建值树,与transit_write的输出字节相同 - 🗜️ Transit 缓存:对重复的键/关键字/符号/标签使用
^N缓存代码,读取器和写入器保持同步 - 🏷️ 丰富的类型系统:关键字、符号、瞬间、UUID、URI、大整数/大十进制数、字符、字节数组、集合、列表和任意标记值
- 🔌 自定义处理器:在读取时将您自己的复合标签解码为丰富的值
- 🧹 内存安全:每个结果树由一个单一的批量竞技场支持——一次
transit_result_free()即可释放所有内容 - 📏 无递归:读取器使用显式的容器栈,因此没有 C 栈深度限制(已测试到 60,000 层嵌套)
- 🔧 零依赖:仅纯 C11 和标准库
- ✅ 一致性测试:针对官方
cognitect/transit-format跨实现示例语料库运行 - 📦 可移植:SSE2/NEON SIMD,带有严格可移植性的标量回退(
NO_SIMD=1);在 Linux、macOS 和 Windows 上构建为静态或共享库
目录
安装
要求
- C11 兼容的编译器(GCC 4.9+, Clang 3.1+, MSVC 2015+)
- Make(Unix/macOS)或 CMake(Windows/跨平台)
- 支持的平台:
- macOS(Apple Silicon, Intel)—— NEON/SSE2 SIMD
- Linux(arm64, x86-64)—— NEON/SSE2 SIMD
- Windows(x86-64, arm64)—— 通过 MSVC/MinGW/Clang
构建库
Unix/macOS/Linux:
# 克隆仓库(包含示例语料库子模块)
git clone --recurse-submodules https://github.com/DotFox/transit.c.git
cd transit.c
# 构建静态库 (build/libtransit.a)
make
# 运行测试以验证构建
make test
Windows:
# 克隆仓库
git clone https://github.com/DotFox/transit.c.git
cd transit.c
# 使用 CMake 构建(支持 MSVC, MinGW, Clang)
.\build.bat
# 或使用 PowerShell 脚本
.\build.ps1 -Test
集成到您的项目
选项 1:链接静态库
# 编译您的代码,链接公共头文件和归档
cc -o myapp myapp.c -I/path/to/transit.c/include -L/path/to/transit.c/build -ltransit -lm
# 或添加到您的 Makefile
CFLAGS += -I/path/to/transit.c/include
LDFLAGS += -L/path/to/transit.c/build -ltransit -lm
选项 2:直接包含源文件
将 include/transit.h 和 src/ 下的所有文件复制到您的项目中,然后一起编译。只有 include/transit.h 是公开接口;src/ 下的头文件是内部使用的。
快速开始
#include "transit.h"
#include <stdio.h>
int main(void) {
/* Transit-JSON 表示的映射 {:name "Alice" :age 30 :langs [:clojure :rust]}。
在紧凑的 Transit-JSON 中,映射是一个以 "^ " 标记开头的数组,关键字写作 "~:name"。 */
const char *input = "[\"^ \",\"~:name\",\"Alice\",\"~:age\",30,\"~:langs\",[\"~:clojure\",\"~:rust\"]]";
/* 读取 Transit(零拷贝:有效载荷从 `input` 借用)。 */
transit_result_t r = transit_read(transit_codec_json(), (const uint8_t *)input, strlen(input));
if (r.error != TRANSIT_OK) {
fprintf(stderr, "读取错误,位置 %zu:%s\n", r.position, r.message);
return 1;
}
printf("解码得到一个映射,包含 %zu 个条目\n", transit_count(r.value));
/* 查找第一个值;span 从 `input` 借用。 */
transit_span_t name = transit_as_span(transit_map_val(r.value, 0));
printf("name: %.*s\n", (int)name.len, name.ptr);
/* 一次调用释放整个树(单个竞技场)。 */
transit_result_free(&r);
return 0;
}
输出:
解码得到一个映射,包含 3 个条目
name: Alice
有线格式
同样的值模型和相同的读/写算法驱动所有三种格式;您通过传入匹配的编解码器来选择一种。
| 编解码器 | 选择器 | 编码 | 缓存 | 说明 |
|---|---|---|---|---|
| JSON | transit_codec_json() | 文本 | 是 | 紧凑的 Transit-over-JSON;交换的默认格式 |
| JSON-Verbose | transit_codec_json_verbose() | 文本 | 否 | 人类可读:映射使用原生 JSON 对象,瞬间使用 RFC3339 |
| MessagePack | transit_codec_msgpack() | 二进制 | 是 | 紧凑的二进制 Transit-over-MessagePack |
添加新的有线格式意味着在 transit_codec_t 描述符背后实现一个标记读取器+写入器;语义层从不分支于格式。
API 参考
整个公共接口位于单个头文件中:
#include "transit.h"
核心函数
transit_read()
使用给定的编解码器从缓冲区读取一个值。
transit_result_t transit_read(const transit_codec_t *codec, const uint8_t *input, size_t len);
参数:
codec:transit_codec_json()、transit_codec_json_verbose()或transit_codec_msgpack()之一input:编码后的字节(必须保持有效且未被修改,以支持零拷贝读取)len:input的字节长度
返回: 一个 transit_result_t(参见错误)。成功时 .error == TRANSIT_OK,.value 包含解码后的树。
重要: 使用 transit_result_free() 释放结果。树中的字符串/字节有效载荷可能直接指向 input,因此缓冲区必须比结果存活得更久。
transit_read_opts()
与 transit_read() 类似,但带有显式选项(详细语义、缓存开关、自定义处理器)。
transit_result_t transit_read_opts(const transit_codec_t *codec, const uint8_t *input, size_t len, const transit_read_options_t *opts);
transit_write()
使用给定的编解码器编码一个值,并追加到一个可增长的输出缓冲区。
int transit_write(const transit_codec_t *codec, transit_value_t v, transit_outbuf_t *out);
参数:
codec:目标有线格式v:要编码的值out:一个已初始化的transit_outbuf_t;编码后的字节追加到out->data(长度为out->len字节)
返回: 成功时返回 0,失败时返回非零的 transit_error_t 值。
transit_write_opts()
与 transit_write() 类似,但带有显式选项(详细输出、缓存开关)。
int transit_write_opts(const transit_codec_t *codec, transit_value_t v, transit_outbuf_t *out, const transit_write_options_t *opts);
transit_result_free()
释放一个结果及其拥有的整个值树。
void transit_result_free(transit_result_t *r);
注意: 这会一次性释放后端的竞技场。不要单独释放值,之后也不要使用任何借用的 span。
编解码器
const transit_codec_t *transit_codec_json(void); /* 紧凑 JSON */
const transit_codec_t *transit_codec_json_verbose(void); /* 详细 JSON */
const transit_codec_t *transit_codec_msgpack(void); /* MessagePack */
每个都返回一个进程全局的不可变描述符,可以在线程间安全共享。
类型系统
transit_value_t 是一个公开的、按值传递的标签联合——您可以直接检查它,或者通过访问器函数来检查。其种类(kind)如下:
transit_kind_t | 有效载荷 | 说明 |
|---|---|---|
TRANSIT_NULL | — | |
TRANSIT_BOOL | bool | |
TRANSIT_INT | int64_t | 有符号 64 位 |
TRANSIT_DOUBLE | double | |
TRANSIT_STRING | transit_span_t | UTF-8 字节 |
TRANSIT_BYTES | transit_span_t | 原始字节(在 JSON 线上为 base64) |
TRANSIT_KEYWORD | transit_span_t | 内部名称,例如 :foo |
TRANSIT_SYMBOL | transit_span_t | |
TRANSIT_URI | transit_span_t | |
TRANSIT_UUID | uint8_t[16] | 解析后的 128 位值 |
TRANSIT_INSTANT | int64_t | 自 Unix 纪元以来的毫秒数 |
TRANSIT_CHAR | int32_t | Unicode 码点 |
TRANSIT_BIGINT | transit_span_t | 文本表示 |
TRANSIT_BIGDEC | transit_span_t | 文本表示 |
TRANSIT_ARRAY | 元素 | 向量 |
TRANSIT_LIST | 元素 | |
TRANSIT_SET | 元素 | |
TRANSIT_MAP | 键 + 值 | 保持条目顺序 |
TRANSIT_TAGGED | 标签 + 表示 | 扩展点 |
transit_span_t 是一个(指针,长度)视图,指向字节——不一定以 NUL 结尾:
typedef struct {
const uint8_t *ptr;
size_t len;
} transit_span_t;
transit_kind_t transit_kind_of(transit_value_t v); /* 值的种类 */
bool transit_is(transit_value_t v, transit_kind_t k); /* 种类 == k */
标量访问器
bool transit_as_bool(transit_value_t v);
int64_t transit_as_int(transit_value_t v);
double transit_as_double(transit_value_t v);
int32_t transit_as_char(transit_value_t v); /* Unicode 码点 */
int64_t transit_as_instant(transit_value_t v); /* 自纪元以来的毫秒数 */
transit_span_t transit_as_span(transit_value_t v); /* 字符串族 + 字节 */
transit_as_span() 涵盖所有基于 span 的种类:STRING、BYTES、KEYWORD、SYMBOL、URI、BIGINT 和 BIGDEC。
集合
size_t transit_count(transit_value_t v); /* 元素/条目计数 */
transit_value_t transit_array_get(transit_value_t v, size_t i); /* ARRAY/LIST/SET */
transit_value_t transit_map_key(transit_value_t v, size_t i); /* 第 i 个 MAP 键 */
transit_value_t transit_map_val(transit_value_t v, size_t i); /* 第 i 个 MAP 值 */
映射通过并行索引进行迭代——transit_map_key(m, i) 和 transit_map_val(m, i) 按文档顺序给出第 i 个条目。
示例: 遍历一个解码后的映射。
for (size_t i = 0; i < transit_count(map); ++i) {
transit_span_t k = transit_as_span(transit_map_key(map, i));
transit_value_t v = transit_map_val(map, i);
printf("%.*s -> kind %d\n", (int)k.len, k.ptr, transit_kind_of(v));
}
构造函数
标量和字符串族的构造函数不分配内存——字符串 span 是从调用者 借用 的,因此字节必须比任何使用它们的写操作存活得更久。
transit_value_t transit_null(void);
transit_value_t transit_bool(bool b);
transit_value_t transit_int(int64_t i);
transit_value_t transit_double(double d);
transit_value_t transit_char(int32_t codepoint);
transit_value_t transit_instant(int64_t millis);
transit_value_t transit_string(const char *s, size_t n);
transit_value_t transit_string_z(const char *s); /* NUL 结尾 */
transit_value_t transit_bytes(const uint8_t *p, size_t n);
transit_value_t transit_keyword(const char *s, size_t n);
transit_value_t transit_symbol(const char *s, size_t n);
transit_value_t transit_uri(const char *s, size_t n);
transit_value_t transit_bigint(const char *s, size_t n);
transit_value_t transit_bigdec(const char *s, size_t n);
transit_value_t transit_uuid(const uint8_t bytes[16]);
transit_value_t transit_tagged(transit_span_t tag, transit_value_t *rep);
容器在竞技场中构建(见下文):
transit_value_t transit_array(transit_arena_t *a);
transit_value_t transit_list(transit_arena_t *a);
transit_value_t transit_set(transit_arena_t *a);
transit_value_t transit_map(transit_arena_t *a);
void transit_array_push(transit_arena_t *a, transit_value_t *arr, transit_value_t item);
void transit_map_put(transit_arena_t *a, transit_value_t *m, transit_value_t key, transit_value_t val);
竞技场和输出缓冲区
竞技场是一个 bump 分配器:在构建时分配,一次性释放所有内容。
transit_arena_t *transit_arena_create(size_t first_block); /* 0 = 合理的默认值 */
void transit_arena_destroy(transit_arena_t *a);
size_t transit_arena_bytes_allocated(const transit_arena_t *a);
输出缓冲区是一个可增长的、堆后备的字节缓冲区,用于写入路径。
typedef struct {
uint8_t *data;
size_t len;
size_t cap;
} transit_outbuf_t;
void transit_outbuf_init(transit_outbuf_t *o);
void transit_outbuf_free(transit_outbuf_t *o);
自定义读取处理器
处理器在读取时将 (tag, representation) 对转换为丰富的值。任何分配都必须来自提供的竞技场。
typedef transit_value_t (*transit_read_handler_fn)(transit_span_t tag, transit_value_t rep, transit_arena_t *arena, void *user);
transit_handlers_t *transit_handlers_create(void);
void transit_handlers_destroy(transit_handlers_t *h);
bool transit_handlers_add(transit_handlers_t *h, const char *tag, transit_read_handler_fn fn, void *user);
transit_handlers_add() 在 tag 是保留的内置复合标签('、set、list、cmap、map)时返回 false——这些不能被覆盖。通过 transit_read_options_t.handlers 传入处理器集合。
示例: 将一个表示形式为 [x, y] 的 point 标签解码为整数 x + y。
static transit_value_t point_sum(transit_span_t tag, transit_value_t rep, transit_arena_t *arena, void *user) {
(void)tag; (void)arena; (void)user;
int64_t x = transit_as_int(transit_array_get(rep, 0));
int64_t y = transit_as_int(transit_array_get(rep, 1));
return transit_int(x + y);
}
transit_handlers_t *h = transit_handlers_create();
transit_handlers_add(h, "point", point_sum, NULL);
transit_read_options_t opts = transit_read_options_default();
相似文章
@shubh6200: 要了解如何处理海量文件,请阅读 @geofflangdale 和 @lemir 的《Parsing Gigabytes of JSON per Second》…
该论文介绍了 simdjson,这是第一个能够在单核上使用 SIMD 指令每秒处理数 GB 数据的验证性 JSON 解析器,相比 RapidJSON 等现有解析器实现了显著的加速。
用于C语言的单头文件解析器组合子
CParseC 是一个基于 C99 的单头文件解析器组合子库,灵感来自 Haskell 的 Parsec,提供零拷贝解析、无隐藏内存分配以及 SIMD 优化的组合子。其目标是提供一种灵活、高性能的手写解析器和 lex/yacc 工具的替代方案。
使用C++26静态反射在编译时解析JSON
C++26的#embed和静态反射,结合simdjson库,允许在编译时解析JSON,将配置文件转化为编译时常量,无运行时开销。
tursodatabase/turso
Turso Database 是一个用 Rust 编写的进程内 SQL 数据库,兼容 SQLite,具备 MVCC、变更数据捕获、向量支持以及多语言绑定功能。
Diplomat:面向 Rust 库的多语言 FFI
Diplomat 是一个多语言单向 FFI 工具,用于封装 Rust 库,旨在将 Rust API 暴露给 C++、JS、Dart 和 JVM 等语言,而无需 FFI 专业知识,填补了 Rust 工具生态系统中的空白。