9个版本
0.1.7 | 2023年11月18日 |
---|---|
0.1.6 | 2020年6月11日 |
0.1.5 | 2020年5月24日 |
0.1.4 | 2020年4月5日 |
0.0.0 | 2020年2月23日 |
#22 在 WebAssembly
70 每月下载量
315KB
6.5K SLoC
wain
wain 是一个从头开始用Safe Rust编写的零依赖的 WebAINterpreter,是 WebAssembly 的实现。
特性
- 无不安全代码。保证内存安全和无未定义行为
- 无外部运行时依赖。这意味着无不安全依赖和更小的二进制大小
- 效率。避免不必要的分配,尽可能快地执行指令而不使用不安全代码
- 模块化实现。二进制格式解析器、文本格式解析器、验证器、执行器作为独立的库开发
请注意,此项目仍在进行中。在v1.0.0之前意味着实验性的。并非所有功能都已实现。当前状态是所有MVP实现都已完成,但还有很多任务待完成。
通往v1.0.0的道路(优先级顺序)
- 为所有库添加足够的测试和解析器的模糊测试
- 通过所有 规范测试
- 添加基准测试以跟踪性能
- 引入中间表示形式以更有效地执行指令
- 为每个公共API添加文档
请参阅 任务板 了解当前进度。
该项目始于娱乐和深入了解Wasm。
安装
wain
Crate尚未发布。请克隆此仓库,并通过 cargo build
构建项目。
最低支持的Rust版本是1.45.0。
$ cargo install wain
$ wain --help
如果您不想运行文本格式代码,则可以省略
# Only run binary format files
$ cargo install wain --no-default-features --features binary
用法
wain
命令
运行二进制格式Wasm源文件
$ wain examples/hello/hello.wasm
Hello, world
运行文本格式Wasm源文件
$ wain examples/hello/hello.wat
Hello, world
如果没有参数,wain
命令从stdin检测二进制格式和文本格式并运行输入。
$ wain < examples/hello/hello.wasm
Hello, world
$ wain < examples/hello/hello.wat
Hello, world
请参阅示例目录获取更多示例。
当前限制如下
- 默认情况下,只有
int putchar(int)
和int getchar()
被实现为外部函数 - wain一次只能运行一个模块。这意味着从其他模块导入东西还不工作
- 许多扩展,如线程、WASI支持、SIMD支持等,尚未实现
作为库
wain由多个crate组成。
- wain: 用于执行给定Wasm源代码的命令行工具
- wain-ast: 抽象语法树定义。Wasm结构规范的实现。这个语法树对于二进制格式和文本格式都是通用的
- wain-syntax-binary: Wasm二进制格式(
.wasm
文件)的解析器。Wasm二进制格式规范的实现。它将&[u8]
值解析为wain_ast::Root
抽象语法树 - wain-syntax-text: Wasm文本格式(
.wat
文件)的解析器。Wasm文本格式规范的实现。它将&str
值解析为wain_ast::Root
抽象语法树 - wain-validate: Wasm抽象语法树的验证器。Wasm验证规范的实现
- wain-exec: 解析Wasm抽象语法树的执行器。Wasm执行规范的实现。它现在直接解析语法树,但将来会将其转换为中间表示形式以高效执行
wain-*
crates作为WebAssembly的模块化实现,是库。它们可以解析、验证、执行WebAssembly代码。
以下是一个从Rust运行解释器的示例代码。
extern crate wain_syntax_binary;
extern crate wain_validate;
extern crate wain_exec;
use std::fs;
use std::process::exit;
use wain_syntax_binary::parse;
use wain_validate::validate;
use wain_exec::execute;
// Read wasm binary
let source = fs::read("foo.wasm").unwrap();
// Parse binary into syntax tree
let tree = match parse(&source) {
Ok(tree) => tree,
Err(err) => {
eprintln!("Could not parse: {}", err);
exit(1);
}
};
// Validate module
if let Err(err) = validate(&tree) {
eprintln!("This .wasm file is invalid!: {}", err);
exit(1);
}
// Execute module
if let Err(trap) = execute(&tree.module) {
eprintln!("Execution was trapped: {}", trap);
exit(1);
}
或者使用参数调用特定的导出函数
// ...(snip)
use wain_exec::{Runtime, DefaultImporter, Value};
use std::io;
// Create default importer to call external function supported by default
let stdin = io::stdin();
let stdout = io::stdout();
let importer = DefaultImporter::with_stdio(stdin.lock(), stdout.lock());
// Make abstract machine runtime. It instantiates a module
let mut runtime = match Runtime::instantiate(&tree.module, importer) {
Ok(m) => m,
Err(err) => {
eprintln!("could not instantiate module: {}", err);
exit(1);
}
};
// Let's say `int add(int, int)` is exported
match runtime.invoke("add", &[Value::I32(10), Value::I32(32)]) {
Ok(ret) => {
// `ret` is type of `Option<Value>` where it contains `Some` value when the invoked
// function returned a value. Otherwise it's `None` value.
if let Some(Value::I32(i)) = ret {
println!("10 + 32 = {}", i);
} else {
unreachable!();
}
}
Err(trap) => eprintln!("Execution was trapped: {}", trap),
}
默认情况下,在env
模块中仅支持以下C函数作为外部函数
int putchar(int)
(在wasm(func (param i32) (result i32))
)int getchar(void)
(在wasm(func (param) (result i32))
)void *memcpy(void *, void *, size_t)
(在wasm(func (param i32 i32 i32) (result i32))
)void abort(void)
(在wasm(func (param) (result))
)
但是,您可以自己实现一个结构体,该结构体从Rust端定义外部函数,实现了wain_exec::Importer
。
extern crate wain_exec;
extern crate wain_ast;
use wain_exec::{Runtime, Stack, Memory, Importer, ImportInvokeError, ImportInvalidError}
use wain_ast::ValType;
struct YourOwnImporter {
// ...
}
impl Importer for YourOwnImporter {
fn validate(&self, name: &str, params: &[ValType], ret: Option<ValType>) -> Option<ImportInvalidError> {
// `name` is a name of function to validate. `params` and `ret` are the function's signature.
// Return ImportInvalidError::NotFound when the name is unknown.
// Return ImportInvalidError::SignatureMismatch when signature does not match.
// wain_exec::check_func_signature() utility is would be useful for the check.
}
fn call(&mut self, name: &str, stack: &mut Stack, memory: &mut Memory) -> Result<(), ImportInvokeError> {
// Implement your own function call. `name` is a name of function and you have full access
// to stack and linear memory. Pop values from stack for getting arguments and push value to
// set return value.
// Note: Consistency between imported function signature and implementation of this method
// is your responsibility.
// On invocation failure, return ImportInvokeError::Fatal. It is trapped by interpreter and it
// stops execution immediately.
};
}
let ast = ...; // Parse abstract syntax tree and validate it
let mut runtime = Runtime::instantiate(&ast.module, YourOwnImporter{ /* ... */ }).unwrap();
let result = runtime.invoke("do_something", &[]);
有关API的用法,工作示例可在examples/api/中找到。
未来工作
- WASI支持
- MVP支持之后的Wasm特性(线程、SIMD、多返回值等)
- 与其他Wasm实现比较基准测试
- 自托管的解释器。将wain编译成Wasm并自行运行
- 将Wasm实现从Wasm v1升级到Wasm v2。
工作原理
在此,我记录了每个解释阶段的要点。
解析
wain-syntax-binary根据二进制格式规范将.wasm
二进制文件解析为wain_ast::Root
抽象语法树。Wasm二进制格式旨在通过基本的LL(1)解析器进行解析。因此,解析非常直接。解析器实现实现小于1000行。
相比之下,解析文本格式的实现更复杂。 wain-syntax-text根据文本格式规范将.wat
文本文件解析为wain_ast::Root
抽象语法树。
- 将
.wat
文件解析为WAT语法树,该语法树是针对文本格式解析许多语法糖的。由于可以在.wat
文件中放置多个模块,因此它可以解析为多个树。 - 将WAT语法树转换为通用的Wasm语法树(
wain_ast::Root
),解析标识符。标识符可能引用尚未定义的东西(向前引用),因此不能直接将.wat
文件解析为通用的Wasm语法树。 - 根据规范从多个Wasm语法树中组合一个模块。
验证
验证通过遍历给定的 Wasm 语法树在 wain-validate 包中进行。符合 规范,以下内容将被验证
- 在 Wasm 中,每个引用都是一个索引。它验证所有索引都没有超出范围
- Wasm 设计用于静态检查栈操作。它通过模拟栈状态来验证指令序列
- 由于多态指令
select
,类型检查是尽力而为的。由于几乎所有指令都不是多态的,几乎所有类型检查都可以在验证中完成
符合规范,wain 验证 unreachable
指令之后的指令。例如,
(unreachable) (i64.const 0) (i32.add)
i32.add
是无效的,因为它应该从栈中取出两个 i32
值,但至少有一个 i64
值在栈中。
执行
wain-exec 包解释一个符合 规范 的 Wasm 语法树。多亏了验证,运行时的检查是最小的(例如间接调用的函数签名)。
- 分配内存、表和全局变量。初始化栈
- 通过将值推入/弹出栈来解释语法树节点
目前 wain 直接解释 Wasm 语法树。我计划定义一个可以更快解释的中间表示
入口点是 'start 函数',它定义为
- 在
start
节中设置的函数 - 在导出节中名为
_start
的导出函数
第 1 项是标准入口点,但 Clang 不生成 start
节。相反,它将 _start
函数作为入口点处理。wain 实现了这两个入口点(第 1 项优先)。