为什么说 WebAssembly 是安全的
作为一个和JS引擎里的虚拟机,安全的问题主要围绕内存管理上,WebAssembly 的内存模型是单项透明的内存模型
- WebAssembly 的代码是执行在虚拟机上
- WebAssembly 执行环境所需的内存是由 JavaScript 使用 ArrayBuffer 分配给的
- WebAssembly 代码能直接访问的数据事实上被限制在 Module.buffer 内部,JavaScript环境中的其他对象无法被 WebAssembly 直接访问因此我们称其为单向透明的内存模型
整体链路是怎么样的
- 工程链路对各个语言进行 wasm 编译,生成 wasm 二进制文件
- 运行时编译 wasm 到各个平台的机器码,编译后通过 exports function 或者共享内存段进行和 JavaScript Runtime 的交互
Emscripten 工具介绍
Emscripten 并不是创建 WebAssembly 模块的唯一编译器,但是基于SDK可以打包 wasm 产物:主要有三种形式(通过参数 o 来区分)
-
让Emscripten生成WebAssembly模块、JavaScript plubming 胶水代码文件,以及HTML模板文件
emcc index.c -o index.html
-
让Emscripten生成WebAssembly模块和 JavaScript plumbing 胶水代码文件
emcc index.c -o index.js
-
让Emscripten只生成WebAssembly模块,STANDALONE_WASM 模式
emcc index.c -o index.wasm
为了学习,建议只用 STANDALONE_WASM 模式打包,好了解 API 的细节
其他重要的参数
--no-entry
表示 wasm 没有主入口 main-s ERROR_ON_UNDEFINED_SYMBOLS=0
忽略只有函数申明场景的错误,在C调用JS方法时可以用到,参考案例一-s EXPORTED_FUNCTIONS=${exports_function_name}
如果代码里没有 EMSCRIPTEN_KEEPALIVE 修饰方法保活,可以用这个参数申明对外 exports 的方法,必须在原加上下划线,参考案例三
数据交互
整体来说有两种:函数调用和内存共享传递
- 通过 Number 传参
- 内存交换数据
- 容器到 JavaScript Runtime,通过返回内存指针
- JavaScript Runtime 到容器,通过内存赋值 + 内存指针
案例一:C function 与 JS function 相互调用
index.c
// 引入 emscripten 头文件
#include <emscripten.h>
// EMSCRIPTEN_KEEPALIVE 修饰 exports function 确保代码不会被优化掉
// 申明一个 js_add 方法,由 importObj 传入
EMSCRIPTEN_KEEPALIVE
int js_add(int a, int b);
// 申明一个 js_add 方法,由 importObj 传入
EMSCRIPTEN_KEEPALIVE
int js_console(int data);
EMSCRIPTEN_KEEPALIVE
void get_result() {
// 调用 js 方法
int number = js_add(1, 2);
// 调用 js 方法
js_console(number);
}
复制代码
构建命令
emcc index.c -s ERROR_ON_UNDEFINED_SYMBOLS=0 --no-entry -o ./index.wasm
复制代码
JavaScript
const resp = await fetch(`./index.wasm?t=${Date.now()}`);
const bytes = await resp.arrayBuffer();
const importObject = {
env: {
// 传入 js_add
js_add: (a, b) => a + b,
// 传入 js_console
js_console: (data) => console.log(data),
}
};
const { instance } = await WebAssembly.instantiate(bytes, importObject);
// 调用 c 定义的 get_result 方法
instance.exports.get_result();
复制代码
案例二:通过共享内存传递数据
index.c
#include <emscripten.h>
int g_int = 42;
// 返回 g_int 的指针地址
EMSCRIPTEN_KEEPALIVE
int* get_int_ptr() {
return &g_int;
}
// 返回 g_init 值
EMSCRIPTEN_KEEPALIVE
int get_data() {
return g_int;
}
复制代码
构建命令
emcc index.c -s ERROR_ON_UNDEFINED_SYMBOLS=0 --no-entry -o ./index.wasm
复制代码
JavaScript
const resp = await fetch(`./index.wasm?t=${Date.now()}`);
const bytes = await resp.arrayBuffer();
const importObject = {
env: {}
};
const { instance } = await WebAssembly.instantiate(bytes, importObject);
const ptr = instance.exports.get_int_ptr();
// 获取 wasm 的内存空间
const memory = new Int32Array(instance.exports.memory.buffer);
// 读取 g_int 值
console.log(instance.exports.get_data());
// 通过内存读取值
// 32 位数据,每个单元占用 4 个字节,wasm 内存地址除以 4 拿到真实数据内容
console.log(memory[ptr/4]); // 42
// 通过内存设置值
memory[ptr/4] = 11;
// 读取 g_int 值
console.log(instance.exports.get_data());
复制代码
案例三:通过创建内存空间传递值
index.c
#include <emscripten.h>
// 接受一个指针地址作为参数
EMSCRIPTEN_KEEPALIVE
int sum(int* ptr) {
return ptr[0] + 1;
}
复制代码
构建命令,这里暴露了 malloc 和 free 方法,分别用于创建内存和释放内存,两个方法是 emscripten 提供的,并且导出时需要加上下划线
emcc index.c -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s EXPORTED_FUNCTIONS=_malloc,_free --no-entry -o ./index.wasm
复制代码
JavaScript
const resp = await fetch(`./index.wasm?t=${Date.now()}`);
const bytes = await resp.arrayBuffer();
const importObject = {
env: {}
};
const { instance } = await WebAssembly.instantiate(bytes, importObject);
// 申请一个 wasm 的内存空间,4 * 8 个字节
const intPtr = instance.exports.malloc(4);
const memory = new Int32Array(instance.exports.memory.buffer);
// 修改指针地址的值
memory[intPtr / 4] = 10;
// 调用 wasm 方法,传入值内存地址
console.log(instance.exports.sum(intPtr)); // 11
// 释放空间
instance.exports.free(intPtr);
复制代码
其他
- 由于传递只支持 number,所以 JavaScript 和 wasm 要对值做转换
- 看得出来,数据交互依赖内存空间的创建和销毁,所以高频操作肯定是消耗性能