原文 | Richard Lander
翻译 | 郑子铭
WebAssembly(Wasm)是一种令人兴奋的新虚拟机和(汇编)指令格式。 Wasm 诞生于浏览器,是 Blazor 项目的重要组成部分。 Wasm 的第二个行动是针对应用程序和功能的云计算。 WebAssembly 系统接口 (WASI) 是新的推动者,为 WebAssembly 代码提供了一种安全地跨语言调用和实现任意 API 的方法。现在可以使用 .NET 8 中的 wasi 实验工作负载通过 .NET 创建 WASI 应用程序。我们正在探索这些新技术并在此环境中运行 .NET 应用程序......。真的,任何地方。
这篇文章将帮助您了解 Wasm 的广泛使用,并描述 .NET 已经可以实现的功能。他们说历史不会重演,但会押韵。我们又回来进行另一轮"一次编写,随处运行"。 WASI 应用程序是可移植的二进制文件,可以在任何硬件或操作系统上运行,并且不特定于任何编程语言。这一次,感觉不一样了。这不仅仅是供应商的神经;一切都是中立的。
Wasm 和 WASI
Wasm 可能会为我们提供云计算的重启,并承诺提供单一云原生二进制文件、更高的密度和更便宜的多租户。出于同样的原因,它也开启了边缘计算的可能性。事实上,CloudFlare 和 Fastly 已经使用 Wasm 在边缘托管公共计算。
Wasm 与在 Linux 容器中运行应用程序不同,后者是对现有标准和代码的(良好且聪明的)重新打包。 Wasm 更像是在没有操作系统的环境中运行应用程序,只有汇编代码、内存和对外部世界的标准化(和门控)访问(通过 WASI)。
Build 2023 上的 Hyperlight 演示(4m 视频)深入了解了支持 Wasm 的云的外观。它演示了在新的轻量级安全虚拟机管理程序中运行的 Blazor 应用程序。 Hyperlight 激发了新托管范例的想象力。
WebAssembly 系统接口 (WASI)、WebAssembly 接口类型 (WIT) 和 WebAssembly 组件模型是最新一轮 Wasm 创新的关键规范。它们基本上仍处于设计阶段并正在经历重大变化。这篇文章(以及 .NET 8 实现)以 WASI Preview 1 为中心。我们希望 .NET 9 实现使用 WASI Preview 2。
WIT 和 wit-bindgen 使用任何源语言编写的组件都可以与主机系统进行通信。 WIT 对 C# 支持的实现由 @silesmo 领导。 Wasm 和 WIT 一起定义了应用程序二进制接口(ABI)。
我们期望 WASI 成为一组标准的 WIT 类型,提供对低级功能的访问(例如获取时间和读取文件)。这些低级类型有效地形成了跨编程语言和操作系统的"Wasm 标准库"。例如,我们从来没有 Rust 开发人员和 .NET 开发人员可以同时使用的标准和共享功能。历史上还没有任何广泛部署的本机代码公开具有 OO 形状(如接口)的 API,可以跨编程语言和操作系统使用。
标准 WIT 类型以 wasi- 开头,定义"平台"。您可以将它们视为与 .NET 中的系统命名空间类似的方式(与 WASI 中的"S"匹配)。继续类比,您可以在 System 命名空间之外创建自己的 .NET 命名空间,WIT 也是如此。
这些帖子在更详细地构建 WASI 方面做得非常出色。
即将到来的承诺是能够采用现有的 .NET 应用程序或库并将其编译为 Wasm 目标。我们的设计本能是在 .NET 堆栈中实现相对较高的 WIT 接口(例如为 wasi-sql 创建 ADO.NET 数据提供程序),这将使现有代码(包括许多现有的 NuGet 包)能够正常工作,特别是对于没有本机依赖项。
Wasm 应用程序在 Wasm 运行时中运行,例如 wasmtime。与 Docker 非常相似,您可以使用特定功能配置该运行时。例如,如果您希望 Wasm 代码能够访问键/值存储,您可以向其公开一个键/值接口,该接口可以由本地数据库或云服务支持。
Wasm 运行时旨在可嵌入到应用程序中。事实上,有一个 wasmtime 包用于在 .NET 应用程序中托管 Wasm。 .NET 代码可以作为 Wasm 运行,但 .NET 应用程序可以托管 wasmtime?!?是的,这个空间开始看起来是圆形的。虽然这些场景看起来很循环,但它们最终可能非常有用,与 AppDomain 的使用方式大致相似。这也让人想起所有"docker in docker"场景。
我们期待更多的创新、更多的 Wasm 运行时和更多的行业参与者。事实上,Wasm 已经升级为 W3C 规范。 W3C 是 Wasm 的完美家园,让它成长为广泛的行业规范,就像之前的 HTML 和 XML 一样。
wasi-实验工作量
.NET 8 包含一个名为 wasi-experimental 的新工作负载。它构建在 Blazor 使用的 Wasm 功能之上,将其扩展为在 wasmtime 中运行并调用 WASI 接口。它还远未完成,但已经实现了有用的功能。
让我们从理论转向演示新功能。
安装 .NET 8 SDK 后,您可以安装 wasi-experimental 工作负载。
dotnet workload install wasi-experimental
注意:此命令可能需要管理员权限,例如在 Linux 和 macOS 上使用 sudo。
您还需要安装 wasmtime 来运行您即将生成的 Wasm 代码。
使用 wasi-console 模板尝试一个简单的示例。
$ dotnet new wasiconsole -o wasiconsole
$ cd wasiconsole
$ cat Program.cs
using System;
Console.WriteLine("Hello, WASI Console!");
$ dotnet run
WasmAppHost --runtime-config /Users/rich/wasiconsole/bin/Debug/net8.0/wasi-wasm/AppBundle/wasiconsole.runtimeconfig.json
Running: wasmtime run --dir . -- dotnet.wasm wasiconsole
Using working directory: /Users/rich/wasiconsole/bin/Debug/net8.0/wasi-wasm/AppBundle
Hello, WASI Console!
该应用程序使用 wasmtime 运行。这里没有 x64 或 Arm64,只有 Wasm。
dotnet run 提供额外的信息(在控制台输出中)来帮助解释发生了什么。未来这种情况可能会改变。与主机系统的所有交互均由 wasmtime 管理。
我们可以更深入地查看 AppBundle 目录。
$ ls -l bin/Release/net8.0/wasi-wasm/AppBundle
total 24872
-rwxr--r-- 1 rich staff 11191074 Oct 31 07:53 dotnet.wasm
-rwxr--r-- 1 rich staff 1526128 Oct 11 14:00 icudt.dat
drwxr-xr-x 6 rich staff 192 Nov 19 19:35 managed
-rwxr-xr-x 1 rich staff 48 Nov 19 19:35 run-wasmtime.sh
-rw-r--r-- 1 rich staff 915 Nov 19 19:35 runtimeconfig.bin
drwxr-xr-x 2 rich staff 64 Nov 19 19:35 tmp
-rw-r--r-- 1 rich staff 1457 Nov 19 19:35 wasiconsole.runtimeconfig.json
$ ls -l bin/Release/net8.0/wasi-wasm/AppBundle/managed
total 3432
-rw-r--r-- 1 rich staff 27136 Nov 19 19:35 System.Console.dll
-rw-r--r-- 1 rich staff 1711616 Nov 19 19:35 System.Private.CoreLib.dll
-rw-r--r-- 1 rich staff 5632 Nov 19 19:35 System.Runtime.dll
-rw-r--r-- 1 rich staff 5120 Nov 19 19:35 wasiconsole.dll
SDK 将应用程序发布到独立部署中。 .NET 运行时 --- dotnet.wasm --- 已经编译为 Wasm(在我们的构建机器上)。应用程序和 dotnet.wasm 在 wasmtime 中一起加载,运行所有代码。应用程序的实际托管代码(位于托管目录中)在运行时解释,就像 Blazor WebAssembly 一样。 @yowl 和 @SingleAccretion 社区成员一直在尝试 Wasm 和原生 AOT。
您可能想知道为什么我们需要将所有这些文件分开,而显然更好的选择是拥有一个 wasiconsole.wasm 文件。我们也可以这样做,但稍后会在帖子中介绍它,因为我们需要在机器上安装更多的软件(目前 wasi 实验工作负载不包含这些软件)。
RuntimeInformation 告诉我们什么?
RuntimeInformation 是我最喜欢的类型之一。它让我们更好地了解目标环境。
我们可以稍微更改示例以显示一些更有用的信息。
using System;
using System.Runtime.InteropServices;
Console.WriteLine($"Hello {RuntimeInformation.OSDescription}:{RuntimeInformation.OSArchitecture}");
Console.WriteLine($"With love from {RuntimeInformation.FrameworkDescription}");
它产生这个输出。
Hello WASI:Wasm
With love from .NET 8.0.0
第一行很有趣。操作系统是WASI,架构是Wasm。这是有道理的,有更多的背景。文章前面提到 Wasm 可以被认为是"无操作系统",但是我们不能简单地称之为 Wasm,因为现有的浏览器和 WASI 环境有很大不同。因此,该环境唯一一致的名称是 WASI,而 Wasm 明确是"芯片架构"。
Wasm 是一个 32 位计算环境,这意味着 2^32 字节是可寻址的。但是,Wasm 运行时可以配置为使用 memory64,从而可以访问 >4GB 的内存。我们还没有对此的支持。
访问主机文件系统
Wasmtime(和其他 Wasm 运行时)提供将主机目录映射到来宾目录的选项。从用户的角度来看,这与使用 Docker 进行卷挂载类似,但实现细节有所不同。
让我们看一个依赖目录安装的简单应用程序。它使用 Markdig 包将 markdown 转换为 HTML。公平地说,Markdig 并不是为了以 Wasm 的身份运行而编写的。只要能够为其创建一个舒适的管理环境,Markdig 就会很高兴,这就是我们所做的。
让我们在 Mac M1 (Arm64) 机器上尝试一下。
$ pwd
/Users/rich/git/wasm-samples/tomarkup
$ dotnet publish
$ cd bin/Release/net8.0/wasi-wasm/AppBundle
$ cat run-wasmtime.sh
wasmtime run --dir . dotnet.wasm tomarkup $*
$ ./run-wasmtime.sh
A valid inputfile must be provided.
$ wasmtime run --dir . --mapdir /markdown::/Users/rich/markdown --mapdir /tmp::/Users/rich dotnet.wasm tomarkup $* /markdown/README.md /tmp/README.html
$ ls ~/*.html
/Users/rich/README.html
$ cat ~/markdown/README.md | head -n 3
# .NET Runtime
[![Build Status](https://dev.azure.com/dnceng-public/public/_apis/build/status/dotnet/runtime/runtime?branchName=main)](https://dev.azure.com/dnceng-public/public/_build/latest?definitionId=129&branchName=main)
$ cat ~/README.html | head -n 3
<h1>.NET Runtime</h1>
<p><a href="https://dev.azure.com/dnceng-public/public/_build/latest?definitionId=129&branchName=main"><img src="https://dev.azure.com/dnceng-public/public/_apis/build/status/dotnet/runtime/runtime?branchName=main" alt="Build Status" /></a>
<a href="https://github.com/dotnet/runtime/labels/help%20wanted"><img src="https://img.shields.io/github/issues/dotnet/runtime/help%20wanted?style=flat-square&color=%232EA043&label=help%20wanted" alt="Help Wanted" /></a>
--mapdir 正在挂载从主机到来宾的目录。
如您所见,Markdown 文件已转换为 HTML。为了简洁起见,显示了每个文件的前三行。
目录挂载所需的 CLI 手势目前有点不方便。这是我们需要在未来版本中考虑的内容。这实际上是一个 dotnet run 和 wasmtime run 应该如何关联的问题。
但它能算字数吗?
我最近出版了《System.IO 的便利》,重点关注字数统计。我们能否获得与 Wasm 相同的代码来运行并看看它的运行速度有多快?
该文章中的字数统计基准测试在 Linux x64 上运行。让我们保持不变,但这次以 Wasm 身份运行。
$ pwd
/Users/rich/git/convenience/wordcount/count
$ grep asm count.csproj
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<WasmSingleFileBundle>true</WasmSingleFileBundle>
$ dotnet publish
$ cd bin/Release/net8.0/wasi-wasm/AppBundle/
$ WASMTIME_NEW_CLI=0 wasmtime run --mapdir /text::/home/rich/git/convenience/wordcount count.wasm $* /text/Clarissa_Harlowe
11716 110023 610515 /text/Clarissa_Harlowe/clarissa_volume1.txt
12124 110407 610557 /text/Clarissa_Harlowe/clarissa_volume2.txt
11961 109622 606948 /text/Clarissa_Harlowe/clarissa_volume3.txt
12168 111908 625888 /text/Clarissa_Harlowe/clarissa_volume4.txt
12626 108593 614062 /text/Clarissa_Harlowe/clarissa_volume5.txt
12434 107576 607619 /text/Clarissa_Harlowe/clarissa_volume6.txt
12818 112713 628322 /text/Clarissa_Harlowe/clarissa_volume7.txt
12331 109785 611792 /text/Clarissa_Harlowe/clarissa_volume8.txt
11771 104934 598265 /text/Clarissa_Harlowe/clarissa_volume9.txt
9 153 1044 /text/Clarissa_Harlowe/summary.md
109958 985714 5515012 total
我更新了项目文件以包含 wasi-wasm 和 true 并注释掉 PublishAot 相关属性。我还添加了一个runtimeconfig.template.json 文件。未对应用程序代码进行任何更改。
现在,我们将整个应用程序放在一个文件包中。
$ ls -l bin/Release/net8.0/wasi-wasm/AppBundle/
total 6684
-rw-r--r-- 1 rich rich 1397 Nov 19 19:59 count.runtimeconfig.json
-rwxr-xr-x 1 rich rich 6827282 Nov 19 19:59 count.wasm
-rw-r--r-- 1 rich rich 915 Nov 19 19:59 runtimeconfig.bin
-rwxr-xr-x 1 rich rich 27 Nov 19 19:59 run-wasmtime.sh
drwxr-xr-x 2 rich rich 4096 Nov 19 19:59 tmp
看起来好多了。该应用程序只有不到 7MB。我必须安装 WASI-SDK 才能使用 WasmSingleFileBundle 属性并设置环境变量以使 dotnetpublish 能够找到所需的工具。
$ echo $WASI_SDK_PATH
/home/rich/wasi-sdk/wasi-sdk-20.0/
wasmtime 最近发生了重大变化。我选择使用 WASMTIME_NEW_CLI=0 来恢复运行示例的旧行为。
让我们回到性能。首先,作为 wasm 运行(通过解释器执行托管代码):
$ time WASMTIME_NEW_CLI=0 wasmtime run --mapdir /text::/home/rich/git/convenience/wordcount count.wasm $* /text/Clarissa_Harlowe
11716 110023 610515 /text/Clarissa_Harlowe/clarissa_volume1.txt
12124 110407 610557 /text/Clarissa_Harlowe/clarissa_volume2.txt
11961 109622 606948 /text/Clarissa_Harlowe/clarissa_volume3.txt
12168 111908 625888 /text/Clarissa_Harlowe/clarissa_volume4.txt
12626 108593 614062 /text/Clarissa_Harlowe/clarissa_volume5.txt
12434 107576 607619 /text/Clarissa_Harlowe/clarissa_volume6.txt
12818 112713 628322 /text/Clarissa_Harlowe/clarissa_volume7.txt
12331 109785 611792 /text/Clarissa_Harlowe/clarissa_volume8.txt
11771 104934 598265 /text/Clarissa_Harlowe/clarissa_volume9.txt
9 153 1044 /text/Clarissa_Harlowe/summary.md
109958 985714 5515012 total
Elapsed time (ms): 821
Elapsed time (us): 821223.8
real 0m0.897s
user 0m0.846s
sys 0m0.030s
现在有了我们对 Wasm 的(甚至更多)实验性原生 AOT 支持。
$ time WASMTIME_NEW_CLI=0 wasmtime run --mapdir /text::/home/rich/git/convenience/wordcount count.wasm $* /text/Clarissa_Harlowe
11716 110023 610515 /text/Clarissa_Harlowe/clarissa_volume1.txt
12124 110407 610557 /text/Clarissa_Harlowe/clarissa_volume2.txt
11961 109622 606948 /text/Clarissa_Harlowe/clarissa_volume3.txt
12168 111908 625888 /text/Clarissa_Harlowe/clarissa_volume4.txt
12626 108593 614062 /text/Clarissa_Harlowe/clarissa_volume5.txt
12434 107576 607619 /text/Clarissa_Harlowe/clarissa_volume6.txt
12818 112713 628322 /text/Clarissa_Harlowe/clarissa_volume7.txt
12331 109785 611792 /text/Clarissa_Harlowe/clarissa_volume8.txt
11771 104934 598265 /text/Clarissa_Harlowe/clarissa_volume9.txt
9 153 1044 /text/Clarissa_Harlowe/summary.md
109958 985714 5515012 total
Elapsed time (ms): 60
Elapsed time (us): 60322.2
real 0m0.107s
user 0m0.064s
sys 0m0.045s
现在,在 Linux x64 上使用 CoreCLR 运行:
$ time ./app/count ../Clarissa_Harlowe/
11716 110023 610515 ../Clarissa_Harlowe/clarissa_volume1.txt
12124 110407 610557 ../Clarissa_Harlowe/clarissa_volume2.txt
11961 109622 606948 ../Clarissa_Harlowe/clarissa_volume3.txt
12168 111908 625888 ../Clarissa_Harlowe/clarissa_volume4.txt
12626 108593 614062 ../Clarissa_Harlowe/clarissa_volume5.txt
12434 107576 607619 ../Clarissa_Harlowe/clarissa_volume6.txt
12818 112713 628322 ../Clarissa_Harlowe/clarissa_volume7.txt
12331 109785 611792 ../Clarissa_Harlowe/clarissa_volume8.txt
11771 104934 598265 ../Clarissa_Harlowe/clarissa_volume9.txt
9 153 1044 ../Clarissa_Harlowe/summary.md
109958 985714 5515012 total
Elapsed time (ms): 77
Elapsed time (us): 77252.9
real 0m0.128s
user 0m0.096s
sys 0m0.014s
这些都是有趣的结果。我们有解释、AOT 和 JIT 代码生成方法可供比较。 Wasm 解释器能够在不到一秒的时间内计算(略低于)一百万个单词,而 AOT 编译的 Wasm 和 JIT 运行时可以在大约 100 毫秒内完成同样的操作。
注意:Main 方法是运行 main 的时间,由 StopWatch 测量。流程是整个流程的持续时间,以时间来衡量。
此图表显示了上下文中的所有结果,包括 System.IO 的便利性帖子中的结果。
wasmtime JIT 将 Wasm 代码编译到目标环境(在本例中为 Linux+x64)。例如,可以使用 wamr 对 Wasm 代码进行 AOT。我将把它留到另一篇文章中。
原文链接
Extending WebAssembly to the Cloud with .NET
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com)