WASI - 带有 Wasmtime 的 WebAssembly 系统接口

2025-06-07

WASI - 带有 Wasmtime 的 WebAssembly 系统接口

WebAssembly 支持在 JavaScript 引擎中运行原生代码。经过编译和优化的二进制文件可确保更佳且一致的性能。JavaScript 引擎提供执行二进制文件所需的运行时。

如果我们将 WebAssembly 的性能和可移植性带到 JavaScript 执行环境之外会怎样?答案就是 WASI。


在这里查看我关于 Rust 和 WebAssembly 的书


WASI,即 WebAssembly 系统接口,是 WebAssembly 平台的系统接口。WASI 支持在任何操作系统或架构上运行 WebAssembly 应用程序,前提是我们有相应的运行时环境。从概念上讲,WASI 类似于 JVM。如果您安装了 JVM,则可以在其上运行任何类 Java 语言。同样,有了运行时环境,您就可以运行 WebAssembly 模块。

它是 Wasmtime 项目设计的 API,提供对多个类似操作系统的功能的访问,包括文件和文件系统、伯克利套接字、时钟和随机数,我们将提议对其进行标准化。

它被设计为独立于浏览器,因此它不依赖于Web API或JS,并且不受与JS兼容的需求的限制。

它集成了基于能力的安全性,因此它扩展了 WebAssembly 的特性沙盒以包含 I/O。

WebAssembly 系统接口是 WebAssembly 旅程的下一步。

WASI 的工作方式很简单。您可以使用自己喜欢的语言(例如 Rust、C 或 C++)编写应用程序。然后,将其构建并编译为针对 WASI 环境的 WebAssembly 二进制文件。生成的二进制文件需要特殊的运行时才能执行。该运行时为系统调用提供必要的接口。

要了解 WASI 的实际运行,我们需要一个运行时。目前有两种不同的运行时可用(希望以后还会有更多)。它们是

  • wasmtime
  • 卢塞特

WASI 提供了可移植性。它提供了一种编写一次即可在任何地方运行的选项。让我们看看 wasmtime 的实际应用。


wasmtime - WebAssembly 的运行时

Wasmtime 是一个独立的、仅支持 wasm 的 WebAssembly 和 WASI 优化运行时。它在 Web 之外运行 WebAssembly 代码,既可以用作命令行实用程序,也可以作为嵌入到大型应用程序中的库。安装该wasmtime运行时即可运行 WebAssembly 二进制文件。

安装 wasmtime 最简单的方法是运行以下命令:

$ curl https://wasmtime.dev/install.sh -sSf | bash
$ ./wasmtime --version
wasmtime 0.9.0
Enter fullscreen mode Exit fullscreen mode

您可以使用--help选项列出wasmtime命令中可用的各种选项。

$ ./wasmtime --help
wasmtime 0.9.0
Wasmtime WebAssembly Runtime

USAGE:
    wasmtime <SUBCOMMAND>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

SUBCOMMANDS:
    config      Controls Wasmtime configuration settings
    help        Prints this message or the help of the given subcommand(s)
    run         Runs a WebAssembly module
    wasm2obj    Translates a WebAssembly module to the native object file
    wast        Runs a WebAssembly test script file

If a subcommand is not provided, the `run` subcommand will be used.

Usage examples:

Running a WebAssembly module with a start function:

  wasmtime example.wasm

Passing command-line arguments to a WebAssembly module:

  wasmtime example.wasm arg1 arg2 arg3

Invoking a specific function (e.g. `add`) in a WebAssembly module:

  wasmtime example.wasm --invoke add 1 2
Enter fullscreen mode Exit fullscreen mode

我们已经成功安装了wasmtime。现在我们在提供的运行时中执行 WebAssembly 二进制代码wasmtime

为了将原生代码应用程序编译为兼容 WASI 的版本,Rust 生态系统提供了相应的wasm32-wasi目标。该目标在 Nightly 版本中可用,然后使用 进行安装rustup

$ rustup target add wasm32-wasi --toolchain nightly
Enter fullscreen mode Exit fullscreen mode

然后我们使用 cargo 为该目标构建应用程序

$ cargo +nightly build --target wasm32-wasi
Enter fullscreen mode Exit fullscreen mode

是时候出去wasmtime转一圈了。

工作原理

让我们使用 Cargo 创建一个新的 Rust 应用程序。要创建新应用程序,请运行以下命令

懒得写代码 - 查看这里的存储库:

GitHub 徽标 sendilkumarn /尺寸测量器

WASI 演示仓库

$ cargo new --bin sizer
Enter fullscreen mode Exit fullscreen mode

这将创建一个二进制应用程序。我们可以使用以下命令运行该应用程序:

$ cargo run 
Hello, world!
Enter fullscreen mode Exit fullscreen mode

src/main.rs现在将以下内容更改为:

use std::{env, fs};

fn process(current_dir: &str) -> Result<(), String> {
    for entry in fs::read_dir(current_dir).map_err(|err| format!("{}", err))? {
        let entry = entry.map_err(|err| format!("{}", err))?;
        let path = entry.path();
        let metadata = fs::metadata(&path).map_err(|err| format!("{}", err))?;
        println!(
            "filename: {:?}, filesize: {:?} bytes",
             path.file_name().ok_or("No filename").map_err(|err| format!("{}",     err))?,
         metadata.len()
         );
     }
 Ok(())
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let program = args[0].clone();
    if args.len() < 2 {
        eprintln!("{} <input_folder>", program);
        return;
     }
    if let Err(err) = process(&args[1]) {
        eprintln!("{}", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

给定一个目录,该process函数遍历该目录并列出所有文件及其大小信息。上面的代码包含两个函数:main 函数和 process 函数。首先调用 main 函数。该函数验证我们是否提供了目录信息作为参数。如果参数长度小于 2,则会抛出错误。如果提供了参数,则会调用该process函数。

process 函数接受当前目录参数。然后,它读取文件夹中所有可用的文件,并打印出文件夹中的文件及其大小。

$ cargo run ./
 filename: "Cargo.toml", filesize: 237 bytes
 filename: "target", filesize: 128 bytes
 filename: "Cargo.lock", filesize: 136 bytes
 filename: ".gitignore", filesize: 8 bytes
 filename: ".git", filesize: 288 bytes
 filename: "src", filesize: 96 bytes
Enter fullscreen mode Exit fullscreen mode

我们将使用以下命令创建 WebAssembly 模块:

$ cargo +nightly build --target wasm32-wasi
 Compiling sizer v0.1.0 (/some/path/to/folder/sizer)
 Finished dev [unoptimized + debuginfo] target(s) in 0.36s
Enter fullscreen mode Exit fullscreen mode

现在让我们运行使用生成的 WebAssembly wasmtime

$ wasmtime target/wasm32-wasi/debug/sizer.wasm
 sizer.wasm <input_folder>
Enter fullscreen mode Exit fullscreen mode

现在让我们将输入参数传递给运行时。

$ wasmtime target/wasm32-wasi/debug/sizer.wasm ./
 failed to find a preopened file descriptor through which "./" could be opened
Enter fullscreen mode Exit fullscreen mode

我们可以看到,wasmtime 没有指定文件夹的访问权限。它抱怨找不到任何文件描述符。默认情况下,wasmtime 没有任何全局权限。查看操作系统中可能正在运行的其他目录和进程的权限。为了赋予 wasmtime 访问文件夹的权限,我们可以使用--dir标志指定目录。dir 标志将创建必要的文件描述符,使 wasmtime 能够访问指定的文件夹。

$ wasmtime target/wasm32-wasi/debug/sizer.wasm --dir=/path/to/sizer/folder /path/to/sizer/folder
filename: "Cargo.toml", filesize: 225 bytes
filename: "target", filesize: 192 bytes
filename: "Cargo.lock", filesize: 137 bytes
filename: ".gitignore", filesize: 19 bytes
filename: ".git", filesize: 288 bytes
filename: "src", filesize: 96 bytes
Enter fullscreen mode Exit fullscreen mode

现在,上述命令将打印预期结果。WASI 使用功能模型来确保安全性。点击此处,了解更多关于功能安全模型的信息。

提供wasmtime了系统调用的包装器 API。它们受到POSIX系统的启发和衍生。但WASI core与 POSIX 有以下不同之处。

  • WASI 核心中没有进程。进程无法提供更简洁的方式来在操作系统中提供分叉和执行。大多数操作系统使用进程来实现分叉和执行,这很复杂且难以维护。在 WebAssembly 系统接口中使用进程将使应用程序遵循操作系统的进程边界。

WASI 提供的 API 仍在积极开发中。但需要注意的是,WASI 提供的 API 与 POSIX API 有所不同。WASI 不使用进程,安全模型也不同。而 POSIX API 则使用进程和安全模型。

  1. WASI API 目前仅支持阻止 API 调用。

  2. WASI API 目前不支持异步。

  3. WASI API 没有真正的“mmap”支持。

一旦我们推出更多 API,这种情况将很快改变。

如何做

WASI API 以命名空间命名wasi_。例如,所有与文件描述符相关的代码都将放在wasi_fd命名空间下。

例如在上一个秘籍中,我们使用了__wasi_fd_filestat_get()。此 API 返回给定文件的属性。

使用 Rust 之类的语言时,我们无需担心这些低级 API 调用。因为大多数 Rust 调用都被转换为必要的包装器 API。但当我们尝试调试或手动编写 WebAssembly 模块时,我们需要使用这些 API。

让我们创建一个 WebAssembly 文本格式并立即使用 WASI API。我们将创建writer.wat

$ touch writer.wat
Enter fullscreen mode Exit fullscreen mode

让我们用我们最喜欢的编辑器打开该文件。

我们将首先定义 WebAssembly 文本格式的基础模块。

(module )
Enter fullscreen mode Exit fullscreen mode

然后,我们必须导入 WASI 函数,以便能够在 WebAssembly 文本格式中调用该函数。导入 WASI 函数的步骤如下:

(module
 (import "wasi_unstable" "fd_write" (func $fdw (param i32 i32 i32 i32) (result i32))
)
Enter fullscreen mode Exit fullscreen mode

然后我们使用 调用 WebAssembly 模块内的函数call

请注意,我们调用的 API 与规范中定义的 API 略有不同。规范中定义的 API 格式与规范中定义的 API 相同,但我们从命名空间中__wasi_fd_write导入了相应的函数fd_writewasi_unstable

但在此之前,我们需要定义我们需要使用的内存和数据。

(module
 ;; define the imported function
 (memory 1)
 (export "memory" (memory 0))

 (data (i32.const 12) "Hooray it's WASI\n")
)
Enter fullscreen mode Exit fullscreen mode

这里我们只是定义并导出了内存。然后我们创建了偏移量为 12 的数据。这意味着我们表示的数据将位于初始偏移量为 12 字节后的线性内存中。

与 Web 版的 WebAssembly 模块不同,这里的 start 函数不是可选的。因此,我们必须定义 start 函数。

(module
 ;; define the imported function
 ;; define memory and data
     (func $main (export "_start")
          (i32.store (i32.const 0) (i32.const 12))
          (i32.store (i32.const 4) (i32.const 20))
          (call $fdw
                (i32.const 1)
                (i32.const 0)
                (i32.const 1)
                (i32.const 20)
           )
      drop
    )
)
Enter fullscreen mode Exit fullscreen mode

主函数 ( func main) 存储来自数据指针的数据。然后使用这些指针来表示iov_baseiov_len

struct iovec {
      void  *iov_base;    /* Starting address */
      size_t iov_len;     /* Number of bytes to transfer */
};
Enter fullscreen mode Exit fullscreen mode

然后我们调用该fd_write方法并传入四个必需的参数。这些参数分别是文件描述符、指向文件的指针iov_base以及iov_len最终要写入内容的新内存位置。

最后,我们从堆栈顶部丢弃写入的字节。

工作原理

wasmtime提供了一种使用 WebAssembly 文本格式的方法。因此我们无需担心将 WebAssembly 文本格式转换为 WebAssembly 模块。

$ wasmtime writer.wat
Hooray It's `WASI`
Enter fullscreen mode Exit fullscreen mode

wasmtime interpreter一个阶段验证文件提供的内容是否正确。验证成功后,第一个阶段将编译应用程序。在此阶段,第一个阶段将wasmtime compiler创建二进制代码,用于启动底层架构的系统调用。

最后,drop 将会运行并从堆栈顶部丢弃一些字节。

需要注意的是,第四个参数或要写入的新内存偏移量应该能够被 4 整除。这是为了防止出现非法字节。

(call $fdw
 (i32.const 1)
 (i32.const 0)
 (i32.const 1)
 (i32.const 20)
)
Enter fullscreen mode Exit fullscreen mode

注意:有第四个参数i32.const 18将导致!!! bad alignment: 18 % 4

如果你想获取正在发生的事情的完整跟踪,包括调用了哪个函数以及每个函数调用花费了多少时间。我们可以使用标志轻松追踪-d

$ RUST_LOG=trace wasmtime writer.wat -d
Enter fullscreen mode Exit fullscreen mode

上述代码将打印日志的跟踪级别输出。它还将打印由 完成的所有代码转换wasmtime

TRACE wasmtime_wasi_c::syscalls > fd_write(fd=1, iovs=0x0, iovs_len=1, nwritten=0xc)
Hooray it's WASI
 TRACE wasmtime_wasi_c::syscalls > | *nwritten=20
 TRACE wasmtime_wasi_c::syscalls > -> errno=__WASI_ESUCCESS
Enter fullscreen mode Exit fullscreen mode

这是实际的跟踪输出WASI system calls

另外,如果您发现syscall由于无法确定函数类型而难以使用syscall,最好先使用类似wabtwat2wasm 可执行文件之类的工具来调试失败的原因,然后根据需要修改函数定义。


WASI 中需要注意的错误

WASI 会发出各种错误,这些错误会在开发过程中为您提供帮助,我们将在这里介绍其中几种。WASI 中的每个系统调用(至少目前)都是阻塞的,而不是异步的。这意味着一旦调用了系统调用,就必须等到调用完成后才能根据结果继续执行。

入门

我们将从一个例子开始。在这个例子中,我们将使用 WASI 创建一个新目录。将它写入WebAssembly Text Formatwasmtime运行总是令人兴奋的WebAssembly Text Format

让我们创建一个名为的文件creator.wast

打开编辑器,我们将开始编写 WebAssembly 文本格式。

如何做

我们首先创建一个模块。所有代码都存放在模块中。

(module )
Enter fullscreen mode Exit fullscreen mode

然后我们导入用于创建新目录的 API。创建目录系统调用的函数签名有三个参数。

__wasi_fd_t-> 文件描述符
const char *ptr-> 这应该是指向 WebAssembly 模块内线性内存数组的开头的指针
size_t path_len-> 存储在内存数组中的值的长度。

导入的函数将如下所示

(module
 (import "wasi_unstable" "path_create_directory" (func $mkdir (param i32 i32 i32) (result i32))
)
Enter fullscreen mode Exit fullscreen mode

我们将像这样定义内存。

(module
 ;; define the imported function
 (memory 1)
 (export "memory" (memory 0))

 (data (i32.const 12) "wasi-folder")
)
Enter fullscreen mode Exit fullscreen mode

我们在偏移量 12 的线性内存数组中定义数据。

现在我们必须定义将调用 $mkdir 函数的启动方法。

(module
 ;; define the imported function
 ;; define memory and data

 (func $main (export "_start")
 (i32.store (i32.const 0) (i32.const 12))
 (i32.store (i32.const 4) (i32.const 20))

 (call $mkdir
 (i32.const 3)
 (i32.const 12)
 (i32.const 11)
 )
 drop
 )
)

Enter fullscreen mode Exit fullscreen mode

工作原理

接受$mkdir三个参数。

  1. 第一个是文件描述符。我们传入了一个 i32.const 3 变量,它接收 process 参数中的第三个值(即 --dir 参数)。
  2. 接下来是定义数据的起始位置。数据从偏移量 12 开始。因此我们在这里使用相同的偏移量。
  3. 最后是我们拥有的数据的长度。在我们的例子中,它是 11。

完成后,让我们使用运行我们的应用程序wasmtime

$ /path/to/wasmtime/target/wasmtime creator.wat --dir=.
$ ls | grep wasi-folder
drwxr-xr-x 2 sendilkumar staff 64B Jun 12 11:35 wasi-folder
Enter fullscreen mode Exit fullscreen mode

请注意,我们使用符号“。”给出了当前目录,但是如果提供给--dir选项,像“..”这样的符号将与wasmtime一起使用。

但如果我们把数据改成(data (i32.const 12) "../some_folder"

然后扩展内存偏移量来反映变化,将导致__WASI_ENOTCAPABLE错误。

无法访问的错误表明 WASI 正在尝试创建或访问它无权访问的内容。

如果我们传入零字节作为路径的长度,那么 WASI 将抛出__WASI_ENOENT错误。

no ent error表明没有可用的文件或目录。

__WASI_EILSEQ当访问线性内存中的任何非法数据序列时,都会抛出错误。


如果你喜欢这篇文章,那么你可能会喜欢我关于 Rust 和 WebAssembly 的书。点击此处查看。


进一步探索

点击此处查看 Lin Clark 关于 WASI 或 WebAssembly 系统接口的精彩文章

点击此处了解有关如何在 C 或 C++ 中使用 wasmtime 的更多信息

点击此处查看更多可用 API

点击此处查看有关 WASI 教程的更多信息

点击此处查看 WASI_API 中的更多错误

点击此处查看更多关于 WASI 背景的信息


讨论🐦 Twitter // 💻 GitHub // ✍️ 博客

如果你喜欢这篇文章,请点赞或者留言。❤️

文章来源:https://dev.to/sendilkumarn/wasi-web assembly-system-interface-with-wasmtime-4cec
PREV
React.js 工厂模式轻松构建复杂 UI
NEXT
在 React 中设置 Tailwind - 最快捷的方法!🚀 大家好 👋