WasmCon-2024-笔记-全-
WasmCon 2024 笔记(全)
002:我们已抵达的技术高峰 🏔️


在本节课中,我们将跟随 Kate Goldenring 的视角,探索 WebAssembly 如何借鉴登山运动的智慧,成功攀登一系列技术高峰。我们将看到 WebAssembly 如何从前人经验中学习、如何为新的环境做好准备、如何确保安全与隔离、如何高效利用资源、如何成为通用工具,以及如何增强现有生态系统。
学习前人经验 📚
在攀登一座山峰之前,登山者会研究前人的“登山报告”,学习他们的成功与失误,以便规划更成功的路线。WebAssembly 也采取了同样的策略。
WebAssembly 研究了早期试图扩展浏览器能力的技术,如 ActiveX、Java、Silverlight 和 Flash。它从这些技术的不足中吸取了教训,特别是它们在安全性方面的缺陷——这些技术最终成为了恶意软件攻击的载体,未能成功登顶。

因此,WebAssembly 规范的设计者将安全性作为核心考量。他们不仅专注于安全设计,还将其打造为一个开放标准,而非专有系统。为了吸引更多人参与,他们力求规范简洁明了。
以下是一页纸的 WebAssembly 核心语义规范,理论上,理解它就能编写一个解释器:
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
这份规范经过科学验证,能够在沙箱中安全地隔离执行代码。
携带地图进入新领域 🗺️

进入荒野时,携带地图至关重要。你需要确保拥有所需的资源,如庇护所、水和前进的路径。当 WebAssembly 离开浏览器,迈向服务器领域时,它也携带了自己的“地图”——WASI。

WASI 是 WebAssembly 系统接口。它确保了 WebAssembly 组件能够访问服务器上的资源,例如文件系统、I/O 和 HTTP。通过 WASI,开发者可以编写能够接收和发送 HTTP 请求的 WebAssembly 服务。
更重要的是,只要 WebAssembly 运行时实现了 WASI,同一个应用程序就可以在任何运行时上执行,成为一个真正的 WebAssembly 服务。这一切都建立在组件模型的语义之上,这是另一个开放标准。

防止“入侵物种”扩散 🚫

作为一名优秀的户外活动者,阻止入侵物种的扩散非常重要。在云计算领域,平台工程师们也秉持着“如有疑问,立即清除”的原则。他们不会允许任何未经严格隔离的程序在多租户云环境中运行。
WebAssembly 非常擅长防止“入侵物种”扩散,因为它具备安全的沙箱和基于能力的安全模型。它比当前云环境中更普遍的容器隔离方式更加安全,但同时保持了相同的可打包性。

WebAssembly 组件可以像容器一样,被打包成 OCI 镜像。CNCF 内的 WebAssembly 工作组在过去一年中致力于标准化 WebAssembly OCI 镜像的布局格式。现在,所有 WebAssembly 运行时都能理解相同的 OCI 格式,并在任何云平台上执行你的组件。
此外,WASI 正在为云原生的 WebAssembly 应用提供特定接口。除了服务器端已知的 HTTP,还有正在开发的 WASI Key-Value(用于访问缓存)和 WASI Config(用于访问密钥)。这意味着,跨云平台,你的应用无需重建就能访问相同的资源。
高效利用能量的“休息步”法 ⚡

攀登高山非常耗费体力。登山者会使用一种名为“休息步”的技巧:要么积极移动,要么静态站立。其核心思想是,只在向目标前进时才消耗能量。
WebAssembly 非常擅长这种“休息步”。它的独特之处在于,作为一种隔离的执行代码形式,它能够瞬时启动。实例化并启动一个 WebAssembly 模块只需不到一毫秒。

这意味着,当你不需要使用它时,可以将其销毁;当需要时,又能立即将其启动,真正实现缩容到零。这对于资源受限的边缘计算环境尤为重要,它为在边缘构建安全、多租户的无服务器平台打开了大门。
轻量且多用途的工具 🧰

为了节省登山时的体力,另一个方法是尽可能减轻负重。登山者会优化装备,选择多用途的装备,而不是携带一堆功能单一的物品。
WebAssembly 正是一个轻量级、多用途的出色工具。没有哪个领域比 IoT 更需要多用途性。目前已有数十亿设备在线,预计到 2030 年将超过 400 亿台。这个领域高度异构,存在不同的板卡、平台和 SDK,成为一个通用的嵌入式开发者非常困难。

有时,开发者甚至被限制只能使用 C 等系统语言。而 WebAssembly 的作用是:只要能在设备上放置一个 WebAssembly 运行时(例如精简版的 wasm-micro-runtime),你就可以在上面运行 WebAssembly 应用,并且可以用任何能编译成 WebAssembly 的语言来编写它。
Bytecode Alliance 内的嵌入式特别兴趣小组正在致力于标准化这个异构的生态系统,他们正在创建针对 IoT 空间的 WASI 接口。
增强现有体验 ✨

最后一个重要的登山原则是:充分利用目的地。一个湖泊可能因为其周边可进行的活动(如攀登邻近的山峰、露营、水上活动)而变得极具吸引力。
WebAssembly 同样非常擅长增强现有体验。它是一种为现有架构提供插件的通用机制。这一切始于浏览器,WebAssembly 提供了一种与语言无关且安全的方式来扩展浏览器功能。
现在,我们已经在各种应用中看到了这一点:
- Envoy 扩展:提供了使用 WebAssembly 扩展 Envoy 网关的方法,是较早的采用者。
- Shopify 函数:现在你可以用任何语言通过 WebAssembly 来编写,以扩展结账体验。
- 代码编辑器:如 VS Code 和新近发布的 Zed 编辑器,也允许使用 WebAssembly 来编写扩展。

总结
本节课中,我们一起穿越了 WebAssembly 的“技术荒野”,探索了它成功登顶的六座高峰。回顾这段旅程,支撑我们攀越所有这些高峰的基石,正是开放标准。
我们从将 WebAssembly 带入浏览器的核心规范开始,接着是使其得以离开浏览器的 WASI,然后是让我们能为模块定义更高级类型的组件模型,最后是统一了 WebAssembly 打包方式的 OCI 镜像规范。
我们可以想象这样一个未来:应用程序不再局限于某一个技术高峰或领域。得益于其可移植性和安全性,以及统一的运行时,我们的应用可以伴随我们从浏览器无缝迁移到边缘。而这一切,都依赖于这些开放标准的持续演进。

感谢你与我一同完成这次 WebAssembly 荒野探索之旅。
003:使用Wasm构建安全高效的传感应用


在本节课中,我们将要学习WebAssembly(Wasm)如何解决边缘计算和计算机视觉应用开发中的关键挑战,包括资源限制、环境碎片化、安全威胁和开发体验问题。我们将探讨Wasm的轻量级、安全隔离和跨平台特性如何为物联网和嵌入式视觉应用提供理想的解决方案。
边缘AI与计算机视觉的需求与挑战

当前,对边缘AI和计算机视觉的需求正在快速增长。这并不令人意外。问题是,由于需求增长过快,资源消耗变得非常巨大。如果将大量视频数据传输到云端并在那里进行所有的计算机视觉处理,这种模式是不可扩展的。因此,业界正努力将计算推向边缘。

当我们将计算推向边缘时,会面临一系列开发挑战。你可能对这些挑战非常熟悉:计算资源匮乏、边缘环境碎片化、安全威胁,以及嵌入式系统开发体验不佳。
边缘设备面临的开发挑战
以下是边缘设备开发中遇到的主要问题:
- 资源高度受限:许多物联网设备或微控制器的资源极其有限。虽然从历史标准看,其内存容量已相当可观,但与云原生世界中丰富的资源相比,这些设备的资源仍然微不足道。
- 环境高度碎片化:在物联网和嵌入式开发领域,存在多个层面的碎片化,例如操作系统、硬件架构、指令集架构和通信协议。WebAssembly有助于缓解其中一些问题。
- 缺乏内存隔离:在这些环境中,缺乏内存管理单元(MMU)和内存保护机制是一个真正的担忧。而通常很大比例的安全漏洞都与内存问题相关,这使得物联网环境非常脆弱。
- 开发语言不匹配:嵌入式开发主要由C/C++语言主导。然而,如今许多AI和数据框架主要使用Python。大量Python开发者不一定愿意深入C/C++世界来完成他们的工作,这就产生了阻抗不匹配。
上一节我们介绍了边缘设备开发面临的多重挑战,本节中我们来看看WebAssembly如何为解决这些问题提供方案。


WebAssembly作为解决方案
WebAssembly为边缘和视觉应用可以解决上述部分问题。
- 轻量级执行环境:我们使用WAMR(WebAssembly Micro Runtime)。其“一次编译,到处运行”的理念,至少在一定程度上减轻了移植的开发负担。
- 默认安全与隔离:这是一个非常重要的方面。在WebAssembly环境中,我们可以强制执行内存保护,并且可以相对容易地限制对不应使用的接口的访问。
- 支持多语言编程:特别是Python,可以绕过开发者所掌握技能与系统需求之间的不匹配。WebAssembly社区对Python的兴趣正在快速增长。
安全模型与Python运行时

WebAssembly的安全模型非常适合物联网。我们可以利用其内存保护功能并限制接口访问,还可以在运行前进行更多的静态分析。
关于Python编程,我们已做了大量工作,以在WASM沙箱环境中高效运行Python。我们尝试了多种方法,包括转译、完整运行CPython,或将CPython代码直接编译为WebAssembly。目前,我们正在使用MicroPython,并将其运行在WAMR沙箱内。在非常受限的环境中,我们并不需要完整的通用Python,只需要类似NumPy的功能就足够了。目前,我们可以在AOT编译后约1.5MB的空间内运行它,这对于某些微控制器来说仍然很大,但我们正在取得进展。
了解了WebAssembly的核心优势后,让我们看一个具体的应用案例,了解这些技术如何整合到实际产品中。
实际应用案例:索尼智能视觉传感器

将上述技术整合起来,索尼开发了智能视觉传感器IMX500。它基本上是将数字信号处理器(DSP)、内存和实际像素单元集成在同一个封装内。我们可以对它进行编程,以运行AI模型。

我们将这项技术用于我们的物联网设备中。我们构建了一个设备栈,其中WebAssembly是栈的关键部分,使我们能够在微控制器上的物联网设备中运行应用程序。
在我们的本地开发环境中,开发者可以用Python开发应用程序,在将其上传到Aitrios云平台进行大规模设备管理和部署之前,先在本地设备上部署、监控和测试它。
跨平台兼容性与总结

作为前瞻,我们可以在不同的设备上运行相同的应用程序,例如树莓派或基于ESP32的微控制器摄像头。


本节课中我们一起学习了WebAssembly如何为边缘计算和计算机视觉应用提供一个安全、高效且开发友好的解决方案。我们探讨了它如何应对资源限制、环境碎片化和安全挑战,并通过支持Python等语言改善了开发体验。WebAssembly的轻量级和可移植性使其成为连接现代AI开发与嵌入式系统需求的理想桥梁。
004:从 WASI 0.2 到 0.3 及未来的增量扩展

在本节课中,我们将要学习 WebAssembly 系统接口(WASI)的核心概念、发展历程以及从 0.2 版本向 0.3 版本演进的关键特性。我们将重点关注 WASI 如何作为 WebAssembly 与外部世界交互的模块化接口,并深入探讨即将到来的异步支持如何改变我们编写和组合组件的方式。
什么是 WASI?🤔
WASI 代表 WebAssembly 系统接口。简单来说,WASI 是 WebAssembly 与外部世界之间一系列可虚拟化的模块化接口集合,目前正处于标准化进程中。
WASI 是 WebAssembly 的自然补充。WebAssembly 抽象了各种物理 CPU,允许我们编写可移植的 WASM 代码。但是,当这些可移植的 WASM 代码需要与外部世界通信时,就需要 WASI。
那么,什么是“外部世界”呢?这个概念有很多不同的视角。最初,外部世界指的是传统操作系统提供的接口,例如文件系统、套接字、随机数和时钟。但随着 WASM 被应用到更多场景,我们的视角必须拓宽。
当 WASM 进入云原生领域时,所需的原始接口看起来有所不同,更侧重于 HTTP、配置存储、键值存储、Blob 存储、消息传递等。当希望 WASM 与更多不同类型的硬件交互时,我们意识到需要新的接口来与 GPU、神经网络、USB、I²C、加密设备等通信。随着 WASM 嵌入到更多环境中,就需要更多的接口。
这说明了我们不能期望一个主机实现所有这些接口。主机只需要实现对其有意义的模块化子集。因此,WASI 是一个模块化接口的集合。
这些接口在设计上也是可虚拟化的。因为当 WASM 与外部世界通信时,有时我们希望外部世界本身就是 WASM。例如,用较低级别的接口(如 WASI 套接字)来实现较高级别的接口(如 WASI HTTP)。
我们正在标准化这些接口,以便能够构建一个共享工具生态系统。这让我们能够分摊在自动化那些繁琐、低级别、易出错且涉及安全性的工作时所需的大量精力,否则我们将不得不重复发明轮子。
这也让我们能够将这些接口上游集成到流行的语言工具链和其他我们希望内置这些接口的流行工具中。如果我们只是某个特定项目或公司特有的接口,并试图将它们上游化,我们可能会被要求先去制定标准。有了标准,我们才能真正将这些内容上游化。
这让我们能够将精力集中在构建平台独特价值上,而不是重复地发明基础功能。标准还让我们能够汇集来自不同背景的深厚经验,最终带来更高质量的成果。
WASI 的发展历程与发布节奏 📅
我们尚未完成标准化,这是一个正在进行的过程。我们正在实践规范和实现的增量协同开发,因为历史经验告诉我们,如果只做其中一项而忽略另一项,结果往往不佳。我们通过一系列版本发布来实现这一点。
你可能已经注意到,WASI 确实在持续发布。上一次主要版本是今年一月的 0.2 版,这是一个包含重要内容的重大发布。它包含了组件模型,该模型定义了一个可组合的、语言中立的代码单元。它包含了 WIT,这是我们用于组件的接口定义语言。
使用 WIT,我们定义了一系列接口,共同构成了 CLI 命令世界,这为我们提供了一个类似 POSIX 的命令执行环境。我们还定义了一个 HTTP 代理世界。如果你将这些世界看作维恩图,它们会相交,因为它们都提供某些接口,如时钟和随机数。但 HTTP 代理世界自然希望以原生 HTTP 方式与外部世界通信,而命令世界则希望使用文件系统和套接字。
这是 0.2 版本,一个重大发布。我们庆祝之后休息了一下,然后继续工作。八月,我们发布了 0.2.1。这个版本的主要特性实际上是发布流程本身,以及支持平稳进行次要版本发布所需的所有工具和运行时支持。我们还为 WIT 添加了一些不稳定和同步门控功能,以帮助我们控制新功能的推出。
然后在八月稍晚,我们发布了 0.2.2,增加了一些 OCI 集成功能,这是非常令人兴奋的内容,我稍后会再提到。此外,还增加了第三个弃用门控。我们计划在十二月发布 0.2.3,希望其中能包含 WASI 键值存储提案,该提案目前已经投入了大量工作。
你在这里看到的模式是,我们计划每两个月发布一次,采用类似火车模型的发布方式。当功能准备就绪时,它们就进入该次发布;如果没准备好,可以等待下一班“火车”。
虽然事情发生的具体顺序尚不明确,但围绕 WASI 配置、Blob、消息传递、加密和 WebGPU 有很多令人兴奋的工作正在进行。利用所有这些新接口,我们可以定义一个新的、更大的云世界,它将是之前维恩图中代理世界的超集。所以有很多令人兴奋的内容。
WASI 0.3:原生异步支持 ⚡
与所有这些工作并行的是 0.3 版本发布,计划在明年上半年进行。WASI 0.3 中的主要特性是原生异步支持。
请不要惊慌,这不是像 0.1 到 0.2 那样的大变化。这不是一个破坏性变更。我们讨论的是对现有 0.2 组件模型二进制格式的类型和选项进行增量添加。因此,所有 WASI 0.2 组件在 WASI 0.3 世界中仍然是有效的组件。
我们将在推出期间同时支持 WASI 0.2 和 WASI 0.3 接口。但最终,利用 WASI 接口的可虚拟化特性,我们将能够用 WASI 0.3 来虚拟化 WASI 0.2 接口。这样,我们就可以从主机中移除 WASI 0.2,减少我们可信计算基的大小,同时保持所有现有的 WASI 0.2 代码正常工作。这正是虚拟化的全部意义所在。
我们实现原生异步支持的目标是:自动与源语言并发特性集成、简化并发接口,并使并发组件可组合。让我们深入探讨这三个目标。
目标一:语言集成
在 WASI 0.2 中,我已经可以声明一个函数 load,例如从一个名称字符串加载模型。我可以用各种语言的各种同步函数来实现它。但从 0.3 开始,我可以用异步函数来实现它。例如,在 C# 中,我可以将 load 实现为一个返回异步任务的函数。因为它是一个异步函数,它可以在返回新模型之前等待一个阻塞操作。
然后,我可以从 Python 导入这个 load 函数,Python 可以将其作为协程导入。这意味着在 Python 中,我可以使用 asyncio 来生成多个并发的 load 任务,然后再收集它们。这样,我现在就可以相当自然地进行跨语言并发调用,当第一个 load 在这里阻塞时,第二个就可以取得进展。
我可以对所有在 0.2 中能表达的函数签名都这样做。但我也可以使用新的 future 和 stream 类型来表达更多的函数签名。例如,我可以编写一个 transform 函数,将 stream<bytes> 转换为 stream<bytes>。
我可以在 JavaScript 中用异步函数实现它,该函数接收一个可读流。因为它只是 JavaScript 标准库内置的可读流,我可以使用像 pipeThrough 和 decompressionStream 这样的方法。然后,我可以从 Go 导入这个函数。Go 会允许我有一个 transform 函数,它接收一个只读通道,然后返回一个只读通道,我可以使用 Go 内置的语法进行迭代。
这里我展示了很多异步函数,但我们当然也希望能够在同步和异步代码中编写和实现这些接口。例如,我的 C# 代码可以像今天一样编写成普通的同步函数,调用它的异步 Python 代码仍然可以工作,只是 load 操作不会并发运行,而是顺序运行。或者反过来,C# 可以是异步的,但调用的 Python 可以是同步的,这样我仍然得到两个顺序的加载操作。
我也可以用传统的、同步的 WASI 读写调用来实现那个 transform 函数,这些调用已经与底层流集成。我可以从 Go 调用它,它会像预期的那样以块的形式流式传输。
因此,我们通过使用组件的隔离性来分离异步代码和同步代码,并让它们互操作,从而避免了由同名博客文章《你的函数是什么颜色?》所提出的“函数颜色问题”。
目标二:简化并发接口
在 WASI 0.3 中,我们还想简化并发接口。举一个具体的例子,在 WASI 0.2 中,HTTP 需要 13 个资源类型和 2 个处理程序接口。我没有时间详细解释为什么,但基本上有两个具有方向的流类型:输入流和输出流。这种方向性影响了所有想要使用它们的类型,因此我们必须有传入和传出的请求和响应,一直冒泡到处理程序接口:一个接收传入请求,一个接收传出请求。然后我们还需要一些模拟未来的资源。
但在 WASI 0.3 中,我可以只有 4 个资源类型,它们基本上是 HTTP 领域中显而易见的类型:字段、请求、响应和主体,以及一个处理程序函数,它接收一个请求并返回一个响应,正如你所期望的那样。
如果你仔细观察,魔法酱料就在资源 body 中。它的构造函数接收一个流,它的 consume 方法返回一个流。因此,可以只有一个资源 body,其他一切都基于它。
目标三:使并发组件可组合
最后,在 WASI 0.3 中,我们希望我们的并发组件更具可组合性。这里有两种有趣的组合方式。
第一种是更一阶风格的 Unix 管道式组合,其中值单向流动。例如,假设我有一个接口 tools,它有三个函数:load(返回一个流)、unzip(转换一个流)和 jq(消费一个流)。在 JavaScript 中,我可以导入这三个函数,它们就是三个 JavaScript 函数。load 的返回类型变成了可读流,而这正是 unzip 的参数类型。因此,我可以用一行 JavaScript 代码,使用普通的函数组合将它们链接在一起,这就能工作。在这一行 JavaScript 代码中,我已经能够连接三个异步函数调用,使结果相互流入,流单向流动。
这是一种一阶风格的组合。我们还可以表达一种稍微更复杂但表达能力更强的高阶风格组合,我称之为服务链式组合。
在这种组合中,假设我们有一个名为 ChainableHandler 的世界,它描述了一个组件的形状,该组件导入并导出一个 HTTP 处理程序。注意这是同一个处理程序接口,因此一个 ChainableHandler 的导出可以是另一个的导入,允许我将它们链接起来。然后,假设我有三个以这个世界为目标的组件,允许我将缓存逻辑、A/B 测试逻辑和压缩逻辑分离开来。最终,这实践了 Unix 哲学:让每个组件做好一件事。
现在假设我想组合这些组件,有多种方法可以做到这一点,我可以在主机中以多种方式完成。但我们一直在开发一种专门用于组件组合的语言,叫做 WAC,代表 WebAssembly 组合。在 WAC 中,我可以说我想通过组合这三个组件来构建一个复合处理程序:从一个 HTTP 缓存开始,用它作为 A/B 测试器的导入,再用那个作为压缩器的导入,然后使用压缩器的 HTTP 处理程序作为整个组合的入口点。
现在,在这四行代码中,我已经能够表达一个异步调用栈,其中请求以流式方式从压缩器流入 A/B 测试器和 HTTP 缓存,然后响应体流从 HTTP 缓存流出到 A/B 测试器再到压缩器。这是一种表达能力更强的组合形式。我们可以使用 WASI 0.3 来表达这种一阶和高阶组合。
WASI 0.3 的进展与未来工作 🚀
好消息是,自从我上次谈到这个话题以来,已经取得了很大进展。
在设计和规范方面,如果你想更技术性地了解其工作原理,我在 Wasm I/O 做了一个名为《组件模型中未来与同步的流式思考》的演示,其内容在技术上仍然基本准确。现在,在组件模型仓库中,你可以通过阅读 README 并点击“异步支持”来查看所有细节和行为规范。
在实现方面,Fermion 的 Joel Dice 英勇地领导了这项工作。首先,Joel 通过两个连续的原型来验证基本设计思想,这两个原型非常有价值:wasm-async 和 component-async-demo。基于这些进展顺利,Joel 现在正在开发一个完整的、适当的预览 3 实现,你可以在 Bytecode Alliance 的项目跟踪板上关注。
一旦这个初始支持在 WASM 工具链中落地,我们可以并行进行许多工作。首先是扩展我们的浏览器垫片,它允许这些组件今天就在浏览器和 Node.js 中运行。很酷的是,我们将使用一个即将在浏览器中推出的全新功能,称为 JavaScript Promise 集成,它让我们无需使用 Asyncify 这个“重锤”就能进行栈切换。关于这个的更多信息,请查看 Calvin 和 Mendy 明天的演讲《使用 WASM 组件在浏览器中做更多事情》。
然后,我们可以在所有已经进行预览 2 工作的不同生产者工具链上开展工作,将它们与这个 0.3 的东西集成。每种语言都有自己不同形式的并发集成,因此我们可以实际测试这个假设:我们可以集成多种语言,并让它们与 Rust、JavaScript、C#、Python 和 Go 组合。这应该能给我们相当强的信心,证明我们有一个相当好的设计。
无需等待:WASI 0.2 的当前应用 🛠️
但重要的是,你无需等待 WASI 0.3。许多人已经在实践中使用 WASI 0.2。例如,在今天的 WasmCon 上,我鼓励大家查看美国运通和 Adobe 人员的演讲,了解他们如何使用 WASI 0.2 和组件。
生产者工具链已经开始集成 WASI 0.2。C# 已经将 WASI 0.2 上游化,并且有一个组件化的 .NET 工具。关于这个的更多信息,请查看 James 的演讲《探索 C# WASM 组件》。Go 已经在 tinygo 中上游化了 WASI 0.2。关于这个,请查看 Randy 和 Joe 的演讲《WASI 到 Go:一次编写,随处运行》。Rust 已经在 cargo-component 工具中上游化了 WASI 0.2。JS 在基于新 Bytecode Alliance StarlingMonkey JS 运行时项目的 componentize-js 中集成了 WASI 0.2。Python 在基于 CPython 的 componentize-py 中集成了 WASI 0.2。
运行时也开始集成 WASI 0.2。当然,Wasmtime 支持 WASI 0.2,并且 Wasmtime 被嵌入到许多其他高级项目中,如 Envoy、NGINX Unit、SPIFFE、Wasm Cloud、Hyperview 等。WAMR 正在为 WASI HTTP 和 Q1 版本努力,并积极组件化 WASI NN。jco transpile 再次成为那个浏览器垫片,让你能够根据已经发布在浏览器和 Node.js 中相当长时间的 WASM 1.0 标准来运行组件。
相关生态:WASM OCI 与工具 🔗
还有一些令人兴奋的工作正在进行,即定义 WASM OCI 工件布局。它定义了组件或模块作为 OCI 工件的规范表示形式,目标是利用已经存在的大量现有 OCI 工具和云基础设施。
这项工作由 CNCF T Runtime 和 WebAssembly 工作组开发,并得到了 Bytecode Alliance 的积极协作和参与。特别是,有一个 WASM 包工具项目,它实现了 wac CLI。wac 让你能够直接将组件和 WIT 包发布和获取到 OCI 注册表。实际上,我们正在使用 wac 作为开发 WASI 本身的一部分。我们能够将 WIT 包的版本(例如 wasi-http-0.2.2)发布到 GHCR。这让生产者工具链能够动态拉取这些新接口并动态绑定生成它们,而无需更新整个工具链来引入这些新接口。这非常酷。关于这个,请查看 Taylor 和 James 的演讲《在 OCI 规范中容器化 WASM》。
展望 0.3 之后 🔮
以上就是 0.3 版本之前的所有内容。那么 0.3 之后呢?这部分比较模糊,所以请带着一点怀疑态度看待。但我认为重要的是要解决剩余的主要开放用例。
有一组用例围绕动态嵌套组件和资源:作用域回调、可选导入和共分配缓冲区。还有一些新的核心 WASM 特性自组件模型启动以来已经添加,我们应该采用它们以实现最佳集成。
WASM GC 已经在浏览器中发布,所以这个肯定要集成。Memory64 刚刚达到 Stage 4,这意味着它应该很快在浏览器中发布。栈切换正在得到浏览器的大量积极工作,所以这是下一个逻辑步骤。然后,共享一切线程真的很酷,虽然目前进展不是特别快,但无论何时准备好,我们都应该集成它。但最终,还需要进行更多的优先级排序和范围讨论,所以请参与这些讨论。
总结与行动号召 🎯
总而言之,WASI 0.2 已经可用、实用并被使用。WASI 0.3 即将到来,它将带来与源语言并发特性的自动集成、可组合的并发性以及更简单的并发接口。

如果这让你感到兴奋并想参与进来,请查看 WASI 0.3 项目板,在 Bytecode Alliance Zulip 聊天中与大家交流,参加任何 Bytecode Alliance 会议(你不需要是正式成员即可参加)。如果你想讨论设计或规范,请参与组件模型 WebAssembly 组件模型仓库的讨论。最后,来 Fastly 展位向我的同事 Leslie 和我打个招呼,一起讨论构建这些 WASM 工具的未来。
005:闭幕致辞与明日安排
在本节中,我们将回顾 WasmCon 2024 第一天的活动,并了解接下来的日程安排、参会建议以及如何提供反馈。
感谢 Luke,感谢所有上台演讲的嘉宾。感谢今天上午主持研讨会的各位。也感谢在座各位的参与。
接下来我们有一个休息时间,直到下午3点。之后我们将有三个并行分会场。
🗺️ 分会场与展厅信息
三个分会场就在展厅区域周围的房间里。我们鼓励大家休息后继续参加这些会议。
同时,也请大家花时间在展厅交流。请务必去各个展位与工作人员交流。Kate、Luke 以及今天在此发言的各位嘉宾都会在场,他们非常乐于为大家提供帮助,并且平易近人。
⏰ 明日活动提醒
明天上午9点,我们将继续活动。请确保定好闹钟,提前喝咖啡、吃早餐,然后再来参加。请注意,我们现在只是第一天的开始。
💡 反馈与建议征集

我想提前告知大家,活动结束后,大概是下周一,各位将会收到一封邮件,邀请您填写对本次活动的感受和建议。

请务必在收到邮件后填写。但是,如果您现在就有一些关于未来活动的想法,比如希望我们增加的内容,或者本次活动中您喜欢的部分,您也完全可以来找我,当面告诉我您的想法。我会很乐意将这些意见转达,因为我们非常渴望让这次活动、这个社区和生态系统,对所有人来说都尽可能精彩。
🤝 积极参与互动
不要害羞,主动与人交流。外面每个展位的工作人员都是为了帮助您而存在的。如果您有任何想法或建议,可以来找我。
现在,请大家去享受休息时间,吃点东西,喝杯咖啡或其他饮料。下午3点,分会场会议再见。我们明天上午9点再会。
本节总结
在本节中,我们一起了解了 WasmCon 2024 第一天下午的休息安排、分会场位置、明日活动时间,并学习了如何通过邮件或当面沟通为活动提供反馈。核心是鼓励大家充分利用休息时间进行交流,并积极参与后续环节。
006:使用新的WASM后端在CPU、GPU和NPU上部署机器学习

在本节课中,我们将学习如何使用WebAssembly系统接口进行神经网络推理,并重点介绍新集成的Windows Machine Learning后端。我们将了解如何利用此后端,将机器学习模型部署到CPU、GPU乃至NPU等不同设备上。
概述:WASI-NN与WML后端
WASI-NN是为WebAssembly应用程序定义的一套机器学习推理API。通过这些API,Wasm应用只需调用几个简单的接口即可加载模型、输入数据,随后Wasm运行时会选择一个后端执行推理并返回结果。
最初,OpenVINO是首个实现的后端。过去一年中,我们增加了更多后端,包括ONNX Runtime、TensorFlow Lite以及新加入的Windows Machine Learning后端。WASI-NN也被其他运行时(如WasmEdge)支持,它们甚至提供了更多后端选项。
WASI-NN推理工作流程
以下是一个简化的WASI-NN推理工作流程。根据实际用例,流程可能更为复杂,例如在设置输入张量前可能需要进行数据预处理。
- 加载模型:首先,你需要加载一个模型。根据模型格式,Wasm运行时会为你选择一个后端。
- 设置输入:使用
set_inputAPI 输入数据。 - 执行计算:设置完所有输入张量后,调用
compute开始推理计算。这个过程可能需要几秒甚至几分钟。 - 获取输出:计算完成后,通过
get_outputAPI 获取推理结果。
请注意,WASI-NN目前处于第二阶段,因此API未来可能会有变动。同时,WASI-NN API同时提供了Web IDL和头文件版本。
认识Windows Machine Learning
WML是Windows 10(1809版)及更高版本的内置组件,这意味着你无需安装任何第三方依赖即可使用它。启用了桌面体验的Windows Server也支持WML。如果你的应用需要支持旧版Windows或未启用桌面体验的Windows Server,则需要安装独立的WML软件包。
WML仅支持ONNX模型。因此,如果你想使用WML后端,你的模型必须是ONNX格式。WML的一个执行提供程序是DirectML,因此它也支持与DirectML兼容的设备,如GPU和NPU。目前NPU支持有限,但我们正在努力改进。
实现WML后端
上一节我们介绍了WML的基本概念,本节中我们来看看WML后端是如何实现的。如果你想为WASI-NN添加新的后端,这个过程相当简单直接。
首先,理解线性推理的基本工作流程,然后将这些步骤映射到WASI-NN API。
- 选择推理设备:对于CPU很简单。但对于WASI-NN中定义的GPU和NPU,没有与WML API一对一的映射。因此,在WML后端中,我们模拟所有DirectX设备,并检查它是否支持图形或仅为计算设备。如果支持图形,我们将其视为GPU;如果是纯计算设备,则视为NPU或GPU。
- 映射后续步骤:其余步骤可以轻松映射,我们只需在WASI-NN的WML后端实现中调用相应的WML API。
由于Wasmtime是用Rust编写的,我们还引入了windows-rs crate来用Rust调用Windows API。
选择目标设备的注意事项
虽然WASI-NN提供了简单的API让你选择目标设备,但在实践中,仅通过更改一个值来切换设备可能并不那么容易。
原因是不同设备可能支持不同的数据类型和不同的算子集合。在新设备上运行模型之前,请检查你的硬件支持的类型。例如,如果你有一个F32模型,但目标设备仅支持F16,那么你可能需要转换模型才能在该设备上运行。
工具如ONNX Runtime可以帮助你转换模型,并针对特定类型的设备进行优化。
目前,对NPU的支持预测尚未合并,因为GitHub CI不支持NPU,很难获得测试覆盖。如果你想尝试,请查看Pull Request #8756。
图像分类示例
这是一个相当简单的用例,虽然不需要太多步骤,但仍需要一些预处理和后处理。

让我们看一下这个例子。
- 加载模型:首先,我们从文件加载模型。模型格式为ONNX。
- 创建执行上下文:然后,我们创建一个执行上下文。
- 读取并处理图像:我们从文件读取图像并将其转换为张量数据。图像来自ImageNet,是一只猫。我们预处理数据,根据模型的要求(即模型训练的方式)对其进行归一化。
- 设置输入并推理:使用
set_input设置输入张量,并用compute运行推理。 - 获取并处理输出:使用
get_output获取结果。结果是类别ID及其概率。ID不便于人类理解,因此我们需要一个包含ID和标签的映射文件,以了解这些ID的含义。后处理数据后,我们生成前五个最可能的结果。
这是一个示例结果,排名第一的是“虎斑猫”,概率超过96%。
然后,我们将目标设备从GPU更改为CPU,重新编译示例并再次运行。由于更改了目标设备,概率可能与之前略有不同,但排名第一的结果仍然是“虎斑猫”。
当前限制与未来展望
目前仍有许多可以改进的地方。Rust的F16支持仍处于实验阶段。对于内部缓冲区,P16张量使用F32类型,这效率不高,但一旦F16支持稳定,这个问题就可以解决。

另一个限制是支持的数据类型和算子内核有限。开发者仍需首先了解其目标设备,以便对模型进行优化,从而获得更好的性能和更低的功耗。
如果你有任何问题,欢迎随时联系我们或提交新的Issue。
总结

本节课中,我们一起学习了WASI-NN的基本概念和工作流程,深入了解了新的Windows Machine Learning后端及其实现原理。我们还探讨了在不同设备(CPU、GPU、NPU)上部署模型时的注意事项,并通过一个图像分类示例演示了完整的推理过程。最后,我们了解了当前实现的限制和未来的改进方向。
007:WasmScore 介绍 - 一款面向 Wasm 效率的新基准测试工具

在本节课中,我们将要学习 WasmScore,这是一款旨在评估 WebAssembly 效率和性能的新型基准测试工具。我们将了解其设计动机、核心特性、使用方法以及它如何帮助不同背景的用户理解和比较 WebAssembly 的性能表现。
动机与需求
上一节我们介绍了课程主题,本节中我们来看看开发 WasmScore 的动机。并非所有对 WebAssembly 性能感兴趣的人都熟悉其技术细节,他们可能来自市场部或其他非开发部门。这些人通常会有一些疑问。
以下是他们常见的问题:
- 应该使用哪个 WebAssembly 基准测试工具?
- 如何追踪性能表现?
- 平台 X 与平台 Y 上的 WebAssembly 性能对比如何?
- 某个基准测试运行了 N 秒,这个结果是好是坏?应该与什么进行对比?
- “WebAssembly 具有接近原生性能”具体意味着什么?“接近”的程度是多少?
- 如果通常编译为原生代码的工作负载改为编译为 WebAssembly,性能是否会有明显下降?
目前,通过 GitHub 或网络搜索可以找到一些 WebAssembly 基准测试工作。这些工作可能是为了撰写文章提供数据支持,或是为了开发运行时功能而创建的基准测试驱动程序和用例。但大多数此类工作都是针对特定仓库的一次性努力,未经同行严格审查,且目标并非通用。它们通常由工程师为特定文章、功能或运行时创建,因此非常具体,不具备运行时无关性、平台无关性或工作负载无关性。
相比之下,成熟的编程目标或技术通常拥有用于标准化性能报告和比较的既定基准,例如 Java 或 C 语言的 SPEC 基准测试。
当前我们拥有的是分散的努力,而我们希望获得更多。我们希望工具易于访问且对新手友好,安装和运行简单,并能提供明确、易于解释的结果。我们希望它能提供一个衡量性能的基线,这意味着运行一次基准测试后,我们就能立即了解平台在 WebAssembly 上的表现。我们希望它是可移植的,能在多个平台上运行并产生可比较的分数。最后,我们需要它是可靠且可重复的,适用于各种工作负载和用例,并能与广大受众相关,从而确立其自身地位。
设计与核心特性
上一节我们介绍了动机和需求,本节中我们来看看 WasmScore 的设计方案。我们从一个基准测试驱动程序开始。虽然存在许多一次性基准测试,但有一个相对可靠的工具是 Sightglass。Sightglass 是 Bytecode Alliance 项目仓库下的一个基准测试套件/工具。它设计精良,最初用于帮助开发 Lucet 运行时,后来转而用于帮助开发 Wasmtime。它正在进行一些重新设计,包括支持为不同运行时添加插件的潜力。它包含许多用于控制运行稳定性的选项,并包含多种可能满足不同用例的基准测试。
这引出一个问题:为什么不直接扩展 Sightglass?这个方案曾被考虑,但最终确定最终需求和用例差异足够大,因此决定创建 WasmScore。
以下是 WasmScore 的设计和一些使其与众不同的特性:
- 使用容器:WasmScore 使用 Docker 容器。这意味着启动速度极快,且具有跨平台性。这便于包含在数据收集或后处理过程中使用的工具。
- 使用分数:这并非全新概念,但在需要汇总多个分数并总结结果时非常有用。
- 利用 Sightglass:WasmScore 直接使用 Sightglass 作为子模块,而不是对其进行分叉。在开发 WasmScore 时,我们向 Sightglass 添加了一个原生引擎。我们同时迭代改进 WasmScore 和 Sightglass。此外,Sightglass 的某些方面(如工作负载仓库和套件定义)并未包含在 WasmScore 中,而是保持独立。
简而言之,设计思路是将 Sightglass 和其他工具放入 Docker 容器中,配置容器以便将结果转储到主机或访问主机进行性能分析等操作。容器内还有一个 Python 脚本,用于驱动运行 Wasm 基准测试的整个过程。独特之处在于,它还会将相同的基准测试编译为原生代码,从而能够进行性能比较。

快速上手与演示
到目前为止,我们花了很多时间讨论动机和设计方面。这里我们将展示如何下载和运行的快速示例。
下载过程就像执行一条 Docker pull 命令来获取镜像一样简单。
docker pull wasmscore/wasmscore
下载完成后,可以直接运行。
docker run wasmscore/wasmscore
运行后,您将看到各种基准测试正在运行以计算分数。请注意,您看到的“原生”基准测试是使用基准测试的高级源代码实时编译的。这与用于编译基准测试预编译 WebAssembly 文件的高级源代码相同。我们只实时编译原生代码,而不编译 Wasm。目前有有限数量的类别和基准测试被汇总以创建分数,但还有更多基准测试可供运行。
您可以看到人工智能推理、矩阵运算、阿克曼函数、斐波那契数列等类别的分数。我们对这些基准测试进行分类,并报告每个类别的时间和效率。这里使用的平均值用于计算整体的 Wasm 效率 和 执行分数,分数越高越好。效率分数只是 Wasm 性能与原生性能的比率,其中单个基准测试性能基于时间的倒数。简而言之,0.63 的效率分数表示 Wasm 的执行效率相当于原生执行代码的 63%。
另外请注意,由于我们使用容器,我们可以将任何需要的工具放入其中。例如,我们有一个调试版本的 WasmScore 容器,其中包含了类似 VTune 的性能分析工具。通过帮助菜单可以了解如何使用它来针对特定基准测试进行分析。性能分析会同时在 WebAssembly 和原生代码上运行。这样,我们不仅可以比较分数,还可以快速评估两者之间的性能问题并进行比较。生成的配置文件会映射到主机上。此功能的补丁目前在一个私有分支中,但我们计划很快将其合并。
结果分析与未来展望

因为我们同时拥有 Wasm 性能和原生性能数据,所以可以快速查看 Wasm 在哪些类别中表现良好,在哪些类别中表现不佳。请记住,这个比率在不同平台上具有可比性。
通过绘制一些数据,我们可以看到一些差距,也可以看到一些异常值。例如,这里突出显示的阿克曼函数和 ED25519 就是异常值。使用我们可以生成的性能分析文件,我们可以看到阿克曼函数的大量时间花费在 lowering 阶段,而 ED25519 的差距是由于对支持 64 位乘法的函数调用造成的,该操作目前尚未以原生方式完成。
简而言之,过去一年并没有进行大量工作,但这是一个正在缓慢迭代的项目。我们希望未来能够添加额外的分数,例如 WAI 或 CD。当我们谈论分数时,我们指的是某种比较,我认为这是 WasmScore 的基础——拥有那个基线比较。对于 CD,可能是标量比较;对于 WAI,可能是 Wasm 之间或与原生代码的比较。我们还希望增加对额外运行时的支持。可以想象一个运行时套件,我们甚至可以有一个“集成”模式,汇总所有运行时的分数,以获得 WebAssembly 的通用基线性能。当然,还需要更多的基准测试,随着 WebAssembly 用例的出现,这些基准测试也在不断变化。性能分析支持即将合并,但我们希望在汇总结果方面做得更好,并且还有其他功能想要添加。最后,需要增加参与度,需要不止几个工程师来共同探讨什么才是好的基准测试和好主意,这是我们一直希望改进的方面。
总结
本节课中我们一起学习了 WasmScore 基准测试工具。我们想以 WasmScore 的访问地址和创建该基准测试所遵循的原则作为结束。我们的目标是易于使用,因此使用 Docker 实现简单部署。我们以易于理解的方式汇总数据,并提供性能分析等工具。这些工具不要求用户是专家,但提供了足够的分析能力。目标是提供开箱即用的基线,我们通过包含原生比较来实现这一点。由于我们实时编译,因此该工具具有跨平台的可移植性,可以为特定平台获取原生分数。我们需要可靠、可重复且相关的结果,为此我们利用了久经考验的 Sightglass。我们没有使用其分叉,而是直接利用它,任何与 Sightglass 相关的支持需求,我们都优先添加到那个项目中,然后再整合到 WasmScore 里。
项目地址:https://github.com/bytecodealliance/wasmscore(注:根据演讲内容推断,实际地址请以官方发布为准)

核心原则:
- 易于使用:通过 Docker 实现简单部署。
- 易于理解:汇总数据,提供分析工具。
- 提供基线:通过包含原生编译实现开箱即用的性能比较。
- 可移植:跨平台运行,实时编译获取平台特定原生分数。
- 可靠相关:直接利用成熟的 Sightglass 项目,确保结果质量。
009:使用 WASI WebGPU 构建精美图形与安全运行 AI


在本节课中,我们将学习如何通过 WASI GFX(原名 WASI WebGPU)为 WebAssembly 应用带来 GPU 能力。我们将探讨其架构、核心组件,并通过实际示例展示如何利用它构建图形应用和运行 AI 推理。
当前 WebAssembly 如何与 GPU 协作?
简短的回答是:目前不能。这是 WebAssembly 设计上的有意限制。
然而,在浏览器等富 Web 应用环境中,存在变通方法。通常,这需要通过 JavaScript 绑定来实现。例如,像 Emscripten 这样的 C++ 框架,允许你将 C++ 代码编译为 WebAssembly,并通过 JS 宿主在浏览器中运行。对于 WebGL,它们将 API 映射到 GLES,并通过浏览器的 JavaScript 绑定与浏览器原生的 GPU API 通信。


同样,对于 WebGPU,在 Rust 社区中,它通过 wgpu 库暴露给 WebGPU。你可以在浏览器中运行 wgpu,它有一个后端,允许通过 JavaScript 绑定接入浏览器的底层 API。
这套系统在浏览器中运行良好。但对于其他用例,例如无头应用、桌面端或服务器端,目前并没有标准化的方法。你可以手动从宿主运行时暴露函数,但这非常繁琐且难以维护。你也可以导出整个图形 API,但这会带来安全问题,因为无法保证第三方代码不会访问 GPU 上的其他资源。
为了解决安全问题,你可以通过复制数据进出 WebAssembly 线性内存来处理所有 GPU 交互,但这会带来严重的性能问题。因此,我们需要一种更好的方法。
为什么需要 WASI GFX?
WebAssembly 最有用的地方是作为一个编译目标,而非一个独立的平台。因此,你可以想象各种应用场景,它们需要与窗口系统交互或在画布上合成视觉层。
以下是几个关键用例:
- 用户界面:在原生应用中,需要将数据渲染到屏幕上的场景。
- 无头图形渲染:例如,在服务器端进程中通过 OpenGL 渲染帧、图像或流式视频。
- 插件系统:将应用内部对象模型的一部分暴露给第三方代码,允许其操作 GPU 上的特定资源,为不同类型的计算开辟新用例。
- 人工智能:这是当前 GPU 最流行的用例。WASI-NN 非常适合在其支持的后端上进行 ML 推理。但如果你需要使用不受支持的后端、进行模型训练或需要底层 GPU 访问,WASI GFX 就能派上用场。
- 通用 GPU 计算:用于科学计算,如 CFD 模拟等,需要访问通用并行处理器。
我们希望听到社区的反馈,了解更多的用例,以构建一个通用的框架。



WASI GFX 的结构
WASI GFX 包含以下几个部分:
- WASI WebGPU:从 WebGPU 的 Web IDL 规范生成,语义上与 WebGPU 基本相同。
- WASI Surface:允许你将内容绘制到屏幕并获取基本的用户事件,可以看作一个非常基础的窗口系统。
- WASI Graphics Context:用于连接 WASI WebGPU 和 WASI Surface。
- WASI Frame Buffer:一个简单的、基于 CPU 的帧缓冲 API。


那么,如何连接这些包呢?


在 Web 上,有 WebGL 和 WebGPU 等图形 API,它们都绘制到 HTML Canvas 元素上,没有其他选择。
但在 WASI 中,我们希望更灵活。我们从 WASI Surface 开始,但也希望支持未来的窗口 API。同时,我们也有多个图形 API。为了确保任何窗口系统都能与任何图形 API 协同工作,我们引入了 WASI Graphics Context。



它的作用很简单:一端接收窗口系统,另一端接收图形 API,由宿主负责确保两者良好协作。
实际运行示例
wgpu 是 Rust 生态中一个重要的图形库,其设计与 WebGPU 非常相似,并且可以运行在 WebGPU 之上。那么,我们能否让 wgpu 也运行在 WASI GFX 之上呢?
答案是肯定的。我们已经成功实现了这一点,并且运行非常流畅。


基础设施架构
让我们看看支撑这一切的实际基础设施架构。
- WIT 生成:我们使用一个名为
webidl-to-wit的特殊工具,将 WebGPU 的 Web IDL 转换为 WIT(组件模型接口语言),并进行一些转换以移除对 JavaScript 或 Web 的假设。 - Guest 端:现在,任何语言都可以使用生成的 WASI WebGPU WIT。对于像 Rust 这样已有标准化 WebGPU 处理方式的语言,我们正在为其开发后端,以便现有代码可以直接编译为 WASI。
- Host 端:你需要一个支持 WASI GFX 的运行时。我们有一个实验性的运行时叫
graphtime,它嵌入了 Wasmtime 和 WASI GFX 运行时扩展。WASI GFX 运行时本身建立在与 Firefox 用于其 WebGPU 实现相同的基础之上,非常可靠。
运行复杂示例:Bevy 游戏引擎

Bevy 是 Rust 中一个非常流行的游戏引擎,它使用 wgpu 进行绘制,包含更多复杂的部件。我们挑战将完整的 Bevy 游戏编译到 WASI GFX 中,并在上周成功实现了。

演示显示,游戏可以正常运行和交互。


人工智能用例
如果你关注 WASI 生态系统,可能听说过 WASI-NN。对于在其支持的后端上进行 ML 推理,WASI-NN 非常出色且易于使用。
但是,如果你需要使用 WASI-NN 尚未支持的后端、进行模型训练,或者需要底层 GPU 访问,WASI GFX 就是你的朋友。事实上,你可以在 WASI GFX 之上实现 WASI-NN。
我们演示了一个使用 WASI GFX 进行图像分类推理的示例。

插件与组件化架构
对于插件或基于组件的架构,我们可能希望让 Guest 代码能够写入现有应用程序。第三方代码通常需要 GPU 来进行渲染。
通过 Renderlet,我们正在构建 GPU 可移植层,允许你为设计工具和游戏等构建插件。在插件模型中,你需要通过 WIT 定义宿主与访客之间的接口,从而在插件和宿主之间交换数据。
这些插件通过 WASI WebGPU 暴露和运行,可以在 GPU 上创建完全沙箱化、由插件拥有的资源。作为宿主应用程序的作者,你需要思考什么样的接口对插件有意义。例如,在游戏或编辑器中,你可能希望让插件能够读取场景图或文档模型的部分内容,这些可以通过纹理或缓冲区暴露。
核心价值在于能够通过 GPU 进行这些操作。插件可以写入特定的、定义了限制的缓冲区,宿主应用程序可以渲染这些数据,或者插件本身也可以渲染到渲染目标上。WASI WebGPU 为你提供了灵活性,来决定宿主与访客之间这种细粒度的交互方式。
我们通过 Renderlet 内部构建的内容演示了一个“程序化建模”的例子,展示了如何通过编写代码(编译为 Wasm 模块)来生成图形数据,并作为插件加载到宿主应用程序(如地图查看器)中,实现参数化渲染和双向通信。
未来发展
WASI GFX 本身在规范方面还有下一步计划:
- 最终确定规范:规范已趋于稳定,但我们仍需要社区反馈。
- C 语言绑定:WebGPU 工作组已标准化了一个
webgpu.h头文件,我们正在为其提供绑定,以便任何针对 WebGPU API 的 C/C++ 应用都能开箱即用地在 WASI GFX 主机上运行。 - 完善后端支持:我们希望为
wgpu和 Bevy 引擎提供完整的上游支持。 - 完成运行时实现:目标是实现与 WebGPU 规范的 100% 兼容。
- 异步支持:随着组件模型对异步的支持,我们将为 WASI GFX 和 WASI WebGPU 带来完整的异步 API 支持。
- 新用例支持:如无障碍功能、VR 支持、不同类型的输入方法以及轻量级/完整的窗口 UI 系统。我们希望听到社区关于规范应支持哪些功能的意见。
我们很高兴地宣布,截至上周,WASI GFX 已成为第二阶段提案。我们还有很长的路要走,非常欢迎社区的帮助、用例分享和贡献。
总结

本节课中,我们一起学习了 WASI GFX 如何为 WebAssembly 带来安全、高效的 GPU 访问能力。我们了解了其架构、核心组件,并通过图形渲染、AI 推理和插件系统等实际示例,看到了它的强大潜力。WASI GFX 旨在成为一个通用的框架,为各种 WebAssembly 应用场景开启 GPU 计算的大门。
010:强强联手——用WASM和Dapr提升你的IDP!🚀


在本教程中,我们将探讨如何将WebAssembly和Dapr集成到内部开发者平台中,以提升开发者体验和平台能力。我们将从平台工程的基础概念讲起,逐步深入到Wasm、Dapr、Keda等具体技术,并最终展示如何构建一个支持Wasm的“黄金路径”。
概述:平台工程的挑战与机遇
在大型企业中,启动一个新项目可能非常复杂。这取决于公司的规模,可能简单到只需打个电话,也可能困难到不知从何入手。问题可能来自源代码管理,也可能来自基础设施配置。即使找到了方法,也可能需要经历漫长的等待,例如通过ServiceNow提交工单。本节将介绍平台工程如何解决这些问题。
平台工程简介 🏗️
上一节我们提到了项目启动的复杂性,本节中我们来看看平台工程如何应对。
平台工程通过提供自助服务能力和自动化基础设施操作,来改善开发者体验和生产力。其核心是为内部客户(即开发者)提供最佳的开发体验。
CNCF(云原生计算基金会)也在推进相关工作,并发布了平台工程白皮书。他们将平台分为不同层次:平台接口、工具(如Backstage)、平台能力(如基础设施即代码)以及各种底层组件(如外部密钥管理、身份管理等)。
以下是平台工程团队的主要职责:
- 设计和实施基础设施。
- 自动化部署流程。
- 提供故障排除支持。
- 保持技术栈的更新。


然而,最重要的是为开发者客户提供最佳的开发体验。平台的成功与否取决于团队是否将开发者视为客户,并建立直接的反馈渠道。
为什么选择WebAssembly?⚡
我们了解了平台工程,现在进入下一个关键部分:WebAssembly。
Wasm以其快速启动时间(毫秒级)、接近原生的性能、轻量级体积和“一次构建,随处运行”的承诺而闻名。其默认的沙箱安全模型也是一大优势。对于开发者平台而言,Wasm能够提供高效、安全且跨架构的运行时环境。
集成SpinKube到平台 🧩
我们知道Wasm很酷,也看到了它的好处。开发者团队可能已经要求将Wasm作为服务提供。现在,我们可以通过SpinKube项目来实现。
SpinKube是一个CNCF项目,用于在Kubernetes上交付Spin Wasm应用程序。它包含以下组件:
- Spin Operator:负责部署Spin应用。
- Containerd Shim:用于在Containerd节点上运行Wasm工作负载。
- RuntimeClass Manager:用于安装Wasm运行时。
- SpinKube Plugin(可选):用于创建Kubernetes部署清单。
其工作流程如下:
- 开发者使用
spin new、spin build等命令创建和构建应用。 - 将构建的OCI镜像推送到内部或公共镜像仓库。
- 通过
kubectl apply应用SpinKube生成的YAML清单。 - Spin Operator识别自定义资源并调度应用到Kubernetes集群。
引入Dapr和Keda:构建“三巨头” 🤝
我们已经有了SpinKube。为了进一步提升开发者生产力,让他们更少关注周边细节,我们可以引入Dapr和Keda。


Dapr是一个分布式应用运行时,它通过侧车模式抽象基础设施细节。开发者只需知道存在某种类型的组件(如发布/订阅),而无需关心其具体实现是Kafka还是RabbitMQ。Dapr基于Actor模型,并提供了状态管理、发布/订阅、安全、可观测性等构建块。
Keda是Kubernetes基于事件驱动的自动伸缩器。它可以监听特定事件源(如RabbitMQ队列或Kafka主题),并根据消息数量等因素自动伸缩部署副本数,甚至缩容到零。结合Wasm的快速启动特性,可以构建出能够根据负载动态、水平伸缩的系统。
将SpinKube、Dapr和Keda结合,我们获得了基础设施抽象、事件驱动伸缩和高效Wasm工作负载交付的强大组合。
定义“黄金路径” 🛣️

我们拥有了所有技术组件。现在,作为平台工程团队,我们如何将其交付给开发者?答案是通过“黄金路径”或“铺平道路”。
“黄金路径”是平台工程团队提供的一种预架构且受支持的方式来构建和部署软件。其核心是与开发者客户合作,收集他们的需求,同时整合安全团队等其他部门的输入,创建出一条标准化的、受支持的部署路径。团队承诺对此路径提供支持,包括升级和维护。
但这并不意味着限制自由。开发者可以选择“偏离”黄金路径进行创新尝试,但需要明白他们将无法获得同等级别的支持。关键在于与利益相关者沟通,将平台作为产品来对待,共同制定最佳路径。
一个“黄金路径”至少应包含以下元素:
- 代码仓库模板:例如GitHub模板仓库,包含预配置的工作流和脚手架。
- 部署清单:针对特定运行时(如Kubernetes)的部署配置。
- 内置可观测性:集成监控和日志。
- 内置安全:集成漏洞扫描和使用安全基础镜像。
整体架构与演示 🎬


最终,我们构建的内部开发者平台包含平台工程团队、消费服务的开发者,以及由安全、DevOps、基础设施、自动化等层次构成的平台本身。平台集成了GitLab、Jenkins、Kubernetes等各种产品,并保持足够的灵活性以集成像Wasm这样的新技术。

在演示部分,我们展示了一个具体实现:
- 平台工程团队使用Pulumi基础设施即代码创建集群,并部署Argo CD和Backstage。
- Argo CD通过GitOps方式,从Git仓库自动部署所有必要的平台组件(外部密钥操作器、证书管理器、PostgreSQL、Dapr等)。
- 开发者通过Backstage访问“黄金路径”模板(一个集成了Dapr和SpinKube的微服务模板)。
- 开发者填写表单(如应用名称、是否部署Kafka等),Backstage会自动在Git仓库中生成对应的、完全配置好的代码库。
- 应用被部署到Kubernetes集群。演示应用是一个Wasm后端,通过Dapr连接到Kafka,并由Keda根据队列深度进行自动伸缩(从0开始扩容)。
总结

在本教程中,我们一起学习了如何通过结合平台工程、WebAssembly、Dapr和Keda来增强内部开发者平台。我们探讨了平台工程的核心是提供卓越的开发者体验,介绍了Wasm的性能与安全优势,展示了如何使用SpinKube在K8s上交付Wasm应用,并解释了Dapr和Keda如何共同提供抽象和弹性。最后,我们定义了“黄金路径”作为标准化、受支持的交付方式,并概述了一个集成了Argo CD和Backstage的完整演示架构。通过这种方式,平台团队能够以合规、受支持的形式,为开发者提供强大的Wasm服务能力。
012:在超分布式云上释放开源 WebAssembly 的力量



在本节课中,我们将探讨如何利用开源 WebAssembly 技术,在跨越边缘和核心数据中心的超分布式云架构上构建和部署应用。我们将通过一个具体的案例——Adobe 内容真实性倡议服务,来演示其实际应用与优势。
为什么企业尚未广泛采用边缘计算?
我叫 Colin Murphy,在 Adobe 工作。这位是 Doug,他在 Akamai 工作。
大家好。

首先,我想提出一个可能有些冒犯的问题:为什么像 Adobe 这样的公司没有大规模采用边缘计算?原因并非我们缺乏尝试。我曾努力推动我们使用几乎每一家云提供商的边缘服务。但现实是,存在一些障碍。
以下是我从公司内部得到的反馈:
- 技术栈差异大:许多工程师熟悉 Java、Spring 或 Go。边缘计算在编程模型和部署方式上与传统服务器端开发不同,需要学习新的框架和技能。
- 部署与协调复杂:为边缘计算组建专门的团队或培养专家后,还需要协调边缘部署与现有数据中心的部署流程,增加了运维复杂度。
- 能力限制:当前的边缘计算平台在功能上可能不如数据中心丰富。这也正是 WasmCon 等会议存在的原因——我们正在努力增强这些能力。
上一节我们探讨了企业采用边缘计算的障碍,本节我们来看看理想中的边缘计算平台应该具备哪些特性。
理想边缘计算平台的特性
那么,我们期望的边缘计算平台是什么样的?
- 多租户:平台应能服务公司内的多个团队,而不是为每个团队搭建独立的基础设施。理想情况下,它应该是一个服务。
- 全局性:部署和更新应能一键覆盖全球,而不是逐个集群管理。
- 开发体验一致:边缘部署的体验不应与数据中心部署有太大差异。
- 持续更新:平台应能快速集成 WebAssembly 生态系统的最新标准和技术,例如 WASI。
- 具备 WebAssembly 优势:继承 WebAssembly 的高效性和安全性。
接下来,我们将介绍一个符合这些理念的开源项目。
引入 WasmCloud 与内容真实性倡议
这里需要声明一下,我是 WasmCloud 项目的维护者。我参与其中是因为我喜欢它的理念。WasmCloud 能保持最新,例如 WASI-P2 标准发布后,它会很快集成。
现在,介绍一下我的工作背景。我隶属于“内容真实性倡议”。这是一个旨在制定行业标准,为数字内容(如图片、视频)添加来源和编辑历史等“溯源”元数据的联盟组织,我们称之为“内容凭证”,以帮助遏制虚假信息,增加在线信任和透明度。
联盟成员包括相机厂商、编辑软件公司(如 Adobe)、社交媒体平台(如 Meta)以及媒体公司(如《纽约时报》、《华尔街日报》)等。
我们还有另一个组织 C2PA,它提供开源库和工具。Adobe 也围绕这些工具构建服务。
核心思想是:当有人拍摄、编辑并发布一张图片后,用户下载该图片时,可以轻松查看其完整的处理链。这些信息被记录在一个经过签名的“清单”中,用于验证图片的来源和编辑历史。它并不能判断一张随机图片的真伪,但能证明“已知为真”的内容的可靠性。

C2PA 的一个优势在于其开源库 C2PA-RS 是一个 Rust 库。Rust 非常适合编译为 WebAssembly。这带来了多种用例:Web 端(已实现)、服务器端(未来可通过 WASI 运行)以及嵌入式设备(如相机)。此外,还有 Python、JavaScript、Node.js、C 和 Rust 的多种 SDK。
一个理想的未来是,我们可以将核心功能打包成 WebAssembly 组件,从而减少维护多个 SDK 的负担。开源也让我们能够与行业先锋合作,共享代码,共同解决问题。

然而,目前存在一个服务缺口。用户使用合规的相机和软件,在合规的社交平台发布图片后,当其他用户通过内容分发网络下载时,图片的元数据通常会在 CDN 的图片处理(如调整大小)过程中被剥离。用户无法获得这些溯源信息。
本节课的核心演示将展示如何解决这个问题。
演示概述:分布式图像处理与签名服务
我们的演示将构建一个服务来解决上述问题。用户请求下载一张图片,并指定所需宽度。服务将在边缘(CDN)调整图片大小,并将这次调整操作记录到 C2PA 清单中,然后对清单进行签名。最终,用户下载到的图片将包含完整的、经过签名的溯源信息。
这个用例的独特之处在于,它无法完全在数据中心或完全在边缘运行。
- 无法完全在边缘:Adobe 的签名密钥非常敏感,不能存放在边缘节点。我们使用 AWS KMS 或云 HSM 来管理密钥。
- 无法完全在数据中心:如果所有图片流量都回源到数据中心,网络成本(尤其是视频等大流量场景)将非常高昂。
因此,我们需要一个架构:在边缘接收请求,与数据中心的密钥服务交互,再将结果返回到边缘。用现有的边缘计算技术实现这一点非常困难。
一年半前,我在阿姆斯特丹演示过一个雏形,但当时需要修改大量代码,且功能不完整。今天的演示则不同:我们将展示一个完全工作的系统,它编译为 WASI,并实际运行在跨越 Akamai 边缘和 AWS 数据中心的 WasmCloud 集群上。

整个应用通过一份统一的 WasmCloud 部署清单(YAML 文件)来定义。这份清单描述了应用的所有组件(2个组件,4个提供者)。我们可以声明式地指定哪些部分运行在 Akamai,哪些部分运行在 AWS。应用一次部署,即可全局更新。
接下来,有请 Doug 详细介绍我们对于超分布式云的愿景。
超分布式云架构愿景
谢谢 Colin。我想谈谈我们对云部署演进的愿景,以支持 Colin 刚才描述的这类解决方案。
首先,是传统的核心云模型。它提供有限的几个区域,但给人一种拥有虚拟无限资源的错觉。
其次,是分布式云。目标是将应用部署到成百上千个更靠近用户的位置,同时为开发者提供与核心云相似的体验。WebAssembly 因其轻量级和安全性,是此类解决方案的绝佳技术。

最后,是传统的边缘计算。通常指在 CDN 最外层运行代码,处理请求/响应事务。它非常普及,但有时受限于无状态和请求周期的约束。
我们的愿景是构建一个超分布式云,融合以上所有层次的优势,创造独特的使用场景。
让我用一个更具体的例子来说明:一个基于天气预报的产品推荐功能。


架构从左到右分为三层:
- 边缘层:在数千个位置接收请求,进行基础处理(如通过本地地理数据库进行 IP 地理定位)。
- 分布式层:通过全局路由器,将用户请求智能地路由到运行在数百个位置的 WasmCloud 服务。路由策略考虑健康状态和地理邻近性,以实现低延迟和节省数据传输成本。此服务作为协调器,调用外部天气 API 获取用户当地的预报。
- 核心层:根据天气信息,查询位于核心数据中心的产品目录数据库,获取推荐商品。
这个架构展示了如何将逻辑拆分,并部署在最合适的层级上。
现在,让我们回到 Colin 的演示,看看实际运行效果。
现场演示

好的,我们来看实际网页。


(刷新网页,页面快速加载)感谢 CDN。正如 Doug 所说,页面显示了基于本地天气的推荐产品。当前温度大约 60 华氏度,所以显示的是温暖天气的产品。


如果我们检查页面中的图片,并使用内容真实性验证网站,可以看到图片的溯源信息。例如,这张图片最初由不支持 C2PA 的相机拍摄,然后上传到 Adobe Stock 并签名,最后在我们运行于 Akamai 边缘的 WasmCloud 代理服务中被调整大小,并添加了新的签名记录(使用测试证书)。

另一张由 Adobe Firefly 生成的图片,也经历了类似的边缘处理流程。

我们还可以手动输入图片 URL 和宽度参数,服务会在服务器端获取、处理并返回带签名的图片。这一切都是在服务器端完成的,为未来集成更复杂的 AI 推理等操作奠定了基础。
未来展望

展望未来,我们希望:
- 制定与静态资源捆绑的通用规范,以便跨 CDN 使用。
- 将更多现有服务从 Kubernetes 迁移到 WasmCloud。
- 探索嵌入式设备用例。
- 发布标准的 C2PA WebAssembly 组件。

这样,任何开发者都可以直接使用这个组件来为应用添加内容真实性功能,无需关心底层实现,从而覆盖 Web、服务器和嵌入式全场景。
总结与问答
本节课中,我们一起学习了如何利用开源 WebAssembly 和 WasmCloud,在超分布式云架构上构建混合边缘-数据中心应用。我们通过 Adobe 内容真实性倡议的实际案例,演示了如何解决图片溯源信息在 CDN 传输中丢失的难题。关键在于,WasmCloud 提供了一致的部署和管理体验,使开发者能够轻松地将逻辑部署在最合适的位置(边缘、分布式节点或核心云),并安全地互联互通。这代表了边缘计算能力的一次重要演进。

现在进入问答环节。

(问答环节内容已整合到教程正文的相关部分,以保持行文连贯。)
014:使用机密计算与可信执行环境


概述
在本教程中,我们将学习如何利用机密计算和可信执行环境来保护 WebAssembly 工作负载,特别是在处理敏感数据(如人工智能算法)的场景下。我们将探讨机密计算的基本概念、现有解决方案,并通过一个具体的演示来展示其工作原理。


章节 1:机密计算简介
上一节我们概述了本课程的目标,本节中我们来看看什么是机密计算。
保护传输中的数据是一个已解决的问题。保护静态数据也可以通过磁盘加密来实现。然而,当 CPU 需要使用实际数据时,它需要从内存中以明文形式读取,此时数据并未受到保护。任何人都可以转储内存并获取机密数据。
目前有几种技术正在发展中以解决此问题。其中一种是纯加密方法,称为全同态加密。这项技术在 2009 年被证实,是一种较新但计算挑战性很大的密码学技术,对于像 AI/ML 算法这样的大量工作负载来说效率仍然很低。
另一种有前景的技术是机密计算,它是基于硬件的。该解决方案基于现代 CPU 供应商提供的扩展,例如 Intel 的 SGX 或 TDX、AMD 的 SEV-SNP、Arm 的 CCA 以及 RISC-V 的 Keystone 等。这项技术之所以有前景,是因为在机密虚拟机与内存之间,CPU 和内存之间会实时进行加密处理。这意味着虚拟机占用的那部分内存始终是加密的。
章节 2:可信执行环境的挑战与机遇
上一节我们介绍了机密计算的基本原理,本节中我们来看看其具体实现形式——可信执行环境所面临的挑战。
我们可以设想在可信执行环境或内存加密的虚拟机中进行集中式机器学习,将数据和模型带入进行推理。我们甚至可以进行分散式的联邦机器学习。
然而,这些 TEE 通常作为裸机虚拟机运行。这意味着没有底层的操作系统,也没有硬件抽象层,这使得事情变得困难。为了提供更高的抽象层,通常需要安装某种 Linux 操作系统。但问题在于,我们希望保持所谓的可信计算基尽可能小。那么,能够在那里提供并运行可靠的多语言应用程序的最小运行时是什么?这自然为 WebAssembly 提供了一个绝佳的机会。
章节 3:Elastic-6G 项目与解决方案探索

上一节我们讨论了 TEE 的抽象层问题,本节中我们来看看一个旨在解决此类问题的研究项目。

Elastic-6G 是一个大型欧盟项目,由 13 个组织参与,包括爱立信、西班牙电信等大公司、研究机构和初创公司。该项目预算为 500 万欧元,为期三年。该项目专注于研究在新型 6G 电信基础设施上对 WebAssembly 功能(即函数即服务)的编排,并且对将机密计算节点纳入该编排非常感兴趣。
Elastic 项目研究能够解决机密计算赋能问题的优秀解决方案。其中一个非常成熟但开源状态并非完美的项目是 Enarx,它已捐赠给 Linux 基金会。Enarx 主要面向在可信执行环境中运行 Wasm,特别是部署那些虚拟机。这是一个不简单的任务,因为这些虚拟机必须经过认证,并且有一个必须执行的流程。在 TEE 内的 Wasm 运行时必须提供足够的能力来运行不同类型的服务,最终启用网络甚至文件系统等。
我们正在研究和开发的另一个解决方案叫做 Coco AI,由 Ultraviolet 公司构建。Coco AI 更侧重于安全多方计算,即多个互不信任的参与方(例如算法提供者和多个数据提供者)可以组合数据集。可信执行环境的安全飞地看起来是一个非常好的候选方案,它可以提供绝对的机密性,使得数据交换和算法应用可以在不向任何其他方泄露数据的情况下进行。
章节 4:Coco AI 平台架构解析
上一节我们提到了 Coco AI 平台,本节中我们来深入了解其架构。
在多方计算场景中,例如多家医院需要交换高度机密的数据,而一家 AI 公司可以提供算法。此时,机密虚拟机或可信执行环境可以成为一个场所,供他们上传数据和算法。算法可以在其中执行,只有推理结果或训练后的模型等结果可以返回给指定的结果消费者。
为了实现这一点,我们需要一种方式来启动可信执行环境,因为它本质上是虚拟机,需要某种能够启动特定类型机密虚拟机的虚拟机管理器。
在机密虚拟机内部,由于它是一个真正的密封盒子,除了可能通过 TLS 连接上传数据外,无人能访问。我们不仅需要硬件抽象层,还需要一种代理来协调这种安全多方计算执行,并接受不同的工作负载、数据集和算法,并拥有一个状态机,以便在所有不同的参与方(例如医院)上传其数据集后启动执行。


管理器需要内置一些逻辑,以便知道部署哪种安全虚拟机。这依赖于架构,因为可以有 AMD 的 SEV-SNP 技术或 Intel 的 TDX 等。管理器还能够抽象不同的架构。


代理的作用是充当协调器,以便上传不同的数据集和算法,并能够启动执行。但机密计算的一个关键主题是所谓的远程认证。远程认证由硬件完成,测量由可信执行环境或 CPU 扩展本身执行。然后,整个虚拟机启动过程的测量值作为认证报告或测量值提供给验证者。验证者可以联系不同类型的背书者(取决于架构和供应商)来验证测量值,从而真正确定正在运行的是这种类型的机密虚拟机,并且飞地内只运行这种类型的软件系统。只有在认证完成并验证后,我们才能确信可以上传算法。

Coco AI 中的代理还具有执行这些远程认证的逻辑。在 Coco AI 的特定案例中,认证被内置在建立 TLS 连接的过程中,即在 TLS 握手期间。这是由 IETF 的 RATS 工作组标准化的技术,称为 Attested TLS。这是一个仍在开发中的新标准。


章节 5:用于 AI 的 Wasm 工作负载与 Burn 库
上一节我们介绍了 Coco AI 的架构,本节中我们来看看用于构建 AI 算法的 Wasm 工作负载。


Coco AI 支持多种工作负载,但今天我们专注于 Wasm。为了测试、制作概念验证并进一步开发,我们需要找到一个优秀的库来在其上构建 Rust 算法。我们发现最好的库之一是 Burn AI。它在某种程度上可与 PyTorch 或其他有趣的库相媲美,支持 CPU 和 GPU。GPU 支持从机密计算的角度来看很有趣,因为 GPU 本身现在有非常相似的问题:在 GPU 中进行的计算也必须在内存的加密部分完成,因此它们需要具备相同的能力。目前市场上只有少数 GPU 具备这些能力,且很难获得,因此很难进一步开发和测试这些功能。

我们使用这个库,你可以在 Ultraviolet 的代码仓库中找到许多使用它构建的示例,用于测试机密计算。相关的博客文章已经发布,解释了演示中看到的内容。
章节 6:实战演示:在 TEE 中运行 Wasm AI 推理

上一节我们介绍了用于 AI 的 Wasm 工具,本节中我们通过一个完整的演示来看看这一切是如何运作的。



演示将展示一个光学字符识别的 MNIST 数据集示例。我们使用 Burn 库训练了一个模型,并构建了其推理版本。我们准备了一个 Wasm 二进制文件,该文件将使用通过 Burn 库训练的模型进行推理。我们还准备了测试数据(数字“4”的图像),并将其转换为字节数组作为输入。

演示步骤涉及在 Coco AI 平台(此处称为 PrimAI)上创建资产(算法)、创建计算任务、关联算法、生成并上传用于 TLS 认证的密钥,最后启动计算。平台会与管理器通信,在 TEE 中启动一个机密虚拟机,并通过 Attested TLS 安全地上传算法和数据。计算完成后,只有被授权的结果消费者可以获取结果(识别出的数字“4”)。演示强调了整个过程中数据的机密性、完整性和计算的不可篡改性。
章节 7:未来工作方向
上一节我们完成了整个流程的演示,本节中我们展望一下未来的研究方向。

未来的工作主要集中在以下几个方面:
- 更好地支持 Enarx,特别是研究位于 Wasm 运行时和 Linux 操作系统之间、作为系统调用代理的微内核。
- 研究其他潜在的 Unikernel 方法,例如使用 HermitCore 或 Unikraft 构建 Linux Unikernel,甚至研究 seL4 微内核。
- 由于 Elastic 项目研究物联网设备上的编排,Zephyr RTOS 也是一个潜在的、可以运行 Wasm 运行时的高抽象层。
- 编排器本身是一个重大的研究课题,因为它现在不仅需要能够编排标准节点上的 Wasm 工作负载,还需要考虑可信执行环境及其必需的认证流程。
- 或许可以重用 CNCF 在机密容器方面的工作,并用 Wasm 替代 Docker。
- RISC-V 是一个非常有吸引力的架构,特别是在物联网领域。我们希望致力于更好地支持 RISC-V,尤其是在 RISC-V 上的机密飞地内运行机密计算和 Wasm。


章节 8:关键补充:远程认证
在结束之前,需要补充一个非常相关且重要的部分:远程认证。
每次与可信执行环境建立连接时,除了标准的双向 TLS 之外,实际上还会进行这种远程认证。它包括许多步骤,本身就是一个主题。简而言之,它就是验证特定软件是否运行在特定硬件上,并且你可以信任它。这涉及到支持可信执行环境的特定硬件(例如我们的私有实例)。目前最流行的是 AMD SEV-SNP,但也有 Intel TDX。希望未来会有更多选择,并且能更多出现在消费级领域(目前主要是服务器 CPU)。

关于运行特定的 Wasm 运行时,在我们的硬件抽象层(即我们的最小化 Linux 发行版)中,更换 Wasm 运行时非常简单容易。目前我们运行的是 Wasmtime,但它可以是任何运行时。这对于在 TEE 中运行 Wasm 工作负载来说也是非常重要的一部分。
总结

在本教程中,我们一起学习了如何利用机密计算和可信执行环境来保护 WebAssembly 工作负载。我们从机密计算的基本概念和挑战出发,探讨了 Enarx 和 Coco AI 等解决方案。通过深入分析 Coco AI 的架构和一个完整的 MNIST 推理演示,我们直观地理解了在安全飞地中执行敏感 Wasm 工作负载的完整流程,包括资产管理、计算编排、远程认证和安全数据传输。最后,我们展望了未来在微内核、Unikernel、跨平台编排以及 RISC-V 支持等方面的研究方向。机密计算为在不受信任环境中处理敏感数据提供了强大的硬件级安全保障,而 WebAssembly 则为其提供了灵活、高效且安全的软件运行时载体。
015:Wasm 工具漫游指南
在本节课中,我们将要学习 wasm-tools,这是一个用于操作 WebAssembly 模块的命令行工具套件和底层库。我们将通过一系列演示,探索如何检查、验证、转换、测试和操作 Wasm 模块及其文本格式。

1:什么是 Wasm Tools? 🛠️
wasm-tools 是一个命令行接口和一套低级库,用于操作 WebAssembly 模块。这是一个托管在 Bytecode Alliance 的开源项目,欢迎任何有兴趣添加各种子命令或功能的人贡献代码。
其核心思想是将大量功能集成到一个名为 wasm-tools 的单一命令中,该命令内部包含许多子命令,用于探索 Wasm 模块并查看其内部功能。


它与其他项目类似,例如 wasm-opt、wabt 或 binaryen(不是其编译部分,而是文本与二进制转换等工具部分)。wasm-tools 的一个重要部分是,它也是 Wasmtime 运行时本身的基础。二进制解码器、二进制翻译、二进制读取以及大量测试都依赖于它。本次演讲将介绍其中许多功能,并演示它们如何工作。所有这些都旨在用于 Wasmtime,并且在生产环境中也是稳定可靠的。
2:探索 Wasm 模块内容 🔍
现在,让我们进入实际操作部分。假设我们想探索一个 Wasm 文件。首先,我们从一些 Rust 代码开始,并将其编译为 Wasm。
rustc --target wasm32-wasi hello.rs -o hello.wasm
这会生成一个 hello.wasm 文件。我们可以使用像 wasmtime 这样的运行时来执行它,它会打印出我们期望的信息。但我们好奇的是,这个模块内部到底是什么。
如果我们直接查看这个模块的二进制内容,会看到很多难以理解的字节。这不是 WebAssembly 二进制格式的预期阅读方式。这时,wasm-tools 就派上用场了。

我们可以使用 wasm-tools 的 print 子命令来查看模块内容。
wasm-tools print hello.wasm
屏幕上会瞬间闪过大量文本,内容非常多。我们可以将其重定向到一个文件中以便查看。
wasm-tools print hello.wasm > hello.wat
这个文件展示了 Wasm 模块的内部结构,即二进制格式的文本表示形式。它对应着我们之前看到的那些字节。文本格式大量使用括号,包含了类型、导入、表、全局变量、函数和指令等。滚动到文件底部,可以看到一些大的二进制数据块,这些是数据段和自定义段,其中包含了一些较大的调试信息。
总之,这让我们能够查看 Wasm 模块的内部结构。
3:验证与特性探测 ✅
接下来,我们可以看看 validate 子命令。一个随机的字节块不一定是有效的 Wasm 二进制文件,必须通过验证谓词检查。
wasm-tools validate hello.wasm
如果没有输出,并且返回码为零,则表示验证成功。我们可以通过 --features 标志来控制启用的 Wasm 特性,甚至可以将其回退到原始的 Wasm 提案版本。
wasm-tools validate hello.wasm --features=-all
这时,模块可能不再有效,说明它使用了一些新特性。为了找出具体是哪些特性,我们可以使用 print 子命令的 --print-offsets 选项。
wasm-tools print -p hello.wasm | less
通过搜索错误信息中提到的偏移量,我们可以定位到导致验证失败的特定指令。例如,可能是 call_indirect 指令(其编码随引用类型提案而改变),或者是 memory.copy 指令(来自批量内存提案),亦或是符号扩展提案中的指令。我们可以逐一启用这些特性来验证。

wasm-tools validate hello.wasm --features=reference-types,bulk-memory,sign-extension
通过这种方式,我们可以探索 Wasm 模块,查看它使用了什么,或者精确定位模块内部出错的位置。

4:简化查看与名称还原 📄
之前打印模块时,内容非常多。print 子命令有一个 --skeleton 参数,可以帮助我们在更高层次上探索模块。
wasm-tools print --skeleton hello.wasm
使用这个参数后,调试信息会被替换为 ...,所有函数体也会被替换为 ...,我们可以快速扫描并了解模块内部的大致情况。
不过,我们首先会注意到,这些函数名看起来像乱码。这是函数名的“混淆”形式,类似于原生平台上的名称修饰。如果我们想去掉这种混淆,可以使用 demangle 子命令。
wasm-tools demangle hello.wasm -t
这会产生大量输出。wasm-tools 的一个特性是我们可以将命令管道连接在一起。例如,我们可以将 demangle 的输出通过管道传递给带有 --skeleton 参数的 print 命令。
wasm-tools demangle hello.wasm -t | wasm-tools print --skeleton
现在,我们看到了漂亮的还原后的函数名,看起来更清晰,也更像原生的 Rust 代码。
5:分析模块大小与剥离 🔪
我们的 hello.wasm 文件有 1.7 MB,这对于一个简单的 Hello World 来说太大了。让我们探索一下为什么它这么大。我们可以使用 objdump 子命令,它是原生 objdump 的一个简化版本。
wasm-tools objdump hello.wasm
输出显示了 Wasm 模块的各个段、它们在二进制文件中的位置、大小以及内部包含的项目数。例如,这个模块内部有 181 个函数。从输出列中,我们可以清楚地看到调试信息是问题所在(这是在没有优化的情况下编译的,且未剥离)。
我们可以使用 strip 子命令来剥离这些信息。
wasm-tools strip hello.wasm -o hello-stripped.wasm
wasm-tools objdump hello-stripped.wasm

现在,调试段都消失了。我们还可以通过指定正则表达式,只删除特定的自定义段,比如调试信息段,而保留如 name、producers 等段。
wasm-tools strip hello.wasm -r 'name\.debug.*' -o hello-no-debug.wasm
wasm-tools objdump hello-no-debug.wasm
剥离后,文件大小从 1.7 MB 减小到了 54 KB。

6:处理文本格式 📝
以上是使用 wasm-tools 处理二进制模块的基础。wasm-tools 包含许多子命令,可以通过 -h 查看。这些子命令通常接受 Wasm 输入并输出 Wasm,可以选择处理文本或二进制格式,默认输出到标准输出,这使得我们可以轻松地将命令管道连接起来,交互式地处理模块。
现在,让我们更多地使用 WebAssembly 的文本格式。我们准备了一个简单的文本格式模块示例 add.wat:
(module
(type (func (param i32 i32) (result i32)))
(func $add (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(export "add" (func $add))
)
这个模块定义了一个将两个数字相加并返回结果的函数。首先,我们可以使用 parse 子命令将其转换为二进制。
wasm-tools parse add.wat -o add.wasm
这会生成一个 add.wasm 文件,它比文本格式小得多。我们可以验证它是一个有效的模块。wasm-tools 的一个便利之处是,大多数命令都可以透明地接受文本格式作为输入,在内部将其编译为二进制后再进行处理。
wasm-tools validate add.wat
如果文本格式有错误,验证会失败。但错误信息指向的是二进制偏移量,而不是原始文本行号。为了帮助定位错误,我们可以在从文本编译到二进制时生成 DWARF 调试信息。
wasm-tools parse add.wat -g -o add.wasm 2>&1 | head -20
现在,错误信息会包含文件名和行号,这对于在大型文本文件中定位错误非常有帮助。
7:Wast 格式与测试 🧪
WebAssembly 还有一种用于规范测试的上游文本格式,称为 .wast 格式。它看起来与 .wat 类似,但可以包含断言。
(module
(func $add (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(export "add" (func $add))
)
(assert_return (invoke "add" (i32.const 1) (i32.const 2)) (i32.const 3))
wasm-tools 提供了 json-from-wast 子命令来处理这种格式。
wasm-tools json-from-wast add.wast -p
这个命令会输出 JSON,它比直接解析整个 .wast 文本格式对工具来说更友好。JSON 中包含了模块二进制和断言信息,便于测试工具消费。
此外,文本格式有很多“语法糖”,使得读写更容易。例如,类型可以隐式定义,导出可以内联声明,指令可以以更符合人类阅读习惯的“折叠”形式书写。
8:生成与变异测试用例 🧬
如果你正在编写一个 Wasm 运行时,可能需要生成测试用例。wasm-tools 的 smith 子命令是一个 Wasm 测试用例生成器。
head -c 100 /dev/random | wasm-tools smith -t
每次运行都会生成一个完全不同的、但有效的 Wasm 模块。我们可以通过许多选项来控制生成模块的特性,例如限制指令类型、函数数量、启用或禁用特定提案(如尾调用、GC)等。
wasm-tools smith --num-types 1 --num-funcs 5 --enable-tail-call=false -t

更有趣的是 --allow-invalid 选项,它可以生成包含随机字节的无效函数,用于测试运行时的错误处理路径。
head -c 100 /dev/random | wasm-tools smith --allow-invalid -o invalid.wasm
wasm-tools validate invalid.wasm # 预期会失败

当遇到一个触发编译器错误的大型复杂测试用例时,我们可以使用 shrink 子命令来缩小它。shrink 会尝试在保持“有趣”属性(例如触发特定错误)的前提下,通过变异不断减小模块体积。
wasm-tools shrink --test-command="./check_panic.sh {input}" large_test.wasm -o shrunk.wasm
这个过程会迭代运行,最终输出一个最小化的、仍能触发错误的测试用例,极大地方便了调试。
9:组件模型支持 🧩
wasm-tools 也全面支持 WebAssembly 组件模型。组件也有文本和二进制格式,并且大多数顶级命令都能透明地处理组件和核心模块。
假设我们有一个简单的组件文本 component.wat:
(component
(core module $m
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
)
(core instance $i (instantiate $m))
(func (export "add") (param i32 i32) (result i32)
(canon lift (core func $i "add")))
)
我们可以像验证模块一样验证它,但需要启用组件模型特性。
wasm-tools validate component.wat --features=component-model
wasm-tools 还有专门针对组件的子命令,例如 component wit 可以提取或操作组件的接口类型。
wasm-tools component wit component.wat
我们还可以使用 component embed 和 component new 等命令,在核心模块与组件之间进行转换,或者处理 WIT 文档的各种格式。

# 从核心模块和 WIT 创建组件
wasm-tools component embed component.wit module.wasm -o embedded.wasm
# 从组件中提取核心模块(近似反向操作)
wasm-tools component new embedded.wasm -t | wasm-tools print
这些工具使得在组件模型领域工作变得更加容易。
10:作为库使用与自举 🚀

wasm-tools 项目中的所有子命令功能也都作为 Rust crate 提供。如果你在编写 Rust 项目,可以轻松地从 crates.io 拉取这些库,在代码内部解析、打印、生成或测试 Wasm 模块,这在模糊测试等场景中尤其有用。
最后,一个有趣的演示是:wasm-tools 本身可以编译成 Wasm。
# 假设我们已经有了 wasm-tools.wasm
wasm-tools validate wasm-tools.wasm
wasmtime wasm-tools.wasm --dir=. validate wasm-tools.wasm

这意味着你可以在浏览器或任何 Wasm 运行时中运行 wasm-tools 来检查它自身,这展示了 WebAssembly 的可移植性和自举能力。

总结 📚
本节课中,我们一起学习了 wasm-tools 这个强大的 WebAssembly 瑞士军刀。我们涵盖了如何:
- 使用
print、validate、objdump等命令检查和验证模块。 - 利用
demangle、strip、--skeleton简化视图和操作。 - 处理文本格式(
.wat/.wast)并进行调试。 - 使用
smith、mutate和shrink生成、变异和缩小测试用例。 - 操作 WebAssembly 组件模型。
- 将
wasm-tools作为库集成到 Rust 项目中。

wasm-tools 是一个开源项目,欢迎任何形式的贡献,无论是新功能、错误修复还是改进建议。希望本教程能帮助你更有效地探索和操作 WebAssembly 模块。
018:从服务器到客户端


概述

在本教程中,我们将学习如何将传统的服务器端 Web 框架 Ruby on Rails 编译为 WebAssembly 模块,使其能够在浏览器中完全运行。我们将探讨其背后的技术原理、实现步骤以及潜在的应用场景。


1:背景与动机

上一节我们介绍了本次主题的背景。本节中,我们来看看为何要将 Ruby on Rails 带入浏览器。

Ruby on Rails 是一个成熟的全栈 Web 框架,通常部署在服务器端。随着 WebAssembly 技术的成熟,特别是 Ruby 语言官方开始支持 WASI,使得将 Ruby 及其生态系统编译为 WebAssembly 模块成为可能。
这开启了新的可能性:让功能完整的 Rails 应用直接在浏览器中运行,无需后端服务器。
2:Rails 应用的核心组件
一个典型的 Rails 应用不仅仅包含 Ruby 代码。以下是构成其技术栈的主要组件:

- Ruby 语言与解释器:应用的基础运行时。
- 依赖库:Ruby 的包称为
gem,其中一些可能包含用 C 或 Rust 编写的原生扩展。 - 系统级依赖:例如用于图像处理的工具。
- 数据库:通常是 SQLite、PostgreSQL 等。
- Web 服务器:用于处理 HTTP 请求。
- 文件存储:用于用户上传的文件或静态资源。
- 后台任务队列:用于处理异步作业。
- 外部 API 调用:与现代服务进行交互。

要将 Rails 移植到浏览器,我们需要为所有这些组件找到在 WebAssembly 环境中的替代实现。



3:Rails 的架构优势

上一节我们列出了 Rails 应用的组件。本节中,我们来看看 Rails 的架构如何使其适合移植。


一个好的框架会将抽象概念与其具体实现清晰分离。Rails 在这方面做得很好,大部分组件(如数据库访问、文件存储)都通过抽象接口定义,允许轻松切换底层实现。



对于少数未完全抽象的部分(如 Bundler 包管理器、Net::HTTP 库),得益于 Ruby 语言的动态性,我们可以通过“打补丁”的方式来修改其行为,以适应 WebAssembly 环境。

这种架构上的灵活性和 Ruby 语言的特性,共同使得“Rails on Wasm”成为可能。


4:实战演示:快速创建 Wasm 版 Rails 应用


以下是创建一个能在浏览器中运行的 Rails 应用的简化步骤:


- 创建标准 Rails 应用:
此命令会生成一个包含模型、视图、控制器等标准结构的 Rails 应用。rails new my_wasm_app

-
使用 Wasm 移植工具包:
通过wasm-rails等工具包,可以自动化处理移植过程。它会执行以下操作:- 将 Ruby 解释器及其所有依赖编译成一个
.wasm模块。 - 生成一个引导用的 JavaScript 应用,包含
index.html和Service Worker定义。
- 将 Ruby 解释器及其所有依赖编译成一个
-
条件化代码:
应用可以检测自身是否运行在 Wasm 环境中,从而启用或禁用特定功能。if defined?(Wasm) # 浏览器环境特有的逻辑 else # 传统服务器环境逻辑 end
完成这些步骤后,无需修改业务逻辑代码,同一个 Rails 应用就可以在浏览器中启动并运行。


5:关键技术实现细节
上一节的演示展示了结果。本节中,我们深入探讨几个关键组件是如何在 Wasm 环境中实现的。
数据库:SQLite
Rails 默认支持 SQLite。SQLite 有官方的 Wasm 版本。我们实现了一个新的 Rails 数据库适配器 sqlite3-wasm。它的大部分逻辑复用现有代码,仅通过 JavaScript 接口(FFI)执行最终的数据库查询。
Web 服务器:Service Worker
浏览器中无法运行传统的 Web 服务器。我们利用 Service Worker 拦截 HTTP 请求。
- Ruby 端的 Web 应用使用
Rack接口。 - 我们在 JavaScript 中实现了一个
Rack兼容层,负责将浏览器的FetchEvent对象转换为Rack请求,并将Rack响应转换回浏览器响应。
其他组件
- 文件存储:使用浏览器的 File System API 或 Origin Private File System。
- 后台任务与通信:使用 Broadcast Channel API。
- 图像处理:通过 Wasm 移植 ImageMagick 等库来实现。
核心模式是:为 Rails 框架中已有的抽象接口,提供基于浏览器 API 或 Wasm 模块的具体实现。
6:意义、挑战与未来
将 Rails 移植到 Wasm 面临一些质疑,例如性能、模块体积和实用性。这些挑战大多是技术演进过程中的暂时性问题。随着 Wasm 标准(如 WASI Preview 2)和运行时性能的持续优化,情况会不断改善。
那么,为什么值得做这件事?
- 教育与原型:创建“尝试、学习、复现”类应用。用户可以在浏览器中直接体验库、框架,或复现 Bug,无需配置本地环境。
- 离线优先应用:使应用在断网时仍能提供核心功能,例如文档阅读器、邮件客户端。
- 数据隐私应用:敏感数据可完全保留在用户本地,仅通过加密方式与外部 API 同步。
- 桌面应用:Rails 强大的 ActiveRecord 组件能优雅地描述业务逻辑,可用于提升桌面应用开发效率。
- 推动边界:促使 Rails 框架更清晰地分离抽象与实现,同时向 Wasm 社区展示服务器端框架的新用例。
核心理念:“服务器端”指的是软件在通信中的角色,而非其必须运行的位置。浏览器也可以成为“服务器”。
7:问答精选
问:整个 Rails 应用都运行在浏览器中的一个 Wasm 模块里吗?
答:是的,目前整个应用被编译进一个 Wasm 模块中。

问:如何实现多用户实时协作应用的数据同步与持久化?
答:有几种策略:
- 数据库层同步:使用像
electric-sql这样的项目,在数据库层实现主从同步。 - 应用层同步:采用事件溯源架构,将操作记录为事件日志,在客户端和服务器间同步日志,并处理冲突解决。
- 功能子集:并非所有功能都需要离线。可以为离线场景提供只读或有限写的功能子集,在线时再同步数据。同步冲突和离线时长是此类应用需要仔细设计的核心问题。
问:多用户应用还能用 SQLite 吗?
答:可以。有像 litefs 这样的 SQLite 复制方案。更实际的做法可能是,在读写冲突少的场景(如记录阅读进度)使用 SQLite,并通过应用层日志在恢复网络后同步。
总结

在本教程中,我们一起学习了将 Ruby on Rails 框架移植到 WebAssembly 环境中的完整思路。我们从其动机讲起,分析了 Rails 应用的组件构成和架构优势,观看了快速移植的演示,深入探讨了数据库、Web 服务器等关键技术的实现方式,最后讨论了此举的意义与未来展望。通过这项探索,我们看到了 WebAssembly 如何模糊服务器与客户端的界限,为全栈框架开辟了全新的应用场景。
019:WebAssembly 组件模型 - Web开发与API的火箭燃料

概述
在本节课中,我们将学习 WebAssembly 组件模型的核心概念、工作原理及其在构建和组合 API 中的应用。我们将从理论背景开始,了解组件与核心模块的区别,然后通过实际演示,展示如何使用工具链创建、组合和运行组件。
理论背景
WebAssembly 核心模块
WebAssembly 核心模块是一种用于基于栈的虚拟机的二进制指令格式。它是多种编程语言(如 Rust、C、Go、Python)的可移植编译目标,支持在 Web 客户端和服务器应用程序上部署。
核心模块的数据交换仅限于简单的数据类型,如整数和浮点数。若要共享字符串或数组等非平凡数据类型,必须通过线性内存进行管理。开发者需要手动处理内存指针和偏移量,这个过程较为复杂。
WebAssembly 组件模型
上一节我们介绍了核心模块的限制,本节中我们来看看更高级的抽象——WebAssembly 组件模型。
组件是核心模块的容器。它们通过 WIT(WebAssembly 接口类型) 文件来表达其接口和依赖关系。组件是自描述的代码单元,只能通过明确定义的接口进行交互,而不是共享内存。一个组件内部可以包含多个传统的核心模块或其他组件。
可以将组件想象成超市里装水果的网袋:它把零散的“水果”(核心模块)打包成一个易于携带和部署的单元。
WIT(WebAssembly 接口类型)
WIT 不是编程语言,而是一种用于描述合约的语言。它定义了组件世界、包和数据类型。
- 独立性:WIT 文件独立于任何编程语言。
- 生成绑定:通过工具,可以从 WIT 文件为特定语言(如 Rust、TypeScript)生成代码绑定。
- 规范ABI:绑定会生成一个“规范应用程序二进制接口”,这是组件之间交换数据的协议。
初次接触 WIT 文件可能会觉得复杂,但关键在于动手实践。从简单的示例开始,例如创建一个返回“Hello World”的组件。
工具链与工作流程
理解了组件和 WIT 的基本概念后,我们来看看支撑整个生态系统的工具链。
使用组件主要涉及四个步骤:
- 创作:创建组件。
- 组合:将多个组件堆叠在一起。
- 运行:在运行时(如
wasmtime、spin)中执行组件。 - 分发:将组件发布到注册中心(如
warg或 OCI 注册中心)以供他人使用。
以下是本演示中将用到的主要工具:
cargo-component:用于创建和构建 Rust 组件的工具。warg:WebAssembly 包注册中心客户端。wasm-tools:用于检查和分析 Wasm 文件的工具集。jco:用于处理 JavaScript 和 WebAssembly 组件的工具。
实践演示:构建“Hello Bob”组件服务
现在,让我们通过一个具体的例子,将理论付诸实践。我们将构建一个简单的 HTTP 服务,它调用另一个组件来返回“Hello Bob”。
第一步:定义 WIT 接口(合约)
首先,我们为“Hello Bob”功能定义一个 WIT 包。这只是一个接口描述,并非可运行的组件。
// hello-bob.wit
package hello:bob-package
interface hello-bob {
hello: func() -> string
}



world hello-bob-world {
export hello-bob
}
这个文件定义了一个包 hello:bob-package,其中包含一个 hello 函数,它返回一个字符串。world 声明了这个包对外提供的接口。

使用 warg 发布此 WIT 包后,可以在 wa.dev 等注册中心查看它。请注意,它被标记为 WIT,而不是 Component。

第二步:实现组件业务逻辑
接下来,我们创建一个 Rust 项目来实现这个 WIT 合约。

cargo component new --lib --target hello:bob-package hello-bob-impl
这个命令会创建一个新的库项目,并设定其目标为实现 hello:bob-package 这个 WIT 包中定义的接口。

首次构建项目时,工具会生成绑定代码(src/bindings.rs)。然后,我们可以在 src/lib.rs 中实现具体的 hello 函数:

// src/lib.rs
use bindings::hello::bob_package::hello;
struct Component;
impl hello::Hello for Component {
fn hello() -> String {
"Hello Bob".to_string()
}
}
绑定文件让 Rust 知道需要实现一个返回 String 的 hello 函数,这正是 WIT 文件中定义的。
构建并发布这个实现后,它在注册中心会被标记为 Component。

第三步:创建 HTTP 服务器组件
现在,我们需要一个 HTTP 服务器组件来接收请求并调用上面的“Hello Bob”组件。

首先,定义一个新的 WIT 包来描述这个服务器:
// hello-bob-web.wit
package hello:bob-web

interface hello-bob-web {
import hello:bob-package/hello-bob
import wasi:http/proxy
}

world hello-bob-web-world {
import hello:bob-package/hello-bob
export wasi:http/incoming-handler
}
这个 WIT 文件做了两件事:
- 导入
hello:bob-package中的hello-bob接口。 - 导出
wasi:http/incoming-handler,以便运行时可以调用它来处理 HTTP 请求。


同样,先发布这个 WIT 接口。
然后,创建并实现这个服务器组件:
cargo component new --lib --target hello:bob-web hello-bob-web-impl
在实现代码中,我们需要处理 HTTP 请求,并调用导入的 hello 函数:
// 简化的实现逻辑
use bindings::wasi::http::...; // HTTP 类型
use bindings::hello::bob_package::hello; // 导入的 hello 函数

impl bindings::exports::wasi::http::incoming_handler::Guest for Component {
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
// 调用另一个组件提供的函数
let greeting = hello();
// ... 构建 HTTP 响应并返回 greeting ...
}
}
构建这个组件。如果尝试直接运行它,会失败,因为它依赖的 hello 函数还没有被“连接”进来。
第四步:组合组件
这是最关键的一步。我们将使用 wasm-tools 把“Hello Bob 实现”组件和“HTTP 服务器”组件组合成一个新的、完整的组件。
wasm-tools compose hello-bob-web-impl.wasm -d hello-bob-impl.wasm -o service.wasm
这个命令将 hello-bob-impl.wasm(依赖项)链接到 hello-bob-web-impl.wasm(主组件)中,生成一个独立的 service.wasm 文件。
第五步:运行组合后的组件
现在,我们可以使用 Wasm 运行时来执行这个组合后的组件了。
wasmtime serve service.wasm
运行时将启动一个 HTTP 服务器。当我们用 curl 访问它时:
curl http://localhost:8080
服务器会处理请求,调用内部“Hello Bob”组件的逻辑,并最终返回响应:Hello Bob。

核心要点与最佳实践总结
本节课中我们一起学习了 WebAssembly 组件模型从概念到实践的完整流程。

让我们总结一下最关键的心得:
- 严格分离合约与实现:始终将 WIT 接口定义(合约)和具体组件实现分开管理。这有助于清晰的架构和团队协作。
- 理解组件与WIT的区别:在注册中心,WIT 是接口描述,Component 是可运行的实现。组合时使用的是 Component,而开发时通过
--target指向的是 WIT 包。 - 动手实践是关键:WebAssembly 组件模型的强大之处在于其可组合性。只有亲自动手创建、组合和调试组件,才能深刻理解其价值和工作原理。建议从像“Hello World”这样的简单例子开始。
- 利用工具链:
cargo-component、warg、wasm-tools等工具构成了强大的生态系统,能极大提升开发效率。

通过组件模型,我们可以像搭积木一样构建应用程序,每个组件职责单一,通过明确定义的接口通信,从而提高了代码的可复用性、安全性和可维护性。
020:使用组件模型在 WASM 上集成 Apache Arrow
概述
在本教程中,我们将学习如何利用 WebAssembly 组件模型,将 Apache Arrow 数据框架集成到流处理引擎中。我们将探讨组件模型的基本概念、集成 Apache Arrow 时遇到的挑战、以及如何通过将 Arrow 建模为资源来解决这些问题。整个过程旨在实现跨语言、高性能的数据处理。
第1章:组件模型简介 🧩
上一节我们概述了本教程的主题,本节中我们来看看核心基础技术——WebAssembly 组件模型。
组件模型允许在所谓的“客座”环境中进行虚拟化执行,这个环境可以是 Rust、Python 或 JavaScript。它通过一种称为 WIT 的接口,实现了从一种语言到另一种语言的函数调用。
不仅如此,组件模型还能通过 WASI 接口调用外部世界,例如 HTTP、文件系统或 GPU 等资源。
以下是组件模型的主要优势:
- 语言互操作性:允许不同语言的代码相互调用。
- 安全与沙箱:提供隔离的执行环境。
- 可移植性:组件可以在不同平台上运行。
- 组件复用:组件可以独立于其实现语言被复用。
目前,WASI 支持广泛的系统调用、文件系统操作以及云服务接口。这些接口主要用于构建微服务或 HTTP 服务,在云服务集成中有大量应用案例。
第2章:构建数据栈的动机 📊
上一节我们介绍了组件模型的基础,本节中我们来看看如何将其应用于构建数据栈。
一个典型的应用场景是结构化数据栈。它涉及从批处理和流处理中处理结构化数据,进行数据仓库处理、构建仪表盘和指标。虽然工具众多,但其核心与微服务类似,都需要进行数据转换、过滤等通用计算操作。
另一个热门领域是通用 AI 栈。虽然涉及 AI 模型和提示工程,但本质上仍然是在处理数据(如向量和张量),同样需要进行数据转换和计算。因此,组件模型的优势同样适用于此。
我们正在开发一个名为 Stateless Dataflow 的流处理引擎协议,用于处理实时数据。它是 Apache Flink、Kafka Streams 等产品的轻量级替代方案,应用于实时分析场景,如欺诈检测、预测性维护和边缘传感器集成。
流处理引擎的核心是转换逻辑和状态管理。这非常适合使用组件模型。在我们的案例中,我们构建的是事件驱动架构,所有数据都被建模为流式事件,从而可以构建任意复杂的数据栈计算。
我们将从组件模型中学到的经验应用到我们的无状态算子中,用组件模型替换了原有的计算逻辑。这样,我们无需在 JVM 或专门的 Python 环境中运行,而是在组件模型和安全的执行引擎中运行,并能通过 WASI 访问 HTTP、GPU 或键值存储等其他资源。
当然,我们面临的挑战是状态管理,接下来我们将以 Apache Arrow 为例进行探讨。
第3章:Apache Arrow 的使用场景 🏹
上一节我们讨论了构建数据栈的动机,本节中我们来看看 Apache Arrow 在其中的具体作用。
在欺诈检测等用例中,我们需要在多个时间窗口内检测数据模式。例如,将数据按时间间隔分区并计数,以检测异常交易(如一张信用卡同时在巴黎和伦敦取款)。
我们通过 DataFrame(数据框架) 来实现这种分析。DataFrame 是数据科学和机器学习社区(如 Pandas、Spark、Polars)中流行的数据结构,类似于表格,由一系列数据列组成,支持复杂的结构化数据分析。
DataFrame 设计为列式存储。数据按列(Series)组织,而非按行。这是因为列式格式对于分析任务效率更高,通常能快 10 到 100 倍,即使不使用 GPU,也能利用 CPU 的 SIMD 指令集加速。
列式与行式的关键区别在于内存布局:相同字段的数据被分组存储在连续的内存区域,这有利于数据装入 CPU 寄存器进行处理。
这就引出了一个问题:如何在我们的“客座”环境中访问这个 DataFrame?
第4章:集成策略与挑战 ⚙️
上一节我们介绍了 Apache Arrow 的使用场景,本节中我们来看看具体的集成策略及其面临的挑战。
为了让用户体验无缝,我们希望来自 Pandas、Rust 或 JavaScript 的开发者能使用他们熟悉的数据框架 API。最直接的想法是将内存共享给客座环境。
WebAssembly 有一个共享内存标准,允许主机和客座环境共享内存。Apache Arrow 的价值在于它为解决不同语言间数据格式不一致的问题提供了标准。Arrow 格式是一种列式数据交换标准,被广泛支持。
因此,第一个实验是:将共享内存设置为 Arrow 格式。主机将数据写入 Arrow 格式,客座环境(如 Python 的 Pandas 库)可以直接导入此格式的数据。对于 Rust,我们需要构建适配器,以便 Polars 库能读取此内存。
然而,这种方法存在挑战:
- WebAssembly 共享内存提案主要针对 JavaScript 与浏览器设计,对 Rust、Go、Python 等其他语言的支持尚不广泛。
- 经过实验,发现其支持度还不够。
于是,我们转向第二种策略:复制缓冲区。即将 Arrow 缓冲区复制到客座环境中,客座环境的数据框架库(如 Pandas、Polars)仍可消费此缓冲区。
我们假设在实时流处理中,每次操作的数据块不会太大。我们为此构建了一个 WIT 库来包装 Arrow 缓冲区。该 API 几乎是一一对应了 Arrow 的规范:Arrow 由一系列数组组成,每个数组有类型(如 i32, i64, f64)和定义类型的元数据列。
通过此 API,客座环境可以复制 Arrow 库并访问数据。
但此方法也有挑战:
- 适配器实现相当复杂,且需要为多种语言(Rust、Go、Pandas 等)构建。
- 当处理多个表或进行连接操作时,可能需要复制大量数据。
- 无法高效处理大型表(尽管在实时流处理中多数表较小,但连接历史数据时可能变大)。
- 客座库(如 Rust)的构建复杂,导致构建时间和问题增加。
第5章:新策略:将 Arrow 建模为资源 💡
上一节我们探讨了直接内存共享和复制缓冲区的局限性,本节中我们来看看一个更强大的新策略。
新的策略是:将 Arrow 数据框架建模为组件模型中的“资源”。
这是组件模型最强大的功能之一,它允许将外部资源表示为带有句柄的任意数据块。通过句柄,可以调用相关函数,并传递这些句柄。这与传统系统(如 Windows)中的句柄概念类似。
关键区别在于,组件模型中的资源句柄是完全类型化的。虽然示例中常用字节数组,但你完全可以定义自己的自定义数据类型(如 DataFrame、向量、张量),因此这是一种非常强大的方式。
思路是:将 Arrow 定义为我们自己的类型接口,并暴露给客座环境。客座环境无需构建复杂的适配器即可消费数据,因为 WASI 工具链会为我们生成适配器。
这样做的权衡是:客座环境可能无法直接使用语言原生的数据框架 API(如 Pandas 或 Polars 的 API),而需要使用我们通过 WIT 定义的接口。我们可能需要在此基础上构建其他适配器。这是用一定的灵活性换取复杂性的降低。
我们仍在探索这种方法的局限性和发展空间。
第6章:数据框架接口设计 🛠️
上一节我们提出了将 Arrow 建模为资源的新策略,本节中我们来看看如何具体设计数据框架的接口。
我们的数据框架接口包含几个部分:
1. 表达式
数据框架的强大之处在于可以使用编程式 API 构建查询表达式,而无需使用 SQL。我们借鉴了另一个流行的数据框架库 datafusion 来建模表达式,它比 Polars 等更简单。最终构建的是一个表达式树。例如,一个过滤表达式 age > 30 由操作符和操作数组成。
2. 数据框架资源
数据框架本身被表示为一个资源句柄。你可以对其运行表达式,从而创建另一个数据框架,并返回一个新的句柄。可以进行的操作包括:
select: 选择列。filter: 过滤行。shape: 获取数据框架的形状(行数和列数)。
3. 行迭代器
获取数据框架后,如何遍历数据?我们引入了另一个句柄:行迭代器。它类似于迭代器概念,可以告诉你何时开始、何时跳过、以及如何获取值。获取的值是字面量,可以是任何基本类型。
4. SQL 接口
除了编程式 API,也支持发送 SQL 语句进行查询。这可以执行任意 SQL 表达式。在我们的实现中,后端使用了 Polars API,但也可以提供 DataFusion 或其他提供者。
所有这些操作都在客座环境中运行。
第7章:完整示例与演示 🎬
上一节我们设计了数据框架的接口,本节中我们通过一个完整示例来看看它是如何运作的。
这是一个聚合算子的完整示例,用于按时间分区并计数(或统计单词数),获取前 10 行。
流程如下:
- 运行一个 SQL 查询。
- 运行一个表达式来检查数据的形状和类型。
- 进行下一步操作(如过滤、聚合)。
- 获取行迭代器进行遍历,并将结果转换为 JSON(可用于发送到仪表盘或下游处理)。
这展示了数据框架 API 的四个部分:SQL 执行、表达式运行、形状检查和迭代。
演示:实时车辆速度分析
我们有一个演示,从赫尔辛基的公共交通系统消费实时事件流(车辆速度、经纬度)。
目标:每5分钟计算一次平均速度最高的前5辆车。
我们的无状态数据流是一个算子链,每个算子都运行在 WebAssembly 中。在 UI 中可以看到这个算子图的运行状态,并实时输出计算结果(车辆ID、路线、速度等)。这演示了在 WASM 中运行数据框架操作的能力。
第8章:未来展望与总结 🌟
上一节我们通过示例看到了集成的效果,本节中我们来总结并展望未来。
我们还在探索集成其他数据类型,如向量和张量,以支持 AI 模型,并将其作为我们产品的一部分。
总结
在本教程中,我们一起学习了:
- WebAssembly 组件模型的核心概念及其在语言互操作、安全沙箱方面的优势。
- 将组件模型应用于构建数据栈和流处理引擎的动机。
- Apache Arrow 作为列式数据交换标准在跨语言数据共享中的作用。
- 集成 Arrow 时尝试的两种策略:共享内存和复制缓冲区,以及它们面临的挑战。
- 创新的将 Arrow 建模为类型化资源的新策略,利用组件模型的强大功能降低集成复杂度。
- 设计包含表达式、数据框架资源、行迭代器和 SQL 接口的完整数据框架 API。
- 通过一个实时车辆速度分析的演示,看到了该方案的实际运行效果。


这种方法使得在轻量级、安全的 WebAssembly 环境中执行高性能的数据转换和分析成为可能,为边缘计算和实时处理提供了新的解决方案。
021:在支持Wasm的环境中分发和运行容器 🚀


在本节课中,我们将学习如何利用 container2wasm 工具,在 WebAssembly 环境中运行未经修改的 Linux 容器。我们将探讨其工作原理、分发方式,并了解新引入的 QEMU 支持如何扩展架构兼容性和提升性能。
概述
container2wasm 是一个工具,它通过 CPU 模拟技术,使得基于 Linux 的容器能够在 WebAssembly 虚拟机中运行。这解决了将现有应用移植到 Wasm 环境时面临的 Linux 兼容性问题。本节课将详细介绍其工作机制、两种容器分发方法,以及新集成的 QEMU 支持带来的优势。
为什么需要将应用移植到 WebAssembly?
将应用运行在 WebAssembly 环境中主要有两大好处。


首先,它允许我们在浏览器中分发和演示广为人知的应用。例如,用于产品演示、开发环境搭建等场景。社区中已有一些服务和项目,让用户无需在主机上安装应用即可在浏览器中轻松运行。
其次,WebAssembly 是一个可移植的沙箱。我们可以在 Wasm VM 内运行未经修改的代码,而无需给予其直接访问主机的权限。此外,Wasm 生态系统也提供了一些对浏览器外现有应用有用的工具。
然而,将应用移植到 Wasm 并不容易。主要原因是基于 Linux 的应用需要在 Wasm VM 内重新实现,而 Wasm 模型本身缺乏 Linux 兼容性,例如不支持 fork 或 exec 系统调用。
那么,我们能否通过直接在 Wasm VM 内运行未经修改的 Linux 应用,来简化移植过程呢?


container2wasm 简介 🛠️
container2wasm 应运而生,它是一个用于在 Wasm VM 内运行基于 Linux 的容器的工具。
其核心原理是:未经修改的容器在模拟的 CPU 上运行真正的 Linux 系统。该工具还支持浏览器和 Wasm 运行时中容器的网络功能,并支持 Wasm 的直接内存映射。
以下是运行容器的两个示例:
- 在支持 WASI 的 Wasm 运行时(如
wasmtime)上运行容器。 - 在浏览器中直接运行容器。
工作原理
container2wasm 使用 CPU 模拟器来在 Wasm 环境中运行容器。
- 对于 x86-64 容器,默认使用
box86模拟器。 - 对于 RISC-V 容器,使用
tinyemu模拟器。 - 在最新版本(0.7)中,实验性地添加了对 QEMU 的支持,以获得更广泛的客户机架构支持和性能优势。
其内部架构大致如下:容器镜像被加载后,在模拟的 CPU 和 Linux 内核上启动。container2wasm 负责处理 Wasm 环境与模拟器之间的桥接,包括文件系统映射和网络配置。

关于
container2wasm内部细节、直接内存映射和网络的更多信息,请参阅其代码仓库中的文档。

向浏览器分发容器
container2wasm 支持两种向浏览器分发容器镜像的方式。

方式一:预转换为 Wasm 模块

第一种方式是预先将容器转换为 Wasm 模块。container2wasm 提供了 c2w 命令来完成此转换。
转换流程如下:
- 使用
c2w命令将容器镜像转换为.wasm文件。 - 将生成的
.wasm文件上传到 Web 服务器。 - 浏览器通过 HTTP 获取并运行该
.wasm文件。
这种方式的好处是部署简单,与静态资源分发类似。
方式二:直接分发 OCI 容器镜像
第二种方式是直接在浏览器内获取和运行标准的 OCI 容器镜像,无需预先转换为 Wasm 模块。
其工作流程如下:
- 浏览器中运行一个名为
imagemount的 Wasm 组件。 imagemount从容器仓库(Registry)拉取未经修改的 OCI 镜像。- 拉取的镜像通过模拟器挂载到容器中。
这种方式允许浏览器直接获取容器镜像,因此服务器需要配置跨域资源共享(CORS)。目前尚无公开支持 CORS 的容器仓库,但你可以使用私有仓库进行尝试。
imagemount 还支持 懒加载(Lazy Pulling) 技术。该技术允许容器在镜像内容尚未完全本地可用时就开始启动,从而缩短容器启动时间。
演示:在浏览器中直接运行容器
以下是一个在浏览器中直接拉取并运行未经修改的 Ubuntu 24.04 容器镜像的演示。
首先,我们在本地机器上使用 Docker 原生拉取并运行该镜像,以确认镜像可用。
docker run -it ubuntu:24.04
命令成功执行,显示这是一个 Ubuntu 24.04 镜像。
接下来,我们在浏览器中运行同一个镜像。我们已经启动了一个提供 imagemount 服务的服务器。当在浏览器中启动 imagemount 页面时,它会从本地的私有仓库直接获取 ubuntu:24.04 镜像并运行。
在浏览器中,我们看到了 shell 提示符。执行 uname -a 和 cat /etc/os-release 可以确认,这正是在浏览器中运行的 Ubuntu 24.04 容器。
关键点:Docker 和浏览器从完全相同的仓库拉取了完全相同的镜像内容,但浏览器是在模拟环境中运行的。
集成 QEMU 支持 🚀
如前所述,container2wasm 最近添加了对使用 QEMU 的实验性支持。
什么是 QEMU?
QEMU 是一个由 Fabrice Bellard 创建的开源模拟器。它能模拟多种 CPU 架构(如 x86, ARM, RISC-V)和各种机器类型(包括开发板)。QEMU 具有可移植性,能在多种主机 CPU 上运行,并通过即时编译(JIT)和多核利用带来性能优势。
为何在 container2wasm 中使用 QEMU?
我们期望通过集成 QEMU 获得:
- 更广泛的客户机架构支持,包括 AArch64 和 ARC64。
- 性能提升,得益于 QEMU 的 JIT 编译和多核利用能力。
然而,QEMU 本身并不支持 Wasm 作为主机环境。为此,我们实验性地将 QEMU 移植到浏览器,称之为 QEMU Wasm。
QEMU Wasm 的实现
QEMU Wasm 是使用 Emscripten 移植的 QEMU 系统模拟器版本。它支持 64 位客户机,并为 Wasm 实现了 JIT 后端,从而能够利用多线程。
技术细节:QEMU 的 TCG 与 Wasm 后端
QEMU 使用一个名为 TCG(Tiny Code Generator)的二进制转换器进行 JIT 编译。TCG 定义了一种中间表示(IR)。客户机代码由前端翻译成 IR,IR 再由后端翻译成主机代码。
为了支持 Wasm 主机,我们为 TCG 添加了一个 Wasm 后端。这个后端接收 IR 并将其翻译成 WebAssembly 代码。
由于 Wasm VM 采用哈佛架构,生成的 Wasm 代码(位于内存中)尚不能直接执行。需要借助浏览器 API:
WebAssembly.Module:编译 Wasm 代码缓冲区。WebAssembly.Instance:实例化模块,提供其中定义的函数。

实例化后的函数通过 Wasm 的 Table 功能暴露给 QEMU 模块。Emscripten 允许以函数指针的形式访问 Table 中的函数,因此 QEMU 可以直接调用 Wasm 模块的入口点。
优化策略:混合执行模式
为每个翻译块(TB)创建独立的 Wasm 模块会带来编译开销。为了缓解这个问题,QEMU Wasm 同时支持 Wasm 后端和 TCI(TCG 解释器)。默认情况下,所有 TB 都在 TCI 上运行。只有被频繁执行的“热”TB 才会被编译成 Wasm 模块。

性能对比演示
我们进行了一项实验,测量在浏览器内的模拟器中,使用 pzstd(支持多进程的 Zstd 压缩工具)压缩 10MB 随机数据所需的时间。
我们对比了 QEMU Wasm 和移植到浏览器的 box86(container2wasm 默认用于 x86-64 容器的模拟器)的性能。
- box86 完成该命令耗时超过 40 秒。
- QEMU Wasm 完成该命令快得多。
- 当启用 4 线程的 MTTCG(多线程 TCG) 时,QEMU Wasm 的性能得到进一步提升。



这个演示表明,QEMU 的 JIT 和多核支持能带来显著的性能优势。

演示:在浏览器中运行 AArch64 容器
现在,我们演示在浏览器中运行 AArch64 架构的 Alpine 容器。
在浏览器中,QEMU Wasm 作为模拟器运行。Alpine 容器启动后,出现了 shell 提示符。
- 执行
uname -m确认这是 AArch64 环境。 - 执行
cat /etc/os-release显示这是 Alpine Linux 容器。 - 可以查看
/目录下的根文件系统。
这证明了通过 QEMU Wasm,container2wasm 能够支持像 AArch64 这样的非 x86 架构容器。
用例与未来展望
在浏览器中运行容器是一项通用功能,相信它可以用于多种用例。
container2wasm 的潜在用例
- VS Code 扩展:用于在浏览器中运行容器,已有实验性实现。
- 交互式浏览器演示:带有模拟机器的产品演示。
- 容器沙箱执行环境:提供安全的隔离环境。
- 浏览器内应用调试器:支持记录和回放的调试场景。
QEMU Wasm 的更广泛用例
QEMU Wasm 的用途不仅限于容器。得益于 QEMU 对多种机器的支持,例如在浏览器中模拟单板计算机也成为可能。
演示:在浏览器中运行树莓派
我们使用 QEMU Wasm 在浏览器中模拟了一个树莓派开发板,并运行了 BusyBox。
uname -m显示是 AArch64。cat /proc/cpuinfo显示模拟的 CPU 是树莓派型号。cat /proc/device-tree/model确认是树莓派环境。- 同样可以查看
/目录下的根文件系统。
未来工作
container2wasm 和 QEMU Wasm 仍处于早期开发阶段,有许多未来工作方向:
- 性能与稳定性提升:修复 QEMU Wasm 当前的限制(如网络支持、启动速度)。
- 集成 AOT(提前编译)方法:探索与现有工具(如
wasi2c)的集成。 - 集成更多 QEMU 功能:如支持更多机器类型、网络、图形显示等。
- 支持 Wasm 运行时:目前 QEMU Wasm 主要面向浏览器,使其也能在 Wasm 运行时(如 Wasmtime)上运行是未来的方向。
- 与生态系统集成:推动包仓库和容器仓库支持 CORS,以便更好地在浏览器环境中使用。
为了促进相关讨论,我们正提议将 container2wasm 作为沙箱项目加入 CNCF。旨在为解决兼容性问题提供方案,并建立一个中立的社区来讨论容器向 Wasm 的迁移。
总结
本节课我们一起学习了以下核心内容:
container2wasm工具:它通过 CPU 模拟,使得未经修改的 Linux 容器能够在支持 WASI 的运行时和浏览器中运行。- 两种分发方式:预转换为 Wasm 模块,或直接在浏览器中拉取运行 OCI 镜像。
- QEMU 集成:新引入的实验性 QEMU Wasm 支持提供了更广泛的客户机架构(如 AArch64)和性能优势(通过 JIT 编译和多核利用)。
- 多样化的用例:从浏览器内演示、沙箱环境到模拟单板计算机,展示了该技术的广泛应用潜力。

通过 container2wasm 和 QEMU Wasm,我们为在 WebAssembly 生态中运行现有容器化应用开辟了一条实用的道路。
022:绿色云计算的未来——Arm64架构上的无服务器WebAssembly

概述
在本节课中,我们将探讨如何构建更环保的云计算应用。我们将聚焦于两个关键技术:无服务器WebAssembly和Arm64架构,并解释如何将它们结合使用,以显著降低软件运行的能耗和碳足迹。

章节 1:问题背景与绿色软件理念
全球变暖是一个严峻的问题,但社会对计算资源的需求仍在持续增长。这给全球社区带来了挑战,例如,在伦敦西部,为了给新建数据中心腾出电力基础设施,2030年前将不会建设新的住房。

传统数据中心每个机架的功耗标准约为10-14千瓦。然而,为满足AI和GPU等高功耗硬件的需求,新建数据中心正在部署功耗高达40-60千瓦的机架。这导致了能源消耗的急剧增加。

作为开发者,虽然我们无法控制数据中心的建设,但我们可以控制代码的编写方式和运行的架构。我们可以通过以下方式提高效率:
- 理解自己有能力做出改变。
- 学习如何衡量代码的碳影响。
- 选择更高效的软件平台和硬件。

绿色软件基金会提出了构建绿色软件的三个核心原则:
- 能源效率:运行软件时消耗更少的电力。
- 硬件意识:选择更高效的硬件,用更少的资源完成相同的任务。
- 碳感知:例如,调整任务运行时间,或将工作负载从高碳强度区域转移到更多使用可再生能源的区域。
章节 2:如何衡量软件碳强度
上一节我们介绍了绿色软件的理念,本节我们来看看如何具体衡量软件的碳影响。
绿色软件基金会定义了软件碳强度(SCI)公式:
SCI = (O + M) / R
其中:
- O 代表运行排放,即软件运行直接消耗能源产生的碳排放。
- M 代表隐含排放,即制造运行该软件的硬件(如芯片)所产生的碳排放。
- R 代表资源,例如处理的请求数或运行时长等度量单位。
我们可以进一步分解运行排放 O:
O = E * I

其中:
- E 是软件消耗的能量。
- I 是能源的碳强度,取决于电力来源(如煤电碳强度高,太阳能碳强度低)。


因此,降低软件碳强度的最佳方式是:
- 尽可能少地使用硬件(减少 R 的分母效应,但更关键的是降低 E)。
- 所使用的硬件本身要尽可能高效(降低单位能耗,并选择 M 更低的硬件)。


章节 3:构建绿色软件技术栈
基于上述公式,为了降低 O 和 M,我们可以构建一个包含四部分的绿色软件技术栈:
- 高效的编程语言:使用更高效的语言编写应用。研究表明,用Rust实现的程序比用Python实现的程序能耗低75倍。
- 高密度的应用架构:采用能在同一硬件上运行更多工作负载的架构。
- 快速切换的隔离机制:使用能快速启动和销毁的隔离技术。
- 节能的基础设施:在功耗意识强的硬件上运行所有组件。



接下来,我们将详细探讨应用架构和隔离机制。
应用架构的演进
应用架构的演进始终围绕着提升硬件利用率:
- 虚拟机:允许在单台物理机上运行多个独立应用。
- 微服务:将应用拆分为独立服务,实现更精细的扩展。
- 无服务器:将扩展粒度细化到单个函数或业务逻辑单元。
然而,当前的无服务器技术尚未完全实现其目标,未能达到理想的密度。部分原因是其底层技术(如微虚拟机)无法真正“缩容到零”,导致资源闲置。
现有无服务器技术的问题
以下是现有无服务器技术面临的主要挑战:
- 微虚拟机:冷启动时间可能长达1秒。云服务商通常需要预热99%的实例来避免冷启动,这意味着在代码实际执行前就在消耗资源。
- 容器:在类似场景下,保持容器“温暖”以备调用的成本非常高,因为销毁后重新启动耗时过长。

研究表明,Kubernetes集群中平均有69%的CPU核心处于闲置状态,这造成了巨大的资源浪费。



章节 4:解决方案:WebAssembly
上一节我们指出了现有架构的密度问题,本节我们将介绍能实现“缩容到零”的解决方案:WebAssembly。
WebAssembly是一种代码隔离机制,适用于多租户环境,其启动时间小于1毫秒。它是一个可移植的编译目标,意味着你可以将用Go、Rust、Python或JavaScript等语言编写的代码,编译成 .wasm 字节码文件。该文件可以在任何搭载了WebAssembly运行时的环境中运行,无论是边缘还是云端,实现了真正的跨平台和跨架构。

WebAssembly成为理想的无服务器隔离机制,主要因为:
- 安全隔离:提供安全的沙箱和基于能力的安全模型,确保只能访问被明确授权的资源。
- 可移植性:一次编译,到处运行。
- 体积小:一个Express.js应用容器镜像约300MB,而对应的WebAssembly组件可能只有3MB,如果用Rust编写甚至可降至200KB。
- 启动快:毫秒级启动速度,实现了真正的按需启动和销毁。


快速开始:使用 Spin
Spin是一个用于构建无服务器WebAssembly应用的开源开发工具,旨在简化开发体验。
以下是使用Spin的三个核心命令:
spin new:创建新应用脚手架。spin build:将代码编译为.wasm文件。spin up:在本地运行应用。




通过一个简单的HTTP应用演示,我们可以在5秒内发起数十万次请求,平均延迟仅3.38毫秒,展示了WebAssembly快速创建、沙箱化执行和销毁的能力。

部署选项
开发完成后,你可以选择多种部署方式:
- Fermyon Cloud:多租户无服务器WebAssembly平台。
- Kubernetes + SpinKube:一个开源项目,让你能在Kubernetes上轻松运行无服务器WebAssembly应用。SpinKube通过一个ContainerD shim,在底层让ContainerD执行WebAssembly模块而非容器,并通过Spin Operator在Kubernetes上管理应用。

章节 5:选择节能的硬件架构
在讨论了软件栈之后,我们来看看绿色软件栈的第四个部分:硬件基础设施。首先,明确几个关键术语:

- 性能:纯粹的速度,例如每秒浮点运算次数。
- 能效:每瓦特功耗所能提供的性能。
- 机架级性能:一个机架内所有服务器能提供的总性能。
- 机架功耗:单个机架消耗的功率(千瓦)。现代高密度机架功耗可达40-60千瓦。

Arm64 与 x86 架构的能效对比
为了实现高利用率和高密度,我们需要在高负载下仍能保持高效能的芯片。Arm64服务器在此方面表现优异。
下图展示了随着负载增加,Arm64服务器能保持线性的性能增长,而x86服务器在负载达到约50%后,能效会显著下降。



这主要源于同步多线程的差异:
- x86:每个物理核心提供两个虚拟线程。当负载较高时,可能出现“吵闹的邻居”问题,即一个线程因I/O等原因阻塞时,会连累同一核心上的另一个线程,导致效率下降。
- Arm64:采用单线程单核心设计,核心数量更多,密度更高。每个线程独立运行,避免了相互干扰,从而在高负载下提供更可预测的性能。

实际影响示例
假设需要处理每秒130万次请求:
- Intel x86:需要82个CPU(假设每服务器2CPU,即41台服务器)。
- Arm64:需要36个CPU(即18台服务器)。
在功耗方面:
- Intel方案总功耗接近35千瓦/时。
- Arm64方案总功耗约为12.7千瓦/时。

Arm64方案不仅服务器数量更少,单台功耗更低,总能耗也大幅下降,并且所有服务器可以整合进一个标准机架,实现了更高的密度和能效。


章节 6:强强联合:Wasm + Arm64
最理想的情况是将高效的软件栈(WebAssembly)与高效的硬件架构(Arm64)相结合。
为了帮助用户评估性能,我们创建了一个性能测试套件,基于K6负载测试工具和SpinKube。测试涵盖了不同语言实现的“Hello World”应用的执行速度、应用副本数扩展时的性能表现,以及请求速率攀升时的CPU使用情况。测试表明,系统能够根据负载快速扩展和收缩。


成功案例:ZeIS 集团
ZeIS集团从容器化无服务器迁移到基于SpinKube的WebAssembly无服务器后,在保持性能的同时实现了更高的部署密度。这使得他们的计算成本降低了60%。成本节约一方面源于更高的密度,另一方面也因为他们可以自由选择更经济的Arm64虚拟机。


章节 7:互动演示与总结
最后,我们有一个贯穿整个展区的互动演示。多个公司的展位将设置基于振动的声音传感器,收集环境音量数据。数据通过MQTT协议发送,由一个由Spin编写的MQTT触发应用接收并存入SQL数据库。后端组件处理数据后,前端会以图表形式实时展示各展位的“活跃度”。这展示了WebAssembly在事件驱动、实时数据处理场景下的应用能力。

总结
在本节课中,我们一起学习了如何通过技术选择构建更绿色的云计算应用。关键要点包括:
- 使用软件碳强度公式来衡量和指导优化方向。
- 采用WebAssembly作为无服务器隔离机制,实现毫秒级启动、高密度部署和真正的缩容到零。
- 选择Arm64架构的硬件,以获得更优的能效和高负载下的稳定性能。
- 将Wasm与Arm64结合,能最大化地提升能效、降低成本和碳足迹。
作为开发者,我们拥有通过技术选择为环境可持续性做出贡献的力量。




024:揭秘组件模型的威力 🚀

在本教程中,我们将学习如何利用 WebAssembly 组件模型,跨越不同编程语言生态系统的界限,复用现有的库。我们将通过一个具体示例,展示如何将 Rust 编写的 Base64 库集成到 Grain 语言编写的 Web 应用中。
语言介绍:Grain 🌾
首先,我们来介绍一种名为 Grain 的编程语言。它始于 2017 年,是一个为 WebAssembly 设计的强类型、宽松的函数式编程语言。
Grain 融合了函数式编程的理念,例如数据优先和代数数据类型。同时,它也包含了命令式编程风格的元素,比如 for 循环和可变变量 let mut,以兼顾更广泛的开发者需求。
构建生态系统的挑战 🏗️
然而,构建一个完整的语言生态系统非常困难。开发者对一个语言的期望远不止一个编译器。
以下是构建生态系统所需的关键部分:
- 健壮的编译器:确保代码能编译并正确运行。
- 完善的测试和调试体验:提供排查代码问题的工具。
- 语法高亮:在代码编辑器中提供视觉辅助。
- 语言服务器:在编辑器中提供代码补全、错误提示等功能。
- 包管理器和注册中心:用于发布和共享代码库。
- 丰富的可用库:像 Python 或 Ruby 那样,拥有覆盖各种需求的库。
因此,构建一个完整的语言生态系统远比编写一个编译器复杂。
组件模型的威力:跨生态共享 🔗
WebAssembly 组件模型为我们提供了解决库生态问题的强大工具。它通过高级类型定义,使得不同语言编译的模块能够无缝组合在一起。
组件模型的核心是 WebAssembly 接口类型。WIT 是一种开发者友好的格式,用于描述组件接口。只要你能为某个功能编写一个 WIT 接口,就可以自动为多种语言生成绑定代码,实现“免费的 SDK”。
这意味着你可以从任何支持组件模型的语言中,随意引入其他语言生态系统的库,这从根本上改变了软件开发的方式。
实战演示:集成 Rust Base64 库到 Grain 应用 🛠️

上一节我们介绍了组件模型的理论优势,本节我们将通过一个具体示例来实践。我们将在一个 Grain 编写的 HTTP 代理应用中,使用 Rust 生态的 Base64 编码库。
步骤 1:识别需求与问题
我们有一个 Grain 应用,需要将用户数据(包含名称和头像字节)转换为 JSON。由于 JSON 不能直接包含二进制数据,我们需要将头像字节进行 Base64 编码。

然而,Grain 的标准库目前没有提供 Base64 编码功能。作为开发者,我们不想从头实现一个 Base64 库。
步骤 2:寻找并定义接口
我们可以在 Rust 生态中找到成熟且高效的 Base64 库(例如 base64 crate)。接下来,我们需要创建一个 WIT 文件来定义 Base64 组件的接口。
// base64.wit
package oscar:base64@1.0.0


interface base64 {
encode: func(bytes: list<u8>) -> string
decode: func(b64: string) -> result<list<u8>, string>
}
world imports {
import base64
}
world exports {
export base64
}
我们使用 warg 工具发布这个接口定义到组件注册中心。




步骤 3:实现 Rust 组件
现在,我们需要创建一个 Rust 组件来实现上述接口。我们使用 cargo-component 工具来搭建项目。
cargo component new base64-rs --lib --world oscar:base64/exports
然后,添加 base64 crate 依赖,并实现 encode 和 decode 函数。
// src/lib.rs
use base64::{Engine as _, engine::general_purpose};
pub fn encode(bytes: Vec<u8>) -> String {
general_purpose::STANDARD.encode(bytes)
}
pub fn decode(b64: String) -> Result<Vec<u8>, String> {
general_purpose::STANDARD.decode(&b64).map_err(|e| e.to_string())
}
使用 cargo component build 命令编译,即可得到一个实现了指定接口的 WebAssembly 组件。
步骤 4:在 Grain 中生成并使用绑定
接下来,我们需要在 Grain 应用中消费这个 Base64 接口。我们使用 wit-bindgen 工具为 Grain 生成绑定代码。
wit-bindgen grain --world imports base64.wit
这会产生一个 Grain 模块文件(例如 base64.gr)。我们将其引入到 Grain 应用中。
// 引入生成的绑定模块
from “./base64.gr” include { imports as base64-imports }
let Base64 = base64-imports.base64
// 在代码中使用
let encodedAvatar = Base64.encode(user.avatar)


现在,在 Grain 代码中调用 Base64.encode,就像调用任何普通的 Grain 函数一样。
步骤 5:组合组件并运行
最后一步是将 Grain 应用组件和 Rust 的 Base64 组件组合成一个完整的应用。我们使用 wasm-tools component embed 将 WIT 描述嵌入到 Grain 编译出的核心模块中,再将其提升为组件。
然后,使用 wac 工具将两个组件链接在一起。
# 使用 wac 的自动链接功能
wac plug app.component.wasm -d base64-rs.component.wasm -o app.composed.wasm
现在,app.composed.wasm 就是一个包含了 Grain 业务逻辑和 Rust Base64 功能的完整组件。我们可以使用支持 WASI HTTP 的运行时(如 wasmtime)来运行它。
wasmtime serve --wasm-features=tail-call app.composed.wasm
通过 curl localhost:8080 访问,即可看到包含 Base64 编码头像的 JSON 响应。
总结与展望 🌟

本节课中,我们一起学习了 WebAssembly 组件模型的强大能力。我们通过一个具体案例,演示了如何在约20分钟内,将 Rust 生态的库集成到 Grain 语言的应用中。这个过程涉及:
- 定义标准的 WIT 接口。
- 用 Rust 实现该接口的组件。
- 为 Grain 生成类型安全的绑定。
- 使用工具将不同语言编写的组件无缝组合。
这标志着软件开发方式的重大变革,未来开发者可以自由地混合使用不同语言生态中最优秀的库,而无需担心语言间的互操作障碍。
参与其中 🤝
如果你对以下内容感兴趣,欢迎参与进来:
- Grain 语言:如果你喜欢编程语言或编译器开发。
- 工具链:如
wasm-tools、warg、wasmtime,这些都是构建生态的关键。 - 文档:帮助完善组件模型等相关文档,降低学习成本。


组件模型正在快速发展,更高的抽象和更优的开发者体验是未来的目标。最终,这些工具将深度集成到各语言的现有工具链中,使跨语言复用库变得像今天在单一生态内一样简单自然。
025:概述与核心概念


在本节课中,我们将要学习 WebAssembly 组件模型的基础知识,并了解 C# 如何支持构建 Wasm 组件。我们将从组件的基本概念开始,逐步深入到工具链的使用和 .NET 如何简化开发体验。
WebAssembly 组件模型扩展了传统 Wasm 模块的能力,使其能够通过定义清晰的接口与外部世界(如主机或其他组件)进行交互。一个组件可以导入所需的功能(如 HTTP 请求),并导出自身提供的功能供他人调用。这种模型带来了跨平台、高性能、安全和小体积等 Wasm 固有优势,同时实现了功能的模块化共享和静态安全分析。
为什么选择 C# 来构建组件?首先,C# 语言本身功能强大且拥有庞大的开发者社区。更重要的是,.NET 能够直接生成符合 Wasm 组件模型规范(WIT)的组件,无需额外的适配层。.NET 还为常见的 Wasm 接口(如 WASI HTTP)提供了高级别的 API 支持,并且有 Componentize.NET 这样的工具来帮助开发者快速上手。
C# Wasm 组件探索:2:.NET 中的 Wasm 支持
上一节我们介绍了 Wasm 组件的基本概念,本节中我们来看看 .NET 目前提供的两种 Wasm 支持方式。
目前 .NET 主要提供两种 Wasm 支持风格:
- Mono 解释器:这种方式与 Blazor 使用的技术类似。它是一个解释器,支持反射等功能,随 SDK 一同发布,能提供完整的堆栈跟踪。需要注意的是,它目前处于实验性支持阶段,主要支持渠道是 GitHub 等社区平台。
- Native AOT(提前编译):这种方式位于 runtime-lab 仓库中。由于代码被提前编译,它具有更快的启动潜力。它通常是 Wasm 新功能最先实现的试验田,待功能稳定后会向上游运行时合并。这项工作主要由社区驱动,尚未集成到官方的 .NET 运行时中。
C# Wasm 组件探索:3:手动构建一个简单组件
上一节我们了解了 .NET 的两种 Wasm 支持方式,本节我们将动手实践,手动构建一个最简单的 Wasm 组件。


我们将构建一个仅导出一个加法函数的组件。这个过程会涉及多个工具,旨在帮助大家理解底层原理。
首先,我们需要一个定义组件接口的 WIT 文件。以下是一个简单的 world 定义:
// adder.wit
package example:adder
world adder {
export add: func(a: s32, b: s32) -> s32
}

对应的 C# 项目文件 .csproj 需要配置以支持 Wasm 编译:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<!-- 指定 Wasm 运行时 -->
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<!-- 生成独立部署的单文件 -->
<PublishSingleFile>true</PublishSingleFile>
<!-- 允许不安全代码块,为绑定生成所需 -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- 引入 Wasm 工具链 SDK -->
<WasmSdk>true</WasmSdk>
</PropertyGroup>
<ItemGroup>
<!-- 添加 Native AOT 编译支持 -->
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="8.0.0-*" />
</ItemGroup>
</Project>

在 C# 代码中,我们使用 [UnmanagedCallersOnly] 和 [Export] 属性来标记要导出的函数:
using System.Runtime.InteropServices;
public class Program
{
// 导出函数给 Wasm 组件调用
[UnmanagedCallersOnly(EntryPoint = "add")]
[Export]
public static int Add(int a, int b)
{
return a + b;
}
// 程序入口点,对于组件,此函数可能由主机调用
public static void Main() { }
}
编译并发布项目后,我们将得到一个 .wasm 文件。可以使用 wasm-tools 来检查这个组件:
wasm-tools component wit ./bin/Release/net8.0/wasi-wasm/publish/MyComponent.wasm
此命令将显示组件导出的接口,确认 add 函数已被成功导出。
C# Wasm 组件探索:4:组件组合与绑定生成
上一节我们成功构建了一个简单的导出组件,本节我们来看看如何组合组件,并利用工具自动生成复杂的类型绑定。

现在假设我们想在调用加法函数前添加一些逻辑(例如,特殊处理结果为 42 的情况)。我们可以创建第二个“包装”组件,它导入第一个组件的 add 函数,处理后再导出。

这个包装组件的 WIT 定义如下:
// wrapper.wit
package example:wrapper
world wrapper {
import adder: interface {
add: func(a: s32, b: s32) -> s32
}
export run: func(a: s32, b: s32) -> s32
}

手动为这样的接口编写 C# 绑定代码非常繁琐,尤其是处理列表、结果等复杂类型时。这时就需要 wit-bindgen 工具。它可以自动根据 WIT 文件生成 C# 绑定代码:
wit-bindgen csharp ./wrapper.wit --out-dir ./Generated

生成的 C# 代码会包含接口定义和用于“提升(lift)”、“降低(lower)”复杂类型的辅助方法。我们的实现类只需要实现生成的接口即可:

using System;
using Generated.Exports; // 注意:由于当前工具链的一个小问题,导入的函数可能在 Exports 命名空间下
public class WrapperComponent : IWrapper
{
// 实现生成的接口方法
public static int Run(int a, int b)
{
// 调用导入的加法函数
int result = adder.add(a, b);
// 添加自定义逻辑
if (result == 42)
{
Console.WriteLine("The answer to life, the universe, and everything!");
}
return result;
}
}
最后,我们需要使用 wac(WebAssembly 组件组合工具)将第一个加法组件和这个包装组件组合起来,形成一个最终的可部署组件。
wac compose -o final.wasm ./adder.component.wasm ./wrapper.component.wasm
C# Wasm 组件探索:5:使用 Componentize.NET 简化开发
上一节我们手动使用了 wit-bindgen 和 wac 等工具,步骤较多。本节我们将介绍 Componentize.NET,它极大地简化了 C# Wasm 组件的开发流程。
Componentize.NET 是一个 NuGet 包,它整合了所有必要的工具(wit-bindgen, wac, Wasm SDK),并自动处理 WIT 绑定生成和组件链接。
简化 HTTP 组件开发
.NET 库已经为 WASI 接口(如 HTTP、Cli)提供了高级别的 API 封装。这意味着开发者可以像在普通 .NET 程序中一样使用 HttpClient,而无需直接处理底层的 WIT 类型。
以下是如何创建一个使用 HTTP 的组件:
- 创建新项目并添加
Componentize.NET包引用。 - 在
.csproj中设置目标运行时为wasi-wasm。 - 编写普通的 C# HTTP 客户端代码。

项目文件配置示例:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<WasmSdk>true</WasmSdk>
</PropertyGroup>
<ItemGroup>
<!-- 添加 Componentize.NET 包 -->
<PackageReference Include="Componentize.NET" Version="0.1.0-*" />
</ItemGroup>
</Project>
C# 代码示例:
using System.Net.Http;
public class Program
{
public static async Task Main()
{
// 像在普通 .NET 程序中一样使用 HttpClient
var client = new HttpClient();
var response = await client.GetStringAsync("https://api.example.com/random");
Console.WriteLine($"Response: {response}");
}
}


发布项目后,Componentize.NET 会自动处理所有底层细节,生成一个完整的 Wasm 组件。使用 wasm-tools 检查,可以看到它自动导入了 WASI HTTP 接口并导出了 run 函数。
简化 WIT 集成与组件组合
Componentize.NET 使得在项目中引用外部 WIT 定义和组合组件变得非常简单。只需在 .csproj 中配置 Wit 属性,它就会自动下载 WIT 包(支持 OCI 注册表)、生成绑定,并在发布时自动组合组件。
<Project Sdk="Microsoft.NET.Sdk">
...
<ItemGroup>
<PackageReference Include="Componentize.NET" Version="0.1.0-*" />
</ItemGroup>
<PropertyGroup>
<!-- 指定要使用的 WIT 文件和世界 -->
<Wit>package.wit</Wit>
<WitWorld>my-world</WitWorld>
<!-- 指定 WIT 包所在的 OCI 注册表 -->
<WitRegistry>https://my-registry.com</WitRegistry>
</PropertyGroup>
</Project>
C# Wasm 组件探索:6:未来展望与总结
在本节课中,我们一起学习了 C# 与 WebAssembly 组件模型的结合。
我们首先建立了 Wasm 组件的基本心智模型,理解了组件通过导入和导出接口与主机或其他组件交互的范式。接着,我们探讨了 .NET 提供的 Mono 和 Native AOT 两种 Wasm 支持方式。
通过手动构建简单组件和包装组件的实践,我们深入了解了 wit-bindgen、wac 等底层工具链的作用。最后,我们介绍了 Componentize.NET 如何将这些复杂步骤封装起来,让开发者能够用熟悉的 .NET 方式(如直接使用 HttpClient)高效开发 Wasm 组件,并轻松集成外部 WIT 定义和组合组件。
未来展望
C# Wasm 组件生态仍在快速发展中。社区正在积极工作,未来可能会带来以下增强:
- 项目规划:在 GitHub 上有一个公开的项目看板,用于跟踪和优先处理各项开发任务。
- 调试支持:正在开发 F5 调试体验,允许开发者像调试普通 .NET 应用一样对 Wasm 组件进行单步调试。
- 更完善的 WASI 支持:包括对 HTTP/2、gRPC、TLS 以及更便捷数据库连接等功能的支持。
- 更好的互操作性:探索与原生代码库(通过 Wasm SDK)更深度集成的可能性。
- 高级别框架集成:未来可能探索将现有 ASP.NET Core 等框架中间件模式更直接地迁移到 Wasm 组件环境。
加入社区
这项工作由社区驱动,欢迎所有开发者参与。你可以通过以下方式贡献:
- 试用现有工具并提供反馈。
- 加入 Bytecode Alliance 的 C# 小组会议。
- 在相关聊天频道(如 Zulip)中交流。
- 从编写测试用例开始,逐步参与代码贡献。

Wasm 组件模型为 C# 开启了一片新的、可移植、安全且高性能的部署天地,而 .NET 正在让进入这片天地的大门变得越来越宽敞和平坦。
026:可组合的并发性 🚀

在本节课中,我们将学习 WebAssembly 组件模型(WASI)在并发性方面的最新进展,特别是即将到来的 WASI Preview 3(P3)如何解决当前 P2 版本的局限性,并引入可组合的并发性。我们将探讨其设计目标、核心概念,并通过代码示例了解其对多语言开发者的影响。
概述
WASI P2 虽然已经发布并可用,但在支持真正的可组合并发方面存在一些已知的局限性。WASI P3 的目标是解决这些问题,通过引入新的异步 ABI、支持栈式和无栈协程、以及简化接口(如 HTTP),来构建一个更强大、更符合语言习惯的组件生态系统。
WASI P2 的局限性
在深入新特性之前,我们有必要回顾一下 WASI P2 存在的几个主要限制。这些并非意外,而是为了尽快交付价值而做出的权衡。
- WASI I/O 的复杂性:WASI I/O 难以虚拟化,限制了组件间的高效通信。它主要处理字节流,对高级数据结构(如记录)的支持有限。
- 实现效率与习惯用法:在 WASI P2 中,实现诸如背压等网络流控机制显得笨拙,难以映射到大多数语言标准库的习惯用法。
poll调用的阻塞性:poll函数一次只能由一个组件调用,这会阻塞其他可能想要工作的组件。现有的变通方案会导致信息泄漏,不符合我们追求的沙箱模型。- WASI HTTP API 的冗余与复杂:API 庞大且存在冗余(例如入站和出站资源有独立的 API)。通过组合来实现流式中间件(如响应体转换)异常困难。
WASI P3 的设计目标
基于上述限制,WASI P3 设定了明确的设计目标。
- 完全向后兼容:确保 WASI P3 与现有的 P2 组件完全兼容。初期宿主将同时支持两者,后续可通过 P3 虚拟化 P2 API。
- 可组合的并发性:提供必要的原语,让各语言(如 C#、Go、Rust)自身的事件循环能够委托给宿主中更高级别的事件循环进行统一调度,实现真正的并发组合。
- 支持两种协程模型:同时支持无栈协程(如 C#、Rust 的
async/await)和栈式协程(如 Go 的 goroutine),让不同语言都能以最习惯的方式使用并发。 - 符合语言习惯的绑定:缩小底层 WASI 绑定与各语言标准库接口之间的差距,使开发体验更自然。
- 弃用 WASI I/O:将 WASI I/O 的功能下移到组件模型本身,用更通用的流和未来(future)等原语来替代。
为什么共享无物的组合如此重要?
在探讨具体设计前,我们需要理解“共享无物”的组合模型为何是 WebAssembly 组件模型的核心优势。
- 多语言互操作性:通过定义良好的接口(而非不安全的 C ABI),可以在不同语言编写的组件之间实现安全、符合习惯的互操作。
- 细粒度沙箱化:组件模型允许将应用划分为不同的信任域。你可以控制每个组件能访问的资源(如网络、文件),从而限制第三方依赖漏洞可能造成的损害。
- 供应链安全:如果一个日志组件不应该有网络访问权限,就不要赋予它。这降低了潜在安全漏洞的影响范围。
- 未来可扩展性:为未来实现运行时实例化和 Erlang 风格的监督树(动态重启子组件)奠定了基础,进一步控制故障的影响范围。
核心设计摘要
上一节我们介绍了设计目标,本节我们来具体看看 WASI P3 为实现这些目标引入的核心机制。
- 新的异步 ABI:为导入和导出函数引入了新的异步调用约定。
- 对于无栈协程,组件提供一个回调,允许宿主异步传递事件。
- 对于栈式协程,组件可以进行阻塞调用,宿主在需要等待(如网络)时会挂起该调用栈(可能切换到其他栈),这依赖于协程栈切换提案。
return-then原语:这是一个关键且强大的新概念。它允许一个导出函数在返回值给调用者(宿主)后,继续执行后续代码。这在返回流(stream)或未来(future)时特别有用,因为组件可以在返回流对象后,继续向其中填充数据。stream与future类型:这是两个新的内置类型,用于组件与宿主之间或组件之间的异步通信。stream:代表一个数据流,适用于 HTTP 响应体等场景。future:可看作单次使用的stream,类似于一次性的通道(channel)。它们共同构成了跨组件通信的基石。
案例分析:WASI HTTP
WASI HTTP 是展示新特性威力的绝佳案例。通过将复杂性下移并精炼接口,P3 版的 WASI HTTP 得到了极大简化。
- 接口简化:资源类型从 13 个减少到 4 个,设计高度对称。处理入站请求和发起出站请求使用相同的类型和函数。
- 核心类型:主要包括
body(封装请求/响应内容)、trailers(用于 HTTP/2 或 gRPC)以及headers。 - 统一处理器:一个
handler接口同时用于处理传入的请求(作为导出)和发起传出的请求(作为导入)。代理模式变得非常简单:只需同时导入和导出handler。
代码示例:流式中间件
理论需要实践来验证。让我们通过一个具体的例子——编写一个压缩响应的流式中间件——来看看 WASI P3 的代码是什么样子。以下是不同语言中的实现。
Python 示例(无栈协程)
在 Python 中,我们利用其内置的 asyncio 和 async/await 语法来实现无栈协程。
async def compress_middleware(request):
# 1. 向上游处理器转发请求,获取响应
upstream_response = await upstream_handler(request)
# 2. 创建新的响应,设置压缩头
compressed_response = Response()
compressed_response.headers = upstream_response.headers
compressed_response.headers.set("content-encoding", "deflate")
# 3. 关键步骤:生成一个后台任务来处理流式压缩
# 这个任务将在 `compress_middleware` 函数返回后继续运行
spawn(stream_and_compress(upstream_response.body, compressed_response.body))
# 4. 立即返回响应对象给宿主,宿主可以开始发送响应头
return compressed_response
async def stream_and_compress(source_body, dest_body):
# 使用标准库进行流式压缩
async with source_body.read_stream() as reader, dest_body.write_stream() as writer:
compressor = zlib.compressobj()
async for chunk in reader:
compressed = compressor.compress(chunk)
if compressed:
await writer.write(compressed)
# 刷新压缩器,写入最后的数据
final = compressor.flush()
if final:
await writer.write(final)
代码说明:spawn 函数是关键,它创建了一个异步任务,该任务在函数返回后继续运行,实现了 return-then 的语义。绑定生成器会为 stream 类型生成符合 Python 习惯的 async for 迭代器。
Go 示例(栈式协程)
Go 使用 goroutine(栈式协程)和 channel 来实现并发,代码模式有所不同但同样直观。
func compressMiddleware(request Request) Response {
// 1. 调用上游处理器(假设是同步或可通过其他方式等待)
upstreamResponse := upstreamHandler(request)
// 2. 创建新的响应和用于流式传输的管道
compressedResponse := NewResponse()
compressedResponse.Headers = upstreamResponse.Headers
compressedResponse.Headers.Set("content-encoding", "deflate")
// 3. 关键步骤:启动一个 goroutine 来处理流式压缩
// Go 关键字创建了一个新的并发执行流
go streamAndCompress(upstreamResponse.Body, compressedResponse.Body)
// 4. 立即返回响应
return compressedResponse
}
func streamAndCompress(sourceBody, destBody Body) {
// 使用 Go 标准库进行流式 IO 和压缩
reader := sourceBody.Reader()
writer := destBody.Writer()
compressor, _ := flate.NewWriter(writer, flate.DefaultCompression)
defer compressor.Close()
io.Copy(compressor, reader) // 这里处理了循环读取、压缩和写入
}
代码说明:go 关键字启动了 goroutine,这与 Python 的 spawn 作用类似。组件通过 channel(在 Body 的读写接口背后)与 goroutine 通信,这非常符合 Go“通过通信来共享内存”的哲学。现在,这种通信可以跨越组件边界。
实现状态与未来展望
目前 WASI P3 的设计与实现正在稳步推进。
- 设计规范:支持异步 ABI、
future和stream的组件模型规范修改已于上周合并。 - 原型与工具链:已有用于
wasm-tools(绑定生成)和wasmtime(运行时)的草案 PR,包含可工作的演示。 - 下一步计划:
- 完善并合并核心工具链的 PR。
- 基于新原语更新 WASI 接口定义(如文件系统)。
- 实现
jco(JavaScript 组件工具链)的支持,确保在浏览器和 Node.js 中可用。 - 为更多语言更新绑定生成器,确保生成符合习惯的代码。
- 性能愿景:设计考虑到了零拷贝的高性能场景。未来的运行时实现可以让 Guest 分配的缓冲区直接由内核填充(例如通过 io_uring),避免数据在主机和 Guest 之间的多次复制。
如何参与贡献?
如果你对这项工作的细节感兴趣并希望贡献力量,可以通过以下方式参与:
- 试用原型:查看提供的演示仓库和原型代码。
- 加入社区:在 Bytecode Alliance 的 Zulip 聊天群中交流。
- 查看项目看板:在 GitHub 项目看板上了解待办事项,我们欢迎讨论设计或代码贡献。
总结

本节课我们一起学习了 WebAssembly 组件模型在并发性方面的演进。我们从 WASI P2 的局限性出发,探讨了 P3 的设计目标:实现可组合的并发性、支持多种协程模型、并提供更符合语言习惯的 API。通过分析核心设计(如异步 ABI、return-then、stream/future)和具体的 HTTP 中间件代码示例,我们看到了 P3 如何让不同语言的开发者都能以自然的方式构建高效、安全、可组合的云原生应用。虽然实现仍在进行中,但其清晰的愿景和开放的开发过程预示着 WebAssembly 在服务端和边缘计算领域的广阔前景。
027:软件定义汽车中的组件模型

在本课程中,我们将学习 WebAssembly 组件模型如何应用于软件定义汽车领域。我们将探讨其核心优势、工作原理,并深入了解一种针对原生二进制优化的“对称ABI”方案。
概述:软件定义汽车的挑战与机遇
软件定义汽车是现代汽车架构的趋势,但其硬件环境复杂多样。典型的架构包含中央车载计算机、区域控制器和众多传感器,它们使用不同的CPU架构和操作系统。这为编写可移植的软件带来了巨大挑战。
WebAssembly 组件模型的优势
上一节我们介绍了软件定义汽车的硬件复杂性,本节中我们来看看 WebAssembly 组件模型如何提供解决方案。
传统的容器技术依赖于特定的架构和操作系统,通常只能在中央车载计算机上运行。这意味着开发者只能将软件部署到众多控制器中的少数几个上,并且必须为不同的操作系统和CPU架构进行适配。
相比之下,WebAssembly 组件具有显著优势:
- 轻量且普适:它们非常轻量,能够在包括微控制器在内的各种设备上运行。
- 安全沙箱:能够安全地运行不受信任的第三方代码,而不会危及整车架构。
- 灵活部署:支持软件在控制器之间迁移,例如在车辆断电时将关键程序(如入侵检测系统)移至功耗更低的边缘设备。
- 无重编译运行:支持在边缘设备上无需重新编译即可运行组件,便于影子部署或红蓝开发。
- 云边协同:可以轻松地将组件移至云端运行,实现数字孪生。
深入组件模型:一个实例


了解了组件模型的整体优势后,我们通过一个具体的发布-订阅应用实例来深入其内部。这个实例结合了 C++ 和 JavaScript,展示了组件模型如何实现语言中立。
组件模型的核心是 WebAssembly 接口类型。它提供了高级数据类型,如 option、result、string、vector、future 和 stream。这些类型与 AUTOSAR 自适应平台的数据类型匹配良好,使得接口定义比传统的 C 接口(例如在 Rust 和 C++ 之间)更加容易和准确,特别是在处理值的所有权传递时。
此外,调用约定通过 规范ABI 实现了标准化,为组件间提供了二进制接口。系统调用也被标准化,从而可以创建独立于操作系统的二进制文件。
规范ABI详解:提升与降低
上一节我们看到了组件模型的实际应用,本节中我们来看看其底层通信机制——规范ABI中的“提升”与“降低”是如何工作的。
我们以一个简单的函数调用为例:my_function 接收一个字符串参数并返回一个字符串。
以下是调用过程中数据传递的步骤:
- 调用方准备:调用方应用将字符串的内存地址和长度(例如地址
42,长度5)传递给其绑定层。绑定层准备一个用于存放返回结果的内存区域。 - 调用运行时:绑定层将源地址、长度和返回区域地址传递给运行时。
- 内存复制(降低):由于调用方和被调用方不共享内存(沙箱隔离),运行时需要在被调用方内存空间中分配新内存(例如地址
68),并将字符串从调用方复制过去。 - 调用实现:运行时调用被调用方的绑定层,传入新分配的字符串地址和长度。绑定层将其转换为对实际实现函数的调用。此时,字符串的所有权转移给了被调用方应用。
- 生成返回结果:被调用方应用生成返回字符串(例如“world”),并分配内存存储(例如地址
210)。由于ABI限制,绑定层需要将返回字符串的地址和长度写入一个已知位置(例如地址230),并返回该位置的地址。 - 内存复制(提升):运行时从地址
230读取返回字符串的信息,在调用方内存中分配新空间(例如地址440),并将字符串从被调用方复制过来。 - 清理与返回:运行时通知被调用方绑定层清理返回字符串的内存。然后,运行时将返回字符串的信息填入调用方之前准备的返回区域。最后,调用方绑定层从返回区域读取信息,并将其传递回调用方应用。
这个过程涉及多次内存分配和复制,虽然保证了安全隔离,但也带来了一定的性能开销。
转向原生二进制:对称ABI优化
上一节我们分析了规范ABI的开销,本节中我们来看看如何针对原生二进制环境进行优化。
为了追求极致的性能和能效以降低车载控制器成本,我们可以选择将组件编译为原生二进制。这牺牲了沙箱隔离,但换来了最高的CPU效率,并且保持了开发者的调试和部署体验不变。
当调用方和被调用方作为原生共享库链接到同一进程时,它们共享一切(内存、线程等)。利用这一点,我们可以优化ABI,减少不必要的复制。这种优化后的ABI称为对称ABI。
在对称ABI中:
- 直接调用:调用方绑定层直接调用被调用方绑定层,无需中间的运行时。
- 简化复制:虽然由于值传递语义可能仍需复制字符串,但所有内存分配和访问都在共享地址空间内完成,效率更高。
- 保持接口:对上层应用和下层的实现而言,接口没有变化,保持了源代码兼容性。
更进一步,如果我们修改调用方与被调用方之间的绑定层接口,使其也使用类似 (ptr, len) 的切片形式传递字符串,甚至可以消除实现侧的复制操作,达到理论上的最高效率。
对称ABI的灵活性与展望
对称ABI不仅高效,而且灵活。通过将组件间的链接从直接链接替换为一个充当代理的共享库或进程间通信桥接,我们可以在不重新编译组件的情况下,重新引入完全隔离。同样,我们也可以嵌入一个 Wasm 运行时,通过一个小桥接将对称ABI调用转换为规范ABI调用,从而无缝集成 WebAssembly 组件以实现沙箱化。
使用对称ABI还能带来一些独特优势:
- 零开销插件:可在 AUTOSAR 运行时或嵌入式系统中创建高效插件。
- 稳定的 Rust 模块接口:为不同编译器版本的 Rust 模块之间提供二进制兼容的稳定接口。
- 完全宿主能力:插件可以无限制地使用宿主能力,便于与硬件或网络交互(但也需充分信任插件)。


当然,在实际生产中应用仍面临一些挑战,例如对 C++ 多线程和异常处理的完整支持、为功能安全设计确定性(零动态)内存分配、以及为高级驾驶辅助系统所需的大数据量传输设计零拷贝共享内存机制等。目前这些领域已有一些原型探索。
总结
本节课中我们一起学习了 WebAssembly 组件模型在软件定义汽车中的应用。我们了解到:
- 组件模型以其轻量、可移植和安全的特点,非常适合异构、复杂的汽车硬件架构。
- 规范ABI通过提升和降低机制,在语言无关的组件间实现了安全的、标准化的通信,但会引入一定的复制开销。
- 针对原生二进制共享内存的场景,对称ABI 可以大幅优化性能,减少不必要的复制,同时保持源代码兼容性。
- 对称ABI设计灵活,允许在效率与隔离之间进行运行时权衡,并支持与 WebAssembly 沙箱组件的混合使用。

总而言之,对称ABI 针对共享内存场景优化了组件模型,它是完全源代码兼容的,并且混合使用沙箱化和共享内存化组件仍然是可能的,这充分发挥了两种模式的优势。
030:WAM 🛠️

概述
在本教程中,我们将学习一个名为 WAM 的新工具。WAM 是一个用于 WebAssembly 字节码插桩的领域特定语言。插桩是指在程序执行过程中注入额外代码,以进行监控、调试或分析等操作。WAM 的独特之处在于,它抽象了底层的注入技术,允许开发者通过统一的 DSL 编写插桩逻辑,然后由 WAM 编译器决定是通过引擎接口还是字节码重写的方式来实现注入。
什么是 WAM?🤔
WAM 是一个新的 WebAssembly 插桩 DSL。它抽象了底层的注入技术,因此你可以通过两种主要方式注入你的插桩逻辑:
- 与引擎接口:通过特定的引擎 API 动态附加回调函数。
- 字节码重写:直接修改 WebAssembly 模块的字节码,注入监控代码。
目前,WAM 可以与 Wizard 研究引擎接口,因为它是目前唯一支持管理插桩的引擎。对于字节码重写,WAM 使用了一个名为 Orca 的新 Rust 库。
为什么插桩很重要?🔍
调试低级别的字节码非常困难。随着 WebAssembly 的应用场景超越浏览器,其工具生态仍然匮乏。通过插桩,我们可以构建强大的工具来改善这一状况。
以下是可以通过插桩构建的工具示例:
- 动态分析:例如,为开发者显示代码执行时的火焰图。
- 覆盖率统计:显示代码运行时的覆盖率信息。
- 控制流信息:展示 WebAssembly 字节码执行过程中发生的不同控制流路径。
- 调试器:支持单步执行代码,这对开发者至关重要。
所有这些工具都可以通过插桩来构建。插桩意味着向程序的执行中注入一些代码,以执行特定的操作(如监控、记录)。其可能性仅受限于你的想象力。
WAM 的插桩方法 ⚙️
WAM 支持两种主要的插桩方法,各有优劣。
方法一:字节码重写
这种方法直接修改原始的 WebAssembly 模块字节码。例如,如果你想监控一个 call 字节码,WAM 会直接在该字节码位置注入更多的监控字节码。
WAM 使用一个名为 Orca 的新 Rust 库来执行这种字节码注入。Orca 取代了旧的、不再维护且不支持组件模型的 wasm-rewrite 库。Orca 的 API 更直观,更容易调试插桩后的模块。
方法二:引擎 API 接口
这种方法通过 Wizard 引擎的 API 进行插桩。当应用程序被引擎加载时,WAM 会动态地将回调函数附加到特定字节码上。这些回调函数使用 Virgil 语言编写。
例如,监控一个 call 指令时,引擎会看到附加的回调并执行相应的 Virgil 函数(例如 increment_count),从而动态地计算该 call 被执行的次数。
权衡与选择
在软件工程中,选择总是伴随着权衡:
- 使用引擎接口:你的插桩逻辑将绑定到特定引擎(例如 Wizard)。优点是可能获得更好的性能或访问引擎特有事件。
- 使用字节码重写:你的插桩逻辑可以在任何支持 WebAssembly 的引擎上运行,因为你只是注入了更多的标准 WebAssembly 字节码。但需要注意,你可能会引入新的依赖。
WAM 的优势在于,它提供了“两全其美”的方案。你可以编写一次插桩脚本,WAM 编译器会根据目标环境自动选择最合适的注入策略(如果目标引擎支持 API 接口,则使用之;否则,回退到字节码重写)。
不过,有一个小限制:有些事件(如垃圾回收、线程管理、程序退出)无法通过字节码直接观察到,只能通过引擎接口进行插桩。
WAM 插桩的构成 📝
一个 WAM 插桩脚本主要包含两部分:
- 匹配规则:指定在应用程序的何处注入代码。
- 注入逻辑:指定注入什么代码。如果 DSL 提供的功能不足,你还可以选择性地提供一个辅助的 WebAssembly 模块来包含更复杂的逻辑。
根据注入方式的不同,编译过程略有差异:
- 字节码重写:输入是应用程序模块和 WAM 脚本。WAM 编译器查找匹配点,注入逻辑,并输出一个已插桩的应用程序模块。
- 引擎 API 接口:输入只有 WAM 脚本。编译器输出一个通用的监控器模块。引擎在加载应用程序时,会读取这个监控器模块中编码的匹配规则,并动态附加相应的回调函数。
核心要点:WAM 编译器可以针对上述任一目标进行编译。

WAM 语法示例 📖
WAM 的语法灵感来源于 DTrace 的 D 语言。以下是一个简单的示例:
global counter = 0; // 全局状态
probe wasm.opcode = br_if { // 匹配规则:当看到 br_if 操作码时
location = before; // 注入时机:在该操作码执行之前
predicate = pc == 25; // 进一步约束:仅当在函数内的 PC 偏移量为 25 时才匹配
body = { // 注入的逻辑体
counter = counter + 1;
}
}
global:定义全局状态变量。probe:定义一个探针。wasm.opcode = br_if指定要匹配的 WebAssembly 操作码。location:指定在匹配点的之前、之后还是替换执行注入逻辑。predicate:使用谓词进一步限制匹配条件。pc是一个由 WAM 提供的全局变量,表示函数内的程序计数器偏移量。body:当匹配成功且谓词为真时要执行的逻辑。
针对引擎的编译与优化 🚀
当 WAM 针对引擎 API 进行编译时,需要将 DSL 脚本编译成引擎可以理解的格式。WAM 选择将回调逻辑编译成 WebAssembly 模块,以保持未来的可移植性,而不是绑定到 Wizard 引擎特有的 Virgil 语言。
编译过程如下:
- 全局状态:被编译为 WASM 模块中的全局变量。
- 逻辑体:被编译为 WebAssembly 函数。
- 匹配规则:被编码在 WASM 模块的导入名称中,告诉引擎在何处附加回调。
- 谓词:被编译为返回布尔值的 WebAssembly 函数,并编码在导出名称中,供引擎在匹配时进行条件判断。
这里存在一个挑战:谓词可能包含动态数据(例如,依赖于运行时栈顶的值),而引擎在加载时(匹配阶段)需要静态地找到所有匹配点。
WAM 的解决方案是进行静态分析,将谓词拆分为静态部分和动态部分:
- 静态部分:在匹配时由引擎评估。如果静态部分为假,则根本不需要附加回调。
- 动态部分:被包装在回调函数体内,在运行时进行条件判断。
编译器还可以进行优化。例如,通过构建真值表和常量传播,可以消除某些匹配点上的所有动态检查,从而显著提升插桩代码的运行效率。
实战演示:编写分支监控器 🎬
上一节我们介绍了 WAM 的基本语法和编译原理,本节我们将通过一个实际例子来看看如何用 WAM 编写一个有用的工具。
我们将编写一个分支监控器,用于动态统计 WebAssembly 代码中分支指令(br 和 br_if)的执行情况。

假设我们有一段 Rust 代码编译成了 WebAssembly。我们想监控其中 calc 和 print_x 函数内的分支行为。
以下是完整的 WAM 脚本:
// 定义一个映射来记录分支次数:键为 (函数ID, PC偏移量, 是否跳转),值为次数
global count = map<(u32, u32, i32), i32>();
// 监控无条件分支 br
probe wasm.opcode = br {
location = before;
// 只监控特定函数内的 br
predicate = func_name == "calc" || func_name == "print_x";
body = {
// br 总是会跳转,所以 taken 为 1
count[(fid, pc, 1)] = count[(fid, pc, 1)] + 1;
}
}
// 监控条件分支 br_if
probe wasm.opcode = br_if {
location = before;
// 只监控特定函数内的 br_if
predicate = func_name == "calc" || func_name == "print_x";
body = {
// arg0 提供了栈顶值,决定是否跳转
i32 taken = (arg0 != 0) ? 1 : 0;
count[(fid, pc, taken)] = count[(fid, pc, taken)] + 1;
}
}
// 定义一个报告变量,用于输出结果
report count;
脚本解析:
- 我们使用
global定义了一个映射count来存储统计结果。 - 我们定义了两个
probe,分别匹配br和br_if操作码。 predicate使用了func_name这个 WAM 提供的全局变量,确保只监控我们关心的函数。- 在
body中:- 对于
br,我们知道它总是跳转,所以taken固定为 1。 - 对于
br_if,我们通过arg0(代表栈顶值)动态判断是否跳转。 - 我们使用
fid(函数ID)和pc(PC偏移量)来唯一标识每个分支点。
- 对于
- 最后,
report count;语句告诉 WAM 需要输出count变量的内容。默认行为是通过wasmtime打印到控制台。

使用 WAM CLI 工具对目标 Wasm 模块应用此脚本后,运行程序,我们就能在控制台看到类似下面的输出,展示了每个分支点被跳转和未跳转的次数。

然而,最初的输出可能不够直观(例如,显示为 (17, 107, 0): 4)。为此,WAM 提供了 “非共享变量” 特性来改善可读性。

我们可以用非共享变量重写监控器:
// 为每个探针点创建独立的‘taken’变量实例
unshared i32 taken;
probe wasm.opcode = br {
location = before;
predicate = func_name == "calc" || func_name == "print_x";
body = {
taken = taken + 1; // br 总是跳转,直接递增
}
}
probe wasm.opcode = br_if {
location = before;
predicate = func_name == "calc" || func_name == "print_x";
body = {
// 根据条件决定是否递增
if (arg0 != 0) {
taken = taken + 1;
}
}
}
report taken;
关键变化:
- 使用
unshared i32 taken;声明变量。这意味着每个匹配到的探针位置都会有自己的taken变量实例,记录该特定分支点的跳转次数。 - 移除了复杂的映射结构,逻辑更清晰。
- 报告输出时,WAM 会自动将每个探针点的信息(如来自哪个函数、哪个PC偏移量)与它的
taken值关联起来输出,可读性大大增强。
未来路线图与发展方向 🗺️
WAM 目前仍在积极开发中,目标是尽快推出一个最小可行产品。未来的计划包括:
- 完善 MVP:完成字节码重写和 Wizard 引擎接口的核心功能。
- 降低系统要求:目前需要 Wasm 多内存和
wasmtime支持,未来将使其更通用。 - 可配置的报告行为:允许用户自定义报告变量的输出方式(如写入文件、发送到网络等),而非仅打印到控制台。
- 支持新特性:添加对 WebAssembly GC 类型、组件模型等的插桩支持。
- 标准化引擎 API:推动一个跨引擎的、标准化的插桩 API,实现真正的可移植性。
- 可组合的插桩:支持对单个应用程序同时应用多个监控器。
- 可视化工具:构建 IDE 插件(如 VS Code 扩展),将低级别的插桩数据转化为开发者友好的可视化界面(如火焰图、源码映射调试)。
- 映射回源代码:与 Orca 库协作,确保插桩后能通过 DWARF 等调试信息将观察到的低级别事件映射回高级语言源代码。
总结
在本教程中,我们一起学习了 WAM,一个用于 WebAssembly 字节码插桩的强大 DSL。
我们首先了解了插桩的概念及其在构建调试和分析工具中的重要性。接着,我们探讨了 WAM 的两种核心插桩方法:字节码重写和引擎 API 接口,并分析了各自的优缺点。WAM 的核心优势在于抽象了这些方法,让开发者用一套 DSL 即可编写可移植的插桩逻辑。
我们深入学习了 WAM 的脚本语法,包括如何定义全局状态、编写匹配规则和注入逻辑。我们还了解了 WAM 编译器如何将脚本智能地编译为 Wasm 模块,并利用静态分析优化谓词判断。
通过一个分支监控器的实战演示,我们看到了如何使用 WAM 编写有用的分析工具,并利用非共享变量等特性提升输出的可读性。



最后,我们展望了 WAM 的未来发展,包括对更复杂 Wasm 特性的支持、标准化引擎 API 的愿景,以及最终为多语言、多平台的 WebAssembly 生态带来丰富、易用的开发者工具链的宏伟目标。

WAM 为 WebAssembly 的工具生态开辟了新的可能性,让开发者能够更轻松地观察、理解和优化他们的 Wasm 应用。
031:安全基础与最佳实践

在本节课中,我们将学习WebAssembly(Wasm)的核心安全特性、它与传统容器技术的区别,以及在构建和运行Wasm模块时应遵循的安全最佳实践。我们将探讨沙箱机制、内存安全、组件模型以及如何通过精细化的权限控制来缩小潜在的攻击面。
概述
WebAssembly是一种可移植的编译目标,其设计初衷就包含了安全沙箱。与传统的容器技术相比,Wasm在默认情况下提供了更强的隔离性。然而,充分利用其安全优势需要开发者转变思维模式,避免将操作系统级别的假设带入Wasm开发中。
从容器到Wasm:安全视角的转变
上一节我们介绍了课程概述,本节中我们来看看对于熟悉容器生态的开发者而言,转向Wasm时需要特别注意的安全差异。
Wasm模块(.wasm文件)是一个编译目标。例如,使用Go语言并指定-target wasm参数会输出一个Wasm二进制文件,该文件可以在任何支持Wasm的运行时上执行。
在云原生环境中,通常会将Wasm模块打包进OCI(Open Container Initiative)镜像中。CNCF的Wasm工作组已经推进了OCI规范,使其能够支持Wasm构件。这意味着现有的容器工具链(如镜像扫描工具)基本上可以继续使用。你可以扫描OCI镜像,了解其中的Wasm模块需要导入哪些API,从而提前评估安全风险。例如,如果一个模块声明需要网络套接字(WASI sockets),你就需要确保运行环境能够安全地沙箱化套接字访问。
从Kubernetes和容器的角度来看,关键区别在于:在Kubernetes中,容器的安全配置(如cgroups、namespaces)实际上是由节点操作系统处理的。而Wasm在执行时,其自身就运行在一个符合规范定义的沙箱内。这意味着你可以在Kubernetes的进程安全层之下,再增加一层Wasm模块级别的沙箱。这种分层安全模型,尤其是在处理每个请求时都能提供独立的沙箱环境,使得Wasm的默认安全态势远优于传统的进程容器。
WebAssembly的安全基石:沙箱与线性内存
理解了与容器的区别后,我们深入探讨Wasm安全的两大支柱:沙箱执行和线性内存模型。
Wasm运行时提供了核心的沙箱环境。一个Wasm模块只能访问它自己创建或通过导入获得的内存。模块中的每次加载或存储操作都会进行边界检查,确保不会越界访问。这意味着,默认情况下,Wasm代码无法干扰宿主进程中的其他内存。
以下是Wasm内存访问的基本约束:
- 代码执行:模块只有在被调用时才会运行。
- 内存访问:只能访问其线性内存边界内的数据。
- 外部交互:只能调用通过导入(imports)提供给它的宿主函数。
这种设计使得沙箱化变得自然而高效,无需像传统容器那样进行大量复杂且针对非虚拟化接口的底层系统调优。安全边界被简化为函数调用和参数传递。
然而,这种强大隔离性的一个关键前提是:运行时的实现必须正确无误。如果运行时本身存在漏洞(例如错误地禁用了边界检查),或者添加了不安全的非标准API,沙箱就可能被破坏。因此,选择经过严格安全审计、持续进行模糊测试和消毒剂测试的运行时至关重要。
常见安全误区与“愚蠢的错误”
即使有了良好的工具和运行时,开发者仍可能因习惯而引入安全风险。本节我们来看看在Wasm开发中常见的思维误区和潜在错误。
所有工程师都会犯错,尤其是在构建复杂系统时。在Wasm的语境下,一个常见的误区是带着操作系统的思维定势进行开发。我们习惯于假设代码下方有一个完整的操作系统,可以随意进行文件操作、网络访问等。
但在Wasm中,每次做出这种操作系统级别的假设,实际上都是在声明“我写的代码可以做任何事情”。Wasm组件模型赋予你的能力,恰恰是约束攻击面。无论是模块内部的代码(guest code)还是外部的其他组件,都能被施加限制。未来十年,这将是分布式计算中一个关键的安全边界。
另一个容易被忽视的细节是:Wasm规范本身目前没有“只读内存”的概念。我们习惯了操作系统提供只读内存段作为安全特性。研究表明,缺乏只读内存是Wasm安全研究中被频繁指出的一个“缺陷”。这意味着模块内的代码有可能读取本不应访问的内存区域。这并非规范设计的疏忽,而是因为Wasm并非操作系统,且跨平台实现真正的内存保护(如mprotect)存在挑战。
幸运的是,通过内存控制提案,一个更简单的解决方案正在推进:允许在内存类型中静态声明开头的若干页为“不可读写”,随后的若干页为“只读”。这能以可移植的方式解决原始需求,而无需依赖硬件的MMU。
核心要点是:在只读内存特性普及之前乃至之后,最高优先级的习惯都应该是缩小爆炸半径和实施最小权限原则。这意味着需要仔细思考代码的功能路径,并将其分解为更小、职责更单一的单元。
组件模型:实现精细化的安全边界
面对上述挑战,Wasm组件模型提供了构建更安全应用的强大工具。本节我们来了解如何利用组件模型实现精细化的权限控制。
组件模型允许开发者灵活选择代码模块之间的隔离级别。你并非必须将每个微小的库都放在独立的组件中。相反,你可以将你认为处于同一信任域的一组代码放在同一个组件内,让它们共享内存和能力,这类似于在JavaScript中导入多个包。
同时,你也可以将不放心的库(例如依赖复杂、来自不同语言或体量巨大)放入独立的组件。此时,你需要仔细考虑授予该组件的导入(能力):
- 限制能力:如果它只需要文件系统,不要授予完整的文件系统访问权,而是提供一个仅包含必要文件的虚拟化文件系统。
- 控制爆炸半径:思考在最坏情况下,这个组件能造成多大破坏,并据此限制其资源。
这种设计使得将“最小权限原则”融入日常编程成为可能,而不再是少数专家的“黑魔法”。通过工具(如wasm-compose),可以像静态链接或动态链接一样,自然地组合这些组件。
行业工具与未来展望
最后,我们展望一下Wasm安全工具生态的未来,以及开发者应如何行动。
与容器相比,Wasm在工具链方面具有先天优势。容器是包含原生代码的可执行二进制“黑盒”,要深入分析其行为,需要追溯整个编译链,扫描工具也容易产生误报或漏报。
而Wasm模块和组件则更具声明性。你可以在运行前直接检查模块,精确了解它需要导入哪些API、它的依赖关系。这为构建强大的安全分析工具生态系统奠定了基础。例如:
- 源代码扫描器:对支持Wasm为编译目标的语言,现有代码扫描工具可以继续使用。
- 组件安全评分卡:类似于OpenSSF Scorecards,为Wasm组件提供安全评估将极具价值。
- 依赖与能力分析工具:在运行组件前,提示用户该组件需要加载大型库(如TensorFlow)并寻求确认。
行业需要共同努力构建这个生态。从开发者的角度,应该:
- 继续使用现有的代码安全工具。
- 在采用Wasm时,重新思考软件架构,拥抱“做一件事并做好”的Unix哲学,利用组件模型构建语义清晰、可虚拟化、可沙箱化的API。
- 避免简单地将Wasm视为“另一种容器”,不要试图将整个操作系统环境原封不动地打包进Wasm模块,这只会放弃Wasm提供的安全优势,而无法获得实质性的改进。
总结
本节课中我们一起学习了WebAssembly的核心安全特性。我们了解到:
- Wasm默认的沙箱和线性内存模型提供了强大的内存安全隔离。
- 从容器转向Wasm需要改变思维,避免操作系统假设,转而思考如何约束能力。
- 目前缺乏硬件无关的只读内存是一个已知点,但可通过静态内存区域提案和组件模型来缓解。
- 缩小爆炸半径和实施最小权限原则是最高优先级的安全实践。
- Wasm组件模型是实现精细化安全边界的强大工具,允许开发者灵活控制模块间的信任与隔离。
- 一个强大的、能进行静态分析和安全评估的工具生态系统是Wasm安全未来的关键,并且已经具备了良好的基础。
通过采用这些实践并利用不断发展的工具链,开发者可以充分发挥WebAssembly在构建安全、可移植、高性能应用方面的潜力。
033:在任意应用中释放 CMS 的全部潜力 🚀

在本节课中,我们将学习如何通过 WebAssembly 技术,将 WordPress 这一强大的内容管理系统(CMS)的能力扩展到传统服务器环境之外,实现在浏览器、桌面应用乃至移动端等任意环境中运行。
概述

我是 Jason Ball,Automattic 的一名工程师。今天要讨论的主题是“WordPress 遇见 WebAssembly”,即如何在任何应用程序中利用 WordPress 作为内容管理系统的全部功能。

我的职业生涯大部分时间都在以非传统的方式使用 WordPress。这要从 2006 年说起,当时我遇到了一个名为 Leo Burnett 广告公司的网站。

那时,这是我见过的最具交互性的网站之一。它给我留下了深刻印象,并让我着迷于学习如何制作类似的交互效果。这最终引导我学习了 Flash,一个基于时间线的编辑器,用于为网页构建交互式站点。
我自学了如何使用 Flash 重建这个 Leo Burnett 网站,并继续使用 Flash 创建了越来越多的网站。有一天,有人问我:“嘿,如果你为我建一个这样的网站,我能在不懂 Flash 的情况下编辑这个 Flash 站点的内容吗?”当时,我只知道如何使用 Flash 甚至是一些 ActionScript 来编辑 Flash 站点,你必须知道如何编译并将其部署到网上。因此我做了一些研究,最终发现了一个叫做 WordPress 的内容管理系统。
那时我甚至没听说过“内容管理系统”这个词。但通过研究,我发现它有一个名为 XML-RPC 的 API,这让我可以编写一个能消费来自内容管理系统的 XML 数据的 Flash 站点。所以最终的答案是肯定的:你可以使用像 WordPress 这样的工具来管理内容,并使用像 Flash 这样的工具从中获取内容。
虽然那个项目最终没有继续下去,但我在 WordPress 领域的职业生涯却开始了。自那以后,我一直在使用 WordPress。多年过去,WordPress 已经发生了很大变化,但我使用它的方式却没有改变——我仍然以非常规的方式使用它。
从传统到现代:WordPress 的演变
上一节我们回顾了 WordPress 如何作为后端 CMS 与前端技术结合。本节中,我们来看看 WordPress 如何演变为一个更灵活、可分离的 API 驱动系统。
2016 年,我创建了一个名为 WPGraphQL 的项目。这是一个免费开源的 WordPress 插件,能将 WordPress 站点转换为 GraphQL API。
// 示例:WPGraphQL 允许通过 GraphQL 查询获取 WordPress 数据
query {
posts {
nodes {
title
content
}
}
}
这使得用户可以将 WordPress 用作 CMS,然后使用 Next.js、Gatsby、Astro 甚至原生 iOS 等技术构建解耦的(或称无头)前端。内容在 WordPress 中管理,但在其他环境中渲染。此时,你可能会想:“嘿,伙计,这跟 WebAssembly 有什么关系?”
WebAssembly 的引入:WordPress Playground
作为 WordPress 插件开发者,我需要为用户提供快速测试插件的能力。通常,这需要他们搭建一个完整的 WordPress 服务器环境,或者在现有服务器上安装并测试插件。这需要 PHP、MySQL、Apache、Linux 等一系列环境。
但我们有一个名为 WordPress Playground 的项目,它允许你在浏览器中完全运行 WordPress。其技术实现如下:
- PHP 被转换为 PHP Wasm。
- MySQL 被 SQLite 替代。
- Apache 的功能被 JavaScript API 替代。
- Linux 的功能被 JavaScript Polyfills 替代。
因此,像我这样的插件开发者可以为用户提供插件的实时预览。当你在 WordPress.org 插件库浏览超过 17,000 个插件时,如果某个插件支持此功能,你可以点击“实时预览”按钮,它将在浏览器中完全打开 WordPress,无需任何依赖,插件已激活,用户可以直接在浏览器中进行测试。
实际应用案例:内容管理的革新
以上介绍了 WordPress Playground 的基本原理。以下是它如何改变内容管理工作流程的具体例子。
对我个人而言,我使用 WordPress 作为传统 CMS 来管理营销和博客内容。但当我想管理文档时,我希望在靠近代码的地方用 Markdown 文件来管理。对于长期使用 WordPress 编辑内容的人来说,直接编辑 Markdown 感觉有些原始。许多管理内容的用户并不想在 Markdown 中管理内容。

WordPress Playground 支持这样一种工作流程:我们可以在浏览器中完全打开 WordPress,连接 GitHub。这样就能在浏览器中获取我的 Markdown 文档,将其导入 WordPress(同样,没有服务器在运行,一切都在浏览器中)。它可以从我的 GitHub 仓库导入 Markdown 文件到 WordPress 中。然后,我可以使用功能完善的 CMS 编辑内容,例如修正拼写错误等。完成更改后,我可以将其作为拉取请求(Pull Request)导回 GitHub。这允许我从任何数据源编辑内容,Markdown 只是一个例子,你可以用它来在浏览器中编辑任何其他数据源的内容,同样无需运行服务器。




无限可能:WordPress 的新应用场景
这些只是你以前可能未曾想到的使用 WordPress 的两个例子。我们可以在终端、桌面、移动前端等各种场景中使用 WordPress。
另一个例子是,你可以将 WordPress 嵌入网页,甚至可以在没有网络连接的情况下执行 PHP 函数。





你可以在移动端的原生应用中使用 WordPress,也可以在终端应用中使用。这是一个名为 Studio 的桌面应用程序,你可以通过点击几下按钮就在本地启动 WordPress 站点进行测试。

我们目前也正在开发一些 WordPress 的 VS Code 扩展。


总结与资源
本节课中,我们一起学习了如何通过 WebAssembly 和 WordPress Playground 项目,打破 WordPress 对传统服务器环境的依赖,使其能够在浏览器等任意环境中运行。这为插件测试、跨平台内容编辑和新型应用集成开辟了新的可能性。

核心在于将 PHP -> PHP Wasm,并用浏览器技术栈替代其他服务器组件。
这一切都是免费开源的。无论你是否对 WordPress 感兴趣,关键问题是:你能用这项技术构建什么?
如果你想了解更多信息,可以访问 wordpress.org/playground。我的 Twitter/X 账号是 @jasonball。谢谢。






036:实现基于 Couchbase 的 Wasm 原生数据库 API


在本教程中,我们将学习如何结合使用 WebAssembly (Wasm)、WasmCloud 和 Couchbase 数据库来构建一个原生数据库 API。我们将从环境设置开始,逐步了解核心概念,并最终动手编写代码。
概述
我们将通过一个实践研讨会,探索如何使用 WasmCloud 组件模型和 Couchbase 的 Wasm 接口,构建一个能够与 Couchbase 数据库交互的 Go 语言 WebAssembly 组件。您将学习到 Wasm 的核心概念、WasmCloud 的开发流程,以及如何将数据库操作封装为可跨语言使用的 Wasm 接口。
第一部分:环境设置与工具介绍 🛠️
首先,我们需要设置开发环境。为了获得最佳体验并避免本地网络问题,我们强烈推荐使用在线 IDE。
以下是推荐的设置步骤:
- 访问代码仓库:扫描二维码或访问下方链接,进入本次研讨会的 GitHub 代码仓库。
- 使用在线 IDE:我们推荐使用 Gitpod 或 GitHub Codespaces。点击仓库 README 中的 “Open in Gitpod” 按钮,这将自动创建一个预配置好的在线 VS Code 环境。
- 自动配置:该环境会自动拉取包含所有必要依赖(如 Couchbase、WasmCloud、
washCLI、TinyGo 等)的 Docker 镜像,无需手动安装。
如果您选择手动设置,仓库中也提供了相关说明,但这可能会花费更多时间。在研讨会期间,如果遇到任何问题,请随时举手示意,我们会提供帮助。
一切就绪后,环境将包含运行 Couchbase 服务器、构建 Wasm 组件以及进行 API 测试所需的所有工具。

第二部分:Couchbase 简介 🗄️
在深入代码之前,让我们先了解一下我们将要使用的数据库:Couchbase。

Couchbase 是一个分布式、内存优先的文档数据库,同时提供持久化存储。它起源于 CouchDB 和 Memcached 项目的合并。最初,查询需要通过编写 JavaScript 的 MapReduce 函数并创建索引来完成,过程较为复杂。后来,Couchbase 增加了完整的 SQL(N1QL)支持、全文检索、AI 集成等多种功能。

在架构上,Couchbase 包含数据服务(负责自动分片和键值存储)、查询服务、索引服务、全文搜索服务等。
对于本次研讨会,最关键的一点是 Couchbase 的数据模型:它以 JSON 文档的形式存储数据。数据组织在 Bucket(桶)、Scope(作用域) 和 Collection(集合) 中。您可以直接对 JSON 文档运行 SQL 查询。

我们的目标就是编写一个 Wasm 组件,连接到指定的 Bucket、Scope 和 Collection,并执行文档的读写操作。
传统上,Couchbase 需要为不同语言(Go、Java、Python等)维护各自的 SDK。而 Wasm 提供了一个“编写一次,随处运行”的可能性,只需一个 Wasm 二进制文件配合相应的接口定义,即可在各种语言环境中使用,这正是我们接下来要探索的。
第三部分:WebAssembly 与 WasmCloud 核心概念 ⚙️
上一节我们介绍了 Couchbase,本节中我们来看看本次技术的另一个核心:WebAssembly 和 WasmCloud。


WebAssembly 最初是作为在浏览器中运行 JavaScript 之外语言的一种方式而诞生的。您可以将其视为一种跨语言的字节码编译目标。就像 Java 编译成 JVM 字节码一样,Rust、Go、C++ 等语言可以编译成 Wasm 字节码。
Wasm 的核心特性包括:
- 可移植性:编译后的 Wasm 二进制文件可以在任何嵌入了 Wasm 运行时的环境中运行(如浏览器、服务器、边缘设备)。
- 强隔离性与安全性:Wasm 核心规范最初只包含数值操作,没有直接的文件或网络访问能力。所有高级功能(如字符串、系统调用)都通过宿主环境以受控的方式提供,这构成了其安全沙箱的基础。
- 高性能:Wasm 执行速度接近原生代码,并且函数调用通常在进程内完成,延迟极低。
WasmTime 是由 Bytecode Alliance 维护的一个旗舰级 Wasm 运行时,可以嵌入到任何应用程序中。
WasmCloud 构建在 WasmTime 之上,使其更易于使用,并增加了分布式能力。它引入了“组件模型”和“接口”的概念,使得 Wasm 模块之间可以通过定义良好的接口进行通信,这些通信可以是进程内的,也可以通过网络进行。
一个关键概念是 WIT。WIT 是 WebAssembly 接口类型语言,用于定义 Wasm 组件对外暴露或依赖的接口。这类似于 gRPC 的 proto 文件或 OpenAPI 规范。
例如,一个键值存储接口可以这样定义(概念上):
// 示例:一个简单的键值存储接口
interface kv-store {
get(key: string) -> result<bytes, error>
set(key: string, value: bytes) -> result<_, error>
}
一旦定义了这样的接口,任何实现了该接口的 Wasm 组件(无论是用 Go、Rust 还是其他语言编写)都可以被任何能消费此接口的宿主调用。Couchbase 的 Wasm 接口就包含了文档管理、键值操作、查询等多种功能。


wash CLI 是 WasmCloud 的瑞士军刀,它可以用来构建、管理、部署 WasmCloud 组件和应用程序,并且支持多种语言。
第四部分:项目结构与初始化 🚀
现在,让我们把目光转回我们的研讨会项目。环境启动后,您会看到项目代码。
项目核心结构如下:
newbase/:这是我们将要主要编写代码的 Go 语言 WasmCloud 组件。它提供了一个 HTTP API,并将请求转换为对 Couchbase 的调用。- WIT 定义文件:在项目根目录或
wit/文件夹下,存放着 Couchbase 功能的接口定义(如documents.wit)。这些文件用于生成 Go 语言绑定代码。 wasmcloud.yaml:这是 WasmCloud 应用部署清单。它定义了如何运行我们的newbase组件,并将其链接到couchbase提供者(Provider)。Provider 是负责实际与 Couchbase 数据库通信的 WasmCloud 组件。justfile:一个任务运行器配置文件,简化了常用命令(如just dev用于启动开发环境)。
启动开发环境非常简单。在终端中,进入项目根目录,运行:
just dev
这个命令会执行 wash dev,它会:
- 启动 Couchbase 服务器。
- 构建
newbaseGo 组件为 Wasm。 - 启动 WasmCloud,并按照
wasmcloud.yaml的配置,将newbase组件与couchbase提供者链接起来。 - 启动一个热重载的开发服务器,当代码变更时会自动重建。
启动成功后,您应该能在终端日志中看到 WasmCloud 和 Couchbase 的运行状态。
第五部分:验证与初次 API 调用 ✅
环境运行起来后,我们需要验证一切是否正常工作。
首先,检查 WasmCloud 组件。打开一个新的终端标签页,运行:
curl http://localhost:8080/api/v1/status
如果返回 {"status":"success","data":{}} 之类的 JSON 响应,说明 newbase 组件的 HTTP 服务器正在运行。
其次,检查 Couchbase 数据库。有几种方式:
- VS Code Couchbase 扩展:在侧边栏找到 Couchbase 图标,点击“+”添加连接。使用地址
localhost,用户名Administrator,密码password进行连接。连接后,您应该能看到已有的 Bucket(如travel-sample)。 - Couchbase 管理 UI:Gitpod 通常会在端口 8091 上运行 Couchbase 的 Web 管理界面。您可以在 Gitpod 的 “Ports” 标签页找到公开的 URL,点击访问。使用相同的用户名和密码登录。
cbsh命令行工具:在终端中直接输入cbsh可以进入 Couchbase Shell,执行如buckets list等命令。
重要提示:我们的 wasmcloud.yaml 配置中指定了 Bucket 名为 newbase。您需要先创建它。
- 在 Couchbase 管理 UI 中:进入 “Buckets” 页面,点击 “Add Bucket”,名称填
newbase。 - 在
cbsh中:执行buckets create newbase。
创建完成后,您可以尝试一个预置的 API 调用来插入文档:
curl -X POST http://localhost:8080/api/v1/documents \
-H "Content-Type: application/json" \
-d '{"doc_id":"test-doc","data":{"hello":"world"}}'
如果成功,您会收到一个包含 Couchbase 元数据(如 cas 值、partition_id)的响应。cas 是乐观锁控制的一种机制,用于确保数据更新的并发安全性。
第六部分:代码深入:Go 组件解析 🔍
现在,让我们打开 newbase/main.go 文件,看看 Go 组件是如何工作的。
首先看导入部分:
import (
// 标准库和通用库
"encoding/json"
"net/http"
"github.com/gorilla/mux" // 或类似路由库(当前示例因TinyGo限制使用了简单路由)
// WasmCloud 相关绑定和类型
"github.com/wasmcloud/component-model/bindings" // 组件模型基础类型
cbdocuments "github.com/wasmcloud/couchbase/bindings/documents" // 自动生成的Couchbase文档接口绑定
)
关键点是 cbdocuments 包,它是通过 wash 工具从 documents.wit 接口定义文件自动生成的 Go 语言绑定代码。这为我们提供了类型安全的函数(如 Get, Insert, Replace)来调用 Couchbase 功能。
接下来看路由和处理函数。示例中实现了一个简单的 HTTP 路由,将不同的路径映射到对应的处理函数,例如 handleUpsert。
我们以 handleUpsert 函数为例,看看它如何桥接 HTTP 和 Wasm 接口:
- 解析请求:从 HTTP 请求体中读取 JSON,解析出文档 ID (
doc_id)、要应用的补丁 (patches) 以及可选的初始插入数据 (insert)。 - 调用 Wasm 接口获取当前文档:使用生成的
cbdocuments.Get()函数,传入文档 ID。这个函数返回一个result类型,需要处理成功和错误两种情况。getRes, err := cbdocuments.Get(ctx, docID, getOpts...) if err != nil { // 处理错误,例如文档不存在 } // 从 result 中提取文档内容 existingDoc := getRes.Unwrap() - 应用 JSON 补丁:如果请求中包含
patches,则使用 JSON Patch 库对获取到的文档进行修改。 - 替换文档:将修改后的文档通过
cbdocuments.Replace()函数写回数据库。这里可以传入cas等选项来实现乐观锁。 - 返回 HTTP 响应:将操作结果(成功或错误信息)封装成 JSON 返回给客户端。
整个流程体现了 WasmCloud 组件的工作模式:作为无状态的业务逻辑单元,它通过定义良好的 WIT 接口与有状态的提供者(如 Couchbase 提供者)进行交互,而自身不直接管理数据库连接等资源。
第七部分:实践任务:实现批量更新 🧩
在提供的代码中,大部分 CRUD 操作已经实现,但 handleBatchUpsert 函数还是一个待完成的 TODO。这就是我们的动手任务。
目标:实现一个批量更新文档的端点。请求体应包含一个更新操作的数组,每个操作指定文档 ID 和要应用的补丁。
以下是实现思路的步骤:
- 修正路由路径:首先,检查
handleBatchUpsert函数注册的路由路径是否正确,确保它能被正确访问。 - 定义请求结构体:创建一个 Go 结构体来映射期望的 JSON 请求体,例如
BatchUpsertRequest,其中包含一个Upsert切片。 - 解析请求:在
handleBatchUpsert中,解析 HTTP 请求体到定义的结构体。 - 循环处理每个更新:遍历
BatchUpsertRequest.Upsert切片。 - 复用更新逻辑:对于数组中的每个元素,其核心操作(获取文档、应用补丁、替换文档)与单个
upsert类似。为了提高代码质量,可以考虑将这部分逻辑重构为一个独立的辅助函数,例如doUpsert(docID string, patches []Patch) error,然后在循环中调用它。 - 聚合结果:收集每个独立操作的成功或失败结果,决定是返回一个聚合响应(如列出所有失败项),还是在第一个错误发生时立即失败。
- 返回响应:向客户端返回适当的 HTTP 状态码和 JSON 响应。
在尝试实现时,您可以参考项目中已完成的 handleUpsert、handleGet 等函数,以及自动生成的 cbdocuments 包中的函数签名。
总结
在本教程中,我们一起学习了:


- 环境搭建:如何使用 Gitpod 快速配置包含 Couchbase、WasmCloud 和 Go 工具链的开发环境。
- 核心概念:了解了 Couchbase 作为文档数据库的特性,以及 WebAssembly 和 WasmCloud 如何通过组件模型和接口定义(WIT)实现跨语言、安全、高性能的服务构建。
- 项目结构:分析了研讨会项目的组成,包括 WasmCloud 组件、WIT 接口定义、应用清单和任务配置。
- 代码实践:深入查看了 Go 语言 WasmCloud 组件的代码结构,理解了它如何通过自动生成的绑定代码调用 Couchbase 接口,并处理 HTTP 请求。
- 动手任务:明确了如何通过重构和复用现有代码,来实现一个批量的文档更新接口。

通过将数据库 SDK 的能力抽象为 Wasm 接口,我们获得了一种语言无关、部署灵活且安全隔离的方式来集成数据库功能。这为构建微服务、边缘计算应用或插件系统提供了新的可能性。希望本次研讨会为您打开了探索 Wasm 原生开发生态的大门。
037:使用 Wasm 进行 FaaS 平台工程
概述
在本节中,我们将学习美国运通技术团队如何构建一个函数即服务平台,以及他们如何利用 WebAssembly 来提升该平台的开发者体验、多语言支持能力和运行效率。


平台概览:一个为开发者赋能的生态系统 🏗️
我在此分享我们关于平台构建的思考。我在美国运通的技术组织工作,领导团队为开发者构建框架和平台,以支持其业务应用的开发。我们拥有的一个核心平台是函数即服务平台。我想分享我们关于该平台如何为开发者提供价值的平台思维。

上一节我们介绍了平台的基本定位,本节中我们来看看这个平台的具体构成和核心价值。
这个函数即服务平台本质上是一个生态系统。作为一个平台,它使开发者从所有横切关注点中解放出来。开发者无需担心服务器运行位置、应用部署位置、基础设施管理、CI/CD 流水线或可观测性等问题。所有这些功能都是开箱即用的。因此,要编写一个企业级的“Hello World”应用,开发者无需在开始真正的业务逻辑工作之前,考虑所有这些横切关注点。平台会处理所有这些事务,让开发者能够专注于编写业务逻辑。
它也是一个框架。我们鼓励契约优先的开发模式。每个函数都会暴露一个特定的契约,用于与之交互。我们还提供特定语言的运行时。目前,我们支持基于 JVM 的 Java 和 Kotlin,以及基于 Node.js 的 JavaScript 和 TypeScript。我们在使用何种语言和库、如何进行异步编程等方面提供了一些有主见的默认设置。因此,开发者无需做出这些底层决策,可以直接开始处理业务逻辑。
此外,平台还提供了大多数开发者所需的工具,具体取决于他们想要构建的应用类型。平台周围有一个庞大的社区,开发者可以相互学习、发现最佳实践、寻找模式并复用代码。
从不同利益相关者的角度来看,不仅仅是开发者受益。开发者非常喜欢它,因为他们可以直接编写业务逻辑,无需考虑其他事情。从数据架构的角度来看,它提供了一种标准化的方式来创建 API。我们鼓励的契约开发模式确保每个函数都有定义良好的契约和可发现的 OpenAPI 规范。数据架构师利用这一点来标准化不同业务线之间如何暴露和编写 API。
同时,它也是进行 API 管理和治理的场所。从基础设施的角度看,它是一个单一的控制点,一个单一的强制执行点,用于处理所有我们想要进行的身份验证、授权、审计等工作,以了解公司内部正在运行哪些 API。从站点可靠性工程的角度看,它提供了开箱即用的可观测性。每个团队无需思考如何解决日志记录、指标监控等问题。他们可以在整个平台上使用相同的技术、工具、仪表板和告警系统。
这个平台非常成功,目前已经相当成熟。
平台的核心优势:冷启动、函数打包与协同定位 ⚡
上一节我们了解了平台的整体架构,本节中我们来看看驱动这个平台成功运作的几个关键技术优势。
真正让这个平台运转起来的是其独特的卖点。首先是冷启动问题。任何函数即服务平台都需要考虑这个问题。我们的函数是小的代码包,并非完整的容器。当我们部署一个函数时,是将其部署到一个正在运行的容器中。以 Java 为例,它是一个只包含业务逻辑及其所用库的小型 JAR 文件,运行在一个已部署的运行时环境中。因此,与基于容器的系统相比,这个平台的冷启动性能得到了显著改善。
另外两个优势是相辅相成的。在任何大型企业中,应用通常由不同的业务线或团队拥有和管理。每个团队管理自己的应用,在自己的空间内进行部署、运行和扩展。当你需要编写一个跨越多个应用的业务流程时,就必须离开当前环境,通过网络进行 API 调用。这也在一定程度上限制了我们如何最佳地利用基础设施,因为所有操作都停留在应用层面。
我们的平台模糊了业务线和应用所有权的界限,将所有函数纳入同一个平台。我们可以利用平台内的智能来发现并调用相互依赖的函数,从而实现高度优化的函数间调用。这些调用不会经过网络。这也使我们有机会以更高的密度打包函数,比按业务线人为划分时能做到的密度更高。
因此,函数打包和函数协同定位是这个平台真正的独特卖点。这里有一些数据,但这个平台确实统一了大量开发者的开发方式。它可能是公司内部最大的单一应用开发方式。当开发者在团队间调动时,他们可以在第一天就高效工作,因为他们可以将应用开发的专业知识带到新团队,而无需学习新的做事方式。
下一步:利用 WebAssembly 迈向新阶段 🚀
上一节我们探讨了平台现有的优势,本节中我们来看看团队希望如何利用 WebAssembly 技术来进一步提升平台能力。
我们下一步希望实现的目标是:目前我提到了 Java、Kotlin、JavaScript 和 TypeScript,但有些团队希望用 Go 开发,有些则希望用 Python,因为这是他们的首选语言或他们其他应用使用的语言。我们希望能够在无需为每种语言编写新运行时的情况下支持这一点。我们认为 WebAssembly 正好可以帮助实现这种多语言特性。
我们希望提高函数密度。虽然我们已经在一个实例中打包了很多函数,但每个函数仍然有一些可以提取出来的公共库。这可以使函数变得更小,从而能够打包更多函数,但需要以负责任的方式进行,确保函数之间不会相互干扰。这时,Wasm 强大的沙箱隔离能力就派上了用场。
语言互操作性是一个额外的优势。目前,当 Java 函数需要与 JavaScript 函数通信时,必须通过 JSON 等格式进行。而借助 Wasm 组件的语言互操作性,我们可以通过更高效的 WPRC 方式来实现,这非常有前景。
今年早些时候我们开始探索时,首先构建了一个基于 Wasmtime 的自定义运行时。随后我们发现,我们做了很多本应在社区中通用的繁重工作。就在这时,我们发现了 WasmCloud。现在,我们正在围绕 WasmCloud 构建我们的运行时,利用 WasmCloud 提供的所有功能。
关于 WasmCloud,我想重点谈两个我们非常喜欢的特性:能力提供者。Wasm 目前只能运行某些类型的工作负载,例如,对 I/O 的支持尚不完善。如果你想连接多个 Wasm 组件或连接数据库,并希望在更高层级进行连接池管理,你必须在模块或组件外部进行。这正是 WasmCloud 中能力提供者概念大放异彩的地方。我们看到了很多机会,可以研究如何通过优化多个函数的数据库连接来进一步提升性能。
老实说,这还处于早期阶段。我们今年才刚刚开始,通过与 WasmCloud 团队的合作,我们已经取得了很大进展。但我们还没有将其投入生产环境,仍处于早期阶段。我们思考的方式是使用 Wasm 组件,并思考如何在其上分层构建平台。对于那些参加了我们昨天演讲的人来说,我们深入探讨了平台组件如何分层在特定业务逻辑函数组件之上的细节。这对我来说非常有前景。我们将观察其在投入生产并获得实际经验后的表现。
此外,仅在过去六个月,我们就看到了 WasmCloud 及其周边生态系统在成熟度方面的巨大增长和改进。但是,我认为还有很长的路要走。对我来说,重点不是使用这项技术,而是作为一个平台来赋能开发者,让他们能够用自己选择的语言编写地道的程序,并且以我能以较低的平台负担支持多种语言的方式来实现。
虽然我喜欢这项技术,但也许这话不受欢迎,我不得不说:很难让人们在一个 wasm32-wasi-preview2 这样的预览版上编写关键业务应用。这无法给社区外的人带来信心。当然,昨天听到 Luke 谈论即将到来的 0.3 预览版时,我非常兴奋。我认为,要获得真正的企业采用并让人们在其上编写超级关键的应用,我们所构建接口的稳定性外观也非常重要。我们希望与社区合作,将其推进到一个阶段,让我们可以自信地说:“嘿,我们作为平台团队喜欢这项技术,你可以信任我们,在此之上运行你的关键业务应用。” 所以还有很多工作要做。我们在这里,我们希望与社区合作,实现这一目标。


总结
本节课中我们一起学习了美国运通 FaaS 平台的核心设计理念、其通过解决冷启动、实现函数协同定位带来的关键优势,以及团队如何规划利用 WebAssembly 和 WasmCloud 来进一步提升平台的多语言支持能力、函数密度和安全性,最终为开发者提供更强大、更灵活的云原生应用开发体验。
038:安全关键与Web原生相遇 - WebAssembly 如何革新嵌入式系统

概述
在本节课程中,我们将探讨 WebAssembly 如何为资源受限的嵌入式系统带来革命性变化。我们将了解其核心优势、在实时与安全关键环境中的应用、面临的挑战以及未来的发展方向。通过本次学习,您将理解 WebAssembly 如何成为连接云原生与嵌入式世界的关键桥梁。
章节 1:小组成员介绍
大家好,我是 David Brod,是 At 公司的联合创始人兼首席技术官。At 是一个面向资源受限设备的边缘应用框架。它允许开发者在最小的受限设备上构建、部署、更新、管理和监控边缘应用。我的背景主要在操作系统领域,曾在微软的 Windows 内核团队工作,并在施耐德电气积累了丰富的工业领域经验。这些经历让我深刻理解如何大规模构建、部署和管理这类系统。
我是 Chris Woods,来自西门子。我负责协调西门子在 WebAssembly 方面的工作,我们对此已研究了四到五年。西门子是一家大型企业,在座的各位很可能都接触过西门子制造、生产或维护的产品。例如,这里的电力供应、火车、轻轨以及 80% 的铁路道口都来自西门子。这些系统内部的软件架构开始变得与我们在云端看到的非常相似,但区别在于,当这些系统出错时,你不会收到 500 服务器错误,而是可能导致人员伤亡或电力中断。这两个领域之间存在惊人的相似之处,这也是我们开始研究 WebAssembly 的原因之一。
我是 Larry Kvallo,一名独立分析师,专注于云原生技术和边缘计算。随着我对边缘计算的兴趣增长,我认为必须关注 WebAssembly,以了解它将在我们接下来要讨论的领域中扮演何种角色。
我是 Dan de Mitriru,来自索尼集团旗下的 Midora。我们主要构建用于边缘计算机视觉的智能传感器。我们是在一个真实的技术需求下偶然发现 WebAssembly 的,即需要为这些传感器的可部署、可变更组件构建安全性和隔离性。我们在这方面已经工作了大约四年。
我是 Dea,正如 Bailey 刚才提到的,我是 Suusa 公司的首席技术倡导者。我的工作主要集中在云原生和 WebAssembly 领域。因此,虽然大家将从这次讨论中学习,但我可能会学到更多。
章节 2:WebAssembly 在资源受限环境中的优势
上一节我们介绍了小组成员的背景,本节中我们来看看 WebAssembly 为资源受限的嵌入式生态系统带来了哪些具体好处。
首先,这些受限设备无处不在,它们存在于我们的汽车、智能家居设备以及许多我们甚至不知道的地方。它们的共同点是资源非常受限,包括有限的 CPU、内存容量和速度、网络连接(如果有的话,也可能非常慢且延迟高),并且通常是电池供电的。
从编程模型来看,这很大程度上类似于 90 年代服务器端的做法:采用 C 或 C++ 语言的单体编程,所有代码链接成一个单一的二进制文件。在大型组织中,这带来了很多摩擦。例如,传感器组、分析组和应用支持组必须协同工作,集成代码,构建单一镜像。即使只修改一行代码,也需要重新测试整个镜像并重新部署。这种模式在更新、打补丁、故障排除或构建模块化架构方面都存在巨大阻力。
当我们审视这个世界并思考如何革新它时,再看看云原生领域,他们拥有容器、组件、动态加载等许多酷炫的工具。我们开始思考,为什么嵌入式设备不能拥有这些?如今嵌入式设备的 CPU 虽然受限,但已具备运行这些功能的能力。因此,我们开始重新构想嵌入式开发,并由此发现了 WebAssembly。
以下是 WebAssembly 带来的关键优势:
- 安全默认设计:WebAssembly 最初为在浏览器中安全运行未知来源的代码而设计,其核心安全特性对嵌入式空间极具吸引力。
- 轻量级运行时:我们可以构建非常小的运行时。我们使用 WebAssembly 微运行时,能够将其压缩到约 50KB,足以适应这类设备。昨天还有演讲提到通过精简运行时可以将其降至约 5KB。
- 软件隔离:这些设备通常没有虚拟内存或内存管理单元,因此缺乏进程边界和硬件隔离。WebAssembly 能够在软件层面实现隔离,这意味着我们可以在更便宜的硬件上大规模实现这一点。
- 多语言支持与模块化:开发者可以使用多种编程语言编写代码,实现模块化,并进行增量更新。这带来了与云中容器类似的开发体验和优势。
此外,我们的一位同事 Dominic 曾指出,WebAssembly 允许在设备上使用 C 以外的语言。随着越来越多的智能被推向边缘,有多少机器学习专家同时也是嵌入式 C 语言专家呢?非常少。WebAssembly 提供了一种将不同技能引入这些设备的方式。
目前,WebAssembly 微运行时已在约 150 万台设备中投入生产。例如,观看亚马逊 Prime 视频、Disney+ 流媒体或使用小米产品时,背后可能就有 WebAssembly 在运行。它已经成功部署,提供了类似云的隔离机制和编程风格,但运行在成本仅为个位数美元的设备上。这正在彻底改变我们构建嵌入式软件的方式。
章节 3:嵌入式环境的关键特性与考量
上一节我们探讨了 WebAssembly 的优势,本节中我们将深入了解在嵌入式环境中必须考虑的一些关键特性,特别是实时性和确定性。
首先,需要理解“实时”的含义。它不仅仅意味着“快”,更意味着“确定性”,即操作必须在已知的、有界的概率时间内完成。为了实现这一点,必须消除非确定性的来源。
以下是嵌入式环境中 WebAssembly 的关键特性与考量:
- 无垃圾回收:这是 WebAssembly 与 Java 或 CLR 等运行时的一个关键区别。垃圾回收会引入非确定性,对于内存可能只有几十到几百 KB 的设备来说是不可接受的。WebAssembly 通过线性内存来实现内存安全,这是一个关键设计。
- 提前编译:在嵌入式平台上,我们通常进行提前编译,而不是即时编译。在仅有 32KB 内存的设备上,JIT 编译是不可行的。
- 混合临界性:这是一个非常令人兴奋的概念。系统的一部分可以是真正的实时安全关键代码(可能用传统的、经过认证的 C 语言编写),而另一部分则可以运行在 WebAssembly 运行时中,用于处理用户界面、配置服务器等任务。即使后者出现故障或变慢,也不会影响前者的正确运行。这类似于云中将不同租户托管在同一服务器上的好处,但现在可以实现在没有管理程序、只运行微型实时操作系统的小芯片上。
- 确定性挑战:除了垃圾回收,内存分配器、运行时与操作系统之间不协调的线程管理,甚至缓存未命中都可能引入非确定性。这些领域仍需持续改进。
从商业价值角度看,实时性在预防性维护和制造质量方面至关重要。能够实时获取信息并立即识别错误,对于保持生产线高可靠运行和降低成本具有巨大价值。
一个现实世界的例子是航空业。飞机上的黑匣子收集所有数据,这部分是 FAA 认证的安全关键系统。但许多航空公司希望消费这些数据用于流程改进、飞行员培训和预测性维护。传统上,这需要一个硬件实现的只读端口,增加了物料成本和设计复杂度。而通过 WebAssembly 和混合临界性,可以在同一块芯片上,通过软件实现这种隔离的“只读端口”,运行数据分析组件,而无需额外的硬件成本。
章节 4:安全、认证与未来展望
上一节我们讨论了实时性和混合临界性,本节我们将关注 WebAssembly 如何增强嵌入式系统的安全性,并探讨其未来的发展方向,特别是在安全关键领域的应用。
在安全关键系统中,当出现故障时,必须以可预测的方式出错,这样你才知道会发生什么。安全性不是指特斯拉汽车在行驶时突然重启给用户带来的感受。
目前,还没有一个完全通过安全关键认证的 WebAssembly 运行时,但这正在到来。令人兴奋的是,其基础构建模块已经开始出现。例如,核心 WebAssembly 规范使用一种名为 Specte 的 DSL 编写,可以被机器读取和解释,并自动生成正确性证明。这为未来构建安全关键运行时奠定了基础。
从安全角度看,WebAssembly 带来了显著优势。传统的嵌入式系统通常是运行在同一进程空间的单体应用。一旦存在安全漏洞被利用,整个系统(包括安全关键部分)都可能被攻陷。而将系统划分为独立运行的模块后,攻击一个模块的影响范围通常会被限制在该模块内。这种模块化和纵深防御的方法可以构建更具弹性的系统。
此外,许多嵌入式设备是单体的,始终暴露着管理端口。而通过 WebAssembly 动态加载和卸载代码的能力,可以在需要时加载管理控制台,不需要时关闭它,从而减少攻击面。
法规也在推动变革。例如,欧盟的《网络弹性法案》即将出台,要求我们能够更新那些原本认为无需更新的设备。类似组件模型和软件物料清单这样的技术,可以让我们精确知道部署的每个软件组件包含什么。如果发生类似 Log4j 的安全事件,我们可以仅替换那个受影响的组件,这非常强大。
另一个关键点是,能够指定一个组件或模块被允许做什么,并且默认情况下什么都不允许。这与在 Linux 服务器上运行的情况非常不同,在服务器上通常默认拥有所有权限。而在嵌入式场景中,情况应该恰恰相反。WebAssembly 有能力实现这一点,尽管可能尚未达到 100% 的完善。
章节 5:对社区的呼吁与总结
在课程的最后,小组成员向社区发出了呼吁并进行了总结。
Stephen:我希望大家能参与我们所做的工作。我们有一个名为 Project Oker 的开源项目,它是我们运行时的核心,也是 Linux 基金会边缘计算项目的一部分。欢迎大家查看,如果有问题可以来找我。如果你正在构建这类系统,想了解如何划分应用、有哪些可用的构建工具链,我们非常乐意听取你的意见并获得你的贡献。
Chris:我们成立了嵌入式特别兴趣小组,它是 Bytecode Alliance 的一部分。我们关注核心 WebAssembly 规范和 WASI 规范的发展,并研究如何让它们在嵌入式设备上工作。如果你正在为嵌入式设备开发软件,欢迎加入我们,我们重视你的专业知识和观点。在这个小组中,思想的碰撞、演进和完善令人兴奋,这也是同行之间(即使在某些领域是竞争对手)交流的绝佳机会。
Larry:边缘计算存在巨大机遇,特别是在制造业回流和自动化需求增长的背景下。未来,甚至可以设想在边缘进行生成式 AI 推理,实现更高水平的自动化。随着边缘视觉处理等需求的增长,将需要 WebAssembly 这样的技术来推动创新和效率。
Dan:我想对非嵌入式领域的同行说一点:我们有一个观念,即任何 WebAssembly 程序都应该能在任何地方运行(“一次编写,到处运行”)。但在嵌入式领域,我们对此的关注度要低得多。是的,我们针对 SDK 编写软件并进行编译,但在实际部署和运行之间,可以采用多种方式。例如,昨天的演讲中提到了 WAMR 到 C 的转换,如果它能工作并满足所有属性,那就是一种完全合理的方式。它不是解释器,也不是 AoT 编译器,而是另一种有效的方法。我们非常愿意在部署前对构建产物进行更多处理。这也回到了组件模型的问题上,我们在哲学上认同它,但实现方式可以多种多样。例如,我们可以用工具链构建组件,然后不依赖尚不完善的规范化 ABI,而是在每个模块内部处理组件间的交互,然后这样部署。从功能角度看,问题解决了。但这可能不完全符合规范。这正是我们需要更广泛讨论的话题,也是你应该加入嵌入式 SIG 的原因。

总结
在本节课中,我们一起学习了 WebAssembly 如何为嵌入式系统带来变革。我们了解到,其轻量级、安全隔离、支持多语言和模块化的特性,使其非常适合资源受限的环境。通过实现混合临界性,WebAssembly 允许安全关键代码与更灵活的应用程序组件共存于同一设备。虽然面向安全关键领域的完全认证运行时仍在发展中,但其基础已奠定。社区通过嵌入式 SIG 等渠道正在积极合作,解决效率、确定性和性能等挑战,这些努力将使整个 WebAssembly 生态系统受益。随着边缘计算和自动化需求的增长,WebAssembly 有望在连接云原生与嵌入式世界、推动下一代边缘创新方面发挥关键作用。
039:概述与编译器介绍 🧑💻

在本节课中,我们将学习 Kotlin/Wasm 编译器的基本知识,并了解其调试功能的实现原理。我们将从编译器架构开始,逐步深入到浏览器内和浏览器外的调试技术。
大家好。感谢参加本次分享。我是 Artem Kobzar。我在 JetBrains 工作,负责 Kotlin 到 Wasm 的编译器。我之前也参与过 Kotlin/JS 编译器的工作,并且协助制定 Source Map 规范。我们将在本次分享中稍微讨论一下它。今天的日程很紧。我会尝试快速介绍 Kotlin/Wasm 编译器的一些方面和事实,以便让大家理解我们做出某些决定的原因。
Kotlin/Wasm 编译器简介


JetBrains 是一家公司,大家可能因为许多优秀的 IDE 而认识它,比如用于 Java、Rust、C++ 的 IDE。但这家公司也创造了 Kotlin 语言。你们中的一些人可能因为 Android 或“更好的 Java”而知道 Kotlin。实际上,Kotlin 是一种多平台和跨平台的语言。目前我们有四个编译器:用于 JVM、JS、通过 LLVM 的 Native,以及一个新的用于 Wasm 的编译器。
Kotlin 是开源的,所以我将引用 GitHub 仓库中的文件和实现细节。这个编译器是从零开始构建的。我们不使用任何 LLVM 或其他类似的东西。我们直接从 Kotlin 前端获取中间表示,并将其编译为 Wasm 二进制文件。同时,我们复用了 Kotlin/JS 编译器的一些组件。我们有两个子目标:一个用于浏览器,一个用于浏览器外。第一个称为 Wasm JS,第二个称为 Wasm Wasm。

我们重度依赖 GC 和函数引用提案。这意味着,如果你尝试在尚未支持这些提案之一的虚拟机中运行我们的编译器生成的二进制文件,它将无法工作。我们软性依赖异常处理提案。这意味着默认情况下我们会开启异常处理,但你有能力将其关闭。如果某些虚拟机不支持它,你可以直接关闭它,这样就能工作。这就是 Kotlin/Wasm。


Kotlin/Wasm 调试揭秘:第2章:浏览器内调试演示 🕸️
上一节我们介绍了 Kotlin/Wasm 编译器的基本架构。本节中,我们将通过一个实际应用来演示如何在浏览器内进行调试。
我们将调试一个应用程序。最初,它是为我们的 KotlinConf 开发的 Android 应用程序。通过 Compose Multiplatform,我们将其编译到了 iOS 和 Web 平台。


让我们看看如何调试它。



首先,我们直接在浏览器中调试。我们进入主屏幕。我们在这里。我们通过源代码进行调试,而不是原始的 Wasm 代码。单步执行可以正常工作。函数名非常可读,我们可以通过调用栈来回跳转。所有变量都被很好地格式化,使其不那么底层。例如,在服务中,我们可以看到一些字符串。这是基于 GC 提案的 Kotlin 字符串,但我们尝试将其格式化为更高级别的形式。

我们进入菜单屏幕,尝试调试一些操作。例如,我们进入搜索功能。我们单步进入搜索函数,尝试理解从服务器获取了什么数据。这里你也能看到一些演讲信息,它们也尝试以更可展开的形式显示。我们为许多结构构建了展开功能,以便我们可以展开并检查其内容。


这就是关于高级数据结构的调试。这是关于浏览器内调试的部分。那么关于 IDE 呢?我们也在 IDE 中设置一个断点。我们看到的情况是一样的:漂亮的名称、基于源代码的调试,以及尽可能组织成高级形式的数据。我们也可以在菜单屏幕的控制器上做同样的事情。我们尝试单步执行,例如调试不同的部分,栈深度为 0,我们单步跳过,现在深度为 1。


这看起来好吗?不完全对。因为我们需要理解可以与什么进行比较。让我们想象一下,如果调试器不向浏览器提供任何调试信息,它会是什么样子。
听起来就像这样。正如你所见,原始的 Wasm 代码,一些随机的名称,数据结构非常底层。我们将尝试逐步改进它,添加越来越多的调试信息,使其变成我们之前看到的样子。



Kotlin/Wasm 调试揭秘:第3章:改进调试信息 - 名称与源码映射 📝

上一节我们看到了缺乏调试信息时的原始状态。本节中,我们将看看如何通过添加名称和源码映射信息来显著改善调试体验。
让我们首先从名称开始。如何改进它们?
Wasm 规范中有一个名为“名称节”的自定义节。这个节的目的是引入名称,以便在你的调试器或 WebAssembly 文本格式中漂亮地显示它们。正如你在这里看到的,它涵盖了模块名称、函数名称和局部变量名称,但还不够多。因为我们使用了 GC 提案,我们也依赖 GC 提案内部的一些扩展,这些扩展使我们能够为类型和字段声明名称。我们还希望覆盖全局变量。这是另一个名为“扩展名称节”的提案。我们也用它来为全局变量命名。
我们从前端的中介表示中获取这些信息,我们的声明有名称。我们只是将其添加到此节中。如果我们尝试使用 wasm-objdump 转储为此应用程序生成的二进制文件,我们会看到模块的名称、函数的名称、局部变量的名称、来自 GC 提案的类型名称以及来自扩展名称节的全局变量名称。
一旦我们提供了所有这些信息,我们的调试器就会从原始状态变成这样,稍微好看了一些。即使在 WebAssembly 文本格式中,你也会看到局部变量和函数的名称发生了变化。但还不够好,对吧?我们肯定希望通过源代码而不是二进制文件进行调试。那么我们如何改进这一点呢?
如果有一种格式可以将我们的指令映射到源代码,那就太好了。确实存在一个名为 Source Maps 的标准,我也在尝试改进它。这实际上是一种相当简单的格式。它是一种文本格式,基于 JSON,功能不多。版本目前总是 3,一旦我们更改并集成新功能,它将会改进,版本可能会变为 4 或 5 等。

以下是该格式的核心结构:
{
"version": 3,
"file": "output.wasm",
"sourceRoot": "",
"sources": ["source.kt"],
"sourcesContent": [null],
"names": [],
"mappings": "..."
}
file 是我们为其生成此格式的文件。sourceRoot 是 sources 中路径的重复前缀部分。sources 是我们想要将指令映射到的源文件。sourceContent 是这些文件的内容。ignoreList 是一个相当有趣的部分,我们还没有使用它,但稍后我会多谈一点为什么。它的作用显然是让调试器忽略某些源文件。对于调试器来说,这意味着定义在标记为忽略的文件中的函数将不会出现在调用栈中,并且也不允许单步进入这些函数。names 部分是因为这种格式不仅适用于 WebAssembly,也适用于 JavaScript 和 CSS,而在 JS 或 CSS 中没有自定义节。mappings 是这份文档的核心。它将我们的指令映射到源代码。
每个段由逗号分隔,每个段代表以下数据:二进制文件内的绝对偏移量。之后的所有字段都是可选的:源文件索引、该文件中的起始行、起始列以及名称索引。我不会深入探讨这种格式的编码细节,因为它也使用了 VLQ 编码和差分计算,每个后续段都依赖于前一个段以减少大小。

我们使用了来自中间表示的所有数据。在 IR 文件条目中,我们有名称,以及我们提供文件内偏移量并获取文件内行和列的函数。一旦我们提供了所有这些信息,我们还需要添加一个名为 sourceMappingURL 的新自定义节。浏览器首先获取你的二进制文件,检查这个自定义节,如果存在,它也会尝试获取映射文件。

一旦我们添加了这个,我们的调试器就变成了这样。友好多了,对吧?但还不完美。
Kotlin/Wasm 调试揭秘:第4章:自定义变量格式化与待改进领域 🛠️

上一节我们通过源码映射实现了基于源代码的调试。本节中,我们将探讨如何自定义变量视图,并讨论当前面临的一些挑战和待改进的领域。

我们还希望稍微自定义变量视图,因为它们太底层了。我们如何做到这一点?这很有趣,因为如果我们在调试期间打开控制台并尝试检查局部变量,我们会发现它们看起来像 JavaScript 对象。这是因为 Chrome DevTools 和 Firefox DevTools 训练你使用表达式求值功能,而唯一可以使用的表达式求值语言是 JavaScript。因此,它们构建了代表 Wasm 内部数据的 JavaScript 对象视图。这实际上有点问题,我稍后会谈到。但因为它是一个 JavaScript 对象,我们可以使用一个名为“自定义格式化器”的功能。这是 Firefox 和 Chrome 支持的功能。它使你能够自定义 JavaScript 对象。


如何操作?你需要声明一个名为 devtoolsFormatters 的全局变量。它是一个对象数组,包含 header、hasBody 和 body 属性。hasBody 表示数据是否复杂,是否应该展开。header 表示数据的预览。body 是展开部分。它接受一个对象,如果对之前的格式满意则返回 null,或者返回一个受限制的 HTML 或 JSON 格式。它看起来像这样:一个数组,第一个元素是字符串标签,之后是一个只允许 style 属性的对象,然后是子元素。如你所见,只允许 span、div、ol、ul 和 li 这些标签。

我们这样声明它,并列出了所有我们想要自定义的 Kotlin 类型。我将展示一个用于类的简单格式化器。因为我们有一些实现细节字段,比如哈希表,这不是用户定义的,而是我们的实现细节,我们想隐藏这些。所以我只是遍历对象并移除这些字段,同时移除字段名的美元符号。

一旦我们将其作为导入添加到蓝图中,我们的浏览器就会看起来像……不,它默认是关闭的,我们需要开启这个功能。只有在开启之后,它才会看起来像这样。
但是 HTML 呢?如果你的 IDE 无法渲染一些随机的 HTML 怎么办?这就是我之前展示的 Fleet IDE 的情况。为了在那里自定义变量,我需要在 Kotlin for JVM 中重新实现与自定义格式化器中定义的相同逻辑。不幸的是,我无法展示源代码,但逻辑与自定义格式化器中的逻辑基本相同。
这就是目前的情况。我们能改进什么?我提到了忽略列表,忽略列表有些特殊。我的意思是,我们现在有一个问题。我来展示给你看。

这实际上非常奇怪,在某种意义上甚至有些疯狂。我们运行它。让我进入主屏幕,在字符串的某个地方。如你所见,我们停在了字符串上。我们可以做一些奇怪的事情。这是一个字符串字面量。但在底层,我们使用了一些内部函数来检查字符串是否已存在于字符串池中,如果存在,我们就不会创建新的,这是一些优化。但因为我们使用了一些函数来构建字符串,我们可以单步进入字符串字面量,进入一些随机的 WebAssembly 代码。这绝对是我们不想提供给用户的。在这里,忽略列表可以帮助我们。我打算做的是声明一个虚拟文件,比如叫做忽略文件,并将所有像这样的指令(比如内部函数)引用到这个文件。这样,这个函数就会被忽略,不会单步进入,也不会出现在上下文中。

为什么我们之前没有使用它?因为它在 Chrome 中有一段时间不工作。感谢 Chrome 团队的 Eric Li 等人,我联系了他们,询问如何实现,他们帮助我实现了它,并且它已经合并到 V8 中,我希望它能在 Chrome 的下一个版本中可用。

下一件事,新的作用域提案。正如我所说,Source Map 是一个标准和规范,它正在逐步改进。有很多新的提案。其中之一就是作用域。如果我们检查这个区域,你会看到有很多在源代码中未声明的随机变量,我们也不想向人们展示它们。作用域提案可以帮助我们解决这个问题。我们只需在 Source Map 中声明作用域,并说明在这个作用域中有以下变量。这样,只有这些变量会显示在调试器中。

它还可以帮助我们解决另一个问题:在 Kotlin 中我们有内联函数。目前,在 Source Maps 中,没有可能声明这是内联函数体。这意味着我们无法单步跳过任何函数,总是会单步进入这个内联函数。作用域提案也可以解决这个问题。
下一件事,表达式求值。这实际上是一个棘手的领域,因为我不知道如何改进它。这是我遇到的第一个问题。因为我使用了自定义格式化器,让我们尝试检查 agendaDelegate.reader.kind,显然它显示在我们的局部变量中,但它是未定义的,因为传递的值是自定义后的,原始值是不同的,所以我们需要使用 .value、.field、.value,这有点烦人。我现在不知道如何解决这个问题,但我们正在与 Chrome 工具团队大量合作以寻找解决方案。
第二个问题,你还记得我提到过所有的 WebAssembly 值都表示为 JavaScript 对象吗?所以你没有对原始结构的引用。如果你想在调试期间调用一些接受该结构作为参数的函数,你会失败,因为你没有原始引用。例如,在这里,我尝试使用字符串并调用已声明的 isWhitespace 函数。如你所见,出错了,因为它是 JavaScript 对象,不是结构体。我们没有对结构体的原始引用。这也是一个问题,我们也在尝试解决。我们提出的一个解决方案是,在这个对象内部也持有对原始结构的某种引用,这样我们就可以提供引用本身,而不是这个 JavaScript 对象。


这就是关于浏览器内调试的内容。那么浏览器外调试呢?


Kotlin/Wasm 调试揭秘:第5章:浏览器外调试与未来展望 🚀
上一节我们探讨了浏览器内调试的细节与挑战。本节中,我们将简要了解浏览器外调试的现状,并展望未来的改进方向。


我无法向你展示演示。这个演示是我不久前创建的。它不像之前的演示那样有前景。我只有几个简单的测试文件。我可以运行它。顺便说一下,这是 Wasmtime。然后运行 LLDB,连接到 LLDB。我们需要稍等片刻。我们停下来了,停在了 startTest 这里。我们可以在 box 函数处设置断点。定义在这里。然后继续执行,我们停在了这里。是的,它不像之前那样好。问题是,正如我所说,这个演示,即生成浏览器外调试信息的基本功能,大约是一周前创建的,我上周二才真正完成。我们继续努力,它也在不断改进。这实际上是一个相当大的领域。
我来解释一下原因。首先,我们仍在积极开发中。我们目前正在努力使其在 Wasmtime 和 Wasmer 上工作,因为这两个运行时为我们提供了可以使用的调试和 GC 提案支持。

其次,DWARF。我不会解释 DWARF,因为其规范大约有 500 页。我需要三场这样的分享才能解释整个格式。让我们看看我生成了什么。这实际上非常基础。我们生成了一些新的节,也是自定义的,如 .debug_info、.debug_str、.debug_line 和 .debug_abbrev。只有 .debug_info 和 .debug_line 对我们有趣。.debug_line 表示从指令到原始源代码的映射,.debug_info 表示更高级的结构,更像是关于声明的信息,比如声明函数、局部变量、类型等。对于我展示给你的这些文件,我们生成了这种调试信息。我们在这里看到的是编译单元,我们声明这是我们生成的二进制文件,以及几个函数:一个是我们中断的 box 函数,第二个是 runBoxTest,第三个是 startTest。
不算多。这里有趣的是偏移量。在 Source Map 格式中,偏移量是绝对的,从二进制文件开头算起。但在 DWARF 中,它是相对于代码节的。这是因为在 Source Maps 中,他们也试图覆盖全局变量中有表达式的情况。而在 DWARF 中,目前你无法覆盖全局变量中的表达式,因为它是相对于代码节的。
第二件有趣的事。我在推特上提到过,这部分我们使用了 Kotlin。为什么我们这里有 C++ 语言?这是因为在 LLDB 内部似乎有一个语言白名单。如果你声明 Kotlin、Swift 或其他语言,实际上它不会工作,你将无法中断。我花了三个小时比较 Clang 的输出和我的输出后才意识到这一点。但这很有趣,实际上也很明显。LLDB 是 C、C++、Objective-C、Objective-C++ 的调试器。
这是调试行信息。它实际上是整个程序中帮助你定义如何映射的虚拟机器。当我们用 dwarfdump 检查它时,我们会看到所有的映射都是这样的。顺便说一下,关于 dwarfdump,我可以使用常规的 LLVM dwarfdump 吗?因为 GC 提案,LLVM dwarfdump 也会尝试解析你的二进制文件。结果,它只是遇到未定义的指令就说“我失败了”。感谢 Gingerly 项目,在他们的示例中有他们自己的 dwarfdump,这就是我使用的那个。另外,有一个名为 addr2line 的 Wasm 工具对我真的很有帮助,我们可以提供二进制文件内的绝对偏移量,它会告诉我们映射到哪个文件以及哪一行。
未来的工作领域很多。首先,我想至少让它在 Wasmtime 上完全工作,能够按行和文件(也许还有列)中断。其次,让它在 Wasmer 上工作,因为即使有这些调试信息,它在 Wasmer 上也不工作,我不知道为什么。第三,我想让它为你的虚拟机工作。如果你是为 Wasmtime、Wasmer 或其他任何支持 GC 提案的虚拟机做贡献的开发者,请联系我。我希望它也能为你的虚拟机工作。当然,因为 JetBrains 提供工具,我们希望有一个好的工具链。我们想将其集成到我们的 IDE 中,以提供无缝且良好的开发者体验。
Kotlin/Wasm 调试揭秘:第6章:总结与资源 📚
本节课中,我们一起学习了 Kotlin/Wasm 调试的方方面面。我们从编译器架构入手,深入探讨了浏览器内调试如何通过名称节、Source Maps 和自定义格式化器实现友好的调试体验。我们还了解了浏览器外调试的初步实现及其依赖的 DWARF 格式,并展望了未来的改进方向。

以下是一些有用的资源链接:
- 如果你对 Kotlin 和 Wasm 感兴趣,想了解更多关于 Kotlin/Wasm 的信息。
- 我们的公共 Slack 频道。你可以加入它,这是一个庞大的 Kotlin Slack,但里面有一个 wasm 房间,你可以在那里找到我。
- 如果你有兴趣合作让 Kotlin/Wasm 在你的运行时上工作,或者你有兴趣为 Source Map 规范做贡献,也请在这里联系我。
- 我也分享了我的 Twitter。谢谢。

浙公网安备 33010602011771号