【免费下载链接】tree-sitter
An incremental parsing system for programming tools
Tree-sitter 的 Wasm 语言模块在编译时不链接任何 C 标准库,但用 C 编写的外部扫描器(external scanner)仍依赖 malloc、memcpy、iswalpha、snprintf 等常用函数。lib/src/wasm-stdlib/ 目录正是为这一场景提供的一套"按需裁剪"的标准库子集:它既定义了 Wasm 模块与环境之间的导入契约(imports.txt),也提供了可重置分配器与内存化 stdio 的具体实现,并最终以单个头文件形式嵌入宿主程序。读完本文,你将理解这套子集的组成与边界、加载方如何提供这些函数、wasm32-unknown-unknown 目标下的另一条链接路径,以及如何用 cargo xtask 刷新 vendor 源码并重新生成嵌入模块。
背景:Wasm 语言模块为什么没有 C 标准库
Tree-sitter 允许把语言(语法解析器)编译为独立的 Wasm 模块,由宿主加载后调用。为了让模块足够小、可移植且行为可预测,这些模块在编译时被刻意设计为"没有 C 标准库"的状态——README 开篇即说明:
Wasm language modules are compiled without a C standard library. Their external scanners may import the functions listed in
imports.txt, and the environment loading the language must provide those functions.
也就是说,Wasm 模块内部的 C 代码(尤其是外部扫描器)可以导入 imports.txt 中列出的函数,但不链接任何标准库实现;真正提供这些函数实现的是"加载该语言的环境"(即宿主运行时,如 Rust 库中的 wasmtime 集成)。这是一种显式的服务契约:模块声明依赖,宿主负责履约。
导入契约:imports.txt 中的 24 个函数
imports.txt 是这份契约的权威清单,全文共 24 个函数名(每个名字带双引号,一行一个)。按用途可分为四组:
| 类别 | 函数 |
|---|---|
| 内存分配 | malloc、calloc、realloc、free |
| 字符串操作 | strcmp、strlen、strncat、strncmp、strncpy、memchr、memcmp、memcpy、memmove、memset |
| 宽字符分类 | iswalnum、iswalpha、iswblank、iswdigit、iswlower、iswpunct、iswspace、iswupper、iswxdigit |
| 大小写转换 | towlower、towupper |
这些名字覆盖了外部扫描器最常触碰的 C 库面:解析时需要按字符分类判断(宽字符版 isw* 函数天然适合多字节输入),需要字符串比较与拼接,需要内存拷贝与分配。值得注意的是,清单里没有 snprintf、fprintf 等格式化函数——它们是 stdio.c 内部实现的,不要求宿主提供。
这份清单不仅定义了模块侧的"可导入集合",也被编译管线直接当作导出列表使用:在 build_wasm.rs 中,Web 绑定构建会读取 imports.txt 并拼进 emscripten 的 EXPORTED_FUNCTIONS;而在生成独立 stdlib 模块时,run_wasm_stdlib 又会把每个名字转成 -Wl,--export=xxx 链接标志。
目录构成:一份子集、四类组件
lib/src/wasm-stdlib/ 下的内容是一份"共享的标准库子集实现",README 将其归纳为四个部分:
libc/:从仓库固定 WASI SDK 所对应的wasi-libc修订版本 vendor 进来的源码。子目录结构为ctype/(isblank.c、iswalnum.c、iswalpha.c、iswblank.c、iswdigit.c、iswlower.c、iswpunct.c、iswspace.c、iswupper.c、iswxdigit.c、towctrans.c)与string/(memchr.c、memcmp.c、memcpy.c、memmove.c、memset.c、stpncpy.c、strchr.c、strchrnul.c、strcmp.c、strlen.c、strncat.c、strncmp.c、strncpy.c、wcschr.c、wcslen.c),另有LICENSE(自 wasi-libc 的 COPYRIGHT 复制而来)。stdio.c:Tree-sitter 自研的、面向扫描器的 stdio 实现。external_scanner_allocator.c:用于独立 Wasm 语言模块的"可重置"分配器。external_scanner_stdlib.h:生成产物——一个把上述 vendor libc 子集、stdio.c和可重置分配器打包编译成的 Wasm 模块,以 C 头文件的形式内嵌。
stdio.c:只做内存格式化,流操作一律空操作
stdio.c 的文件头注释点明了设计哲学:
Scanner-oriented stdio compatibility functions. Formatting writes to memory, while stream operations intentionally perform no I/O.
即:格式化写内存,流操作不做任何 I/O。外部扫描器可能偶尔调用 snprintf 拼一段调试输出,但绝不会需要真正的文件读写。
其核心是 vsnprintf_impl 与 snprintf / vsnprintf 两个入口。实现支持:
- 格式标志:
-(左对齐)、0(零填充)、+(显示正号)、(空格前缀)、#(备用形式),由parse_format_spec解析(stdio.c); - 宽度与精度:数字宽度、
.后精度; - 转换符:
%s、%d/%i、%u、%x/%X(大写十六进制)、%p(输出为0x...)、%c、%z(长度修饰,%zu),未知转换符按%+ 原字符回退输出;%%输出字面%; - 输出边界:严格受
buffsz约束,越界不写、正确返回"应写入的字符总数",与 C 标准snprintf的语义一致。
而面向流的接口则全部"虚化":fclose、fputs、fprintf 返回 0,fdopen 返回 NULL,fputc 原样返回字符,fwrite 直接返回 size * nmemb(stdio.c)。这些函数存在的意义只是"让代码能链接、能编译",而非真正执行 I/O。
external_scanner_allocator.c:可一次重置的 bump 分配器
external_scanner_allocator.c 的注释清楚描述了算法:分配时 bump 一个静态指针、按需增长堆;释放时把区域挂进空闲链表;reset 时一次性释放全部分配。几个关键设计点:
- 页与上限:
PAGESIZE为0x10000(64 KB),堆上限MAX_HEAP_SIZE为 4 MB(external_scanner_allocator.c),超出上限拒绝增长; - 堆操作依赖 Wasm 内建指令:
grow_heap调用__builtin_wasm_memory_grow(0, ...)、get_heap_end调用__builtin_wasm_memory_size(0),这是模块自有线性内存内的自给自足式管理,不依赖宿主; - 区域元数据:每个分配块前有一个
Region头(size+next),region_for_ptr由用户指针反推头部;分配结果按 4 字节对齐; - 空闲链表优先:
malloc先扫描free_list找size >= 请求的区域,找不到才 bump 并增长堆; free的两种路径:释放的是"最后分配的区域"(region_end == next)则直接回退next指针复用;否则挂入空闲链表;realloc原地扩展:对最后分配的区域可原地 resize 并跳过拷贝,否则走"新分配 + memcpy + free"路径(external_scanner_allocator.c);reset_heap:把heap_start、next重置到给定地址、清空free_list,从而一次释放所有内存(external_scanner_allocator.c);abort直接__builtin_trap()。
之所以需要"可重置",是因为 Tree-sitter 的 Wasm 语言模块在每次解析会话之间会复用同一实例,宿主通过调用模块导出的 reset_heap 函数把堆归零,从而避免逐块 free 的开销——这也是 reset_heap 会出现在链接器显式导出列表(build_wasm.rs)里的原因。
external_scanner_stdlib.h:把一切打包成一个嵌入数组
external_scanner_stdlib.h 是编译与优化后的 Wasm 模块,内容为 unsigned char STDLIB_WASM[] = {...}(共 1450 行、STDLIB_WASM_LEN 为 17357 字节)以及导出的字符分类查找表数据。从二进制中可以看到该模块导出 memchr、memcmp、memcpy、memmove、memset、strlen、strcmp、strncat、strncmp、strncpy、全部 isw*/tow* 函数、reset_heap、malloc、free、calloc、realloc 以及 __wasm_call_ctors、__stack_pointer——与 imports.txt 和链接标志严格对应,并且导入 env.memory。
宿主侧的使用点在 wasm_store.c:wasmtime_module_new(engine, STDLIB_WASM, STDLIB_WASM_LEN, &stdlib_module) 用这份字节码创建独立实例,之后在导出表中寻找 reset_heap 函数(wasm_store.c),并在 ts_wasm_store_reset_heap 中调用它(wasm_store.c)。换句话说,external_scanner_stdlib.h 是"宿主要为 Wasm 语言模块提供的标准库服务"的预编译实现,随宿主静态链接,无需外部 .wasm 文件。
另一种形态:wasm32-unknown-unknown 下的直接链接
README 还强调了一个容易忽略的路径:当 Tree-sitter 的 Rust 库本身以 wasm32-unknown-unknown 目标编译时,同一份 vendor libc 源码和 stdio.c 会直接链接进宿主应用,此时分配不再由上述可重置分配器负责,而是由 Rust 应用选定的全局分配器提供。
这一分支的落地在 lib.c:在 TREE_SITTER_WASM_STDLIB 宏下,lib.c 直接 #include "./wasm-stdlib/libc.c" 与 "./wasm-stdlib/stdio.c"——即整个库编译为一个整体,字符串/宽字符函数与格式化实现成为库的一部分,而内存分配交给宿主 Rust 侧的全局分配器(不链接 external_scanner_allocator.c)。这解释了为什么分配器单独拆成一个文件、并声明 extern void tree_sitter_debug_message(...) 之类的宿主符号:它只在"模块独立成 .wasm"的场景才被编入。
刷新 vendor 源码与重新生成嵌入模块
这份子集不是写死的快照,仓库提供了两条 xtask 命令维护它(README):
cargo xtask vendor-wasm-stdlib
cargo xtask build-wasm-stdlib
两条命令分别对应 build_wasm.rs 中的两个流程:
vendor_wasm_stdlib(build_wasm.rs):按固定的WASI_LIBC_REVISION修订号,从wasi-libc仓库下载归档到缓存目录(~/.cache/tree-sitter/wasi-libc/<revision>),解压后把libc-top-half/musl下的 ctype/string 源码复制进lib/src/wasm-stdlib/libc/,并写入LICENSE。执行时需本机装有curl并能访问网络。run_wasm_stdlib(build_wasm.rs):先用 WASI SDK 的clang以-nostdlib、-Os、-fPIC编译libc.c、stdio.c、external_scanner_allocator.c,并显式导出__wasm_call_ctors、__stack_pointer、reset_heap及imports.txt中的全部函数;再用 Binaryen 的wasm-opt -Os优化;最后用xxd -C -i把二进制转成 C 数组,覆盖写入external_scanner_stdlib.h。因此重新生成需要 WASI SDK、Binaryen 与xxd三个工具(xtask 会自动下载/缓存这些工具链)。
日常使用(仅加载已有 Wasm 语言模块)并不需要运行这些命令;它们面向的是升级 wasi-libc 基线或修改 stdio.c / 分配器实现后同步嵌入模块的维护场景。
小结
Tree-sitter 的 Wasm 标准库是一个"以小博大"的工程范例:用一份约 17 KB 的预编译模块,就为外部扫描器提供了内存分配、字符串处理、宽字符分类与内存化格式化能力。理解它需要同时把握三条线索——imports.txt 定义的导入契约、external_scanner_allocator.c 与 stdio.c 的最小实现、以及 external_scanner_stdlib.h 作为生成产物在宿主侧的加载方式;而 wasm32-unknown-unknown 目标的直接链接分支,则展示了同一套源码如何在不同的内存管理语境下复用。相关实现与构建脚本可继续查阅:
- 模块加载与
reset_heap调用:wasm_store.c - 直接链接分支:lib.c
- 构建与 vendor 流程:build_wasm.rs
【免费下载链接】tree-sitter
An incremental parsing system for programming tools
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



