关于 WebAssembly 的一些工程化总结

为什么说 WebAssembly 是安全的

作为一个和JS引擎里的虚拟机,安全的问题主要围绕内存管理上,WebAssembly 的内存模型是单项透明的内存模型

  1. WebAssembly 的代码是执行在虚拟机上
  2. WebAssembly 执行环境所需的内存是由 JavaScript 使用 ArrayBuffer 分配给的
  3. WebAssembly 代码能直接访问的数据事实上被限制在 Module.buffer 内部,JavaScript环境中的其他对象无法被 WebAssembly 直接访问因此我们称其为单向透明的内存模型

整体链路是怎么样的

  1. 工程链路对各个语言进行 wasm 编译,生成 wasm 二进制文件
  2. 运行时编译 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 的方法,必须在原加上下划线,参考案例三

数据交互

整体来说有两种:函数调用内存共享传递

  1. 通过 Number 传参
  2. 内存交换数据
    1. 容器到 JavaScript Runtime,通过返回内存指针
    2. 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);
复制代码

其他

  1. 由于传递只支持 number,所以 JavaScript 和 wasm 要对值做转换
  2. 看得出来,数据交互依赖内存空间的创建和销毁,所以高频操作肯定是消耗性能

おすすめ

転載: juejin.im/post/7035108268066209829