WebAssembly关键概念
要了解WebAssembly如何在浏览器中运行,需要几个关键概念。所有这些概念都在WebAssembly的JavaScript API中得到了1:1的反映。
模块(Module)。代表一个WebAssembly二进制文件,已经被浏览器编译成可执行的机器代码。模块是无状态的(stateless),因此,像Blob一样,可以明确地在窗口和工作者之间共享(通过postMessage())。一个模块就像ES模块一样声明导入和导出。
内存(Memory)。一个可调整大小的ArrayBuffer,包含由WebAssembly的底层(low-level)内存访问指令(access instructions)读取和写入的线性阵列。
表(Table)。一个可调整大小的类型化的(typed)引用数组(例如函数),否则不能作为原始字节存储在Memory中(出于安全和可移植性的原因)。
实例(Instance)。一个模块与它在运行时使用的所有状态配对,包括内存、表和一组导入值。一个实例就像一个ES模块,它已经被加载到一个特定的全局中,并带有一组特定的导入(imports)。
PS:
Blob: Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。
postMessage:window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。
JavaScript API为开发者提供了创建模块、内存、表格和实例的能力。给定一个WebAssembly实例,JavaScript代码可以同步调用它的出口(exports),这些出口被暴露为正常的JavaScript函数。任意的JavaScript函数也可以被WebAssembly代码同步调用,通过传递这些JavaScript函数作为WebAssembly实例的导入(imports)。
由于JavaScript可以完全控制WebAssembly代码的下载、编译和运行,JavaScript开发者甚至可以把WebAssembly看作只是一个有效生成高性能函数的JavaScript功能。
在未来,WebAssembly模块将像ES模块一样可以加载(使用
WASM 概述
Chrome 和 Node.js 中运行 WASM 的运行时
Chrome 和 Node.js 中使用的是 V8 JavaScript 引擎,它内置了对 WebAssembly 的完整支持:
- V8 包含一个完整的 WebAssembly 编译和执行引擎
- WebAssembly 字节码会被编译为高效的机器码(通过 TurboFan 编译器)
- 完全支持 WebAssembly Core Specification(包括 SIMD、线程、GC、异常等提案)
- 沙箱安全机制由 V8 提供
运行 WASM
通用运行 WASM
如下代码是使用 AssemblyScript 生成的示例代码
async function instantiate(module, imports = {}) {
const { exports } = await WebAssembly.instantiate(module, imports);
return exports;
}
export const {
memory,
add,
} = await (async url => instantiate(
await (async () => {
const isNodeOrBun = typeof process != "undefined" && process.versions != null && (process.versions.node != null || process.versions.bun != null);
if (isNodeOrBun) { return globalThis.WebAssembly.compile(await (await import("node:fs/promises")).readFile(url)); }
else { return await globalThis.WebAssembly.compileStreaming(globalThis.fetch(url)); }
})(), {
}
))(new URL("debug.wasm", import.meta.url));
<script type="module">
import { add } from "./build/debug.js";
document.body.innerText = add(1, 2);
</script>
通用运行 WASM(进阶)
在 AssemblyScript 中使用了 console.log("hello");
的情况下,asc 编译得到的 debug.js
async function instantiate(module, imports = {}) {
const adaptedImports = {
env: Object.setPrototypeOf({
"console.log"(text) {
// ~lib/bindings/dom/console.log(~lib/string/String) => void
text = __liftString(text >>> 0);
console.log(text);
},
abort(message, fileName, lineNumber, columnNumber) {
// ~lib/builtins/abort(~lib/string/String | null?, ~lib/string/String | null?, u32?, u32?) => void
message = __liftString(message >>> 0);
fileName = __liftString(fileName >>> 0);
lineNumber = lineNumber >>> 0;
columnNumber = columnNumber >>> 0;
(() => {
// @external.js
throw Error(`${message} in ${fileName}:${lineNumber}:${columnNumber}`);
})();
},
}, Object.assign(Object.create(globalThis), imports.env || {})),
};
const { exports } = await WebAssembly.instantiate(module, adaptedImports);
const memory = exports.memory || imports.env.memory;
function __liftString(pointer) {
if (!pointer) return null;
const
end = pointer + new Uint32Array(memory.buffer)[pointer - 4 >>> 2] >>> 1,
memoryU16 = new Uint16Array(memory.buffer);
let
start = pointer >>> 1,
string = "";
while (end - start > 1024) string += String.fromCharCode(...memoryU16.subarray(start, start += 1024));
return string + String.fromCharCode(...memoryU16.subarray(start, end));
}
return exports;
}
export const {
memory,
add,
} = await (async url => instantiate(
await (async () => {
const isNodeOrBun = typeof process != "undefined" && process.versions != null && (process.versions.node != null || process.versions.bun != null);
if (isNodeOrBun) { return globalThis.WebAssembly.compile(await (await import("node:fs/promises")).readFile(url)); }
else { return await globalThis.WebAssembly.compileStreaming(globalThis.fetch(url)); }
})(), {
}
))(new URL("debug.wasm", import.meta.url));
Chrome 中运行 WASM
创建 index.html 并使用 python -m http.server
访问 index.html
<script type="module">
(async () => {
const url = new URL("debug.wasm", import.meta.url);
const wasmBuffer = fetch(url);
const wasm = await WebAssembly.compileStreaming(wasmBuffer);
const memory = new WebAssembly.Memory({ initial: 256 });
const importObject = {
env: { memory: memory, }
};
const instance = await WebAssembly.instantiate(wasm, importObject);
console.log(instance.exports.add(3, 4)); // 输出 7
})();
</script>
Node.js 中运行 WASM
创建 nodejs 项目 index.js 并运行 node index.js
const fs = require('fs');
(async () => {
const wasmBuffer = fs.readFileSync('debug.wasm');
const wasm = await WebAssembly.compile(wasmBuffer);
const memory = new WebAssembly.Memory({ initial: 256 });
const importObject = {
env: { memory: memory, }
};
const instance = await WebAssembly.instantiate(wasm, importObject);
console.log(instance.exports.add(3, 4)); // 输出 7
})();
创建 Wasm 项目
字节码快速创建 Wasm 项目
参考 https://zhuanlan.zhihu.com/p/620767652 直接手写二进制机器码的方式生成了一段 wasm 代码,并使用了 WebAssembly.compile 接口来进行编译,最后调用了 wasm 实现的 add 和 square 函数
WebAssembly.compile(new Uint8Array(`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16))
)).then(module => {
const instance = new WebAssembly.Instance(module)
const { add, square } = instance.exports
console.log('2 + 4 =', add(2, 4))
console.log('3^2 =', square(3))
console.log('(2 + 5)^2 =', square(add(2 + 5)))
})
使用 wat 创建 Wasm 项目
(module
(func $add (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add))
使用如下 Wasm Toolkit 可以将一个 wat 编译成 wasm 文件
wat2wasm index.wat -o index.wasm
wasm2wat index.wasm -o index2.wat
AssemblyScript 创建 Wasm 项目
# 使用 npm init 创建一个项目
npm init
# 安装 assemblyscript 开发依赖
pnpm i --save-dev assemblyscript
# 安装 assemblyscript 初始化工程项目(包含测试代码等)
npx asinit
生成的项目中包含如下 scripts,分别使用构建 wasm 文件
"asbuild:debug": "asc assembly/index.ts --target debug",
"asbuild:release": "asc assembly/index.ts --target release",
C 创建 Wasm 项目
// add.cpp
#include <emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE // 使用 EMSCRIPTEN_KEEPALIVE 防止函数被优化掉
int add(int a, int b) {
return a + b;
}
}
编译为 Wasm 文件
emcc -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_add']" add.cpp -o add.wasm
编译时生成 JS 胶水文件
emcc -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_add']" -s EXPORTED_RUNTIME_METHODS="['cwrap']" add.cpp -o add.wasm.js
编写如下 index.html,并使用 python -m http.server 访问 index.html
<script src="add.wasm.js"></script>
<script>
Module.onRuntimeInitialized = function () {
const addFunc = Module.cwrap('add', 'number', ['number', 'number']);
console.log("add(3, 4) =", addFunc(3, 4)); // 输出: 7
};
</script>
C 创建 Wasm 项目(主函数)
编写如下 hello.cpp
代码
#include <stdio.h>
int main()
{
printf("Hello, WasmEdge!\n");
return 0;
}
# 激活 emcc
emsdk activate
# 编译为 Wasm
emcc hello.cpp -s WASM=1 -o hello.wasm
# 生成 .html、.js 和 .wasm 的完整页面(更加推荐使用第二条命令)
emcc -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_main']" hello.cpp -o hello.wasm.js
emcc -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_main']" -s EXPORTED_RUNTIME_METHODS="['cwrap']" hello.cpp -o hello.wasm.js
# 生成一个完整的 HTML 页面
emcc -O3 -s WASM=1 -o hello.html hello.cpp
使用如下 index.html,并使用 python -m http.server 访问 index.html
<script src="hello.wasm.js"></script>
C# 创建 Wasm 项目
引用
https://webassembly.org/getting-started/developers-guide/
https://www.assemblyscript.org/getting-started.html#setting-up-a-new-project
https://zhuanlan.zhihu.com/p/620767652
WASM汇编入门教程