---
title: "Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作-底层运行全剖析"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/unity-webgl-csharp-js-native-interop-internals/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/05-游戏图形与运行时/WebGL/Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作-底层运行全剖析.md"
content_hash: 771356fe764f685fc3063ebaf05e02f9651e5e6b0336090c3fa8043a287ddbfb
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作-底层运行全剖析

﻿# Unity WebGL 中 **C# ↔ JavaScript ↔ Native C/C++** 互操作——底层运行全剖析

> **阅读定位**
> 本笔记按 **编译期 → 加载期 → 运行期** 的时间轴，把三种语言互调所经历的每一步、每一个内核数据结构都摊开。配合你给出的示例代码即可逐行对照。理解之后你能做到：
>
> * 独立修改调用约定（如传结构体、回调链）。
> * 精确定位「为什么在 Safari WebGL 上崩溃」这类深层 bug。

---

## 0 统一世界观

### 0-1 一个 Wasm 实例 + JS 运行时

```python
┌────────── Browser / 小程序WebView ──────────┐
│        JavaScript (Emscripten runtime)      │
│   - asm.js loader / glue code               │
│   - 自动生成的 JS stub (for DllImport)       │
│   - 手写 *.jslib / Embind 包装               │
│                                              │
│┌─────────────── WebAssembly Engine ────────┐ │
││  统一的 Wasm Instance                      │ │
││  • IL2CPP 产出的字节码                     │ │
││  • 你静态链接的 native_lib.a               │ │
││                                           │ │
││  Export Table      Import Table           │ │
││  ───────────      ─────────────           │ │
││  _Multiply         JS_Hello               │ │
││  _Distance         UTF8ToString (mem addr)│ │
││  ...                                      │ │
││                                           │ │
││  Linear Memory (静态段 | 栈 | 堆)          │ │
│└───────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```

* **唯一真实 CPU 指令流**：WebAssembly（IL2CPP + C/C++）。
* **统一内存**：所有语言看到的都是 **同一块线性内存**。
* **JavaScript** 只是“宿主” + “胶水”，本身不执行重计算。

---

## 1 编译期：符号是怎样写进 Wasm 的

### 1-1 托管层 C# → Wasm

| 步骤             | 工具                             | 产物                                                    |
| -------------- | ------------------------------ | ----------------------------------------------------- |
| C# 源码          | **IL2CPP**                     | ① C++ 源文件② 记录 `[DllImport]` 的 *P/Invoke metadata* |
| C++ → LLVM IR  | clang++ (Emscripten toolchain) | `.o`                                                  |
| LLVM IR → Wasm | lld (wasm-ld)                  | **IL2CPP.o**                                          |

*IL2CPP* 把

```csharp
[DllImport("__Internal")] static extern void JS_Hello(IntPtr,int);
```

翻译成 C++：

```cpp
extern "C" void JS_Hello(void*,int)
      __attribute__((import_module("__Internal"), import_name("JS_Hello")));
```

> 关键：`import_module="__Internal"` = “去 *Import Table* 找符号”。

### 1-2 Native C/C++ → Wasm

* 普通 `emcc -c native.cpp` → `.o`；
* 链接时若函数带 `EMSCRIPTEN_KEEPALIVE` 或列在
  `-sEXPORTED_FUNCTIONS="['_Multiply']"`，lld 把它写进 **Export Table**：

```wat
(func $Multiply (export "_Multiply") (param i32 i32) (result i32))
```

### 1-3 `.jslib` 合并

* `mergeInto(LibraryManager.library,{ JS_Hello: … })`
  ➜ 打包脚本把键值放进 `asmLibraryArg` 供 **Import Table** 使用。

### 1-4 Embind / `--bind`

* clang 为每个 `emscripten::function()` 生成 **桥函数** `__embind_register_Foo`.
* 链接器再在 JS 侧生成：

```js
Module["Foo"] = function(a,b){ return _Foo(a,b); }
```

---

## 2 加载期：实例化 & 表格配线

```text
UnityLoader.js
  ↓ fetch .wasm
WebAssembly.instantiateStreaming()
  • 创建 Table(初始 = 导出函数数 + addFunction 保留槽)
  • 按 ImportTable 填充：
       env.JS_Hello           ← asmLibraryArg.JS_Hello
       env.UTF8ToString       ← runtime helper
  • 生成 Instance.exports = { _Multiply, _Distance, ... }
```

> 你在 `simple_bridge.jslib` 里写的函数，此刻成为 `env.JS_Hello`，供 Wasm 直接 `call`.

---

## 3 运行期三大调用链——逐 CPU 指令追踪

### 3-1 C# → JavaScript（进口）

1. **托管栈**：IL2CPP 把 `string` pin 在托管堆 ➜ 得到 `ptr,len`。
2. **Wasm 指令**：

   ```wat
   (call $JS_Hello (local.get $ptr) (local.get $len))
   ```

   触发 Import；JS 引擎把控制权切给 `asmLibraryArg.JS_Hello`。
3. **JS 函数体**：

   ```js
   var msg = UTF8ToString(ptr,len);   // HEAPU8.subarray() → decode
   console.log(...);
   return; // no copy back
   ```

> 无堆复制；JS 直接读线性内存视图。

---

### 3-2 C# → Native C（宿主内呼叫）

1. IL2CPP stub = 普通 C 函数。
2. 链接时发现 `_Multiply` 已在 Export 表 → lld **内部重定向** Import → Export。
3. 运行期 `call _Multiply` = **Wasm 内部跳转**，指令流不出沙盒。
4. 返回值放在 Wasm ABI 约定的寄存器（Chrome = `R0`），IL2CPP 取回。

耗时 \~ 几十纳秒，与纯 C 调 C 无异。

---

### 3-3 Native C → JavaScript（出口）

> 用 `addFunction` + `wasmTable`。

```cpp
int idx = emscripten_run_script_int("RegisterOnResult()");
...
invoke_vii(idx, result);   // ＝ dynCall_vi
```

1. `invoke_vii` (JS) 过程

   ```js
   var fn = wasmTable.get(idx);    // O(1) 查表
   try { fn(result); } catch(e){ ... }  // 调 JS
   ```
2. JS 执行完返回，Emscripten runtime 再把异常 > Wasm trap 等还原。

> **对比**：Native→JS 要跨 VM 边界一次；JS→Native 也是一次；C#→Native 完全无边界。

---

## 4 栈 & 内存管理细节

| 区段         | 地址空间               | 管理者                     | 典型用法                             |
| ---------- | ------------------ | ----------------------- | -------------------------------- |
| **Static** | 0 – `__data_end`   | 链接器                     | 全局变量、字符串常量                       |
| **Stack**  | 随 `SP` 向低地址扩       | Emscripten (`STACKTOP`) | Wasm 本地调用、JS → C stub            |
| **Heap**   | `DYNAMICTOP_PTR`→… | `dlmalloc`/`emmalloc`   | `new`, `malloc`、Pin UTF-8 string |
| JS 对应视图    | `HEAP8/HEAP32/...` | TypedArray              | JS 端读写                           |

* 字符串：`stringToUTF8(str, ptr, len)` 写入堆；`UTF8ToString` 读回。
* 堆增长：`-sALLOW_MEMORY_GROWTH` 让 `sbrk` 调整 `WebAssembly.memory.grow()`。

---

## 5 函数指针、回调与异常

1. **Wasmtime Table**：一张 `WebAssembly.Table({initial:N, element:"anyfunc"})`

   * C 指针 = 表索引；`addFunction` 分配空槽返回索引。
2. **跨 VM 抛异常**

   * JS → C 捕获：`invoke_vii` 包了一层 `try/catch`；异常转成 `longjmp`。
   * C → JS 捕获：`emscripten_rethrow_if_number` 把 `wasm32.trap` 译成 JS Error。

---

## 6 与 Unity 管线的耦合点

| 阶段       | Unity 角色              | 你能插手的钩子                  |
| -------- | --------------------- | ------------------------ |
| **编译**   | `il2cpp.exe` + `emcc` | 插入 `.a`, `.jslib`        |
| **压缩**   | `wasm.br` / `wasm.gz` | 关闭 or 自行解压 (小游戏)         |
| **多线程**  | 勾选 “Threads Support”  | 记得 `-sUSE_PTHREADS=1`    |
| **安全沙箱** | WebGL2 + WebAssembly  | 仅能 `window.*`；不允许 `eval` |

---

## 7 性能与陷阱

* **JS ↔ Wasm 往返成本 ≈ 0.2–0.5 µs/次** (Chrome M121, i9-12900)
  → 频次高时用 **SharedArrayBuffer / ring buffer** 批量通信。
* **GCHandle.Pin** 时间随对象大小线性；及时 `Free`。
* Safari 16 之前不支持 `WebAssembly.Table` 多线程共享 → 避免 PThreads + 回调。
* Mini-Game 平台欠缺实施：iOS   微信 <16 MB 内存硬上限，禁用栈扩张。

---

### 一条记忆口令

> **“import 进，export 出，表里塞函数指针；线性内存同读写，IL2CPP 在里边。”**

真正理解这句，你就能在任意 WebAssembly 宿主里自如拼装三种语言，且心里有数每一步的成本与边界。
