Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作-底层运行全剖析
Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作 底层运行全剖析
本文目录 17 个章节
Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作-底层运行全剖析
# Unity WebGL 中 C# ↔ JavaScript ↔ Native C/C++ 互操作——底层运行全剖析
阅读定位 本笔记按 编译期 → 加载期 → 运行期 的时间轴,把三种语言互调所经历的每一步、每一个内核数据结构都摊开。配合你给出的示例代码即可逐行对照。理解之后你能做到:
- 独立修改调用约定(如传结构体、回调链)。
- 精确定位「为什么在 Safari WebGL 上崩溃」这类深层 bug。
0 统一世界观
0-1 一个 Wasm 实例 + JS 运行时
┌────────── 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 把
[DllImport("__Internal")] static extern void JS_Hello(IntPtr,int);
翻译成 C++:
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:
(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 侧生成:
Module["Foo"] = function(a,b){ return _Foo(a,b); }
2 加载期:实例化 & 表格配线
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(进口)
托管栈:IL2CPP 把
stringpin 在托管堆 ➜ 得到ptr,len。Wasm 指令:
(call $JS_Hello (local.get $ptr) (local.get $len))触发 Import;JS 引擎把控制权切给
asmLibraryArg.JS_Hello。JS 函数体:
var msg = UTF8ToString(ptr,len); // HEAPU8.subarray() → decode console.log(...); return; // no copy back
无堆复制;JS 直接读线性内存视图。
3-2 C# → Native C(宿主内呼叫)
- IL2CPP stub = 普通 C 函数。
- 链接时发现
_Multiply已在 Export 表 → lld 内部重定向 Import → Export。 - 运行期
call _Multiply= Wasm 内部跳转,指令流不出沙盒。 - 返回值放在 Wasm ABI 约定的寄存器(Chrome =
R0),IL2CPP 取回。
耗时 ~ 几十纳秒,与纯 C 调 C 无异。
3-3 Native C → JavaScript(出口)
用
addFunction+wasmTable。
int idx = emscripten_run_script_int("RegisterOnResult()");
...
invoke_vii(idx, result); // = dynCall_vi
invoke_vii(JS) 过程var fn = wasmTable.get(idx); // O(1) 查表 try { fn(result); } catch(e){ ... } // 调 JSJS 执行完返回,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 函数指针、回调与异常
Wasmtime Table:一张
WebAssembly.Table({initial:N, element:"anyfunc"})- C 指针 = 表索引;
addFunction分配空槽返回索引。
- C 指针 = 表索引;
跨 VM 抛异常
- JS → C 捕获:
invoke_vii包了一层try/catch;异常转成longjmp。 - C → JS 捕获:
emscripten_rethrow_if_number把wasm32.trap译成 JS Error。
- JS → C 捕获:
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 宿主里自如拼装三种语言,且心里有数每一步的成本与边界。