- 引言 — 浏览器只是个开始
- 三种性质 — 为什么 WASM 是通用运行时
- WASI — WASM 与系统对话的方式
- 边缘与无服务器 — 威胁容器
- 插件系统 — WASM 正在成为答案的地方
- 组件模型 — 超越模块
- 为什么这是一种通用运行时 — 大图景
- 冷静的现实 — 仍然存在的课题
- 结语 — 运行时的再发明
- 参考资料
引言 — 浏览器只是个开始
第一次听说 WebAssembly(简称 WASM)时,大多数人会把它理解成"在浏览器里跑 C++ 的技术"。这话不算错。WASM 确实诞生于浏览器,是为了让 JavaScript 力不从心的重度运算能够在网页上实现而造出来的。
但 WASM 真正的野心在浏览器之外。仔细想想就会发现,WASM 在浏览器里解决的问题——安全隔离、可从任意语言编译、以近原生速度运行的可移植二进制文件——本质上和浏览器毫无关系。这些性质在服务器上、在边缘、在插件系统里同样有吸引力。
这篇文章讲的是离开浏览器之后的 WebAssembly。我们会看 WASM 为了和系统对话而造出的 WASI 接口、WASM 为何在边缘与无服务器场景中威胁到容器、插件系统里 WASM 为何正在成为答案,以及组件模型如何把这一切串起来。如果想亲自体验 WASM 是如何做到近原生速度的,WASM 原理实验室是个不错的起点;如果想手写 WAT(WebAssembly 文本格式)再编译成二进制,可以在WebAssembly 工作室里试验。
三种性质 — 为什么 WASM 是通用运行时
WASM 之所以能扩展到浏览器之外,根本原因在于三种性质。这三者原本是为浏览器设计的,但后来发现它们其实是在更广阔的场景里同样成立的普遍美德。
- 近原生速度:WASM 是预编译的低层字节码,解析快,并通过 JIT/AOT 以接近机器码的方式执行。它不像解释型语言那样慢,分发的也不是源码而是可移植的二进制文件。
- 沙箱隔离:WASM 模块默认从"什么都做不了"的状态开始。它的内存被限制在自己的线性内存里,只能访问宿主明确交给它的系统资源。运行不可信代码,没有比这更好的起点了。
- 可移植性:WASM 字节码与 CPU 架构(x86、ARM 等)无关。一次编译出来的
.wasm,可以在任意架构、任意操作系统的 WASM 运行时上原样运行。这已经相当接近"编译一次,到处运行"的老梦想了。
把这三种性质摆在一起看,就能看出这个组合为什么强大。容器提供隔离与可移植性,但启动重、慢。原生二进制文件快,但隔离弱且被架构绑死。解释型语言可移植性好,但慢。WASM 试图把这三者的优点(快、安全、可移植)装进同一个信封里。关键在于,这个组合发挥作用的地方不只是浏览器。
WASI — WASM 与系统对话的方式
这里有一个根本性的问题。WASM 模块默认什么都做不了——不能读文件,不能连网络,不能看时钟。在浏览器里这不是问题,因为 JavaScript 会把需要的功能都递过来。但在浏览器之外呢?在没有 JavaScript 的服务器上,WASM 想读一个文件,该依靠什么?
填补这个空白的是 WASI(WebAssembly System Interface)。WASI 是 WASM 模块访问系统资源的标准化接口,可以说是一种类 POSIX 的系统调用规约。它把打开文件、读、写、读取时钟、获取随机数这类基本功能,定义成 WASM 模块可以调用的标准函数。
核心在于 WASI 遵循一种基于能力(capability-based)的安全模型。传统程序一旦运行,就能访问该用户能访问的所有文件。WASI 模块不同:默认无法访问任何文件,只能使用宿主明确交给它的目录或文件句柄。
传统进程:
运行 = "原样继承该用户拥有的一切权限"
(程序可以随意翻遍整个文件系统)
WASI 模块:
运行 = "不带任何权限启动"
只能使用宿主交给它的东西
(例如: "只准读这一个目录" — 其余的连存在都不存在)
这种模型的含义很大。可以只给 WASM 模块恰好需要的能力,让其余的东西彻底不可见。运行不可信代码时,"这个程序会不会不小心或恶意地碰到什么"这种担忧被从根本上削减了,因为隔离被翻转成了"默认拒绝,明确许可"这个安全的方向。
WASI 仍在持续演进。早期 WASI 专注于文件、标准输入输出这类基础功能,之后的版本则把范围扩展到网络、异步 I/O 这类更丰富的能力。这个演进方向和后面要讲的组件模型是咬合在一起的。
边缘与无服务器 — 威胁容器
WASM 在浏览器之外用得最火热的地方,是边缘计算和无服务器。随着 Fastly 的 Compute、Cloudflare Workers 这类平台把 WASM 当作执行单元,"请求该如何处理"的格局正在改变。
为什么偏偏是 WASM?无服务器与边缘的核心难题是冷启动——请求到来时,能多快地把执行代码的环境拉起来,是关键所在。这里容器和 WASM 的差异非常鲜明。
- 容器:为了隔离,要搭建 Linux 命名空间、cgroup、文件系统层。就算够轻量,冷启动也可能是几百毫秒到几秒。
- WASM 模块:由于沙箱已经在语言层面得到保证,不需要搭建笨重的操作系统级隔离。实例创建极其轻量,冷启动能降到毫秒以下。
这个差异带来的结果很大。一旦冷启动实际上消失,每个请求都启动一个新实例就不再是负担。于是,在离用户最近的边缘节点上,只在需要时瞬间拉起、处理完就丢弃这种模式就变得现实了。不必在每个边缘节点上装笨重的容器编排,一个轻量的 WASM 运行时就能安全地隔离并运行大量租户的代码。
密度(density)这一点也很重要。因为 WASM 实例比容器轻得多,同样的硬件上能同时跑更多实例。在边缘这种资源受限的环境里,这种密度直接等于经济性。在一个进程里跑数千个隔离的 WASM 实例,比跑数千个容器便宜得多。
当然也有权衡。WASM/WASI 环境不是完整的 Linux,既有的容器镜像没法直接搬过去。如果应用依赖 WASI 尚不支持的系统功能,就会卡住。也就是说,边缘 WASM 是一种"轻、快,但受限"的环境,需要完整操作系统的负载,仍然更适合容器。
插件系统 — WASM 正在成为答案的地方
WASM 正在悄悄地、却也决定性地站稳脚跟的另一个领域是插件系统。想把第三方写的代码安全地嵌入某个应用时,WASM 几乎是理想答案。
想想插件这个由来已久的两难。插件要有足够的能力才有用,但能力越大就越危险。原生插件(比如共享库)和宿主进程运行在同一个地址空间里,一个 bug 就能拖垮整个宿主,恶意插件更是无所不能。反过来,把插件隔离成独立进程虽然安全,却又慢又复杂。
WASM 干净利落地解开了这个两难。把插件做成 WASM 模块,它就能在和宿主同一个进程里快速运行,同时被沙箱隔离。插件只能调用宿主明确交出的函数,无法随意碰宿主的内存。而且插件可以用任意语言编写,插件开发者不必和宿主使用同一种语言。
真实案例印证了这个方向。
- Envoy 代理:网络代理 Envoy 让过滤器可以用 WASM 扩展。拦截并处理流量的自定义逻辑,可以作为 WASM 模块注入,而不必重新编译代理本身。这套扩展规约后来发展、标准化为一个叫 proxy-wasm 的接口。
- Figma:设计工具 Figma 采用基于 WASM 的方式,在沙箱里安全地运行插件。第三方插件在处理用户设计数据的同时,其执行被安全地圈住。
- 数据库:不少现代数据库都在探索用 WASM 运行用户自定义函数(UDF)或扩展。把用户写的代码放进数据库进程里运行,传统上极其危险,而 WASM 沙箱让这件事变得安全。
这些案例的共同点是"要在不威胁宿主的前提下,快速运行不可信的扩展代码"这一需求。这正是 WASM 三种性质(快、安全、语言无关)大放异彩的地方。如果你正在重新设计一个插件系统,现在把 WASM 认真列入候选是值得的。
组件模型 — 超越模块
到目前为止讨论的 WASM,基本上都是以"模块"为单位的。但纯粹的 WASM 模块在实务中有一个很痛的局限:模块之间、或者模块和宿主之间能传递的东西,基本上只有数字(整数和浮点数)。哪怕只想传一个字符串、一个列表、一个结构体,也没有标准化的方法,只能各自手动对齐一套交换内存指针和长度的低层规约。
解决这个问题的是组件模型(Component Model)。组件模型在 WASM 模块之上叠加了一层高层类型系统和接口定义。它在接口层面定义字符串、列表、记录、变体这类丰富类型,让用不同语言写成的组件能够通过这些类型自然地通信。
核心思路可以归纳为这样几点。
- 以语言中立的方式描述接口:用一种叫 WIT(WebAssembly Interface Types)的语言,声明"这个组件提供哪些函数、交换哪些类型"。这份描述不绑定于任何特定语言。
- 跨语言组合变得自然:用 Rust 写的组件和用 Go 写的组件,不需要了解彼此的内部内存表示,就能通过接口类型安全地交换值。字符串以字符串的形式往来,列表以列表的形式往来。
- 复用和组装变得容易:组件可以像乐高积木一样组合起来——把一个组件的输出接到另一个组件的输入上,只替换需要的那一部分。
组件模型之所以重要,是因为它把 WASM 从"在浏览器里跑 C++ 的技术"提升为"一种语言中立的软件组装规格"。前面提到的 WASI 最新的发展方向,也是建立在这个组件模型之上定义的。也就是说,就连系统接口本身也被表达为组件接口,让平台提供的能力和模块要求的能力,用同一种类型语言咬合在一起。
为什么这是一种通用运行时 — 大图景
把到目前为止的碎片拼到一起看。WASM 以近原生速度运行、与语言无关、被强力沙箱化、可跨架构移植。WASI 以基于能力的方式标准化了系统访问,组件模型让跨语言组装成为可能。把这幅图拉远来看,会浮现出某个熟悉的轮廓——正是"通用运行时"的轮廓。
历史上,我们不止一次追逐过"写一次,到处运行"的梦想。每一次都付出了代价:为了获得可移植性而装上笨重的运行时,或者放弃性能,或者被锁死在某种特定语言里。WASM 有趣的地方在于,它试图同时削减这些代价。
- 比容器更轻:无需操作系统级隔离,仅在语言层面隔离,因而在冷启动和密度上占优。
- 比语言运行时更可移植:不绑定于特定语言的虚拟机,任意语言都可以编译进来。
- 比原生代码更安全:沙箱是默认状态,因而适合运行不可信代码。
这正是 WASM 之所以能从浏览器扩展到服务器、边缘、插件、乃至物联网的原因。"安全隔离、快速、可移植的代码执行单元"这种需求无处不在,而 WASM 是对这种需求相当不错的一个答案。
冷静的现实 — 仍然存在的课题
当然,说 WASM 已经征服了一切,是种夸张。浏览器之外的 WASM,仍有许多正在成熟中的部分。
- 生态成熟度:容器生态经过多年打磨,积累了庞大的工具、镜像仓库和运维经验。WASM 这边发展很快,但还没那么厚实。生产环境运维所需的可观测性、调试、部署工具仍在持续补齐中。
- WASI 的标准化仍在进行:网络、异步、文件系统等核心功能的标准仍在演进。正因为标准还在成型的过程中,可能会碰到版本差异或尚未支持的功能。
- 各语言支持参差不齐:像 Rust 这样 WASM 目标已经成熟的语言存在,但也有一些语言因为运行时太重而不太适合 WASM,或者支持还处于早期阶段。用哪种语言编译什么,体验差异会很大。
- 不是一个完整的系统:正如前面反复强调的,WASM/WASI 不是完整的操作系统。想把现有的 Linux 应用原样搬过去,会被不支持的系统调用挡住。WASM 是一种"要为它重新写"的环境,而不是"能原样容纳既有东西"的环境。
综合这些课题来看,浏览器之外的 WASM 是一种"潜力明确,但仍在被填补"的技术。它今天还替代不了所有容器,但在需要轻量隔离不可信代码的特定领域(边缘函数、插件、多租户执行)中,已经在实战里显出优势。
结语 — 运行时的再发明
WebAssembly 诞生于浏览器,但它真正在重新发明的,是"如何在任何地方安全、快速地运行代码"这件事本身。近原生速度、沙箱隔离、架构可移植性这三种性质,并不只是浏览器的需求,而是遍布整个计算领域的需求,WASM 用一个信封回应了这份需求。
WASI 以基于能力的方式标准化了与系统的对话,边缘与无服务器打破了冷启动的壁垒,插件系统解开了信任的两难,组件模型降低了语言的壁垒。把这些碎片汇聚起来,WASM 就不再只是某个特定平台的技术,而更接近一种横跨平台的通用执行规格。
当然,前面的路还很长。生态仍在被填补,标准仍在成型,它也不会替代完整的操作系统。但方向是清楚的。下次当你为"该如何安全地运行这段不可信的代码"或"该如何在多种语言、多种环境中复用这段逻辑"发愁时,请把浏览器之外的 WebAssembly 列入候选清单。这份清单上,WASM 所占的位置每年都在扩大。
参考资料
- WebAssembly 官方网站: https://webassembly.org/
- WASI: https://wasi.dev/
- 组件模型 (WebAssembly): https://component-model.bytecodealliance.org/
- Bytecode Alliance: https://bytecodealliance.org/
- Fastly Compute: https://www.fastly.com/products/compute
- Cloudflare Workers: https://developers.cloudflare.com/workers/
- proxy-wasm (Envoy 扩展): https://github.com/proxy-wasm/spec
현재 단락 (1/64)
第一次听说 WebAssembly(简称 WASM)时,大多数人会把它理解成"在浏览器里跑 C++ 的技术"。这话不算错。WASM 确实诞生于浏览器,是为了让 JavaScript 力不从心的重度运算...