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

Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作-底层运行全剖析

Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作 底层运行全剖析

更多
Markdown 结构化数据
本文目录 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: … }) ➜ 打包脚本把键值放进 asmLibraryArgImport 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(进口)

  1. 托管栈:IL2CPP 把 string pin 在托管堆 ➜ 得到 ptr,len

  2. Wasm 指令

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

    触发 Import;JS 引擎把控制权切给 asmLibraryArg.JS_Hello

  3. 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

int idx = emscripten_run_script_int("RegisterOnResult()");
...
invoke_vii(idx, result);   // = dynCall_vi
  1. invoke_vii (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_GROWTHsbrk 调整 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_numberwasm32.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 宿主里自如拼装三种语言,且心里有数每一步的成本与边界。