Zig WebAssembly 开发实战

Zig 原生支持 WebAssembly 目标编译,无需额外工具链。本文详解 wasm32 构建配置、浏览器宿主交互、内存共享、WASI 运行时以及将现有 Zig 代码编译为浏览器可用的 WASM 模块。

1. 为什么用 Zig 编写 WebAssembly?

WebAssembly(简称 WASM)是一种可移植的二进制指令格式,设计目标是为 Web 提供接近原生的性能。然而,传统的 WASM 开发通常依赖 C/C++ 配合 Emscripten 工具链或 Rust 配合 wasm-pack,这些方案虽然成熟,但工具链复杂、构建步骤冗长。

Zig 对 WebAssembly 的支持从编译器层面原生集成,带来了几个核心优势:

  • 单一工具链:只需一个 zig 命令即可编译到 wasm32,无需 LLVM 前端或 Emscripten
  • 极小产物体积:Zig 生成的 WASM 模块体积可比 Rust/C++ 方案小百分之三十到五十
  • 零成本抽象:借用 Zig 的 comptime,大量的运行时计算可以转换为编译时常量
  • 双向互操作:Zig 既能导出函数供 JavaScript 调用,也能通过声明调用 JS 函数
  • WASI 原生支持:编写的 WASI 模块可在任何兼容运行时(Node.js、Wasmtime、Wasmer)中执行

对于算法密集型任务(加密运算、图像处理、音视频编解码),Zig 编译的 WASM 能够在浏览器沙箱中以接近原生速度执行,同时避免了将敏感计算暴露给服务端带来的延迟和隐私风险。

2. 编译到 wasm32 目标

2.1 浏览器目标

Zig 支持多种 wasm32 子目标,每个子目标定义了不同的宿主环境和系统接口:

# 纯浏览器目标:没有标准库支持,需自行处理内存分配
zig build-exe src/main.zig -target wasm32-freestanding -O ReleaseSmall

# WASI 目标:提供文件系统、时钟、环境变量等 POSIX 子集
zig build-exe src/main.zig -target wasm32-wasi -O ReleaseSmall

# WASM 动态库:导出函数供宿主调用
zig build-lib src/lib.zig -target wasm32-freestanding -dynamic

2.2 build.zig 配置

pub fn build(b: *std.Build) void {
    const wasm = b.addExecutable(.{
        .name = "app",
        .root_source_file = b.path("src/main.zig"),
        .target = b.resolveTargetQuery(.{
            .cpu_arch = .wasm32,
            .os_tag = .freestanding,
        }),
        .optimize = .ReleaseSmall,
    });

    // 禁用入口函数,WASM 模块不需要主函数
    wasm.entry = .disabled;
    // 导出符号表,让宿主导入这些函数
    wasm.rdynamic = true;
    // 显式导出内存,与宿主共享线性内存
    wasm.export_memory = true;

    b.installArtifact(wasm);
}

wasm32-freestanding 是最常用的浏览器目标,它不假设任何操作系统存在。但它意味着 Zig 标准库的绝大部分功能不可用——你需要自行实现内存分配(通常通过导出 memory 并接收宿主的分配器),不能使用文件系统、网络或线程 API。

3. 浏览器宿主交互

3.1 导出函数给 JavaScript

这是 WASM 最常见的使用模式:将计算密集型逻辑用 Zig 实现,然后从 JavaScript 调用。

// src/math.zig
export fn add(a: i32, b: i32) i32 {
    return a + b;
}

export fn fibonacci(n: i32) i32 {
    if (n <= 1) return n;
    var a: i32 = 0;
    var b: i32 = 1;
    var i: i32 = 2;
    while (i <= n) : (i += 1) {
        const temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

export fn factorial(n: u64) u64 {
    if (n <= 1) return 1;
    var result: u64 = 1;
    var i: u64 = 2;
    while (i <= n) : (i += 1) {
        result *= i;
    }
    return result;
}
// browser.js
async function init() {
    const wasmModule = await WebAssembly.instantiateStreaming(
        fetch("zig-out/bin/app.wasm")
    );

    const { add, fibonacci, factorial, memory } = wasmModule.instance.exports;

    console.log(add(10, 20));           // 30
    console.log(fibonacci(40));         // 102334155
    console.log(factorial(20));         // 2432902008176640000
}

init().catch(console.error);

3.2 从 JavaScript 传递字符串

WASM 函数的参数类型仅限于整数和浮点数。传递字符串、数组或自定义数据结构需要约定内存布局,然后通过指针和长度间接引用。

// 导出全局线性内存,与宿主共享
export var memory: [1024 * 64]u8 = undefined;

var allocator_state = std.heap.FixedBufferAllocator.init(&memory);

export fn allocate(size: usize) usize {
    const ptr = allocator_state.allocator().alloc(u8, size) catch return 0;
    return @intFromPtr(ptr.ptr);
}

export fn deallocate(ptr: usize, size: usize) void {
    const slice = @as([*]u8, @ptrFromInt(ptr))[0..size];
    allocator_state.allocator().free(slice);
}

export fn to_uppercase(in_ptr: usize, in_len: usize, out_ptr: usize, out_capacity: usize) usize {
    const input = @as([*]const u8, @ptrFromInt(in_ptr))[0..in_len];
    const output = @as([*]u8, @ptrFromInt(out_ptr))[0..out_capacity];

    const write_len = @min(in_len, out_capacity);
    for (input[0..write_len], 0..) |c, i| {
        output[i] = if (c >= 'a' and c <= 'z') c - 'a' + 'A' else c;
    }
    return write_len;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();

function zigStringToJS(ptr, len) {
    const bytes = new Uint8Array(memory.buffer, ptr, len);
    return decoder.decode(bytes);
}

function jsStringToZig(str) {
    const bytes = encoder.encode(str);
    const ptr = allocate(bytes.length);
    const wasmMem = new Uint8Array(memory.buffer);
    wasmMem.set(bytes, ptr);
    return { ptr, len: bytes.length };
}

// 使用
const input = jsStringToZig("Hello, World!");
const outPtr = allocate(256);
const resultLen = to_uppercase(input.ptr, input.len, outPtr, 256);
console.log(zigStringToJS(outPtr, resultLen));  // "HELLO, WORLD!"
deallocate(input.ptr, input.len);
deallocate(outPtr, 256);

4. 与 DOM 交互(通过 JS API)

由于 WASM 沙箱禁止直接访问浏览器 API,需要借助 JavaScript shim 间接调用:

// 声明需要从宿主提供的函数
extern fn js_console_log(ptr: [*]const u8, len: usize) void;
extern fn js_alert(ptr: [*]const u8, len: usize) void;
extern fn js_dom_query_selector(ptr: [*]const u8, len: usize) usize;
extern fn js_dom_inner_html(ptr: [*]const u8, len: usize, content_ptr: [*]const u8, content_len: usize) void;

// 导出 Zig 函数,内部调用 JS shim
export fn log_message(msg_ptr: [*]const u8, msg_len: usize) void {
    js_console_log(msg_ptr, msg_len);
}

export fn set_title(ptr: [*]const u8, len: usize) void {
    js_alert(ptr, len);
}

export fn render_html(element_ptr: [*]const u8, element_len: usize, html_ptr: [*]const u8, html_len: usize) void {
    js_dom_inner_html(element_ptr, element_len, html_ptr, html_len);
}
// js-shim.js
const imports = {
    env: {
        memory: new WebAssembly.Memory({ initial: 1, maximum: 10 }),
        
        js_console_log: (ptr, len) => {
            const bytes = new Uint8Array(imports.env.memory.buffer, ptr, len);
            console.log(decoder.decode(bytes));
        },
        
        js_alert: (ptr, len) => {
            const bytes = new Uint8Array(imports.env.memory.buffer, ptr, len);
            alert(decoder.decode(bytes));
        },
        
        js_dom_inner_html: (selPtr, selLen, htmlPtr, htmlLen) => {
            const selBytes = new Uint8Array(imports.env.memory.buffer, selPtr, selLen);
            const htmlBytes = new Uint8Array(imports.env.memory.buffer, htmlPtr, htmlLen);
            const element = document.querySelector(decoder.decode(selBytes));
            if (element) element.innerHTML = decoder.decode(htmlBytes);
        },
    }
};

const wasm = await WebAssembly.instantiateStreaming(fetch("app.wasm"), imports);

5. WASI 运行时

5.1 编译 WASI 模块

WASI(WebAssembly System Interface)是 WASM 的系统接口标准,它定义了文件系统访问、网络、随机数等能力。与纯浏览器 WASM 不同,WASI 模块可以直接使用 Zig 标准库的大部分功能。

zig build-exe src/cli.zig -target wasm32-wasi

WASI 模块可以在以下运行时中执行:

  • Node.js:通过 --experimental-wasi-unstable-preview1 标志支持
  • Wasmtime:Bytecode Alliance 开发的轻量级 WASI 运行时
  • Wasmer:高性能 WASM 运行时,支持多种语言嵌入
  • WasmEdge:面向边缘计算和云原生的高性能运行时

5.2 文件系统访问

const std = @import("std");

pub fn main() !void {
    var file = try std.fs.cwd().createFile("output.txt", .{});
    defer file.close();
    try file.writeAll("从 WASI 模块写入的数据\n");

    var content = try std.fs.cwd().readFileAlloc(
        std.heap.page_allocator,
        "input.txt",
        1024 * 1024,
    );
    defer std.heap.page_allocator.free(content);

    std.debug.print("文件内容({d} 字节): {s}\n", .{ content.len, content });
}
# 通过 Wasmtime 运行,映射当前目录
wasmtime run --dir=. app.wasm

# Node.js 运行
node --experimental-wasi-unstable-preview1 run.js

6. SIMD 向量运算

WASM 的 SIMD 扩展支持 128 位向量操作,适合数值密集型计算。Zig 的 @Vector 类型可以直接映射到 WASM SIMD:

const Vec4 = @Vector(4, f32);
const Vec16u8 = @Vector(16, u8);

export fn simd_add_arrays(out: [*]f32, a: [*]const f32, b: [*]const f32, n: usize) void {
    const Vec8 = @Vector(8, f32);
    var i: usize = 0;

    // 每次处理 8 个浮点数
    while (i + 8 <= n) : (i += 8) {
        const va: Vec8 = a[i..][0..8].*;
        const vb: Vec8 = b[i..][0..8].*;
        const result = va + vb;
        out[i..][0..8].* = result;
    }

    // 处理剩余的不足 8 个元素
    while (i < n) : (i += 1) {
        out[i] = a[i] + b[i];
    }
}

启用 SIMD 编译:

zig build-exe -target wasm32-freestanding -mcpu=baseline+simd128

7. 打包与部署

7.1 最小化输出

为了减少网络传输,应该通过构建选项去除不必要的导出符号:

// 不导出函数表(如果没有间接调用需求)
wasm.export_table = false;

// 不导出内存名称(如果宿主通过默认名称导入)
wasm.export_memory = true;

// 构建时启用 LTO(链接时优化)
wasm.want_lto = true;

7.2 HTML 集成示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Zig WASM 演示</title>
</head>
<body>
    <h1>Zig WebAssembly 演示</h1>
    <div id="output">加载中...</div>
    <script type="module">
        async function loadWasm() {
            const response = await fetch('app.wasm');
            const bytes = await response.arrayBuffer();
            const wasm = await WebAssembly.instantiate(bytes, {
                env: {
                    memory: new WebAssembly.Memory({ initial: 1 })
                }
            });

            const result = wasm.instance.exports.fibonacci(30);
            document.getElementById('output').textContent = 
                `Zig fib(30) = ${result}`;
        }
        loadWasm();
    </script>
</body>
</html>

8. 总结

Zig 的 WebAssembly 支持让系统级开发者能够以前所未有的便捷性进入前端和边缘计算领域:

维度Zig + WASMRust wasm-packC/C++ + Emscripten
工具链单个 zig 命令cargo + wasm-pack + wasm-bindgenLLVM + Emscripten + CMake
产物体积极优(最小至 KB 级)中等(通常数十 KB)较大(数百 KB 起步)
构建速度中等
调试体验Zig 编译器精确错误依赖 LLVM 调试依赖工具链
WASI 支持原生wasm32-wasi 目标通过 WASI SDK

对于算法密集型模块(加密、图像处理、数值计算),Zig 编译的 WASM 提供了接近原生性能的计算能力,同时保持了浏览器的沙箱安全边界。更令人兴奋的是,随着 WASI Preview 2 的推进,Zig 编写的应用程序将能够在服务器、边缘设备和浏览器之间无缝迁移,真正实现"一次编写,到处运行"。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页