ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

wazero 语言支持指南:如何将 TinyGo、Rust 与 Zig 源码编译为 Wasm 并在 Go 中嵌入运行

2026/10/8 7:54:36 拓冰建站 浏览量
wazero 语言支持指南:如何将 TinyGo、Rust 与 Zig 源码编译为 Wasm 并在 Go 中嵌入运行 开发工具系统底层【免费下载链接】wazerowazero: the zero dependency WebAssembly runtime for Go developers项目地址https://gitcode.com/gh_mirrors/wa/wazero点击查看免费下载WebAssembly 采用宿主Host 客机Guest的虚拟机架构宿主是嵌入 WebAssembly 运行时的进程客机则是被编译为 WebAssembly 二进制格式Wasm的程序。本文基于 wazero 官方文档中关于编程语言支持的总览与各语言专项笔记TinyGo、Rust、Zig系统讲解从源码到.wasm的编译流程、WebAssembly 的约束边界、系统调用WASI现状、并发模型以及如何在 wazero 运行时中调用编译产物。读完本文你将掌握三种主流语言的 Wasm 编译命令、内存分配host allocation模式与二进制体积/性能优化手段并能根据 WebAssembly Core 规范与 WASI 的支持情况规划自己的嵌入方案。从源码到 Wasm宿主与客机模型WebAssembly 的核心抽象是虚拟机的字节码而非某个具体操作系统的可执行文件。因此使用任何语言开发 WebAssembly 程序的第一步都是先把源码编译成 Wasm 字节码第二步才是由宿主进程如 wazero 所在的 Go 程序加载并执行它。以 Go 源码为例可以使用 TinyGo 编译器完成这一步.-----------. .----------------------. .-----------. / main.go /----| tinygo -targetwasi ----/ main.wasm / ---------- ---------------------- -----------wazero 是一个嵌入在 Go 应用中的运行时而非浏览器因此它的语言笔记全部偏向后端backend使用场景而非浏览器场景。截至本文wazero 已按语言字母序维护了以下三份编译笔记语言文档最小编译命令TinyGoTinyGo 指南tinygo build -o X.wasm -targetwasi X.goRustRust 指南rustc -o X.wasm --target wasm32-wasi X.rsZigZig 指南zig build-exe X.zig -target wasm32-wasi需要说明的是这些笔记由 wazero 社区维护并非各语言官方文档也不代表语言编译器维护团队的观点。如果你发现错误可以参与维护本文中的所有命令、特性与限制均以当前仓库与对应工具链的现状为准。约束WebAssembly 能做什么不能做什么WebAssembly Core 规范定义了一个基于栈的虚拟机。默认可用的特性本质上都是计算性的——模块之间唯一的通信方式是函数、线性内存与全局变量。由此带来几个直接后果没有标准库与系统调用接口操作系统本应提供的功能如 fork 进程无法实现控制台输出等常见 I/O 能力在不同编译器中支持程度不一详见下文 系统调用。生态仍处于早期WebAssembly 作为编译目标仍相对小众维护与开发资源有限某些特性即使技术上可行也可能尚未实现。编译期诊断能力弱与更成熟的目标平台相比开发 WebAssembly 程序时更多问题只能在运行时暴露可能表现为运行时报错甚至 panic错误信息可能具有误导性语言维护者也可能不熟悉相关问题或依赖较少的核心维护者。这些约束直接影响你的库设计与依赖选择——你不仅需要考虑自己源码的能力边界还要考虑所用第三方库是否能在 Wasm 目标下正常工作。在极端情况下约束与支持顾虑可能促使开发者转向较新的语言如 Zig。缓解约束用目标运行时做单元测试无论使用哪种编程语言wazero 给出的最佳实践都是对你的代码做单元测试并且用你最终打算使用的 WebAssembly 运行时比如 wazero 本身来跑这些测试。测试应覆盖代码的关键路径包括错误分支。这样做可以保护你的时间相比临时上报的问题你会拥有更高的信心和更高效的沟通手段。从仓库的实现来看这一建议完全可行——imports/wasi_snapshot_preview1 目录下提供了args_test.go、clock_test.go、fs_test.go、poll_test.go、proc_test.go等大量针对 WASI 宿主函数的测试而 examples/allocation/tinygo、examples/allocation/rust、examples/allocation/zig 则是可直接运行的宿主侧集成示例均带有对应的_test.go文件。系统调用WASI 现状与各语言差异WebAssembly 是基于栈的虚拟机规范运行层级低于操作系统。凡操作系统会提供的功能都需要系统接口来补齐。编程语言的标准库通常包含需要 I/O 的特性如控制台输出可移植性通常依赖 POSIX 一致的实现例如fd_read。**WebAssembly System InterfaceWASI**定义了基于 POSIX 的宿主函数wazero 对其支持状况与函数清单详见 specs 文档。但必须强调几个事实WASI 并非标准它是 WebAssembly 社区子组的工作成果尚未发布任何工作草案最后一个稳定点是 2020 年底打 tag 的wasi_snapshot_preview1且 tag 之后还有函数被增删例如proc_raise被移除、sock_accept被加入。语言编译器并不总是支持 WASI例如 AssemblyScript 曾经支持 WASI现已不再支持即使基于 wasi-libc 编译到 WASI 的工具链也存在缺口——TinyGo 指南明确指出 TinyGo 尚不支持fd_readdiros.ReadDir会运行时报错。存在混合方案例如 Emscripten 用 WASI 做控制台输出但使用自己的虚拟文件系统函数。WASI 正在被替换WASI 团队正在开发一个不兼容的、模块化的替代版本。此外即使系统接口受支持部分用户仍偏好freestanding 编译目标完全独立、不依赖宿主接口以控制二进制体积与性能。总结而言WebAssembly 的系统接口既不标准也不成熟开发者必须理解并测试自己所依赖的系统接口——测试不仅能确认当前能力还能在生态演进时持续验证它们仍然可用。WASI 宿主函数的支持矩阵根据 specs 文档 中的完整清单以wasi_snapshot_preview1为准wazero 已实现的函数覆盖了参数、环境、时钟、文件系统、路径、轮询、进程、随机数与套接字等类别。其中被实际使用的主要函数及对应编译器包括参数与环境args_get、args_sizes_get、environ_get、environ_sizes_get已知被 TinyGo 使用时钟clock_res_get、clock_time_get被 TinyGo 使用文件描述符fd_close、fd_fdstat_get、fd_prestat_get、fd_prestat_dir_name、fd_read、fd_seek、fd_write等Rust/TinyGo/Zig 均有使用目录与路径fd_readdirRust/Zig、path_open、path_create_directory、path_remove_directory、path_rename、path_unlink_file等Rust/TinyGo/Zig 均有使用进程与调度proc_exitRust/TinyGo/Zig、sched_yieldRust随机数random_getRust/TinyGo/Zig套接字sock_accept、sock_recv、sock_send、sock_shutdownRust/Zig注意清单中标记为 的函数如fd_fdstat_set_rights、proc_raise属于后来从 WASI 移除的函数。wazero 并不会仅为了填满清单而实现所有 WASI 函数若你需要尚未实现的特性建议携带具体用例例如用哪门语言编译、目标是哪个 Wasm向维护者反馈。并发WebAssembly 尚无真正的并行WebAssembly尚不支持真正的并行它缺少多线程、原子操作与内存屏障支持threads 提案仍在推进中。举例来说编译到 WASI 的程序会生成一个对应源码中main的_start函数当运行时调用_start时执行会一直停留在同一线程上直到该函数完成。具体到 wazero一次 Wasm 函数调用会停留在发起调用的 goroutine 上直到调用完成。虽然宿主函数理论上可以做任何事包括启动进程但只要 Wasm 二进制遵循 WebAssembly Core 规范除非使用规范尚未定义的非标准指令或约定否则无法并行做任何事。把并行代码编译成串行 Wasm在 threads 提案落地之前语言编译器无法生成能在函数内部控制调度或安全并行修改内存的 Wasm——一个函数内部无法并行执行任何事情。这直接影响了语言原语向 Wasm 的翻译垃圾回收在运行时宿主的调用线程上触发而不是在后台进行。语言级线程或协程编译失败或退化为顺序处理。锁与屏障编译失败或以不安全方式实现。异步函数含 I/O只能顺序执行。语言编译器通常共享 LLVM、Binaryen 等基础设施。在翻译过程中一个有用的工具是 Binaryen 的Asyncify它可以让语言以异步方式支持同步操作。TinyGo 正是靠它实现 goroutine 调度的详见下文。通过编排实现伪并发为了绕开 WebAssembly Core 抽象层并发能力的缺失业界通常的做法是编排一组 worker 池并保证池中每个模块只被顺序使用。例如 waPC 提供了 WASM 模块池使得宿主回调可以在无法共享内存的情况下并行调用。这类编排模式与 wazero 的调用停留在发起 goroutine 上的模型天然契合——你可以在 Go 侧用多个 goroutine 各自驱动一个互不共享的模块实例来获得并行。TinyGowasi目标实战TinyGo 是 Go 源码的替代编译器支持两个 Wasm 目标wasm浏览器/JavaScript 场景与wasi浏览器之外。wazero 笔记只讨论wasi目标。完整笔记见 TinyGo 指南。最小可运行示例——导出add函数package main //export add func add(x, y uint32) uint32 { return x y } // main is required for the wasi target, even if it isnt used. func main() {}tinygo build -o main.wasm -targetwasi main.go编译出的 Wasm 会导出add函数宿主无论是否用 Go 编写都能调用它。标准库缺口与系统调用缺口TinyGo 在wasi目标下并未完整实现 Go 标准库首个让用户踩坑的是encoding/json能编译但运行时会 panic受限于反射支持。典型的运行期报错还包括目录读取package main import os func main() { if _, err : os.ReadDir(.); err ! nil { println(err) } }上面这段代码能编译但运行时会打印readdir unimplemented : errno 54。底层错误通常但不总是是syscall.ENOSYS——这是系统调用尚未实现的标准打桩方式。从 TinyGo 源码结构看syscall.ReadDirent正是以返回syscall.ENOSYS的方式被 stub 掉的。内存与宿主分配TinyGo 编译 Go 到 Wasm 时会把线性内存初始大小配置为2 页128KB并标记堆基址堆基址之后的内存全部用于 Go 堆。Go 内的分配由分配器管理可调用memory.grow直到宿主返回 -1。当宿主函数需要直接分配内存例如先写入一段 JSON再调用导出函数解析它时TinyGo 导出的函数可以这样写//export configure func configure(ptr uintptr, size uint32) { json : ptrToString(ptr, size) }注意WebAssembly 使用 32 位内存寻址因此uintptr是 32 位。通用的调用模式分三种需要分清内存所有权宿主分配字符串并调用 Guest 导出函数宿主调用内置导出malloc拿到写入内存的偏移量把字符串写入该偏移ptr然后调用 Guest 函数并传入ptr与长度。此分配归宿主所有完成后必须调用内置导出free。Guest 向导入的宿主函数传递字符串Guest 用stringToPtr取得宿主需要的偏移量宿主直接从 Wasm 内存读取。字符串仍归 Guest 管理受垃圾回收支配宿主不应调用free。Guest 从导出函数返回字符串Guest 用ptrToLeakedString取得偏移量并连同长度一起返回。这是所有权转移字符串不会被 Guest 回收宿主读取后必须调用free。宿主侧调用的内置malloc/free在 WebAssembly 文本格式中形如(func (export malloc) (param $size i32) (result (;$ptr;) i32)) (func (export free) (param $ptr i32))这些ptrToString等辅助函数代码量较大可以直接从 examples/allocation/tinygo 中复制或者引入 tinymem 依赖。仓库中的 greet.go 宿主示例 完整演示了调用malloc分配 →mod.Memory().Write写入名字 → 调用导出的greet→ 从greeting的返回值uint64中拆出高 32 位指针与低 32 位长度读出问候语 →defer free.Call释放的全过程对应的 testdata/greet.go 则展示了//go:wasmimport env log导入宿主函数、//go:wasmexport greet导出函数等写法。WASI 内部结构从 TinyGo 的wasi目标配置看TinyGo 在底层复用了wasm32-unknown-wasi这个 LLVM 目标作为系统调用层libc最终由 wasi-libc 实现。与普通代码类似TinyGo 依据 GOOS/GOARCH 后缀与构建标签决定使用哪套抽象——例如os.Args直接在runtime_wasm_wasi.go中用 Wasm 宿主函数实现syscall.Chdir复用与其他架构相同的syscall_libc.go而syscall.ReadDirent则在syscall_libc_wasi.go中被 stub。TinyGo 的并发goroutine 与 AsyncifyTinyGo 无论目标如何都只使用单核/单线程这恰好匹配 Wasm 当前缺乏多线程支持的现状。其 goroutine 调度器目前基于 Binaryen 的Asyncify。结论是TinyGo 默认支持 goroutine行为等价于GOMAXPROCS1。由于 goroutine 不是线程下面的程序即使按相反依赖顺序定义 goroutine也会输出预期结果package main import fmt func main() { msg : make(chan int) finished : make(chan int) go func() { -msg fmt.Println(consumer) finished - 1 }() go func() { fmt.Println(producer) msg - 1 }() -finished }但也有缺陷例如同一个函数若被导出//export notMain并在 main 未运行的情况下被调用创建 goroutine 的那行目前会在运行时 panic。鉴于此类问题有些人选择用-schedulernone让它在编译期直接失败——反正 Wasm 代码往往需要定制去掉 goroutine 支持的影响可能有限。TinyGo 优化选项追求更小二进制体积-schedulernone减小体积但 goroutine 会在编译期报错。一个简单的cat程序可从默认约 260KB 降到约 60KB配合下面的选项。--no-debug剥离 DWARF但保留 WebAssembly name section。追求运行性能-gcleaking避开 GC对短生命周期程序可提升性能。-opt2启用额外优化常以增大二进制体积为代价。TinyGo 常见问题为什么必须定义 main使用wasi目标至少要写一个空func main() {}否则模块实例化会失败除非宿主侧导出了(func (import env main.main) (param i32) (result i32))。怎么用 jsonTinyGo 尚未实现encoding/json所需的反射 API多数用户改用非反射解析器如 gjson。为什么没用到 WASI 也会导入 WASI 函数至少实现panic就需要写控制台fd_write正被用于此。一个裸的独立 Wasm 目标目前还不存在。为什么我的 Wasm 这么大TinyGo 至少要内置垃圾回收与panic这部分 Wasm 通常不大约 4KB真正的大头常是看似简单但需要大量支撑函数的 API例如fmt.Println可能就需要约 100KB 的 Wasm。Rustwasm32-unknown-unknown与wasm32-wasi实战自 Rust 1.30 起Rust 可通过三个目标生成.wasmwasm32-unknown-emscripten主要面向浏览器、wasm32-unknown-unknown独立使用浏览器内外皆可、wasm32-wasi浏览器之外。wazero 笔记聚焦经 wazero 测试的wasm32-unknown-unknown与wasm32-wasi并出于精确与简洁以rustc而非cargo为例。完整笔记见 Rust 指南。最小示例——导出add函数#[no_mangle] pub extern C fn add(x: i32, y: i32) - i32 { x y }rustc -o lib.wasm --target wasm32-unknown-unknown --crate-type cdylib lib.rs#[no_mangle]与extern C用于标记要导出给 WebAssembly 宿主的函数--crate-type cdylib表示以库而非带main的可执行程序方式编译。Rust 内存与宿主分配Rust 编译到 Wasm 时线性内存初始大小为17 页约 1.1MB并标记堆基址其后的内存全部用作 Rust 堆。Rust 内的分配由global_allocator管理可调用alloc_pages直到宿主memory.grow返回 -1。当宿主需要先分配内存再调用导出函数时例如写入一段 JSON 再调用解析函数宿主侧流程与 TinyGo 一致先调用分配函数拿到内存偏移写入数据调用导出函数传入ptr与长度最后无条件用同一ptr调用 free 函数防止内存泄漏。WebAssembly 使用 32 位内存寻址所以uintptr是 32 位。需要自定义导出malloc与free(func (export malloc) (param $size i32) (result (;$ptr;) i32)) (func (export free) (param $ptr i32) (param $size i32))Rust 实现示例use std::mem::MaybeUninit; use std::slice; unsafe fn ptr_to_string(ptr: u32, len: u32) - String { let slice slice::from_raw_parts_mut(ptr as *mut u8, len as usize); let utf8 std::str::from_utf8_unchecked_mut(slice); return String::from(utf8); } #[no_mangle] pub extern C fn alloc(size: u32) - *mut u8 { // Allocate the amount of bytes needed. let vec: VecMaybeUninitu8 Vec::with_capacity(size as usize); // into_raw leaks the memory to the caller. Box::into_raw(vec.into_boxed_slice()) as *mut u8 } #[no_mangle] pub unsafe extern C fn free(ptr: u32, size: u32) { // Retake the pointer which allows its memory to be freed. let _ Vec::from_raw_parts(ptr as *mut u8, 0, size as usize); }仓库中的 examples/allocation/rust 提供了完整对照宿主侧 greet.go 调用导出的allocate/deallocate并读写mod.Memory()客机侧 testdata/greet.rs 演示了#[link(wasm_import_module env)]导入宿主log函数、#[cfg_attr(all(target_arch wasm32), export_name greeting)]导出函数以及用wee_alloc作为#[global_allocator]压缩体积。这类技术性代码应尽量与业务逻辑分离。Rust 系统调用与优化需要操作系统功能时必须使用wasm32-wasi目标它会导入 WASI 宿主函数。例如fn main() { println!(Hello World!); }用rustc -o hello.wasm --target wasm32-wasi hello.rs编译后main会变成导出的 WASI_start函数。wazero 还提供了无任何 WebAssembly 特定代码、纯 Rust 实现cat的 WASI 示例项目。优化选项方面追求小体积可修改源码如使用 wee_alloc 这个小巧的、面向 WebAssembly 调优的分配器或设置rustc标志-C debuginfo0剥离 DWARF保留 name section、-C opt-level3包含全部体积优化。使用 cargo 时--release即对应这两项。使用wasm32-wasi目标的用户可考虑 cargo-wasi 工具它能显著减小 Wasm 体积。追求性能则可设-C opt-level2但常以增大体积为代价。Zigwasm32-freestanding与wasm32-wasi实战自 Zig 0.4.0 起Zig 可通过wasm32-emscripten主要面向浏览器、wasm32-freestanding独立使用浏览器内外皆可、wasm32-wasi浏览器之外三个目标生成 Wasm。wazero 笔记面向 Zig 0.10.1聚焦wasm32-freestanding与wasm32-wasi。完整笔记见 Zig 指南。最小示例——导出add函数export fn add(a: i32, b: i32) i32 { return a b; }zig build-lib -dynamic -target wasm32-freestanding main.zigzig build-lib -dynamic表示以库无main函数方式编译产物会导出add供宿主调用。Zig 的内存模型与宿主分配Zig 语言本身不为程序员管理内存也没有默认分配器——需要分配的函数都接受一个Allocator参数。宿主需要先分配内存时流程同样为宿主调用分配函数 → 写入数据如 JSON→ 调用导出函数传入ptr与size→ 完成后无条件调用 free。pub export fn configure(ptr: [*]const u8, size: u32) void { _configure(message[0..size]) catch |err| panic(switch (err) { error.OutOfMemory out of memory, }); }由于 Zig 允许用户方便地接入自定义分配器导出malloc/free对很容易实现。例如基于page_allocatorconst allocator std.heap.page_allocator; pub export fn malloc(length: usize) ?[*]u8 { const buff allocator.alloc(u8, length) catch return null; return buff.ptr; } pub export fn free(buf: [*]u8, length: usize) void { allocator.free(buf[0..length]); }examples/allocation/zig 中的示例还展示了几个值得注意的技巧用ptrToInt把 Zig 指针转成数值类型、用[*]u8作为参数接收指针并切片重建字符串、以及用extern指令导入宿主函数。Zig 系统调用与优化需要操作系统功能时须使用wasm32-wasi目标。Zig 标准库对 WASI 的支持仍在积极开发中一般情况下编译到wasm32-wasi时应优先使用标准库如std.io。同样wazero 提供了纯 Zig 实现cat的 WASI 示例。体积优化方面Zig 提供了多个控制二进制体积、执行速度与安全检测的构建模式-ODebug构建快、开启安全检测、运行性能较慢、体积较大-OReleaseSafe运行性能中等、开启安全检测、编译较慢、体积较大-OReleaseSmall运行性能中等、关闭安全检测、编译较慢、体积较小性能优化则用-OReleaseFast启用额外优化可能以增大体积为代价。此外更换分配器如挑选更小的分配器实现也能显著影响体积。常见问题为什么我的.wasm二进制这么大三种语言的默认配置都可被覆盖TinyGo 用-schedulernone、--no-debugRust 用-C debuginfo0、-C opt-level3或 cargo 的--releaseWASI 目标还可借助 cargo-wasiZig 用-OReleaseSmall等构建模式。在牺牲特性或性能换取更小体积之后进一步调整源码如换用更小的分配器还能继续压缩。以 TinyGo 为例GC 与panic的最小实现通常只有约 4KB真正占体积的往往是fmt.Println这类看似简单实则依赖大量支撑函数的 API。宿主分配字符串时应该由谁调用 free取决于所有权归属宿主分配的经malloc由宿主调用freeGuest 内部字符串经stringToPtr仍归 Guest 管理宿主不应释放Guest 导出的泄漏字符串所有权转移必须由宿主调用free。Rust 示例中用std::mem::forget完成所有权转移TinyGo 示例中用C.mallocstringToLeakedPtr实现同样的语义——仓库中两个 greet 示例的宿主代码都通过defer保证释放。总结把一门语言编译到 WebAssembly 并嵌入 wazero 运行是一条清晰但需要审慎对待的路径先用tinygo、rustc或zig按目标wasi、wasm32-unknown-unknown、wasm32-freestanding等产出.wasm再用 Go 侧 wazero.NewRuntime 加载模块、以ExportedFunction调用函数、通过mod.Memory()读写线性内存完成数据交换。务必牢记三点系统接口WASI不标准且不成熟需要针对你依赖的接口做单元测试WebAssembly 尚无真正的并行调用会停留在发起 goroutine 上需要借助模块池编排来获得并行内存所有权谁分配谁释放必须显式约定才能避免泄漏与悬垂指针。想进一步深入可从 TinyGo 指南、Rust 指南、Zig 指南 以及 examples/allocation 三个可直接运行的示例项目开始。赞分享开发工具系统底层【免费下载链接】wazerowazero: the zero dependency WebAssembly runtime for Go developers项目地址https://gitcode.com/gh_mirrors/wa/wazero点击查看免费下载相关推荐如何在wazero中运行Rust、TinyGo和Zig编译的WASM完整跨语言指南如何在wazero中运行Rust、TinyGo和Zig编译的WASM完整跨语言指南 WebAssemblyWASM正在改变我们构建跨平台应用的方式而wa开发工具系统底层wazero 与 TinyGo 实战指南从 Go 源码到可嵌入的 wasi Wasm 模块wazero 与 TinyGo 实战指南从 Go 源码到可嵌入的 wasi Wasm 模块 导读 本文围绕 wazero 仓库中维护的 site/conten开发工具系统底层Roc 编译器嵌入指南在 Zig 程序中内嵌编译并执行 .roc 模块Roc 编译器嵌入指南在 Zig 程序中内嵌编译并执行 .roc 模块 本文以 src/compile/README.md https://link.gitc上一篇LearningVideo部署指南从源码编译到APK生成的完整流程下一篇如何在5分钟内上手e5-small-v2零代码实现文本相似度计算创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考