导入与导出

在熟悉了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_numberinstability这两个用于传递的对象,随后在创建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的boolusize,抑或是JavaScript的number等等,以及各种用户自定义类型。而WASM中,我们只有i32i64f32f64这几个基本的数字类型,那我们在调用接口的过程中,究竟是怎样做类型转换的呢?

在本章中,我们将集中关注高级编程语言的基本数字类型与WASM的数字类型的转换,而对于自定义类型、结构体、数组等,我们将在之后关注。

对于C/C++和Rust而言,目前遵循的是BasicCABI,其中值得注意的是,将所有小于8字节的整型(例如boolu8u16u32)均转化为WASM中的i32,而8字节整型(Rust中的u64,C中的unsigned long long)转化为WASM中的i64

而WASM类型与JavaScript中类型的互相转化则遵循ToJSValueToWebAssemblyValue