---
title: "WebGL应用-Wasm加载使用原理"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/webgl-wasm-loading-usage/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/05-游戏图形与运行时/WebGL/WebGL应用-Wasm加载使用原理.md"
content_hash: 357df8ecde88a43bbf5383454103005ddb138bc1dd493c4f4725069565902b7e
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# WebGL应用-Wasm加载使用原理

```bash
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的加载

## 整体框架

```js
var createMyLib = (() => {
  var _scriptDir = typeof document !== 'undefined' && document.currentScript
                   ? document.currentScript.src
                   : undefined;
  return (function (createMyLib) { … })(/* 参数留空 */);
})();
```

**IIFE** (`(() => { … })()`)

立即执行并返回 _一个工厂函数_，赋给全局变量 `createMyLib` （编译时可设置该函数变量名， 不重要）

该工厂函数定义如下, 功能: 创建Module, 返回ready
```js
function(createMyLib) {
  createMyLib = createMyLib || {};

...
var Module = Object.assign({}, createMyLib);
...

  return createMyLib.ready;
}

```

Module.ready 定义如下:
```js
Module['ready'] = new Promise(function(resolve, reject) {
  readyPromiseResolve = resolve;
  readyPromiseReject = reject;
});
```

```js
  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 默认生成:
```js
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 构建管线会在文件尾部插入一行

```js
export default createMyLib;
```

因此调用方式是：
```js
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. 全局准备

```js
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**

```js
var wasmBinaryFile = 'my_test_lib.wasm';
if (!isDataURI(wasmBinaryFile)) wasmBinaryFile = locateFile(wasmBinaryFile);
```

* **默认文件名**来自 `-o xxx.js` ⇒ 同名 `xxx.wasm`
* `locateFile` 可以被外部覆写——

  ```js
  createMyLib({ locateFile: (f) => 'src/wasm/' + f })
  ```

  ——让运行时找到真实路径（Creator 构建后常用）。

---

### 2. **读取二进制**

```js
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() 主入口**

```js
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()：决定 “流式” 还是 “整包”**

```js
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() 真正编译**

```js
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**

```js
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)`。

调用者就能：

```ts
const Module = await createMyLib();   // 等同于 await Module.ready
```

---

### 7. **内存、表、HEAP 视图初始化**

```js
wasmMemory = exports['memory'];               // SharedArrayBuffer
updateGlobalBufferAndViews(wasmMemory.buffer); // HEAP8/16/32/… 视图

wasmTable  = exports['__indirect_function_table'];
```

* **HEAPU8\[i] = …** 这样的 JS 视图现在才开始指向真正的线性内存。
* 导出函数地址表 `wasmTable` 也绑定好，供间接调用（函数指针）使用。

---

### 8. **静态构造 & 运行时就绪**

```js
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*

```js
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()`，运行时会

  ```javascript
  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 预览）

```ts
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. 调用工厂：
```ts
createMyLib({ key:value, … });
```

   这两种都会在 `createWasm()` 执行前被合并进内部 Module。
3. **对已实例化后才能调用的接口**（如 `Module.asm`、`HEAPU8`）
   应挂在 `onRuntimeInitialized` / `Module.ready.then`.


可覆写的过程包括：

## 资源定位 & 实例化阶段

### `locateFile` —— 改写 .wasm 真正的 URL

```ts
// 目录结构：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代码：
```js
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);
```

```ts
// 微信小游戏：必须走 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原始代码:
```js
var wasmBinary;
if (Module['wasmBinary']) wasmBinary = Module['wasmBinary'];legacyModuleProp('wasmBinary', 'wasmBinary');
```

```html
<script>
  // 已经提前用 XHR/FS 读到二进制
  Module = {
    wasmBinary: myArrayBuffer,   // 或 new Uint8Array(ab)
  };
</script>
<script src="my_test_lib.js"></script>
```

### `Module.readBinary` —— Node/Electron 里同步读文件

```js
global.Module = {
  readBinary: path => require('fs').readFileSync(path, null).buffer
};
require('./my_test_lib.js');    // 同步实例化
```

---

## 运行时行为

### `noExitRuntime` —— 允许 / 禁止退出

```js
Module = {
  noExitRuntime: false   // 调用 main 返回后允许 runtime 彻底退出
}
```

### `preRun / postRun` —— 追加任务队列

```js
Module.preRun  = [ () => console.log('preRun1') ];
Module.postRun = (Module.postRun || []);
Module.postRun.push(() => console.log('cleanup done'));
```

> 放数组则顺序执行；放函数则当作数组的单元素。

### `onRuntimeInitialized` —— “等价 ready” 同步钩子

```ts
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));
  }
});
```

### 自定义日志 & 报错

```js
Module.print     = txt => console.log('[WASM]', txt);
Module.printErr  = txt => console.warn('[ERR]', txt);
Module.onAbort   = reason => {
  reportCrashToServer(reason);
};
```

## 内存与线程

### `INITIAL_MEMORY`（编译期）+ `Module.TOTAL_MEMORY`（运行时旧接口）

```bash
emcc foo.cpp -s INITIAL_MEMORY=64MB -o foo.js
```

若没重新编译只能用旧接口：

```js
Module.TOTAL_MEMORY = 64 * 1024 * 1024;
```

> 必须在加载脚本前写入；否则已创建的 `WebAssembly.Memory` 容量无法改变。

### `allowMemoryGrowth` + 运行时增长

```bash
emcc foo.cpp -s ALLOW_MEMORY_GROWTH=1 -o foo.js
```

```js
Module = { allowMemoryGrowth: true };
```

在 C++ 侧：

```cpp
emscripten_resize_heap(128 * 1024 * 1024);   // 增至 128 MiB
```

JS 侧会触发 `emscripten_notify_memory_growth()`，
`updateGlobalBufferAndViews()` 自动刷新 `HEAP*` 视图——无需手动介入。

---

## 定制文件系统 / 虚拟包

### 在 `preRun` 中安装文件

```js
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 插件配置方案
