---
title: "Unity WebGL 中 JavaScript层调用 C++ 接口"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/unity-webgl-js-call-cpp-interface/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/05-游戏图形与运行时/WebGL/Unity WebGL 中 JavaScript层调用 C++ 接口.md"
content_hash: 85bb16c6db38bc57fe1bdf0e08a2ded71193adf1541005f0eaf0515e33b44d68
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# Unity WebGL 中 JavaScript层调用 C++ 接口

## C++ 接口编写方式 1

```cpp
extern "C"
{
    EMSCRIPTEN_KEEPALIVE
    void func1(const char* a, const char* b, const char* c) {
        // captureException(errorType, errorMessage, stackTrace);
        LOGI("func1 called!!!!!!!!!!!!!!!!!!!!!!!!!");
    }
}
```

### Unity3D->WebGL C++生成JS Stub
* **`EMSCRIPTEN_KEEPALIVE`** 只能保证符号在 `.a` 文件里保留，不能保证该静态库在 Unity3D 面向 WebGL 平台最终链接时会被拉入并写入导出表。

> 生成的启动脚本里需要出现
> `var _func1 = Module["_func1"] = createExportWrapper("func1");`

* 只有当 **对应 `*.cpp` 里的接口被 C# 代码显式引用** 时，该对象文件才会在 Unity 链接阶段被拉入，并写入导出表，生成同样的
  `var _func1 = Module["_func1"] = createExportWrapper("func1");`

* 因此，C# 侧需显式声明：

```csharp
[DllImport("__Internal")]
internal static extern void func1(string errorType,
                                  string errorMessage,
                                  string stackTrace);
```

### TypeScript / JavaScript 调用方式

在 TypeScript 脚本中先声明接口：

```ts
interface EmscriptenModule {
  ccall: (funcName: string,
          returnType: string,
          argTypes: string[],
          args: any[]) => any;
  cwrap: (funcName: string,
          returnType: string,
          argTypes: string[]) => (...args: any[]) => any;
  _malloc: (size: number) => number;
  _free:  (ptr: number) => void;
  _func1:        (errorType: string,
                  errorMessage: string,
                  stackTrace: string) => void;
}

declare var Module: EmscriptenModule;
```

### 调用方案与测试结果

- `Module._functionName`: Direct pointers to exported C functions (with underscore)
```js
Module._func1(a, b, c);
```
**调用成功**，但字符串参数传递失败

```js
Module._func1(
  allocateUTF8OnStack(a),
  allocateUTF8OnStack(b),
  allocateUTF8OnStack(c));
```
(该方法未测试)

* Module.ccall: Helper to call C functions with automatic type conversion
```js
Module.ccall('func1', 'void', ['string','string','string'], [a, b, c]);
```
调用成功, 字符串参数传递成功.


> 以上方案直接在 TypeScript 内调用；脚本经 rollup 打包后，通过
> `mergeInto(LibraryManager.library, { InitCrashReport: function() { … } })`
> 注册到 `globalThis`。

### 另一种调用方案

在 `mergeInto(LibraryManager.library, { InitCrashReport: function() { … } })`
中注册
`globalThis.CrashReport.native.func1`，再内部调用：

| 调用方式                        | 结果               |
| --------------------------- | ---------------- |
| `_func1`                    | 调用成功，**参数传递失败**  |
| `Module._func1(...)`        | 同上，字符串仍为 `null`  |
| `Module.ccall('func1', … )` | **调用成功，且参数传递成功** |

## C++ 暴露接口的方案 2（Embind）

```cpp
EMSCRIPTEN_BINDINGS(crash_reporter) {
    emscripten::function("func3",
                         &func3,
                         emscripten::allow_raw_pointers());
}
```

对应 JavaScript 调用：

```js
Module.func3(param1, param2, param3);
```

> Unity3D 编译阶段未加入 `--bind` 或 `-lembind`，将报
> **missing function: \_embind\_register\_function**
> 因此此方案尚未测试。

---

## C++ 导出接口的方案 3（EXPORTED\_FUNCTIONS）

> 不使用 `EMSCRIPTEN_KEEPALIVE`，而在编译配置中通过
> `-s EXPORTED_FUNCTIONS=['_func1', ...]`
> 显式导出（**未测试**）。


## Module的概念
从编译到运行的视角快速梳理

1. **编译 (`em++`)**
    - 生成二进制 Wasm (`*.wasm`) + JS glue (`*.framework.js`)
    - `Module` 对象的**定义**就在 glue 里——它负责加载 Wasm、提供 runtime API（`ccall`、`_malloc` …）。

2. **链接 & 导出表**
    - 所有写在 `EXPORTED_FUNCTIONS` 列表里的符号，或打了 `EMSCRIPTEN_KEEPALIVE` 的符号，在 Wasm 里生成 `(export "_foo" ...)`。
    - glue 文件为每个导出插入 `var _foo = Module["_foo"] = createExportWrapper("foo");`

3. **运行时**
    - HTML 模板先加载 `UnityLoader.js` → 再加载 `*.framework.js`.
    - glue 创建 `Module`（或 `Module()` Promise），拉取 `*.wasm`， 完成实例化。
    - `Module.ccall()` 内部：查表找 `Module["_foo"]` → 把 JS 参数转成底层指针 → 调用 `wasmInstance.exports["_foo"]`.
下面按 **编译—链接—运行时导出** 的顺序，把上面三条要点扩展成更“工程师/工具链”视角的专业说明，帮助你彻底理解 **`EMSCRIPTEN_KEEPALIVE`**、Unity WebGL 链接流程与 **C# 显式引用** 之间的关系。

---

## 从C++到wasm的过程
`EMSCRIPTEN_KEEPALIVE` 的本质：仅阻止 *编译期*／*链接后优化* 删除符号

| 阶段                                                | Emscripten / LLVM 所做的事                                                          | `EMSCRIPTEN_KEEPALIVE` 能力                                                                            |
| ------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Clang ➜ LLVM IR**(源→`.o`)                   | 把 C++ 转成 LLVM IR；前端优化 (inline, DCE)。                                            | 把目标函数/变量打上 `__attribute__((used))` 与 `__attribute__((visibility("default")))`，强制保留 IR 定义并设为“可导出” |
| **lld (wasm-ld) 全局链接**(`.o` + `.a` ➜ `.wasm`) | 1️⃣ 解析静态库 (`.a`) **按需**抽取 object file；2️⃣ 合并符号表；3️⃣ LTO / GC dead code。 | *仅*对已被抽取进来的 object file 生效：符号不被 LTO / GC 删除。但如果 object 根本没被抽取，KEEPALIVE 也救不了。                    |

> 🔑 **关键点**：`archive (.a)` 的抽取规则与 ELF/COFF 完全一致——**只有当链接器在解析“未定义符号”时看到需要的符号，才会把对应 `.o` 拉进来**。`KEEPALIVE` 并不会自动把整个 `.o` 标记成“必须抽取”。

---

### Unity WebGL 的静态库归档与最终链接

```text
crash.a (包含 func1.o)
             │
            (归档后放入 Assets/Plugins/WebGL/)
             │
  ┌──────────▼───────────┐
  │      il2cpp.exe      │  ↩️ 生成 .cpp stub，记录所有 [DllImport] 符号
  └──────────┬───────────┘
             │
  ┌──────────▼───────────┐
  │   em++（编译 stub）   │  ↩️ 产生 undefined symbol: func1
  └──────────┬───────────┘
             │
  ┌──────────▼───────────┐
  │   wasm-ld / lld      │  ↩️ 解析 undefined symbol → 抽取 func1.o
  └──────────┬───────────┘
             │
      【Link & LTO】
             │
           crash.wasm
           crash.js
```

* **C# `[DllImport("__Internal")]`** ⇒ IL2CPP 生成一个 *裸外部符号* `func1`，在 stub C++ 中以 `extern "C"` 声明；
  这让 **wasm-ld** 在解析时发现 “func1 未定义”，于是从 `crash.a` **抽取 `func1.o`**。
  （没有这个未定义引用就不会抽取，导致 `func1` 被完全略过。）

* Unity 的 emscripten 参数默认不开 `--whole-archive`，因此 `.a` 里的 object 必须“用到”才能被拉入。

* 一旦 `func1.o` 被抽取，`EMSCRIPTEN_KEEPALIVE` 才能阻止后续 LTO/DCE 把 `func1` 优化掉。

---

## 3  导出表与 `createExportWrapper`

> 生成的启动脚本需出现

```js
var _func1 = Module["_func1"] = createExportWrapper("func1");
```

* **生成时机**：`emcc` 在最终 *JS shell* 阶段把 `EXPORTED_FUNCTIONS` 数组写进 `wasmExports`，再用 `createExportWrapper` 生成 JS stub。
* **组成来源**
  1. 自动扫描 `EMSCRIPTEN_KEEPALIVE` 符号；
  2. 手动 `-s EXPORTED_FUNCTIONS=['_func1']`；
  3. 默认运行时方法列表（`_malloc` 等）。
* 若 `func1` 没有进入最终 `.wasm`，则不会出现在 `EXPORTED_FUNCTIONS`，对应 JS stub 也不会生成 ⇒ 浏览器侧报 “missing function”。
