导入与导出
在熟悉了WASM的基本语法之后,我们会发现,正如最开始所说,WASM的核心还是对各种数的计算,而缺少与系统的交互,主要功能是做一些计算密集型的操作,也就是提供一些例如加密、哈希等操作。那么,WASM究竟怎样才能与外界系统交互呢?从这一章开始,我们就要讨论这个问题。在这一章中,我们讨论的第一个方法是导入(Import)与导出(Export)。
在WASM中,导入与导出非常的简单,我们直接以一个例子来说明。我们创建common.wat
并写入以下代码:
(module
(import "outer" "log_number" (func $log_number (param i32)))
(import "outer" "instability" (global $instability i32))
(func $peek0 (param i32) (result i32 i32)
local.get 0
local.get 0
)
(func (export "wasm_mul") (param $left i32) (param $right i32) (result i32)
(i32.add
(i32.mul (local.get $left) (local.get $right))
(global.get $instability)
)
call $peek0
call $log_number
)
)
这段程序想要实现的是一个脑子多少有点问题的计算器,它可以用来计算乘法,但算出来的结果总会有些偏移。
我们首先看到的是两个import
语句,这个语句就是WASM中的「导入」。import
后跟着两个字符串,例如"outer"和"log_number",这个的意思是导入outer模块的log_number
函数。关于这里的模块怎么用,我们后面会提到。紧接着这两个字符串,就声明了这个导入的类型,这里是一个接受一个i32
类型参数的函数$log_number
,而下一个import
则导入的是一个i32
类型的全局变量$instability
。
而导出,则是我们之前已经用了很多次的export
语句。在这段程序里,我们导出了一个名叫wasm_mul
的函数。其实现为,将输入的两个数相乘,然后加上之前导入的偏移值全局变量$instability
,调用导入的函数$log_number
作个日志输出,然后返回。
从上面的描述可以知道,我们的WASM程序终于有了主动输出的能力了,虽然这个能力是导入的,但终究还是能自己控制的。
导入与导出的使用
说了这么多,我们来看看使用效果。
在Rust中使用wasmer库
与之前使用wasmer来调用adder.wasm
十分类似,我们来看看这次的代码是怎么写的:
let wasm_bytes = fs::read("./common.wasm")?;
let mut store = Store::default();
let module = Module::new(&store, wasm_bytes)?;
let imports = imports! {
"outer" => {
"log_number" =>
Function::new_typed(
&mut store,
|number: i32| println!("In WASM, we got {number}")
),
"instability" => Global::new(&mut store, Value::I32(-5)),
}
};
let instance = Instance::new(&mut store, &module, &imports)?;
let wasm_mul: TypedFunction<(u32, u32), u32> = instance
.exports
.get_typed_function(&mut store, "wasm_mul")?;
println!("Calculating 5 x 8 with instability -5 ...");
let prod = wasm_mul.call(&mut store, 5, 8)?;
println!("From outside, we got {prod}");
运行这段程序,我们可以看到输出:
Calculating 5 x 8 with instability -5 ...
In WASM, we got 35
From outside, we got 35
可以看到,我们之前写的WASM程序确实像我们说的一样,想计算5乘8的结果,但是脑子出了点问题,算出来的结果是五八三十五。特别值得注意的是"In WASM, we got 35",这段话是在WASM执行的过程中输出的!说明我们的WASM程序,确实通过导入函数的方法,实现了与系统的交互。
具体到wasmer的API中来看,我们与之前和调用adder.wasm
的程序作对比,会发现,这里主要是新增了imports
的相关语句。wasmer提供了imports!
宏,我们在这个宏中,首先声明了第一层的"outer",这对应了我们在WASM的导入中第一个字符串,然后分别定义了log_number
函数和instability
全局变量,其定义方法非常直观。随后,在Instance
实例创建的过程中传入这个对象,就可以将Rust中的函数、数据传入WASM中。
在Rust中使用wasmtime库
与之前类似,我们来看看如何使用wasmtime库来建立导入导出:
let wasm_bytes = fs::read("./common.wasm")?;
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let module = Module::new(&engine, wasm_bytes)?;
let log_number = Func::wrap(&mut store, |number: i32| {
println!("In WASM, we got {number}");
});
let instability = Global::new(
&mut store,
GlobalType::new(ValType::I32, Mutability::Const),
(-5i32).into(),
)?;
let instance = Instance::new(
&mut store,
&module,
&[log_number.into(), instability.into()],
)?;
let wasm_mul = instance.get_typed_func::<(u32, u32), u32>(&mut store, "wasm_mul")?;
println!("Calculating 5 x 8 with instability -5 ...");
let prod = wasm_mul.call(&mut store, (5, 8))?;
println!("From outside, we got {prod}");
可以看到,我们也是创建了log_number
和instability
这两个用于传递的对象,随后在创建Instance
实例的过程中传入,即可将Rust中的函数与数据传入WASM。
在Web中
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WASM Test</title>
</head>
<body>
<script>
const importObject = {
outer: {
log_number: (number) => console.log(`In WASM, we got ${number}`),
instability: new WebAssembly.Global({ value: "i32", mutable: false }, -5)
}
};
WebAssembly.instantiateStreaming(fetch("./common.wasm"), importObject)
.then(obj => {
console.log('Calculating 5 x 8 with instability -5 ...');
const prod = obj.instance.exports.wasm_mul(5, 8);
console.log(`From outside, we got ${prod}`);
});
</script>
</body>
</html>
在Web中使用WASM的导入和导出,只需要在instantiateSteaming
函数中提供第二个参数importObject
即可。这个参数的创建也很清楚,这里不再赘述。
值得一提的是,如果我们想使用ES Module的方案,也就是目前利用webpack来做import { wasm_mul } from './common.wasm'
,则并不是那么容易,需要修改WASM中的代码,具体可以看ES Module Integration Proposal的示例。
类型转换
上述讲了若干种高级编程语言与WASM程序通过导入与导出来交互的例子,有一个问题我们需要注意:类型转换。无论在Rust还是JavaScript中,我们都有若干种类型,例如Rust的bool
、usize
,抑或是JavaScript的number
等等,以及各种用户自定义类型。而WASM中,我们只有i32
、i64
、f32
、f64
这几个基本的数字类型,那我们在调用接口的过程中,究竟是怎样做类型转换的呢?
在本章中,我们将集中关注高级编程语言的基本数字类型与WASM的数字类型的转换,而对于自定义类型、结构体、数组等,我们将在之后关注。
对于C/C++和Rust而言,目前遵循的是BasicCABI,其中值得注意的是,将所有小于8字节的整型(例如bool
、u8
、u16
、u32
)均转化为WASM中的i32
,而8字节整型(Rust中的u64
,C中的unsigned long long
)转化为WASM中的i64
。
而WASM类型与JavaScript中类型的互相转化则遵循ToJSValue和ToWebAssemblyValue。