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 就算完成了。