概述

讨论 WebAssembly 时,很多文章会直接进入语法、WASI 或边缘部署,但真正决定它如何落地的,是运行时架构。只有理解模块怎样被加载、验证、链接、实例化和调用,才能看清浏览器 Wasm、服务器 Wasm 与组件模型之间的关系。

WebAssembly 运行时可以理解为一个受控执行环境:它负责读取 .wasm 二进制、验证安全性、准备内存与表、连接宿主函数,并在调用时维持隔离边界。1

运行时在系统中的位置

一个典型的 WebAssembly 执行栈可以抽象为:

源码(Rust/C++/Go/AssemblyScript)
        ↓ 编译
    WebAssembly 模块 / 组件
        ↓ 加载
      运行时(浏览器或 Wasmtime/Wasmer/WasmEdge)
        ↓ 链接
  宿主能力(文件、网络、时钟、随机数、日志)

     应用行为与业务逻辑

这里的关键点不是“Wasm 替代宿主”,而是“Wasm 在宿主中被安全执行”。因此运行时既像虚拟机,又比传统虚拟机更强调最小能力暴露。

核心组成

模块、实例与 Store

WebAssembly 程序通常先以模块(module)形式存在。模块中保存函数、类型、导入导出声明、内存与表定义,但模块本身还不能直接运行。

当运行时把模块和具体的宿主环境绑定后,会产生实例(instance)。实例拥有真正可用的函数入口、线性内存、表项以及已解析的 imports。

很多服务器侧运行时还会引入 store 概念,用来承载实例执行时关联的状态,例如:

  • 模块实例
  • 线性内存
  • 宿主对象句柄
  • 资源限制
  • 调用上下文

如果把模块看作类定义,那么实例更像是带状态的对象,而 store 则是这些对象所属的执行上下文。2

线性内存

Wasm 的默认内存模型是线性内存(linear memory)。它本质上是一段连续的字节数组,由运行时负责分配与扩容,Guest 程序只能在边界之内读写。1

这带来两个直接后果:

  • 宿主与 Guest 交换复杂数据时,经常需要通过指针 + 长度的方式读写内存。
  • 安全边界变得清晰:越界访问会触发 trap,而不是像原生程序那样直接破坏进程地址空间。

Table 与间接调用

除了内存,运行时还要维护函数表(table)。表主要用于间接调用,例如函数指针、虚函数分派、回调等场景。运行时会在验证阶段确保表项与函数签名兼容,从而避免任意跳转。1

宿主函数

纯 Wasm 模块不能直接读文件、发网络请求或获取时间,这些能力必须通过 imports 由宿主显式提供。常见宿主函数包括:

  • 日志输出
  • 文件读取
  • HTTP 请求
  • 随机数生成
  • 时钟查询

在浏览器中,这些能力常常通过 JavaScript API 间接暴露;在服务器侧,则经常通过 WASI 0.3.0新特性 或自定义宿主接口暴露。

执行流程

1. 加载与解码

运行时首先读取 .wasm 二进制,并将其解析为内部表示。这个阶段关注的是格式是否符合规范,例如 section 布局是否正确、类型定义是否完整。

2. 验证

验证是 WebAssembly 安全模型的重要基础。运行时会检查:

  • 指令栈类型是否一致
  • 间接调用是否满足签名约束
  • 导入导出声明是否合理
  • 内存与表定义是否满足限制

这一步的意义是:在真正执行之前,就把大量“本来会在原生程序运行时炸掉”的问题前置拦截。1

3. 编译

不同运行时会选择不同的执行策略:

  • 解释执行:启动简单,但运行性能通常较低。
  • JIT:在加载或热点路径上动态编译为机器码,适合通用场景。
  • AOT:提前编译为本地代码,适合追求稳定启动时间与部署可控性。

这也是为什么同样是 Wasm,不同运行时在冷启动、吞吐与调试体验上差异很大。

4. 链接

链接阶段会把模块声明的 imports 与宿主真正提供的对象对上,例如:

模块导入:env.log, wasi:cli/stdout, myapp.cache.get
宿主提供:log 函数、stdout 句柄、缓存接口实现

如果导入缺失、签名不匹配,实例化通常会直接失败。

5. 实例化

实例化阶段为模块创建真实执行状态,包括:

  • 分配线性内存
  • 初始化全局变量
  • 装载函数表
  • 绑定宿主 imports
  • 运行 start 函数(如果存在)

实例化之后,导出函数才能被外部调用。

6. 调用与 Trap

调用阶段由宿主触发导出函数执行。运行中如果发生除零、越界访问、非法间接调用等错误,运行时不会把宿主整个进程拖垮,而是抛出 trap,让宿主决定如何记录、重试或终止该实例。2

浏览器运行时与服务器运行时

浏览器侧

浏览器中的 WebAssembly 主要和 JavaScript、DOM、渲染管线协作,典型目标是:

  • 计算密集型逻辑加速
  • 复用 C/C++/Rust 代码
  • 提供比 JavaScript 更稳定的性能边界

浏览器运行时通常内建于 JavaScript 引擎中,因此更强调与 JS 互操作、下载体积、编译缓存、调试工具链。

服务器与边缘侧

服务器侧运行时更看重:

  • 多租户隔离
  • 宿主能力控制
  • 冷启动与资源占用
  • 嵌入式集成能力

这类场景下,Wasm 通常不是页面脚本,而是插件、边缘函数、规则引擎或轻量服务组件。此时 WebAssembly安全沙盒机制WebAssembly宿主集成 就比 DOM 互操作更重要。

主流运行时的定位

Wasmtime

Wasmtime 是 Bytecode Alliance 推动的通用运行时,也是许多 Component Model 与 WASI 示例默认采用的参考实现。它的优势在于:

  • 文档与示例相对完善
  • 与组件模型生态结合紧密
  • 适合作为宿主嵌入应用
  • WebAssembly Component Model 支持较成熟2

Wasmer

Wasmer 更强调跨平台分发和多后端执行策略,在“把 Wasm 当作可分发应用单元”这一方向上更活跃。它适合关注开发体验、打包分发和执行后端可切换的场景。

WasmEdge

WasmEdge 更常出现在边缘计算、云原生与 AI 推理话题中。它强调轻量、启动快,以及与 wasi-nn 等能力结合的工程实践。

要注意,这三者并不是互相完全替代的关系,更像是在不同落地重点上的权衡。

与组件模型的关系

传统 Wasm 模块解决的是“安全执行一段低级二进制代码”;组件模型进一步解决的是“如何以类型化接口组合多个 Wasm 单元”。

因此可以把两者理解为:

  • 核心 Wasm 运行时:负责验证、实例化、执行
  • 组件模型:负责更高层的接口描述、绑定生成与组件组合

组件最终仍然需要运行时来承载,只是运行时除了处理核心模块,还要理解 WIT、资源类型与 canonical ABI。3

JIT、AOT 与部署取舍

维度JITAOT
首次启动中等通常更稳定
峰值性能较高较高
部署复杂度较低更高
调试体验通常更灵活依赖工具链
适用场景通用服务、开发环境边缘部署、受控生产环境

实践中没有绝对最优解:

  • 插件系统往往更看重安全加载和兼容性。
  • 边缘函数更看重冷启动与体积。
  • 长生命周期服务更关注吞吐和内存占用。

所以理解运行时架构的意义,不是记住术语,而是能根据场景做取舍。

典型理解误区

“Wasm 就是更快的 JavaScript”

这只描述了浏览器中的一小部分情况。Wasm 更重要的价值在于提供一个可验证、可嵌入、可控能力暴露的执行格式。

“Wasm 运行时等于容器运行时”

二者都能承载隔离执行单元,但隔离机制、能力模型和分发方式完全不同。容器关注操作系统级打包与进程隔离,Wasm 运行时关注字节码验证与宿主能力控制。

“有了 WASI 就能直接替代传统后端”

WASI 提供的是标准化宿主接口,不是完整应用平台。数据库驱动、网络协议栈、调试体验、生态依赖仍然要看具体运行时和宿主系统。

相关主题

参考资料

Footnotes

  1. WebAssembly Concepts - webassembly.org 2 3 4

  2. Wasmtime Documentation 2 3

  3. Component Model Concepts - Bytecode Alliance