一个用C编写的快速、零拷贝Transit格式读写器

Lobsters Hottest 工具

摘要

一个快速、零拷贝的C11库,用于读写Transit数据格式,具备SIMD加速,通过一个与编解码器无关的单一引擎支持JSON、JSON-Verbose和MessagePack线格式。

<strong>什么是Transit?</strong><p><strong><a href="https://github.com/cognitect/transit-format" rel="ugc">Transit</a></strong> 是一种格式和一组库,用于在不同语言编写的应用程序之间传递值。它基于JSON和MessagePack之上,因此你可以获得它们的工具和速度,但具有更丰富的类型系统和内置的有效载荷压缩。可以将其视为“能够往返真实类型的JSON”:</p><ul><li><strong>宿主格式的基础类型</strong>:映射、数组、字符串、数字、布尔值、null</li><li><strong>JSON缺少的扩展类型</strong>:关键字 <code>:foo</code>、符号、时间点(时间戳)、UUID、URI、大整数/小数、字符、字节数组、集合和列表</li><li><strong>内置压缩(缓存)</strong>:重复的映射键、关键字、符号和标签只写入一次,然后通过短代码 <code>^N</code> 引用,因此冗长且键多的有效载荷会显著缩小</li><li><strong>通过标记值扩展</strong>:通过标签和表示形式在网络上传输自定义类型,并使用自定义处理程序解码它们——不再需要 <code>{"__type": "Date", "value": "..."}</code> 这样的 hack</li><li><strong>语言无关 &amp; 自描述</strong>:最初来自Cognitect/Clojure,现已有多种语言的实现</li></ul><p><strong>了解更多:</strong> <a href="https://github.com/cognitect/transit-format" rel="ugc">Transit官方规范</a></p><p><a href="https://lobste.rs/s/i7moeh/fast_zero_copy_transit_format_reader">评论</a></p>
查看原文
查看缓存全文

缓存时间: 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.hsrc/ 下的所有文件复制到您的项目中,然后一起编译。只有 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

有线格式

同样的值模型和相同的读/写算法驱动所有三种格式;您通过传入匹配的编解码器来选择一种。

编解码器选择器编码缓存说明
JSONtransit_codec_json()文本紧凑的 Transit-over-JSON;交换的默认格式
JSON-Verbosetransit_codec_json_verbose()文本人类可读:映射使用原生 JSON 对象,瞬间使用 RFC3339
MessagePacktransit_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);

参数:

  • codectransit_codec_json()transit_codec_json_verbose()transit_codec_msgpack() 之一
  • input:编码后的字节(必须保持有效且未被修改,以支持零拷贝读取)
  • leninput 的字节长度

返回: 一个 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_BOOLbool
TRANSIT_INTint64_t有符号 64 位
TRANSIT_DOUBLEdouble
TRANSIT_STRINGtransit_span_tUTF-8 字节
TRANSIT_BYTEStransit_span_t原始字节(在 JSON 线上为 base64)
TRANSIT_KEYWORDtransit_span_t内部名称,例如 :foo
TRANSIT_SYMBOLtransit_span_t
TRANSIT_URItransit_span_t
TRANSIT_UUIDuint8_t[16]解析后的 128 位值
TRANSIT_INSTANTint64_t自 Unix 纪元以来的毫秒数
TRANSIT_CHARint32_tUnicode 码点
TRANSIT_BIGINTtransit_span_t文本表示
TRANSIT_BIGDECtransit_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 的种类:STRINGBYTESKEYWORDSYMBOLURIBIGINTBIGDEC

集合

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 是保留的内置复合标签('setlistcmapmap)时返回 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();

相似文章

用于C语言的单头文件解析器组合子

Hacker News Top

CParseC 是一个基于 C99 的单头文件解析器组合子库,灵感来自 Haskell 的 Parsec,提供零拷贝解析、无隐藏内存分配以及 SIMD 优化的组合子。其目标是提供一种灵活、高性能的手写解析器和 lex/yacc 工具的替代方案。

tursodatabase/turso

GitHub Trending (daily)

Turso Database 是一个用 Rust 编写的进程内 SQL 数据库,兼容 SQLite,具备 MVCC、变更数据捕获、向量支持以及多语言绑定功能。

Diplomat:面向 Rust 库的多语言 FFI

Lobsters Hottest

Diplomat 是一个多语言单向 FFI 工具,用于封装 Rust 库,旨在将 Rust API 暴露给 C++、JS、Dart 和 JVM 等语言,而无需 FFI 专业知识,填补了 Rust 工具生态系统中的空白。