Table of Contents
Web Assembly 这个技术出来很长时间了,但一直没有用过,主要是不会写后端,也用不到。之前也打算尝试一下,可是安装 emscripten 后测试编译 c 代码失败了。记得好像是版本问题。但我只有一台服务器,也不打算折腾,就放下了。
这几天看到有文章里是用 docker 编译的 wasm。因为之前也看过一点 docker,顿时觉得应该再试试,毕竟 docker 里面怎么折腾也没关系。大不了再实例化一个容器就是了。
安装容器环境
关于用 docker 编译 wasm,在 docker 的官方仓库里有已经配置好的镜像,按照镜像的文档操作就可以了。
首先,在某个文件夹下创建一个 cpp 文件(官方示例是 c++,可以换成自己习惯的语言)。
// helloword.cpp #include <iostream> int main() { std::cout << "Hello World!" << std::endl; return 0; }
然后,执行命令:
docker run \ --rm \ -v $(pwd):/src \ -u $(id -u):$(id -g) \ emscripten/emsdk \ emcc helloworld.cpp -o helloworld.js
关于命令的解释文档里有。
使用 wasm
nignx 配置
对于 wasm 文件,服务端在发送的时候是作为流处理的。但浏览器端需要类型为 application/wasm。因此,需要配置 mime.types 文件。这个文件 nginx/conf 目录下。打开后添加 application/wasm wasm 再重启 nginx 即可。
加载并实例化 wasm
对于 wasm 文件的加载,mdn 里提了两种,一种是 fetch,还有一种是 XMLHttpRequest。对于 wasm 字节码的实例化也有很多方法。比如:
fetch('simple.wasm') .then(res => res.arrayBuffer() ).then(bytes => WebAssembly.instantiate(bytes, importObject) ).then(results => { results.instance.exports.exported_func(); });
使用 WebAssembly.instantiate() 方法需要先将请求的结果转成 ArrayBuffer 才能实例化。
还有一种是 WebAssembly.instantiateStreaming()。这种方法可以直接编译实例化。
WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObject) .then(obj => obj.instance.exports.exported_func());
因为我需要将 wasm 传到 worker 里面使用,所以用了另一种——WebAssembly.compileStreaming()。
// index.js /** * 加载 wasm * @param {string} path 模块路径 * @param {string} module 模块名称 **/ function loadWasm(path, module) { WebAssembly.compileStreaming(fetch(path)) .then(mod => { // 发送到 worker。这里只说明相关 api 用法,所以先省略。 }); }
// worker.js WebAssembly.instantiate(mod) .then((instance) => { console.log(instance) })
实例化参数
上面的方法都有第二个可选参数:importObject。这个参数是一个对象,包含需要导入到 wasm 模块的函数、WebAssembly.Memory 对象等。wasm 模块默认的内存(instance.exports.memory)是 16m。这可能不够用,此时就可以通过 importObject 传入一个更大的内存块给模块使用。
wasm 模块函数不存在问题
我根据网上的文章写了一个 c 模块并编译成了 wasm,但实例化后的模块并没有相关函数。
int add (int x, int y) { return x + y; } int square (int x) { return x * x; }
这是因为默认情况下,emscripten 生成的代码只会调用 main 函数,其他的是为无用代码。为了避免这种情况,需要在函数名前添加 EMSCRIPTEN_KEEPALIVE。
#include <emscripten/emscripten.h> // 因为 emscripten 编译的时候只保留 main 函数,所以为了让其他函数也导出,需要在其他函数前面加上 EMSCRIPTEN_KEEPALIVE。而这个标识符是在 emscripten.h 中声明的。 int EMSCRIPTEN_KEEPALIVE add (int x, int y) { return x + y; } int EMSCRIPTEN_KEEPALIVE square (int x) { return x * x; }
demo 代码
// index.js // 加载 worker const worker = new Worker('./script/worker.js') // worker 响应 function handleWorkerMessage(e) { // console.log(e) if (e.data === 'ready') { status.worker = 1 return } if (!e.data || !e.data.key) { console.log('worker 返回数据错误') } let task = tasks.get(e.data.key) if (task) { task.resolve(e.data.result) } } // worker 监听 worker.addEventListener('message', handleWorkerMessage) // 任务列表 const tasks = new Map() // 发送任务 function dispatch(obj) { return new Promise((resolve, reject) => { let timestamp = Date.now() let key = Math.random().toString().substr(2) const task = { ...obj, timestamp, key, resolve, reject } tasks.set(key, task) worker.postMessage({ ...obj, timestamp, key, }) }) } // 状态管理 const status = new Proxy({ worker: 0, // 0--未就绪,1--就绪 wasm: 0, 'c/add': 0 }, { set: (obj, prop, value) => { let oldValue = obj[prop] let handler = null obj[prop] = value switch (prop) { case 'worker': console.log('worker change', value) break case 'c/add': console.log('wasm c/add', value) break } // 处理数据监听 handler = watches.get(`status.${prop}`) if (handler) { handler(value, oldValue) } } }) // 监听 const watches = new Map() function watch(key, handler) { watches.set(key, handler) } /** * 加载 wasm * @param {string} path 模块路径 * @param {string} module 模块名称 * @param {object} importObject 导入对象,可选 **/ function loadWasm(path, module, importObject) { WebAssembly.compileStreaming(fetch(path)) .then(mod => { dispatch({ action: 'wasm', param: [module, mod, importObject] }) .then(res => { if (res) { status[module] = 1 } else { console.log(`${module} 模块加载失败`) } }) }); } // 检查 wasm 是否都加载完成 function checkWasmStatus() { const modules = ['c/add'] if (modules.every(module => status[module])) { status.wasm = 1 } } // 监听 status.worker 属性 watch('status.worker', (status) => { if (status) { // 加载 c/add 模块 loadWasm('./module/c/add.wasm', 'c/add') } }) // 监听 status.c/add watch('status.c/add', (status) => { if (status) { // 检查 wasm 是否都加载完成 checkWasmStatus() } }) // 监听 status.wasm watch('status.wasm', (status) => { if (status) { dispatch({ action: 'printActions', }) dispatch({ action: 'c/add.add', param: [1, 2] }) .then(res => { console.log(res) }) } })
// worker.js onmessage = async (e) => { // console.log(e) const { action, key, param = [] } = e.data if (!actions[action]) { console.log(`操作${action || ''}不存在`) return } else { const res = { key, result: await actions[action](...param) } postMessage(res) } } // 通知准备就绪 postMessage('ready') /** * 加载 wasm * @param {string} module 模块名称 * @param {ArrayBuffer} mod wasm字节码 * @param {object} importObject 导入对象,可选 **/ async function wasm(module, mod, importObject = {}) { return new Promise((resolve, reject) => { console.log(importObject) WebAssembly.instantiate(mod, importObject) .then((instance) => { Object.keys(instance.exports).map(key => { actions[`${module}.${key}`] = instance.exports[key] }) resolve(true) }) }) } /** * 打印 actions **/ function printActions() { console.log(actions) } const actions = { wasm, printActions }
至此,一个简单的在 worker 中运行 wasm 的 demo 就算完成了。