前言
如今WebAssembly是一个激动人心的技术,这是一个真正的开发平台,很多人都认为JavaScript会变成通用编译语言,做到编写一次处处运行,甚至我们有“Univeral JavaScript” 和 “Isomorphic JavaScript.”这样的概念
我们已经接近了这个目标,但还有些许的差距,这主要在于JavaScript不善于处理CPU密集型问题。WebAssembly是为了web而创造,可以字节码的形式到处运行,最酷的是浏览器支持运行WebAssembly
正文
创建WebAssembly模块
将高级语言代码编译成WebAssembly并不困难,难的是如何选择用它来实现什么有用的功能。因为WebAssembly只能接受并返回数字,这看起来似乎WebAssembly只擅长处理数学,不过其实对计算机你也可以这么说。计算机本质上就是一台自动算数机,但它可以处理很多事情
waPC
waPC 定义了WebAssembly过程调用(Procedure Calls)协议,它类似是一个WebAssembly插件框架。在waPC世界里,一个实现者是主机,WebAssembly模块是客户端。在Rust中你可以创建一个WebAssembly模块
我们这里有三篇关于waPC的文章,第二篇介绍在Rust中构建WebAssembly模块,第三篇则是介绍如何在Node.js中运行
- Building WebAssembly platforms with waPC
- Getting Started with waPC & WebAssembly
- Building a waPC Host in Node.js
测试模块
我们提前设置好了一个 测试模块 ,该模块有一个公开的接口hello,以一个字符串为参数并返回一个字符串
建立waPC主机
我们在my_lib中创建项目,我们测试了Module::from_file函数,现在要对其进行完善。
读取文件
你可以通过std::fs::read和std::fs::read_to_string实现基本的读操作,因为我们加载的是二进制数据,所以我们需要用fs::read获取到Vec<u8>类型的字节列表
pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, std::io::Error> {
debug!("Loading wasm file from {:?}", path.as_ref());
let bytes = fs::read(path.as_ref())?;
// ...
}
现在我们获取到了字节,下面我们该如何处理他们呢?我们的Module应该有一个构造函数,有一个new方法,接受字节,让我们继续改造下
crates/my-lib/src/lib.rs:
pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, std::io::Error> {
debug!("Loading wasm file from {:?}", path.as_ref());
let bytes = fs::read(path.as_ref())?;
Self::new(&bytes)
}
pub fn new(bytes: &[u8]) -> Result<Self, ???> {
// ...
}
现在我们会遇到和之前 教程14 一样的问题,我们知道new()会返回有个Result,但其失败存在各种可能,我们需要一个通用错误来捕获各种类型的情况。是时候创建一个自定义错误了,我们在my-lib项目的Cargo.toml文件中依赖中添加thiserror
[dependencies]
log = "0.4"
thiserror = "1.0"
创建一个error.rs文件,新建一个Error枚举
crates/my-lib/src/error.rs:
use std::path::PathBuf;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Could not read file {0}: {1}")]
FileNotReadable(PathBuf, String),
}
相比于封装io::Error,我们增加了一个自定义信息以及多个参数以便可以定制错误信息。为了使用这个Error,首先声明error模块然后导入Error:
pub mod error;
use error::Error;
现在我们可以将Result类型改为返回错误并且使用.map_err()来将io::Error映射为Error
pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, Error> {
debug!("Loading wasm file from {:?}", path.as_ref());
let bytes = fs::read(path.as_ref())
.map_err(|e| Error::FileNotReadable(path.as_ref().to_path_buf(), e.to_string()))?;
Self::new(&bytes)
}
.map_err()是一个转化错误类型的常见方法,特别是和?运算符结合在一起。我们没有添加From<io::Error>的实现是因为我们希望对错误信息有更多的控制,需要手动来做映射
读wapc、wasmtime-provider
wapc包提供了WapcHost数据结构和WebAssemblyEngineProvider trait,WebAssemblyEngineProvider允许我们可以轻松的交换多个WebAssembly引擎,我们接下来会使用 wasmtime 引擎,不过你可以简单的使用 wasm3 或对任何一个新引擎实现一个新的WebAssemblyEngineProvider
在Cargo.toml中添加wapc和wasmtime-provider依赖:
crates/my-lib/Cargo.toml
[dependencies]
log = "0.4"
thiserror = "1.0"
wapc = "0.10.1"
wasmtime-provider = "0.0.7"
在我们创建WapcHost之前,首先需要初始化引擎:
let engine = wasmtime_provider::WasmtimeEngineProvider::new(bytes, None);
第二个参数是我们的WASI配置,现在先忽略
WapcHost构造函数接受两个参数一个是引擎,另一个是WebAssembly客户端调用时运行的函数,现在我们没什么特殊的需要,只返回一个错误:
let host = WapcHost::new(Box::new(engine), |_id, binding, ns, operation, payload| {
trace!(
"Guest called: binding={}, namespace={}, operation={}, payload={:?}",
binding,
ns,
operation,
payload
);
Err("Not implemented".into())
})
构造函数返回了一个含有新错误类型的Result,所以我们须臾奥添加一个新的错误枚举:
crates/my-lib/src/error.rs
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
WapcError(#[from] wapc::errors::Error),
#[error("Could not read file {0}: {1}")]
FileNotReadable(PathBuf, String),
}
我们还需要在Module结构体中存储host:
pub struct Module {
host: WapcHost,
}
最终代码如下所示:
impl Module {
pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, Error> {
debug!("Loading wasm file from {:?}", path.as_ref());
let bytes = fs::read(path.as_ref())
.map_err(|e| Error::FileNotReadable(path.as_ref().to_path_buf(), e.to_string()))?;
Self::new(&bytes)
}
pub fn new(bytes: &[u8]) -> Result<Self, Error> {
let engine = wasmtime_provider::WasmtimeEngineProvider::new(bytes, None);
let host = WapcHost::new(Box::new(engine), |_id, binding, ns, operation, payload| {
trace!(
"Guest called: binding={}, namespace={}, operation={}, payload={:?}",
binding,
ns,
operation,
payload
);
Err("Not implemented".into())
})?;
Ok(Module { host })
}
}
现在我们实现了from_file读取文件并实例化一个新的Module,如果我们运行cargo test可以看到测试通过:
$ cargo test
[snipped]
Running unittests (target/debug/deps/my_lib-afb9e0792e0763e4)
running 1 test
test tests::loads_wasm_file ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.84s
[snipped]
下一步是运行wasm
调用WebAssembly方法
将数据转进转出WebAssembly就是序列化与反序列化,waPC不需要特定得序列化格式,它的默认代码生成器用的是MessagePack,这是我们后面会用到的,不过你也可以自由的更改它
在Cargo.toml中添加rmp-serde依赖:
[dev-dependencies]
rmp-serde = "0.15"
设计测试
在实现之前我们先设计测试,我们的测试模块包含一个导出的函数hello,功能是返回一个字符串,如果传入的是"World"则返回的是"Hello, World."
crates/my-lib/src/lib.rs
#[cfg(test)]
mod tests {
// ...snipped
#[test]
fn runs_operation() -> Result<(), Error> {
let module = Module::from_file("./tests/test.wasm")?;
let bytes = rmp_serde::to_vec("World").unwrap();
let payload = module.run("hello", &bytes)?;
let unpacked: String = rmp_serde::decode::from_read_ref(&payload).unwrap();
assert_eq!(unpacked, "Hello, World.");
Ok(())
}
}
逐行分析:
- 第6行:加载我们的测试模块
- 第8行:将
"World"编码成MessagePack字节格式 - 第9行:调用
.run()方法,接收两个参数 - 第10行:解码返回的结果
- 第11行:判断返回结果是否是
"Hello, World."
当你进行测试的时候,上述代码不会通过编译,因为我们没有实现.run()。实现.run()需要用到创建的WapcHost来调用WebAssembly并返回结果,具体会用到.call()函数:
pub fn run(&self, operation: &str, payload: &[u8]) -> Result<Vec<u8>, Error> {
debug!("Invoking {}", operation);
let result = self.host.call(operation, payload)?;
Ok(result)
}
现在再次运行测试,现在就可以通过了
$ cargo test
[snipped]
Running unittests (target/debug/deps/my_lib-afb9e0792e0763e4)
running 2 tests
test tests::runs_operation ... ok
test tests::loads_wasm_file ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.84s
[snipped]
升级命令行
现在我们的库可以加载并运行WebAssembly了,我们还需要将这种能力导出到命令行,现在我们需要在CliOptions中增加两个额外的新参数,一个是操作的名称,另一个数传输的数据:
crates/cli/src/main.rs
struct CliOptions {
/// The WebAssembly file to load.
#[structopt(parse(from_os_str))]
pub(crate) file_path: PathBuf,
/// The operation to invoke in the WASM file.
#[structopt()]
pub(crate) operation: String,
/// The data to pass to the operation
#[structopt()]
pub(crate) data: String,
}
一开始我们对错误处理不足,导致如果main()函数报错,我们收到的错误信息比较令人困惑和不专业。下面将业务逻辑提取到一个run()函数中,该函数生成一个Result,我们可以在main()中测试它。如果我们得到了错误结果,则将其打印然后并以一个非0值退出表示程序运行失败
crates/cli/src/main.rs
fn main() {
env_logger::init();
debug!("Initialized logger");
let options = CliOptions::from_args();
match run(options) {
Ok(output) => {
println!("{}", output);
info!("Done");
}
Err(e) => {
error!("Module failed to load: {}", e);
std::process::exit(1);
}
};
}
fn run(options: CliOptions) -> anyhow::Result<String> {
//...
}
注意我们这里用到了anyhow,如果你仔细阅读了 教程14 那你对anyhow包应该还有印象。除了anyhow,我们还需要添加rmp-serde在发送给WebAssembly之前做数据的序列化
[dependencies]
my-lib = { path = "../my-lib" }
log = "0.4"
env_logger = "0.9"
structopt = "0.3"
rmp-serde = "0.15"
anyhow = "1.0"
.run()函数和测试很像,区别只是数据来自于CliOptions而非硬编码:
fn run(options: CliOptions) -> anyhow::Result<String> {
let module = Module::from_file(&options.file_path)?;
info!("Module loaded");
let bytes = rmp_serde::to_vec(&options.data)?;
let result = module.run(&options.operation, &bytes)?;
let unpacked: String = rmp_serde::from_read_ref(&result)?;
Ok(unpacked)
}
现在我们可以用命令行来运行我们的程序了
» cargo run -p cli -- crates/my-lib/tests/test.wasm hello "Potter"
[snipped]
Hello, Potter.
现在我们已经实现了一个很棒的程序,不过它现在只能传递和返回字符串,我们接下来还可以做的更好
相关阅读
总结
这节教程有点长,跟起来会不那么容易。开人员利用WebAssembly又很广阔的应用场景, waPC 只是其中之一,除此之外 Wasm-bindgen 也是很有名的。无论你选择哪条路线,你都不可避免地需要在危险的地形中开辟出自己的道路,WebAssembly很稳定,但目前还并未成为主流,最佳实践也远未形成
在下一篇教程里,我们将会拓展我们命令行程序的能力,让它可以接收JSON数据以便可以处理任何的WASM模块
更多
- 写给前端看的Rust教程(1)从nvm到rust
- 写给前端看的Rust教程(2)从npm到cargo
- 写给前端看的Rust教程(3)配置Visual Studio Code
- 写给前端看的Rust教程(4)Hello World
- 写给前端看的Rust教程(5)借用&所有权
- 写给前端看的Rust教程(6)String 第一部分
- 写给前端看的Rust教程(7)语言篇[上]
- 写给前端看的Rust教程(8)语言篇[中]
- 写给前端看的Rust教程(9)语言篇[下]
- 写给前端看的Rust教程(10)从 Mixins 到 Traits
- 写给前端看的Rust教程(11)Module
- 写给前端看的Rust教程(12)String 第二部分
- 写给前端看的Rust教程(13)Results & Options
- 写给前端看的Rust教程(14)Errors
- 写给前端看的Rust教程(15)闭包
- 写给前端看的Rust教程(16)生命周期
- 写给前端看的Rust教程(17)迭代
- 写给前端看的Rust教程(18)异步
- 写给前端看的Rust教程(19)实战 第一部分
- 写给前端看的Rust教程(20)实战 第二部分
- 写给前端看的Rust教程(21)实战 第三部分