# WASM 输入与输出 **Published by:** [Found Pan Tiger](https://paragraph.com/@aliez/) **Published on:** 2021-10-21 **URL:** https://paragraph.com/@aliez/wasm ## Content WASM 简介WASM(WebAssembly)是一种用于堆栈式虚拟机的二进制指令格式。它是是一种文件格式标准,不是特定的虚拟机实现,因此有各种 compiler、interpreter 以及 runtime,如wastime、Wasmer、V8、 Lucet、WAMR、wasm3。 WASM 在设计之初,考虑了以下几点:🔗高效和快速:体积小,加载速度快。可以流式加载,边下载边初始化,减少时间和内存占用。理论上可以达到原生的运行速度。安全:WASM 描述了一个内存安全的沙箱执行环境,甚至可以在现有的 JavaScript 虚拟机中实现。开放可调试:可以翻译为可读的文本格式.wat(可以理解为 WASM 平台的汇编语言),配合 source map 方便调试。Web 标准:模块能够传入和传出 JavaScript 上下文,并通过 JavaScript 访问 Web API 来访问浏览器功能。同时也支持非浏览器环境中运行。但也有以下限制:基础数据类型只有 i32、i64、f32、f64。高级数据结构(如:字符串)每种语言需要自己实现。🔗只能进行同步计算,不支持异步函数。对于 Go、Python 这种带运行时的语言编译到 WASM,需要将运行时一起打包,生成的文件体积较大。接下来我们将使用 Node.js(V8)作为 runtime 语言,AssemblyScript 作为编译到 WASM 的语言,来做点实验。AssemblyScriptA language made for WebAssembly.对于很多在 WASM 诞生前就已经存在的语言,WASM 就像 x86、ARM64 一样,只是一种新的 platform target。 而 AssemblyScript 是一个为 WASM 而生的语言,目前只能编译到 WASM。它的语法是一种 TypeScript 的变体,但实际写起来时,有时却要像写 C 语言那样思考。 AssemblyScript 项目搭建请参考Quick Start。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互相转换。我们以文本格式为例:; 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这篇文档,我们可以知道 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,因此我们用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,封装了这种高级数据结构传递的方法;Rust 也提供了wasm-bingen,可以生成.js和.d.ts文件,在 js 代码中直接 import 就可以调用 Rust 编写的 WASM 模块中的函数。 到这里,相信你对如何在 WASM 与 runtime 之间传递数据已经有了初步了解。 ## Publication Information - [Found Pan Tiger](https://paragraph.com/@aliez/): Publication homepage - [All Posts](https://paragraph.com/@aliez/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@aliez): Subscribe to updates - [Twitter](https://twitter.com/RenzHoly): Follow on Twitter