WebAssembly介绍与Rust后端集成
LI Rui

什么是WebAssembly

WebAssembly或Wasm是一种运行在基于堆栈虚拟机上的二进制指令格式。^1目前四大主流浏览器(Firefox, Chrome, Edge, Safari)已经支持Wasm,我们可以分发WebAssembly二进制以在浏览器中进行一些计算,完成页面交互等。

一些有趣的例子:

  • v86:在浏览器中的x86虚拟化,实时编译x86指令到Wasm指令。我们可以在浏览器中运行Windows 2000、Arch Linux等操作系统。
  • pyodide: WebAssembly实现的Python虚拟机,在浏览器中即可运行Python。
  • Yew:多线程前端App框架,使用Rust + Wasm技术。

什么是WASI

WASI全称为The WebAssembly System Interface,在2019年有一篇文章Standardizing WASI: A system interface to run WebAssembly outside the web,介绍了将WebAssembly搬出浏览器的标准化努力。(下图出自该文章,文章作者为Lin Clark

我们可以把Wasm模块放进Runtime提供的沙箱中运行,Runtime可以限制模块的对于操作系统的访问,为其提供有限的文件系统访问等。而Runtime可以被设计成跨平台的,我们可以在Linux/Windows/MacOS/嵌入式等平台上运行同一个Wasm模块。

这就相当于我们有了跨平台且安全的二进制执行方法。

WebAssembly Runtime

目前有几大Runtime:

WasmEdge是C++编写的轻量高性能可拓展的WebAssembly Runtime,CNCF的官方沙箱项目。

Wasmtime是Bytecode Alliance推出的独立WebAssembly Runtime,作者中有Rust社区大佬Alex Crichton。

使用Rust编写一个WASI模块

Rust目前已经支持编译到wasm32-wasi平台,在编译的时候指定该平台即可。下面我们将编写一个简单的计算加法程序,并使用WasmEdge去执行。

使用Cargo新建一个名为add的library。

1
cargo new add --lib

src/lib.rs:

1
2
3
4
#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
return a + b;
}

Cargo.toml:

1
2
3
4
5
6
7
8
9
10
11
[package]
name = "add"
version = "0.1.0"
edition = "2021"

[lib]
name = "add"
path = "src/lib.rs"
crate-type =["cdylib"]

[dependencies]

编译到wasm32-wasi平台。

1
cargo build --target wasm32-wasi

下面我们将使用WasmEdge去执行编译后的模块,如果没有安装WasmEdge,可以使用下面的方法安装:

1
curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash

也提供了卸载脚本:

1
bash <(curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/uninstall.sh)

执行Wasm:

1
2
$ wasmedge --reactor ./target/wasm32-wasi/debug/add.wasm add 1 2
3

我们的模块成功执行,这里由于我们的函数并非_start所以使用--reactor参数并在末尾指定了需要触发的函数。

在Rust后端中进行集成

wasmedge-sys目前还在开发阶段,这里的例子作为当前代码的预览。

目前WasmEdge正在开发Rust SDK中,我们现在使用贴近原生的wasmedge-sys去调用WasmEdge。之后预计会有High-level的API出现。

下面的例子我们会在Poem Web框架中将我们的add模块集成进去,这允许用户调用我们的API在沙箱内执行一些操作而不会对我们的物理机造成影响。使用wasmedge-sys需要安装WasmEdge,同时WASMEDGE_DIR环境变量需要被设置为WasmEdge源码所在目录。

在PR#1018中我新增了Poem调用WasmEdge的例子,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
use poem::{get, handler, listener::TcpListener, web::Query, Route, Server};
use wasmedge_sys::{Config, Module, Value, Vm};

#[derive(serde::Deserialize)]
struct AddReq {
a: i32,
b: i32,
}

#[handler]
fn call_add(Query(AddReq { a, b }): Query<AddReq>) -> String {
let config = Config::create().expect("Failed to create Config");

// Source code of add.wasm
// #[no_mangle]
// pub extern "C" fn add(a: i32, b: i32) -> i32 {
// a + b
// }
let mut module =
Module::create_from_file(&config, "examples/add.wasm").expect("Failed to create Module");

let mut vm = Vm::create(Some(&config), None)
.expect("Failed to create VM")
.load_wasm_from_module(&mut module)
.expect("Failed to register wasm")
.validate()
.expect("Failed to validate vm")
.instantiate()
.expect("Failed to instantiate vm");

match vm.run_function("add", [a.into(), b.into()]) {
Ok(v) => {
let res = v.collect::<Vec<Value>>()[0];
format!("a + b = {:?}", res)
}
Err(e) => format!("Failed to execute function: {:?}", e),
}
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
// http://127.0.0.1:3000/add?a=1&b=2
// a + b = I32(3)
Server::new(TcpListener::bind("127.0.0.1:3000"))
.run(Route::new().at("/add", get(call_add)))
.await
}

wasmedge-sys的目录下执行下面的命令运行例子:

1
cargo run --example poem

就能在浏览器调用Wasm模块。这里我们每次调用都会去创建一个Vm实例,感觉不太优雅,之后在正式的高阶SDK出来后希望可以能够出现“全局VM”的概念。

RustPython

最后来介绍一下RustPython和我的Fork。RustPython是由Rust编写的Python虚拟机,但由于WASI接口的关系,socket、select等模块实际上并不被支持。目前WASI关于Socket的讨论还在进行中,WasmEdge已经内置了一些可用的网络接口:wasimodule.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
addHostFunc("sock_open", std::make_unique<WasiSockOpen>(Env));
addHostFunc("sock_bind", std::make_unique<WasiSockBind>(Env));
addHostFunc("sock_connect", std::make_unique<WasiSockConnect>(Env));
addHostFunc("sock_listen", std::make_unique<WasiSockListen>(Env));
addHostFunc("sock_accept", std::make_unique<WasiSockAccept>(Env));
addHostFunc("sock_recv", std::make_unique<WasiSockRecv>(Env));
addHostFunc("sock_send", std::make_unique<WasiSockSend>(Env));
addHostFunc("sock_shutdown", std::make_unique<WasiSockShutdown>(Env));
addHostFunc("sock_getsockopt", std::make_unique<WasiSockGetOpt>(Env));
addHostFunc("sock_setsockopt", std::make_unique<WasiSockSetOpt>(Env));
addHostFunc("sock_getlocaladdr", std::make_unique<WasiSockGetLocalAddr>(Env));
addHostFunc("sock_getpeeraddr", std::make_unique<WasiSockGetPeerAddr>(Env));
addHostFunc("sock_getaddrinfo", std::make_unique<WasiGetAddrinfo>(Env));

因此我们可以自己实现socket库集成到Python中,实现后的仓库在:KernelErr/RustPython。使用下面的命令可以编译到wasm模块并启用WasmEdge支持:

1
cargo build --release --target wasm32-wasi --features="freeze-stdlib,wasmedge"

WasmEdge支持AOT来获得更好的性能,同时采用了Custom Section来使得预编译后的模块在其他Runtime中也能直接运行。

1
wasmedgec ./target/wasm32-wasi/release/rustpython.wasm ./target/wasm32-wasi/release/rustpython.wasm

之后我们可以执行RustPython启动一个Tcp Server:

目前最基本的TCP收发已经实现,HTTP(非HTTPS)的简单GET请求也已经实现。

  • 本文标题:WebAssembly介绍与Rust后端集成
  • 本文作者:LI Rui
  • 创建时间:2022-01-21 10:50:46
  • 本文链接:https://www.lirui.tech/post/2022/bf75512a88f1.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-SA 许可协议。转载请注明出处!