通过WebAssembly实现插件机制 创建于:2021/2/22 发布于:2021/2/22 文集:随笔
前两天在鼓捣coco 的插件系统,我们常说要面向接口开发而不是面向实现,插件这个东西,就像后端框架里的中间件,我们按照框架定义的接口实现中间件,这也可以算一种插件,我们有很多机制实现“编译前插件”,但是像coco这样要编译发布的二进制程序,有什么办法让用户定义插件来补充功能呢?不能在运行期间插入用户的代码再重新编译整个程序吧?有办法在运行时加载用户的库文件吗?有的,这项技术被称为Dynamic Loading 。
在coco中借助dlopen 这个crate实现了动态加载,但是在Rust中动态加载似乎必须要unsafe ,就在为coco实现插件机制的某个瞬间,我突然有了个新的想法。
WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。——MDN
WebAssembly最开始设计是在浏览器中运行的,不过就像JS的node一样,现在WebAssembly也有了独立于浏览器之外的运行时,比如wasmtime 和wasmer ,并且它们都提供了嵌入各个主流语言的辅助库,也就是说,我可以在这个运行时支持的语言内自由地调用wasm二进制文件内的函数咯?试试看!
我尝试用wasmtime做了个demo,首先新建一个crate:
cargo new adder --lib
打开src/lib.rs
文件,写一个简单的求和函数:
#[ no_mangle ]
pub extern " C " fn adder ( a : i32 , b : i32 ) -> i32 {
a + b
}
no_mangle
告诉Rust编译器不要修改函数名称,以便后续调用。另外还要修改Cargo.toml
文件:
[ lib ]
crate-type = [ ' cdylib ' ]
接着就可以这个命令编译:
# rustup target add wasm32-wasi
# 如果没有设置target要用上面的命令设置下
cargo build --target wasm32-wasi
这下就可以在项目下的target目录里找到对应的adder.wasm
文件了。
接着再创建一个crate:
cargo new wasm_test
在main.rs
写入如下代码:
use std :: error :: Error ;
use wasmtime ::* ;
fn main () -> Result <(), Box < dyn Error >> {
let engine = Engine :: default ();
let store = Store :: new ( & engine );
let module = Module :: from_file ( & engine , " adder.wasm " ) ? ;
let instance = Instance :: new ( & store , & module , & []) ? ;
let adder = instance
. get_func ( " adder " )
. expect ( " adder was not an exported function " );
let adder = adder . get2 :: < i32 , i32 , i32 >() ? ;
let result = adder ( 2 , 4 ) ? ;
println! ( " result is {} " , result );
Ok (())
}
不要忘了引入wasmtime依赖,不过现在运行会直接报错:
Error: wrong number of imports provided, 0 != 4
调试后发现错误出在创建instance
的地方,浏览文档发现如果wasm中有import依赖项,那么这里第三个参数就不能是空数组,而是包括所有依赖的数组,不过这么简单的函数哪来的依赖呢?通过wasm2wat工具将wasm文件转成文本格式,再用grep搜索一下,果然,编译后自动添加了一些依赖:
❯ wasm2wat adder.wasm | grep import
( import " wasi_snapshot_preview1 " " fd_write " (func $_ZN4wasi13lib_generated22wasi_snapshot_preview18fd_write17ha0aef7cef0a152b0E (type 6 ) ))
( import " wasi_snapshot_preview1 " " environ_sizes_get " (func $__wasi_environ_sizes_get (type 2 ) ))
( import " wasi_snapshot_preview1 " " proc_exit " (func $__wasi_proc_exit (type 0 ) ))
( import " wasi_snapshot_preview1 " " environ_get " (func $__wasi_environ_get (type 2 ) ))
因为我们编译的目标是wasm32-wasi
,wasi全称是WebAssembly System Interface ,是一个标准化的WebAssembly系统接口,而wasmtime在说明如何在Rust中使用的部分给的代码却是适用于wasm32-unknown-unknown
的(不得不说文档质量不太好),这里如果把编译target改成wasm32-unknown-unknown就可以直接运行。
不过不改编译目标就要稍微修改下程序,根据搜到的issue ,我改了下代码:
use std :: error :: Error ;
use wasi_cap_std_sync :: WasiCtxBuilder ;
use wasmtime ::* ;
use wasmtime_wasi :: Wasi ;
fn main () -> Result <(), Box < dyn Error >> {
let engine = Engine :: default ();
let store = Store :: new ( & engine );
let mut linker = Linker :: new ( & store );
let wasi = Wasi :: new (
& store ,
WasiCtxBuilder :: new ()
. inherit_stdio ()
. inherit_args () ?
. build () ? ,
);
wasi . add_to_linker ( & mut linker ) ? ;
let module = Module :: from_file ( & engine , " adder.wasm " ) ? ;
let instance = linker . instantiate ( & module ) ? ;
let adder = instance
. get_func ( " adder " )
. expect ( " adder was not an exported function " );
let adder = adder . get2 :: < i32 , i32 , i32 >() ? ;
let answer = adder ( 1 , 7 ) ? ;
println! ( " the answer is {} " , answer );
Ok (())
}
搞定!wasmtime目前支持五种语言,wasmer支持更多,这样用户可以用C/C++或者Go来写插件啦,我们可以在Rust程序中调用,并且不用写unsafe了。
通过WebAssembly我们可以在Rust中调用其它语言写的库,反过来其实也可以,WebAssembly成为了一种中间语言或者说虚拟机,例如在Python中,由于动态类型特性,可以这样使用:
import wasmtime . loader
import adder # 直接引入adder.wasm
print ( adder . adder ( 1 , 7 ))
我很看好WebAssembly的前景,独立运行时的出现,使得WebAssembly成为一个通用的公共语言运行时 ,实现“Run any code on any client ”。