本文目录 34 个章节
WebGL应用-Wasm加载使用原理
emcc my_test_lib.cpp ^
-O0 ^
-sWASM=1 ^
-sMODULARIZE=1 ^
-sEXPORT_NAME=createMyLib ^
-sENVIRONMENT=web ^
-sEXPORTED_RUNTIME_METHODS="['cwrap']" ^
-o wasm/my_test_lib.js
使用该方式编译C++代码,会生成wasm和对应的js glue js glue 负责对wasm进行加载, 接口导出, 外部接口导入等工作. 加载该js脚本, 即可完成对wasm的加载
整体框架
var createMyLib = (() => {
var _scriptDir = typeof document !== 'undefined' && document.currentScript
? document.currentScript.src
: undefined;
return (function (createMyLib) { … })(/* 参数留空 */);
})();
IIFE ((() => { … })())
立即执行并返回 一个工厂函数,赋给全局变量 createMyLib (编译时可设置该函数变量名, 不重要)
该工厂函数定义如下, 功能: 创建Module, 返回ready
function(createMyLib) {
createMyLib = createMyLib || {};
...
var Module = Object.assign({}, createMyLib);
...
return createMyLib.ready;
}
Module.ready 定义如下:
Module['ready'] = new Promise(function(resolve, reject) {
readyPromiseResolve = resolve;
readyPromiseReject = reject;
});
function doRun() {
// run may have just been called through dependencies being fulfilled just in this very frame,
// or while the async setStatus time below was happening
if (calledRun) return;
calledRun = true;
Module['calledRun'] = true;
if (ABORT) return;
initRuntime();
readyPromiseResolve(Module);
if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized']();
assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]');
postRun();
}
doRun的时候, 将 Module Resolve .
工厂函数的导出方式:
emscripten 默认生成:
if (typeof exports === 'object' && typeof module === 'object')
module.exports = createMyLib;
else if (typeof define === 'function' && define['amd'])
define([], function() { return createMyLib; });
else if (typeof exports === 'object')
exports["createMyLib"] = createMyLib;
ChatGPT认为:
当你在资源管理器里勾上 Is Plugin(或在 3.8+ 默认走 ESBuild)—— Creator 构建管线会在文件尾部插入一行
export default createMyLib;
因此调用方式是:
const createMyLib = (await import('./wasm/my_test_lib.js')).default;
// └─────────────── default 导出就是那行 export default
const { default: createMyLib } = await import('./wasm/my_test_lib.js');
代码流程
下面按执行先后顺序把你截出的 Emscripten runtime 代码拆成 8 个阶段,并在关键行插入一段「发生了什么 → 为什么要这么做」的说明。跟着阅读即可形成一份笔记。
0. 全局准备
var wasmBinary; // 若用户自行塞入 ArrayBuffer
if (Module['wasmBinary']) wasmBinary = Module['wasmBinary'];
var noExitRuntime = Module['noExitRuntime'] || true;
if (typeof WebAssembly != 'object')
abort('no native wasm support detected');
| 作用 | 解释 |
|---|---|
wasmBinary |
如果调用者在 <script> 前提前设定 Module.wasmBinary,这里保存下来,后面就不用网络请求。 |
noExitRuntime |
缺省改为 true(Web 场景一般不退出进程)。 |
| WebAssembly 检测 | 早期浏览器没有 Wasm 就直接 abort()。 |
1. 确定 .wasm 的 URL
var wasmBinaryFile = 'my_test_lib.wasm';
if (!isDataURI(wasmBinaryFile)) wasmBinaryFile = locateFile(wasmBinaryFile);
默认文件名来自
-o xxx.js⇒ 同名xxx.wasmlocateFile可以被外部覆写——createMyLib({ locateFile: (f) => 'src/wasm/' + f })——让运行时找到真实路径(Creator 构建后常用)。
2. 读取二进制
function getBinary(file) { … }
function getBinaryPromise() {
if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) {
if (typeof fetch == 'function') {
return fetch(wasmBinaryFile, {credentials:'same-origin'})
.then(r => r.arrayBuffer())
.catch(() => getBinary(wasmBinaryFile));
}
}
return Promise.resolve().then(() => getBinary(wasmBinaryFile));
}
| 路径 | 细节 |
|---|---|
① Module.wasmBinary 已给 → 直接 Uint8Array |
零网络请求(适合 SINGLE_FILE 嵌入或手动内联) |
② 浏览器/Worker 支持 fetch → 异步获取 |
若服务器 MIME 错或 file://,落回 ③ |
③ readBinary (Node fs.readFileSync) |
Node、本地壳 |
3. createWasm() 主入口
function createWasm() {
// 3-1 生成 import object
var info = {
env: asmLibraryArg,
wasi_snapshot_preview1: asmLibraryArg
};
// 3-2 定义 receiveInstance:拿到 exports → 挂到 Module
function receiveInstance(instance, module) {
var exports = instance.exports;
Module['asm'] = exports;
wasmMemory = exports['memory'];
updateGlobalBufferAndViews(wasmMemory.buffer);
wasmTable = exports['__indirect_function_table'];
addOnInit(exports['__wasm_call_ctors']); // 调静态构造
removeRunDependency('wasm-instantiate');
}
addRunDependency('wasm-instantiate'); // 阻塞 Module.ready
// 3-3 如果用户覆写 Module.instantiateWasm,则走用户自定义
if (Module['instantiateWasm']) {
try {
var exports = Module['instantiateWasm'](info, receiveInstance);
return exports; // 覆写需要同步返回 exports
} catch(e) { err('… failed …'); return false; }
}
// 3-4 否则走内部异步流程
instantiateAsync().catch(readyPromiseReject);
return {}; // 立即返回空对象;真正的 exports 稍后填充
}
4. instantiateAsync():决定 “流式” 还是 “整包”
function instantiateAsync() {
if (!wasmBinary && WebAssembly.instantiateStreaming && fetch && !isDataURI) {
// 4-A 现代浏览器首选:边下边编译
return fetch(wasmBinaryFile, {credentials:'same-origin'})
.then(response =>
WebAssembly.instantiateStreaming(response, info)
.then(receiveInstantiationResult,
reason => { // MIME 不对或 CDN 阻断
err('streaming compile failed: '+reason);
return instantiateArrayBuffer(receiveInstantiationResult);
}));
} else {
// 4-B 其它环境 fallback:先拿 ArrayBuffer 再 instantiate
return instantiateArrayBuffer(receiveInstantiationResult);
}
}
5. instantiateArrayBuffer() 真正编译
function instantiateArrayBuffer(receiver) {
return getBinaryPromise() // 读 bytes
.then(binary => WebAssembly.instantiate(binary, info))
.then(instance => instance) // 兼容链
.then(receiver, err => abort(err));
}
成品 {module, instance} 传给 receiveInstantiationResult → receiveInstance → 填充 Module.asm。
6. 依赖计数 & ready Promise
var runDependencies = 0;
function addRunDependency(id) { runDependencies++; … }
function removeRunDependency(id){ runDependencies--; … }
Module['ready'] = new Promise((resolve, reject) => {
readyPromiseResolve = resolve;
readyPromiseReject = reject;
});
- 当
removeRunDependency('wasm-instantiate')使计数归零时 →readyPromiseResolve(Module)。
调用者就能:
const Module = await createMyLib(); // 等同于 await Module.ready
7. 内存、表、HEAP 视图初始化
wasmMemory = exports['memory']; // SharedArrayBuffer
updateGlobalBufferAndViews(wasmMemory.buffer); // HEAP8/16/32/… 视图
wasmTable = exports['__indirect_function_table'];
- HEAPU8[i] = … 这样的 JS 视图现在才开始指向真正的线性内存。
- 导出函数地址表
wasmTable也绑定好,供间接调用(函数指针)使用。
8. 静态构造 & 运行时就绪
addOnInit(exports['__wasm_call_ctors']); // C/C++ static constructors
_GLOBAL__sub_I_xx在这一步执行。Module.readyPromise resolve;用户代码可以安全调用Module.cwrap('Multiply', …)。
需要再补充的三个环节
1.1 生命周期钩子 preRun / onRuntimeInitialized / postRun
Module.preRun // 运行前:可 push(cb) 做自定义内存填充、FS 装载
Module.onRuntimeInitialized // createWasm 完成,静态构造也跑完;等价于 Module.ready.then
Module.postRun // run() 返回后触发;可做收尾清理
这些数组或函数在 receiveInstance → addOnInit → run() 之前/之后被依次调用,用来插入用户代码而不改 generated JS。
1.2 内存增长与 emscripten_notify_memory_growth
如果在 C++ 侧调用
emscripten_resize_heap(),运行时会wasmMemory.grow(pages); updateGlobalBufferAndViews(...); // 再次刷新 HEAP* 视图 Module['asm']['emscripten_notify_memory_growth'](...)你的脚本若持有指向旧
HEAPU8.buffer的 TypedArray,增长后必须重新获取。
1.3 异常捕获与 abort() 路径
abort(reason)会- 触发
Module['onAbort'] && onAbort(reason) readyPromiseReject(reason)– 让await createMyLib()直接抛错
- 触发
在小游戏或 Creator 里可覆写
Module.onAbort = msg => alert(msg)以便调试。
加载方式
使用者侧的最简代码(浏览器 / Creator 预览)
const { default: createMyLib } =
await import('./wasm/my_test_lib.js');
const Module = await createMyLib({
locateFile: f => new URL(`./${f}`, import.meta.url).href
});
const mul = Module.cwrap('Multiply', 'number', ['number','number']);
console.log(mul(6, 7)); // 42
在 小游戏运行时,只需在对象中再加一个
instantiateWasm(imports, cb),里头调用CCWebAssembly.instantiate(…); cb(instance,module)即可。
wasm被加载实例化的过程可以被定制, 定制方法是:
- 提前创建
window.Module = { … } - 调用工厂:
createMyLib({ key:value, … });
这两种都会在 createWasm() 执行前被合并进内部 Module。
3. 对已实例化后才能调用的接口(如 Module.asm、HEAPU8)
应挂在 onRuntimeInitialized / Module.ready.then.
可覆写的过程包括:
资源定位 & 实例化阶段
locateFile —— 改写 .wasm 真正的 URL
// 目录结构:build/web-mobile/src/wasm/xxx
const { default: createMyLib } = await import('./wasm/my_test_lib.js');
const Module = await createMyLib({
locateFile (filename: string, scriptDir: string) {
// filename 是 my_test_lib.wasm / my_test_lib.data
// scriptDir 是 my_test_lib.js 所在目录
return scriptDir + '/sub/' + filename; // 指向自定义子目录
}
});
instantiateWasm(imports, successCallback) —— 完全接管实例化
Glue代码:
if (Module['instantiateWasm']) {
try {
var exports = Module['instantiateWasm'](info, receiveInstance);
return exports;
} catch(e) {
err('Module.instantiateWasm callback failed with error: ' + e);
return false;
}
}
// If instantiation fails, reject the module ready promise.
instantiateAsync().catch(readyPromiseReject);
// 微信小游戏:必须走 CCWebAssembly
const Module = await createMyLib({
locateFile : (f) => new URL(`./${f}`, import.meta.url).href,
instantiateWasm (imports: any, success: any) {
// @ts-ignore
CCWebAssembly.instantiate(
new URL('./my_test_lib.wasm', import.meta.url).href,
imports
).then(res => success(res.instance, res.module));
return {}; // 告诉 runtime “我已经开始实例化了”
}
});
Module.wasmBinary —— 预塞 Uint8Array 切断网络请求
Glue原始代码:
var wasmBinary;
if (Module['wasmBinary']) wasmBinary = Module['wasmBinary'];legacyModuleProp('wasmBinary', 'wasmBinary');
<script>
// 已经提前用 XHR/FS 读到二进制
Module = {
wasmBinary: myArrayBuffer, // 或 new Uint8Array(ab)
};
</script>
<script src="my_test_lib.js"></script>
Module.readBinary —— Node/Electron 里同步读文件
global.Module = {
readBinary: path => require('fs').readFileSync(path, null).buffer
};
require('./my_test_lib.js'); // 同步实例化
运行时行为
noExitRuntime —— 允许 / 禁止退出
Module = {
noExitRuntime: false // 调用 main 返回后允许 runtime 彻底退出
}
preRun / postRun —— 追加任务队列
Module.preRun = [ () => console.log('preRun1') ];
Module.postRun = (Module.postRun || []);
Module.postRun.push(() => console.log('cleanup done'));
放数组则顺序执行;放函数则当作数组的单元素。
onRuntimeInitialized —— “等价 ready” 同步钩子
const { default: createMyLib } = await import('./wasm/my_test_lib.js');
const mod = await createMyLib({
onRuntimeInitialized () {
// 这里已经可安全访问 cwrap
const add = mod.cwrap('Add', 'number', ['number','number']);
console.log(add(1, 2));
}
});
自定义日志 & 报错
Module.print = txt => console.log('[WASM]', txt);
Module.printErr = txt => console.warn('[ERR]', txt);
Module.onAbort = reason => {
reportCrashToServer(reason);
};
内存与线程
INITIAL_MEMORY(编译期)+ Module.TOTAL_MEMORY(运行时旧接口)
emcc foo.cpp -s INITIAL_MEMORY=64MB -o foo.js
若没重新编译只能用旧接口:
Module.TOTAL_MEMORY = 64 * 1024 * 1024;
必须在加载脚本前写入;否则已创建的
WebAssembly.Memory容量无法改变。
allowMemoryGrowth + 运行时增长
emcc foo.cpp -s ALLOW_MEMORY_GROWTH=1 -o foo.js
Module = { allowMemoryGrowth: true };
在 C++ 侧:
emscripten_resize_heap(128 * 1024 * 1024); // 增至 128 MiB
JS 侧会触发 emscripten_notify_memory_growth(),
updateGlobalBufferAndViews() 自动刷新 HEAP* 视图——无需手动介入。
定制文件系统 / 虚拟包
在 preRun 中安装文件
Module.preRun = Module.preRun || [];
Module.preRun.push(function () {
// 把浏览器 File 对象挂到 /input.bin
FS.writeFile('/input.bin', new Uint8Array(fileArrayBuffer));
});
游戏启动后就能在 C++ 里 fopen("/input.bin","rb").