Go to my github

【译】使用.NET将WebAssembly扩展到云(一)

原文 | Richard Lander

翻译 | 郑子铭

WebAssembly(Wasm)是一种令人兴奋的新虚拟机和(汇编)指令格式。 Wasm 诞生于浏览器,是 Blazor 项目的重要组成部分。 Wasm 的第二个行动是针对应用程序和功能的云计算。 WebAssembly 系统接口 (WASI) 是新的推动者,为 WebAssembly 代码提供了一种安全地跨语言调用和实现任意 API 的方法。现在可以使用 .NET 8 中的 wasi 实验工作负载通过 .NET 创建 WASI 应用程序。我们正在探索这些新技术并在此环境中运行 .NET 应用程序……。真的,任何地方。

这篇文章将帮助您了解 Wasm 的广泛使用,并描述 .NET 已经可以实现的功能。他们说历史不会重演,但会押韵。我们又回来进行另一轮“一次编写,随处运行”。 WASI 应用程序是可移植的二进制文件,可以在任何硬件或操作系统上运行,并且不特定于任何编程语言。这一次,感觉不一样了。这不仅仅是供应商的神经;一切都是中立的。

Wasm 和 WASI

Wasm 可能会为我们提供云计算的重启,并承诺提供单一云原生二进制文件、更高的密度和更便宜的多租户。出于同样的原因,它也开启了边缘计算的可能性。事实上,CloudFlareFastly 已经使用 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&amp;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&amp;color=%232EA043&amp;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-wasmtrue 并注释掉 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)

posted @ 2024-02-14 08:00  郑子铭  阅读(406)  评论(0编辑  收藏  举报