跳至主要内容

在 macOS 和 Windows 上解锁 WebAssembly

·阅读时长 11 分钟
Florent Benoit
首席软件工程师

在 macOS 和 Windows 上无缝运行 WebAssembly/Wasm 二进制文件

您可能最近听说过 Wasm 和 WASI 的兴奋。想象一个世界,您可以在其中轻松运行 Wasm 二进制文件并使用开放容器倡议 (OCI) 容器镜像分发它们 - 一个可以在多个架构上部署的单个镜像。

虽然这个概念看起来很简单,但实际上要完成这项任务非常具有挑战性,尤其是在 macOS 和 Windows 上。复杂性来自于运行 Linux 的附加虚拟机。这台机器需要所有依赖项和先决条件正确设置。

等待已结束。我们的博文揭示了解决方案,指导您完成在 macOS 和 Windows 上启用 Wasm 工作负载的过程。

hero


什么是 WebAssembly?

WebAssembly(缩写为 Wasm)旨在成为编程语言的便携式编译目标,从而提高 Web 应用程序(包括游戏/模拟器)的性能和可移植性。使用低级二进制格式而不是 JavaScript 可以提高应用程序的性能,使其接近原生性能。

二进制格式充当编译目标,它允许使用更广泛的编程语言,例如 C、C++ 和 Rust。虽然它是一种浏览器/客户端技术,但现在它正在超越 Web 发展,例如被改编为后端或边缘技术(例如,Java 最初是为客户端设计的,然后才登陆服务器端)。

Wasm 二进制格式旨在安全。Wasm 模块与系统其余部分隔离,它们无法访问任何系统资源,除非获得明确许可。这使得 Wasm 模块非常安全运行,即使是在不可信的环境中。但另一方面,对于开发后端应用程序来说,这种限制限制了 Wasm 的使用。

WebAssembly 的扩展

WebAssembly 系统接口 (WASI) 诞生于 WebAssembly 的重要补充。

它是一个系统接口,它将 WebAssembly 的功能扩展到浏览器之外,使其适用于更广泛的环境,包括服务器、边缘设备等等。

虽然使用 Wasm 您可以有限地访问主机资源,但 WASI 提供了一组标准的系统调用,使 WebAssembly 模块能够以安全且一致的方式与主机操作系统交互:它包括文件系统访问、套接字和其他低级资源。

在浏览器之外运行 WebAssembly

Wasm 已在主要的浏览器引擎中发布,因此可以在浏览器领域中使用 Wasm,而无需任何第三方添加。但当涉及边缘/系统使用时,您需要找到一个支持 WASI 扩展的虚拟机来运行这些工作负载。而且不仅仅有一个应用程序来运行它们,还有几个 Wasm 运行时,例如 WasmEdge、Wasmtime、Wasmer 等等。所有运行时都支持不同的 CPU 架构。

由于 WASI 仍在成熟,这些运行时中提供的一些 API 尚未达到标准,因此用户需要注意编写不依赖于给定运行时的可移植应用程序。

除了在您的计算机上运行 Wasm/WASI 工作负载之外,还有一个问题是如何打包、共享和分发这种二进制格式。分发和运行这些工作负载的一种方便方法是使用 OCI 镜像,因为它提供了所有基础:二进制文件的打包、存储和分发。然后是执行部分。

将 Podman 引擎与 Wasm 配合使用

在 macOS 或 Windows 上使用 Podman 与容器时,您有一个称为“Podman 机器”的虚拟机,它正在执行 Linux 环境。我们需要在这个 Linux 环境中添加对 Wasm 的支持。Podman 使用 crun 项目作为其 OCI 运行时,因此 crun 需要能够运行或委托给 Wasm 运行时执行。幸运的是,crun 支持 Wasm 执行。

从用户的角度来看,对 Wasm 的支持作为附加平台提供。因此,在执行 Wasm 工作负载时,我们将指定平台为 --platform=wasi/wasm,而不是例如 --platform=linux/arm64--platform=linux/amd64

使用 podman 运行 Wasm 工作负载

设置

在 Windows 上,确保您的 podman 机器是最近的。您可以使用 podman version 命令进行检查。

根据命令的输出,您可能需要执行额外的步骤。

  • 客户端版本和服务器端版本 >= v4.7.0:无需执行任何操作,Wasm 支持已经存在,默认使用 wasmedge 运行时。
  • 客户端版本 >= 4.6.0 但服务器端版本 < 4.7。您需要使用命令 podman machine init --now wasm 创建一个新的 podman 机器。
  • 旧客户端/旧服务器 (< 4.7.0) 或未安装 podman:请按照 podman.io 上的入门指南操作。

运行 Wasm 镜像

让我们尝试一个简单的 hello world 示例。

我们将使用来自 https://github.com/redhat-developer/podman-desktop-demo/tree/main/wasm/rust-hello-world 的示例。

quay.io 上已经存在一个 OCI 镜像。

要运行工作负载,我们将使用以下命令。

$ podman run --platform wasi/wasm quay.io/podman-desktop-demo/wasm-rust-hello-world

运行命令时,您将看到一个 Podman Hello World,它是在编译时使用 Rust 项目使用 println 函数编译的,并在编译时使用 --target wasm32-wasi 参数编译到 Wasm。

Hello World example running

您可以省略 --platform wasi/wasm 标志,但如果您这样做,您将收到一个警告,说明镜像的平台与您的计算机的平台不匹配(WARNING: image platform (wasi/wasm) does not match the expected platform (linux/arm64))。

从这一点开始,您可以运行其他使用 Wasm 工作负载的 OCI 镜像,而不仅仅是 podman hello world 示例。

注意:如果您在 podman 机器中没有安装先决条件,您将看到此错误:Error: requested OCI runtime crun-wasm is not available: invalid argument

在这种情况下,您应该检查上一节中是否满足先决条件。

使用 podman 构建 Wasm OCI 镜像

使用特定平台/架构构建

从消费者的角度来看,运行 Wasm 工作负载是一个有趣的用例。它有助于使用 Wasm 二进制文件。但另一个有趣的用例是分发和构建这些 Wasm 镜像,以便任何人都可以快速运行它们。

目标是拥有一个只包含 Wasm 二进制文件的最小镜像。为此,我们将使用多阶段构建。第一阶段将是构建/编译 .wasm 二进制文件的平台,第二/最后阶段将把二进制文件复制到一个 scratch 镜像中。

构建镜像时,它将默认使用主机操作系统的架构。如果您使用的是带有 ARM 芯片的 Mac 计算机,那么 Linux 镜像将默认设置为 linux/arm64。使用 mac/intel,它将默认设置为 linux/amd64 镜像。对于 Wasm 工作负载,预期目标平台为 wasi/wasm

使用 podman,我们可以在 podman build 命令上使用标志 --platform=wasi/wasm 来指定系统/架构。但如果我们这样做,这意味着如果 Dockerfile 或 Containerfile 包含 FROM docker.io/redhat/ubi9-minimal 作为基本镜像,例如,它将尝试使用 wasi/wasm 平台获取 ubi9-minimal 镜像,但这当然不存在。

因此,我们需要调整 Containerfile,在其中包含一个--platform指令。

Containerfile 示例

FROM --platform=$BUILDPLATFORM docker.io/redhat/ubi9-minimal as builder

使用这种方法,我们将获取与我们的主机架构匹配的镜像,但由于命令行中仍然存在--platform=wasi/wasm,因此生成的镜像将使用正确的平台。

源代码

这是一个使用wasm32-wasi 二进制输出和多层 OCI 镜像构建 rust 应用程序的简单 Containerfile。一层用于构建(安装 rust、依赖项和编译应用程序),另一层是 scratch 层,我们只在其中添加.wasm输出并将其标记为入口点。

源代码可在 https://github.com/redhat-developer/podman-desktop-demo/tree/main/wasm/rust-hello-world 获取

Containerfile 内容

# Build using the host platform (and not target platform wasi/wasm)
FROM --platform=$BUILDPLATFORM docker.io/redhat/ubi9-minimal as builder

# install rust and Wasm/WASI target
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
    && source "$HOME/.cargo/env" && rustup target add wasm32-wasi

# copy source code
COPY Cargo.toml /app/
COPY src /app/src 

# change working directory
WORKDIR /app

# Build
RUN source "$HOME/.cargo/env" && cd /app && cargo build --target wasm32-wasi --release

# now copy the Wasm binary and flag it as the entrypoint
FROM scratch
ENTRYPOINT [ "/rust-hello-world.wasm" ]
COPY --from=builder /app/target/wasm32-wasi/release/rust-hello.wasm /rust-hello-world.wasm
 

Cargo.toml 内容

[package]
name = "rust-hello-world"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "rust-hello"
path = "src/main.rs"

以及 rust 程序src/main.rs


fn main() {

    // use of strings literal for multi-line string
    // https://doc.rust-lang.net.cn/reference/tokens.html#raw-string-literals

    // ascii art from Máirín Duffy @mairin
    let hello = r#"
!... Hello Podman Wasm World ...!

         .--"--.
       / -     - \
      / (O)   (O) \
   ~~~| -=(,Y,)=- |
    .---. /`  \   |~~
 ~/  o  o \~~~~.----. ~~
  | =(X)= |~  / (O (O) \
   ~~~~~~~  ~| =(Y_)=-  |
  ~~~~    ~~~|   U      |~~

Project:   https://github.com/containers/podman
Website:   https://podman.org.cn
Documents: https://docs.podman.org.cn
Twitter:   @Podman_io
"#;
    println!("{}", hello);
    
  }

所有源代码都可以在 https://github.com/redhat-developer/podman-desktop-demo/tree/main/wasm/rust-hello-world 获取

构建 Wasm 镜像

如果您克隆了存储库,则从wasm/rust-hello-world文件夹中运行命令,或者从所有文件所在的目录中运行命令。

$ podman build --platform=wasi/wasm -t rust-hello-world-wasm .

输出示例

[1/2] STEP 1/6: FROM docker.io/redhat/ubi9-minimal AS builder
Trying to pull docker.io/redhat/ubi9-minimal:latest...
Getting image source signatures
Copying blob sha256:472e9d218c02b84dcd7425232d8b1ac2928602de2de0efc01a7360d1d42bf2f6
Copying config sha256:317fc66dad246d1fac6996189a26f85554dc9fc92ca23bf1e7bf10e16ead7c8c
Writing manifest to image destination
[1/2] STEP 2/6: RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y     && source "$HOME/.cargo/env" && rustup target add wasm32-wasi
info: downloading installer
info: profile set to 'default'
info: default host triple is aarch64-unknown-linux-gnu
info: syncing channel updates for 'stable-aarch64-unknown-linux-gnu'
info: latest update on 2023-10-05, rust version 1.73.0 (cc66ad468 2023-10-03)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
info: default toolchain set to 'stable-aarch64-unknown-linux-gnu'

  stable-aarch64-unknown-linux-gnu installed - rustc 1.73.0 (cc66ad468 2023-10-03)


Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).

To configure your current shell, run:
source "$HOME/.cargo/env"
info: downloading component 'rust-std' for 'wasm32-wasi'
info: installing component 'rust-std' for 'wasm32-wasi'
--> c93a3433d432
[1/2] STEP 3/6: COPY Cargo.toml /app/
--> cf4488993835
[1/2] STEP 4/6: COPY src /app/src
--> 531b9389857c
[1/2] STEP 5/6: WORKDIR /app
--> 23379392f585
[1/2] STEP 6/6: RUN source "$HOME/.cargo/env" && cd /app && cargo build --target wasm32-wasi --release
   Compiling rust-hello-world v0.1.0 (/app)
    Finished release [optimized] target(s) in 0.15s
--> e3582e06f45b
[2/2] STEP 1/3: FROM scratch
[2/2] STEP 2/3: ENTRYPOINT [ "/rust-hello-world.wasm" ]
--> 069b1742d906
[2/2] STEP 3/3: COPY --from=builder /app/target/wasm32-wasi/release/rust-hello.wasm /rust-hello-world.wasm
[2/2] COMMIT rust-hello-world-wasm
--> e0948298c0be
Successfully tagged localhost/rust-hello-world-wasm:latest
e0948298c0be20e11da5d92646a2d6453f05e66671f72f0f792c1e1ff8de75ba

这是一个多阶段构建,但最终我们只得到一个包含 Wasm 二进制文件的较小镜像。

使用以下命令快速启动

$ podman run rust-hello-world-wasm

我们将看到预期的输出

WARNING: image platform (wasi/wasm/v8) does not match the expected platform (linux/arm64)

!... Hello Podman Wasm World ...!

         .--"--.
       / -     - \
      / (O)   (O) \
   ~~~| -=(,Y,)=- |
    .---. /`  \   |~~
 ~/  o  o \~~~~.----. ~~
  | =(X)= |~  / (O (O) \
   ~~~~~~~  ~| =(Y_)=-  |
  ~~~~    ~~~|   U      |~~

Project:   https://github.com/containers/podman
Website:   https://podman.org.cn
Documents: https://docs.podman.org.cn
Twitter:   @Podman_io

结论

通过 podman 在 Windows 和 macOS 上无缝执行和创建 WebAssembly (Wasm) 工作负载后,无限的可能性近在咫尺。

现在,您需要开始探索、实验和突破界限的旅程。

运行和构建新的示例,并不要犹豫,通过报告和讨论这些问题来为 podman 社区做出贡献。