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
您可以使用--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
我们已经成功安装了wasmtime
。现在我们在提供的运行时中执行 WebAssembly 二进制代码wasmtime
。
为了将原生代码应用程序编译为兼容 WASI 的版本,Rust 生态系统提供了相应的wasm32-wasi
目标。该目标在 Nightly 版本中可用,然后使用 进行安装rustup
。
$ rustup target add wasm32-wasi --toolchain nightly
然后我们使用 cargo 为该目标构建应用程序
$ cargo +nightly build --target wasm32-wasi
是时候出去wasmtime
转一圈了。
工作原理
让我们使用 Cargo 创建一个新的 Rust 应用程序。要创建新应用程序,请运行以下命令
懒得写代码 - 查看这里的存储库:
sendilkumarn /尺寸测量器
WASI 演示仓库
$ cargo new --bin sizer
这将创建一个二进制应用程序。我们可以使用以下命令运行该应用程序:
$ cargo run
Hello, world!
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)
}
}
给定一个目录,该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
我们将使用以下命令创建 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
现在让我们运行使用生成的 WebAssembly wasmtime
。
$ wasmtime target/wasm32-wasi/debug/sizer.wasm
sizer.wasm <input_folder>
现在让我们将输入参数传递给运行时。
$ wasmtime target/wasm32-wasi/debug/sizer.wasm ./
failed to find a preopened file descriptor through which "./" could be opened
我们可以看到,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
现在,上述命令将打印预期结果。WASI 使用功能模型来确保安全性。点击此处,了解更多关于功能安全模型的信息。
提供wasmtime
了系统调用的包装器 API。它们受到POSIX
系统的启发和衍生。但WASI core
与 POSIX 有以下不同之处。
- WASI 核心中没有进程。进程无法提供更简洁的方式来在操作系统中提供分叉和执行。大多数操作系统使用进程来实现分叉和执行,这很复杂且难以维护。在 WebAssembly 系统接口中使用进程将使应用程序遵循操作系统的进程边界。
WASI 提供的 API 仍在积极开发中。但需要注意的是,WASI 提供的 API 与 POSIX API 有所不同。WASI 不使用进程,安全模型也不同。而 POSIX API 则使用进程和安全模型。
-
WASI API 目前仅支持阻止 API 调用。
-
WASI API 目前不支持异步。
-
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
让我们用我们最喜欢的编辑器打开该文件。
我们将首先定义 WebAssembly 文本格式的基础模块。
(module )
然后,我们必须导入 WASI 函数,以便能够在 WebAssembly 文本格式中调用该函数。导入 WASI 函数的步骤如下:
(module
(import "wasi_unstable" "fd_write" (func $fdw (param i32 i32 i32 i32) (result i32))
)
然后我们使用 调用 WebAssembly 模块内的函数call
。
请注意,我们调用的 API 与规范中定义的 API 略有不同。规范中定义的 API 格式与规范中定义的 API 相同,但我们从命名空间中
__wasi_fd_write
导入了相应的函数。fd_write
wasi_unstable
但在此之前,我们需要定义我们需要使用的内存和数据。
(module
;; define the imported function
(memory 1)
(export "memory" (memory 0))
(data (i32.const 12) "Hooray it's WASI\n")
)
这里我们只是定义并导出了内存。然后我们创建了偏移量为 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
)
)
主函数 ( func main
) 存储来自数据指针的数据。然后使用这些指针来表示iov_base
和iov_len
。
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
然后我们调用该fd_write
方法并传入四个必需的参数。这些参数分别是文件描述符、指向文件的指针iov_base
以及iov_len
最终要写入内容的新内存位置。
最后,我们从堆栈顶部丢弃写入的字节。
工作原理
这wasmtime
提供了一种使用 WebAssembly 文本格式的方法。因此我们无需担心将 WebAssembly 文本格式转换为 WebAssembly 模块。
$ wasmtime writer.wat
Hooray It's `WASI`
第wasmtime interpreter
一个阶段验证文件提供的内容是否正确。验证成功后,第一个阶段将编译应用程序。在此阶段,第一个阶段将wasmtime compiler
创建二进制代码,用于启动底层架构的系统调用。
最后,drop 将会运行并从堆栈顶部丢弃一些字节。
需要注意的是,第四个参数或要写入的新内存偏移量应该能够被 4 整除。这是为了防止出现非法字节。
(call $fdw
(i32.const 1)
(i32.const 0)
(i32.const 1)
(i32.const 20)
)
注意:有第四个参数
i32.const 18
将导致!!! bad alignment: 18 % 4
。
如果你想获取正在发生的事情的完整跟踪,包括调用了哪个函数以及每个函数调用花费了多少时间。我们可以使用标志轻松追踪-d
。
$ RUST_LOG=trace wasmtime writer.wat -d
上述代码将打印日志的跟踪级别输出。它还将打印由 完成的所有代码转换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
这是实际的跟踪输出WASI system calls
。
另外,如果您发现syscall
由于无法确定函数类型而难以使用syscall
,最好先使用类似wabt
wat2wasm 可执行文件之类的工具来调试失败的原因,然后根据需要修改函数定义。
WASI 中需要注意的错误
WASI 会发出各种错误,这些错误会在开发过程中为您提供帮助,我们将在这里介绍其中几种。WASI 中的每个系统调用(至少目前)都是阻塞的,而不是异步的。这意味着一旦调用了系统调用,就必须等到调用完成后才能根据结果继续执行。
入门
我们将从一个例子开始。在这个例子中,我们将使用 WASI 创建一个新目录。将它写入WebAssembly Text Format
并wasmtime
运行总是令人兴奋的WebAssembly Text Format
。
让我们创建一个名为的文件creator.wast
。
打开编辑器,我们将开始编写 WebAssembly 文本格式。
如何做
我们首先创建一个模块。所有代码都存放在模块中。
(module )
然后我们导入用于创建新目录的 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))
)
我们将像这样定义内存。
(module
;; define the imported function
(memory 1)
(export "memory" (memory 0))
(data (i32.const 12) "wasi-folder")
)
我们在偏移量 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
)
)
工作原理
接受$mkdir
三个参数。
- 第一个是文件描述符。我们传入了一个 i32.const 3 变量,它接收 process 参数中的第三个值(即 --dir 参数)。
- 接下来是定义数据的起始位置。数据从偏移量 12 开始。因此我们在这里使用相同的偏移量。
- 最后是我们拥有的数据的长度。在我们的例子中,它是 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
请注意,我们使用符号“。”给出了当前目录,但是如果提供给--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