WASM 是来取代 javascript 的吗?
WebAssembly 是一种与 JavaScript 不同的语言,但并非旨在取代它。相反,它旨在作为 JavaScript 的补充并与其协同工作,使 Web 开发人员能够充分利用两种语言各自的优势:
-
JavaScript 是一种高级语言,足够灵活和表达力强以编写 Web 应用程序。它有许多优点 — 动态类型、无需编译步骤,并拥有庞大的生态系统,提供强大的框架、库和其他工具。
-
WebAssembly 和 JavaScript 可以灵活的互相调用。
由于这两个语言的特点,页面、操作逻辑用 Javascript 实现,CPU 密集型的计算使用 wasm。
WASM 是如何工作的
C、C++ 和 Rust 等源语言编译成 .wasm 文件,.wasm 文件中存储的是二进制字节码,通过 JavaScript API 加载 .wasm 文件后浏览器会将其编译成可执行机器码。
- JavaScript 代码能够调用 wasm 导出的代码。
- 通过把 JavaScript 函数导入到 WebAssembly 实例中,任意的 JavaScript 函数都能被 WebAssembly 代码同步调用。
当下主流的 WebAssembly 生态:
-
使用 Emscripten 移植一个 C/C++ 应用程序。
-
直接在汇编层,编写或生成 WebAssembly 代码。
-
编写 Rust 程序,将 WebAssembly 作为它的输出。
-
使用 AssemblyScript,它类似于 TypeScript 并且可编译成二进制 WebAssembly 代码。
直接编写 WebAssembly 的实践
我们可以通过编写 wat(WebAssembly Text Format) 代码,再将 wat 转换成 wasm 的方式来直观的编写和理解 WebAssembly 模块。
wat 示例如下:
;; wat 转换工具: https://webassembly.github.io/wabt/demo/wat2wasm/
(module
;; 从 JavaScript 导入 consoleLogWithPrefix 方法
(import "env" "consoleLogWithPrefix" (func $consoleLogWithPrefix (param i32) (param i32)))
;; 和 js 共享的内存
(import "env" "memory" (memory $mem 1))
;; 定义一个 helloWorld 方法并返回一个 String
(func $helloWorld (result i32)
;; 定义要返回的字符串
(i32.const 0)
(i32.const 11)
(call $consoleLogWithPrefix)
;; 返回字符串的内存地址
(i32.const 0)
)
;; 导出 helloWorld 方法给
(export "helloWorld" (func $helloWorld))
;; 定义 helloWorld 方法返回的字符串
(data (i32.const 0) "hello wasm")
)
Javascript 引入 wasm 示例:
<!DOCTYPE html>
<html>
<head>
<title>WASM Example</title>
</head>
<body>
<script>
// 定义导入给 wasm 的方法
const consoleLogWithPrefix = (ptr, len) => {
const memory = new Uint8Array(wasmMemory.buffer, ptr, len);
const string = new TextDecoder('utf8').decode(memory);
console.log("[wasm]: " + string);
}
const wasmModule = fetch('test.wasm');
// 初始化内存为 10 页,一页 64KB
const wasmMemory = new WebAssembly.Memory({ initial: 10 });
// 内存扩大 10 页
wasmMemory.grow(10);
WebAssembly.instantiateStreaming(fetch('test.wasm'), {
env: {
consoleLogWithPrefix,
memory: wasmMemory, // 共享内存
},
}).then((result) => {
const { instance } = result;
const ptr = instance.exports.helloWorld();
const memory = new Uint8Array(wasmMemory.buffer, ptr, 10);
const message = new TextDecoder('utf8').decode(memory);
console.log(message);
});
</script>
</body>
</html>
以为 Emscripten 为例的实践
从上述直接编写 WebAssembly 的实践来看,直接写 wat 文件、自己实现内存管理、自己实现胶水代码是十分繁琐的。同时还要重新了解 WebAssembly 写法,所以在开发领域常用的还是 Emscripten 类的完整工具链,自动帮我们实现和优化这些繁琐的东西。
Emscripten 的工作流程:
-
Emscripten 首先把 C/C++ 提供给 clang+LLVM。
-
Emscripten 将 clang+LLVM 编译的结果转换为 Wasm 二进制文件。
-
加载 WASM,以及生成 JavaScript 和 WASM 交互的胶水代码,方便互相调用。
编译 C/C++ 为 WebAssembly (结合代码):
参考:developer.mozilla.org/zh-CN/docs/… emsdk(Emscripten 工具链 sdk) 源码,并安装、激活工具链,完成后 emcc 指令就安装好了。
常用指令示例:
// 基于 test.cc 生成 test.wasm 以 test.js(胶水代码)
emcc test.cc -o test.js
// 给 c++ 代码中导入 javascript 方法
emcc test.cc --js-library pkg.js -o test.js
// 按需导出内置的工具方法,如 UTF8ToString 是将 ptr 专成 string 的方法
// 默认只导出必须的,需要啥要在这里设置,详情需看 emscriptern 的文档
emcc test.cc -s EXPORTED_RUNTIME_METHODS='["UTF8ToString"]' -o test.js
// 指定栈内存、堆内存
emcc index.cc -s TOTAL_MEMORY=67108864 -s TOTAL_STACK=3145728 -o index.js
// 指定 64 位,默认 32 位
emcc index.cc -sMEMORY64=2 -o index.js
// 指定内存模式
emcc mem.cc -s MALLOC="emmalloc" -o mem.js
emcc mem.cc -s MALLOC="dlmalloc" -o mem.js
// 设置优化等级, 不仅仅 C++,javascript 胶水代码也会同步压缩
emcc -O1 mem.cc -o mem.js
emcc -O2 mem.cc -o mem.js
emcc -O3 mem.cc -o mem.js
运行在非浏览器环境
由于 wasm 的跨平台特性,它可以运行在任意平台,通过 WASI(WebAssembly System Interface) 和原生系统进行交互实现业务功能:
主流解决方案:wasmedge、wasmtime、wasmer、WAVM 。
WASM 运行时
WebAssembly 运行时由模块加载和解析器、执行引擎以及与宿主的系统交互接口(WASI,浏览器环境) 等关键部分组成,类似于 JVM。