# WASM 输入与输出

By [Found Pan Tiger](https://paragraph.com/@aliez) · 2021-10-21

---

WASM 简介
-------

WASM（WebAssembly）是一种用于堆栈式虚拟机的二进制指令格式。它是是一种文件格式标准，不是特定的虚拟机实现，因此有各种 compiler、interpreter 以及 runtime，如[wastime](https://github.com/bytecodealliance/wasmtime)、[Wasmer](https://wasmer.io/)、[V8](https://v8.dev/docs/wasm-compilation-pipeline)、 [Lucet](https://github.com/bytecodealliance/lucet/)、[WAMR](https://github.com/bytecodealliance/wasm-micro-runtime)、[wasm3](https://github.com/wasm3/wasm3/)。

WASM 在设计之初，考虑了以下几点：[🔗](https://webassembly.org/)

*   **高效和快速**：体积小，加载速度快。可以流式加载，边下载边初始化，减少时间和内存占用。理论上可以达到原生的运行速度。
    
*   **安全**：WASM 描述了一个内存安全的沙箱执行环境，甚至可以在现有的 JavaScript 虚拟机中实现。
    
*   **开放可调试**：可以翻译为可读的文本格式`.wat`（可以理解为 WASM 平台的汇编语言），配合 source map 方便调试。
    
*   **Web 标准**：模块能够传入和传出 JavaScript 上下文，并通过 JavaScript 访问 Web API 来访问浏览器功能。同时也**支持非浏览器环境中运行**。
    

但也有以下限制：

*   基础数据类型只有 i32、i64、f32、f64。高级数据结构（如：字符串）每种语言需要自己实现。[🔗](https://www.assemblyscript.org/memory.html#internals)
    
*   只能进行同步计算，不支持异步函数。
    
*   对于 Go、Python 这种带运行时的语言编译到 WASM，需要将运行时一起打包，生成的文件体积较大。
    

接下来我们将使用 Node.js（V8）作为 runtime 语言，AssemblyScript 作为编译到 WASM 的语言，来做点实验。

AssemblyScript
--------------

> A language made for WebAssembly.

对于很多在 WASM 诞生前就已经存在的语言，WASM 就像 x86、ARM64 一样，只是一种新的 platform target。

而 AssemblyScript 是一个为 WASM 而生的语言，目前只能编译到 WASM。它的语法是一种 TypeScript 的变体，但实际写起来时，有时却要像写 C 语言那样思考。

AssemblyScript 项目搭建请参考[Quick Start](https://www.assemblyscript.org/quick-start.html)。

a+b
---

我们来写一个简单的 a+b，为什么不是 hello world？因为字符串在 WASM 里是高级数据结构。我们先从最简单的 i32 开始：

    // sum.ts
    export function sum(a: i32, b: i32): i32 {
      return a + b;
    }
    

编译后得到一个二进制文件`sum.wasm`，和一个文本文件`sum.wat`。两者可以使用[wabt](https://github.com/WebAssembly/wabt)互相转换。我们以文本格式为例：

    ; sum.wat
    (module
      (type $t0 (func (param i32 i32) (result i32)))
      (func $sum (type $t0) (param $p0 i32) (param $p1 i32) (result i32)
        local.get $p0
        local.get $p1
        i32.add)
      (memory $memory 0)
      (export "sum" (func $sum))
      (export "memory" (memory 0)))
    

如果学过汇编，不了解 WAT 语法也可以看懂个大概。值得注意的是`export "memory"`，这是 WSAM 把内存暴露给 runtime，后面的例子会用到。

接下来在 Node.js 中调用：

    import fs from "fs";
    
    const file = await fs.promises.readFile("./sum.wasm");
    const module = new WebAssembly.Module(file);
    const instance = new WebAssembly.Instance(module);
    
    console.log((instance.exports.sum as Function)(1, 2));
    // stdout: 3
    

只需几行代码就完成了调用，并输出了预期的结果。

'hello, world!'
---------------

来看看字符串是如何在 WASM 与 runtime 之间传递的。

    // hello-world.ts
    export function hello_world(): string {
      return `hello, world!`;
    }
    

编译后得到：

    ; hello-world.wat
    (module
      (type $t0 (func (result i32)))
      (func $hello_world (type $t0) (result i32)
        i32.const 1056)
      (memory $memory 1)
      (export "hello_world" (func $hello_world))
      (export "memory" (memory 0))
      (data $d0 (i32.const 1036) ",")
      (data $d1 (i32.const 1048) "\01\00\00\00\1a\00\00\00h\00e\00l\00l\00o\00,\00 \00w\00o\00r\00l\00d\00!"))
    

在 Node.js 中调用：

    import fs from "fs";
    
    const file = await fs.promises.readFile("./hello-world.wasm");
    const module = new WebAssembly.Module(file);
    const instance = new WebAssembly.Instance(module);
    
    console.log((instance.exports.hello_world as Function)());
    // stdout: 1056
    

结果不是"hello, world!"，而是一个数字，发生什么事了？

这个数字其实是一个指针的地址，它指向了一个字符串。通过[string-layout](https://www.assemblyscript.org/memory.html#string-layout)这篇文档，我们可以知道 AssemblyScript 的字符串是如何在内存中排布的，因此我们可以编写代码把字符串取出来。

    // ...
    const offset = (instance.exports.hello_world as Function)();
    const memory = instance.exports.memory as WebAssembly.Memory;
    const buffer = Buffer.from(memory.buffer);
    const len = buffer.readUInt32LE(offset - 4);
    
    console.log(buffer.slice(offset, offset + len).toString());
    // stdout: hello, world!
    

WASM 使用的是[little endian byte order](https://webassembly.github.io/spec/core/syntax/instructions.html#memory-instructions)，因此我们用`readUInt32LE`而不是`readUInt32BE`。

\`hello, ${str}!\`
------------------

我们已经学会把字符串从 WASM 传到 runtime 中，接下来我们学习反向操作。

    // hello.ts
    export function hello(str: string): string {
      return `hello, ${str}!`;
    }
    

使用编译参数：

    asc hello.ts --use abort= --target release
    

我们只改了几个字符，但这次编译出的 hello.wat 已经有 400+行。

在前两个例子中，内存是静态的，而在这个例子里我们进行了字符串拼接，就涉及到了内存的动态管理。因此 AssemblyScript 需要把内存回收运行时打包进 WASM，就增加了文件体积。

    // ...
    const memory = instance.exports.memory as WebAssembly.Memory;
    const buffer = Buffer.from(memory.buffer);
    const str = "wasm";
    const ptr = 1024;
    buffer.writeUInt32LE(str.length, ptr - 4);
    Buffer.from(str).copy(buffer, ptr);
    const offset = (instance.exports.hello as Function)(ptr);
    const len = buffer.readUInt32LE(offset - 4);
    
    console.log(buffer.slice(offset, offset + len).toString());
    // stdout: hello, wasm!
    

这里的`ptr = 1024`是一个危险的做法，这个例子里我们假定 WASM 不会使用第 1024 个字节附近的内存，实际使用中不能这么写，这样可能会覆盖掉 WASM 所需要的数据，使 WASM 运行异常。

AssemblyScript 提供了[Loader](https://www.assemblyscript.org/loader.html#loader)，封装了这种高级数据结构传递的方法；Rust 也提供了[wasm-bingen](https://github.com/rustwasm/wasm-bindgen)，可以生成`.js`和`.d.ts`文件，在 js 代码中直接 import 就可以调用 Rust 编写的 WASM 模块中的函数。

到这里，相信你对如何在 WASM 与 runtime 之间传递数据已经有了初步了解。

---

*Originally published on [Found Pan Tiger](https://paragraph.com/@aliez/wasm)*
