返回「计算机、信息技术与工程」

WebGL应用-Wasm加载使用原理

WebGL应用 Wasm加载使用原理

更多
Markdown 结构化数据
本文目录 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.wasm

  • locateFile 可以被外部覆写——

    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} 传给 receiveInstantiationResultreceiveInstance填充 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
  1. _GLOBAL__sub_I_xx 在这一步执行。
  2. Module.ready Promise 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)

    1. 触发 Module['onAbort'] && onAbort(reason)
    2. 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被加载实例化的过程可以被定制, 定制方法是:

  1. 提前创建 window.Module = { … }
  2. 调用工厂:
createMyLib({ key:value, … });

这两种都会在 createWasm() 执行前被合并进内部 Module。 3. 对已实例化后才能调用的接口(如 Module.asmHEAPU8) 应挂在 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").

Cocos 覆写的过程

wasm 插件配置方案