---
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-guide/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/05-游戏图形与运行时/WebGL/Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作编写方法.md"
content_hash: 3665ad35d8f9ee8125cb80db89ed0cf30f951b4eff496104abf3b48c335e658f
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作编写方法

| 概念         | 说明                                                                                                                       | 常用命令/设置                                          |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| **定位**     | LLVM-based 交叉编译器，把 C/C++ → WebAssembly (+JS runtime)。                                                                    | emcc / em++                                      |
| **核心产物**   | `xxx.wasm` (代码) + `xxx.js` (运行时＋glue)                                                                                    | `-o out.js`（会同时写 wasm）                           |
| **运行时模式**  | 1) **Mono JS 文件**（默认）；2) **MODULARIZE** → 实际输出一个 JS Module；3) **WASM=2** (`-sSTANDALONE_WASM`) 纯 wasm + minimalist JS 引导 | `-sMODULARIZE=1 -sEXPORT_ES6=1`                  |
| **交互 API** | - **cwrap/ccall**：JS 调 C；- **EXPORTED\_FUNCTIONS** / `EMSCRIPTEN_KEEPALIVE`：把符号放进导出表；- **Embind**：C++/JS 面向对象互调          | docs Interacting-with-code ([emscripten.org][1]) |
| **内存模型**   | 线性内存 = Static + Stack + Heap；`ALLOW_MEMORY_GROWTH=1` 让 Heap 动态扩；TypedArray 直接映射                                          | `HEAP8/HEAP32…`                                  |
| **文件系统**   | 内置 `MEMFS`, `IDBFS`, `WORKERFS`；可 `FS.mount`                                                                             | `--preload-file`, `--embed-file`                 |
| **多线程**    | PThreads → Web Workers；需 `-sUSE_PTHREADS=1` 并在 HTML/小游戏里勾 “多线程”                                                          |                                                  |
| **压缩**     | Emscripten 不压缩；Unity WebGL 会生成 `.wasm.br` / `.wasm.gz`；微信小游戏 **不支持** Unity 的自解压，需禁用或自行处理                                 | Player Settings → Compression: Disabled          |
| **调试**     | `-g4` 生成 source-map；`-sSAFE_HEAP=1` 越界检测；`EM_LOG=1` 打印 C `printf` 到 console                                              |                                                  |

[1]: https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html?utm_source=chatgpt.com "Interacting with code — Emscripten 4.0.9-git (dev) documentation"


### 编写 `.jslib`

```js
// Assets/Plugins/WebGL/simple_bridge.jslib
mergeInto(LibraryManager.library, {
  // 打个招呼：把字符串打印到浏览器控制台
  JS_Hello: function (ptr, len) {
    // 把 C# 侧传来的 UTF-8 字符串读出来
    var msg = UTF8ToString(ptr, len);
    console.log("[JS] Hello from C#: " + msg);
  },

  // 返回浏览器窗口宽度
  JS_GetScreenWidth: function () {
    return window.innerWidth | 0;   // 32-bit int
  }
});
```


### C#声明调用

```csharp
using System;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;

public class JsDemo : MonoBehaviour
{
#if UNITY_WEBGL && !UNITY_EDITOR
    // 把字符串地址和长度传给 JS
    [DllImport("__Internal")]
    private static extern void JS_Hello(IntPtr strPtr, int length);

    [DllImport("__Internal")]
    private static extern int  JS_GetScreenWidth();
#endif

    void Start()
    {
        // -------- 调 JS 打招呼 --------
        string msg = "你好，JavaScript!";
        // 转成 UTF-8 bytes，固定到 GCHeap，拿指针
        byte[] utf8 = Encoding.UTF8.GetBytes(msg + '\0');
        GCHandle handle = GCHandle.Alloc(utf8, GCHandleType.Pinned);
        try
        {
            IntPtr ptr = handle.AddrOfPinnedObject();
            JS_Hello(ptr, utf8.Length - 1);   // 不含 '\0'
        }
        finally
        {
            handle.Free();
        }

        // -------- 调 JS 取窗口宽 --------
        int w = JS_GetScreenWidth();
        Debug.Log("Window width from JS = " + w);
    }
}

```
`[DllImport("__Internal")]` 告诉 IL2CPP 去当前 Wasm 模块里找符号 `JS_Hello`。

## JS → C\#

如果你想让浏览器侧错误反向通知 Unity，只需：

```ts
// 在 unity-bridge.ts
import { detectSDK } from './adapters/platform';
const game = (window as any).gameInstance;   // UnityLoader 生成的实例

export function forwardToUnity(eventId: string): void {
  if (game) game.SendMessage('CrashSightBootstrap', 'OnJsError', eventId);
}

// 每次 sendEvent 成功后调用 forwardToUnity()
```


## CSharp直接调用Native

- 只用到 **int/float 指标量**，没有类/模板，不用 `--bind`，包体最小。


### 编写Cpp代码

```cpp
// Plugins/WebGL/native_lib.cpp
#include <emscripten/emscripten.h>
#include <cmath>

// 用 extern "C" 消除 C++ 名字改编，让导出符号保持 "Multiply"
extern "C" {

// 一定要 EMSCRIPTEN_KEEPALIVE，或稍后用 -s EXPORTED_FUNCTIONS 把名字写进去
EMSCRIPTEN_KEEPALIVE
int Multiply(int a, int b)
{
    return a * b;
}

EMSCRIPTEN_KEEPALIVE
float Distance(float x1, float y1, float x2, float y2)
{
    float dx = x1 - x2, dy = y1 - y2;
    return std::sqrt(dx * dx + dy * dy);
}

} // extern "C"

```

通过 EMSCRIPTEN_KEEPALIVE 标记对函数进行导出。
若不想写 `KEEPALIVE`，可在编译时 `-sEXPORTED_FUNCTIONS="['_Multiply','_Distance']"`。

### 使用Emscripten编译成WebAssembly代码
```bash
rem ① 用 Emscripten 3.x 编译 .o
emcc -c native_lib.cpp -O3 --no-entry -sWASM=1 -sALLOW_MEMORY_GROWTH=1 -o native_lib.o

rem ② 打包成静态库
emar rcs native_lib.a native_lib.o
```

- **`--no-entry`**：声明“这是库文件，没有 `main()`”。
- **`-sALLOW_MEMORY_GROWTH=1`**：可按需增内存，免手算 `-sINITIAL_MEMORY`。

把生成的 `native_lib.a` 放进 **Assets/Plugins/WebGL/**
Unity 看到 **`.a` + WebGL 勾选** 就会在最终链接阶段把它拉进主 Wasm。

### CSharp声明调用

```csharp
// Scripts/Native.cs
using System.Runtime.InteropServices;
using UnityEngine;

public static class Native
{
#if UNITY_WEBGL && !UNITY_EDITOR      // 避免 Editor/GameView 报找不到符号
    [DllImport("__Internal")] private static extern int   Multiply(int a, int b);
    [DllImport("__Internal")] private static extern float Distance(float x1, float y1,
                                                                   float x2, float y2);
#else
    private static int   Multiply(int a, int b)            => 0;
    private static float Distance(float x1,float y1,
                                   float x2,float y2)      => 0f;
#endif

    public static void Demo()
    {
        Debug.Log("5 × 7 = " + Multiply(5, 7));
        Debug.Log("dist = " + Distance(0, 0, 3, 4));      // -> 5
    }
}

```

- **`__Internal`** = “从当前 Wasm 模块导出表找符号”。

### 运行时流程

1. **UnityLoader.js** 下载+实例化 Wasm → 导出表包含 `_Multiply/_Distance`。
2. IL2CPP 的 `DllImport` 调用 → JS glue (`cwrap`) 自动生成一层 JS stub。
3. stub 把 C# 参数序列化到线性内存，再调用导出函数，返回值复制回托管侧。


> **目标函数：** `Multiply(int,int)`，已出现在 Wasm 导出表里 (`_Multiply`)
1. **加载阶段**

```text
UnityLoader.js
  └─ fetch("build.wasm")         （XHR/”小游戏”本地FS）
	  └─ WebAssembly.instantiateStreaming()
		  → Instance.exports = { …, _Multiply, … }
```

2. **IL2CPP 生成的 JS stub**
    _Unity 构建流程在 `wasm.framework.js` 里插入一段：_
```js
// 自动执行的 init code（节选）
var _Multiply = Module.cwrap(
	  'Multiply',       // C 符号（无下划线）
	  'number',         // 返回值类型
	  ['number','number']);   // 参数类型数组
```

`cwrap` 来自 Emscripten 运行时，会：
- 查 `instance.exports['_Multiply']`
- 生成 JS 函数：

```js
function js_Multiply(a,b){
  // ① 把 JS number 写进栈
  // ② 调 Wasm _Multiply(a,b)
  // ③ 把返回值 rewrap 成 JS number
}
```
- 把它挂到 `_Multiply` 变量，供 IL2CPP 调用。

3. **托管端 (C#) 调用**
    _IL2CPP 在 `Native.Demo()` 里生成如下 JS 调用：_

    ```js
    var ret = _Multiply(a,b);         // a,b 已转成 JS number
    // ret 仍是 JS number，IL2CPP 再转成 C# int 提交给脚本层
    ```

4. **数据在内存里的流向**
```text
C# int       ─┐ (P/Invoke marshaller)
			  ▼
JS number    ─┐ (cwrap 填栈，HEAP32)
			  ▼
Wasm int32   ←┘ (真正执行 a*b)
	 ▲
	 └─────────  返回值路径同理
```

## CSharp通过 js glue 调用native
### 编写Cpp代码
```cpp
#include <emscripten/bind.h>

int Multiply(int a, int b)      { return a *  b; }
int CrashDivideByZero()         { return 1 / 0; }                 // trap
int CrashOutOfBounds() {
    volatile int* p = reinterpret_cast<int*>(0x80000000);         // >2 GB
    return *p;                                                    // trap
}

EMSCRIPTEN_BINDINGS(my_mod) {
    emscripten::function("Multiply",         &Multiply);
    emscripten::function("CrashDivideByZero",&CrashDivideByZero);
    emscripten::function("CrashOutOfBounds", &CrashOutOfBounds);
}

```

### 使用Emscripten编译成WebAssembly代码
```bash
rem ① 用 Emscripten 3.x 编译 .o
emcc -c native_lib_js.cpp -O3 --bind --no-entry -sWASM=1 -sALLOW_MEMORY_GROWTH=1 -o native_lib_js.o


rem ② 打包成静态库
[emar rcs native_lib.a native_lib.o](<emar rcs native_lib_js.a native_lib_js.o>)
```

把生成的 `native_lib.a` 放进 **Assets/Plugins/WebGL/**

**什么时候用 Embind / `--bind` ？**
* 暴露 **C++ 类、模板容器、`std::string/std::vector`** 给 JS
* 需要 **JS↔C++ 双向回调**

配置 `--bind` Emscripten 会：
1. 复制一段 **Embind runtime (~20–30 KB JS)**；
2. 生成 `Module['Multiply'] = function(){ … }` 包装函数。

### 编写jslib（Glue → Native）
```js
// Assets/Plugins/WebGL/native_bridge.jslib
mergeInto(LibraryManager.library, {
  NativeLib_Init: function () {               // ← 用 function，不用箭头
    return 0;
  },

  JsMultiply: function (a, b) {
    // 方式①：直接调用链接符号（推荐）
    return _Multiply(a, b);                   // 前导下划线在链接时决议

    // 方式②：如果你喜欢 Module：
    // return Module['_Multiply'](a, b);
  },

  CrashDivideByZero: function () {
    return _CrashDivideByZero();
  },

  CrashOutOfBounds: function () {
    return _CrashOutOfBounds();
  }
});

```

- `_Multiply` 前导下划线：链接后 C 符号惯例；也可写 `Module['_Multiply']`。
- `.jslib` 与主 Wasm 在 **同一作用域**，可直接调用裸符号`_Multiply`。


### CSharp声明调用
```csharp
[DllImport("__Internal")] static extern int JsMultiply(int a, int b);

[DllImport("__Internal")] static extern int CrashDivideByZero();

[DllImport("__Internal")] static extern int CrashOutOfBounds();
```
这里导出的其实是 **`.jslib` 里那段 JS 函数**，JS 再调用 Embind 包装 → 真正 Wasm 函数。

### Embind的作用 - “类/回调”
* 暴露 **C++ 类、模板容器、`std::string/std::vector`** 给 JS
* 要 **JS↔C++ 双向回调**

C++ (`calc.cpp`)
```cpp
#include <emscripten/bind.h>
#include <functional>

static std::function<void(int)> g_cb;   // 存一份全局回调

void registerCallback(emscripten::val jsFunc)
{
    // 把 JS 函数包装成 std::function
    g_cb = [jsFunc](int result) {
        jsFunc(result);                 // 调用 JS
    };
}

void heavyCalc(int x, int y)
{
    int r = x * y;          // 假装这是很重的计算
    if (g_cb) g_cb(r);      // 计算完回调 JS
}

EMSCRIPTEN_BINDINGS(my)
{
    emscripten::function("registerCallback", &registerCallback);
    emscripten::function("heavyCalc",        &heavyCalc);
}

```

JS 调用 (`index.js / .jslib`)
```js
Module.onRuntimeInitialized = () => {
  Module.registerCallback(function (r) {   // JS → C++
    console.log('C++ result =', r);        //     → JS
  });
  Module.heavyCalc(6, 7);                  // => 控制台打印 42
};
```


## Native -> JS

| 方法                   | 写法                                                 | 何时用             | 额外编译参数                                                 |
| -------------------- | -------------------------------------------------- | --------------- | ------------------------------------------------------ |
| **① `EM_JS`**        | 把 JS 当作 **C 函数实现** 内联写在 .cpp                       | 简单逻辑、少量函数       | 无                                                      |
| **② `EM_ASM_*`**     | 在现有 C 函数里“插一句 JS”                                  | 只想偶尔执行 JS 语句    | 无                                                      |
| **③ `--js-library`** | 把 JS 函数放到独立 `library_xxx.js`，在 C/C++ 里 `extern` 调用 | 函数多、想保持 C/JS 分离 | `-sEXPORTED_FUNCTIONS` + `--js-library library_xxx.js` |

## EM\_JS：最直接的“一键同步调用”

```cpp
#include <emscripten/emscripten.h>
#include <cstdlib>

extern "C" {

// JS 实现，C 侧看起来就是普通函数
EM_JS(void, LogInt, (int v), {
  console.log('Native says: ' + v);
});

// 带返回值
EM_JS(int, Rand100, (), {
  return Math.floor(Math.random() * 100);
});

EMSCRIPTEN_KEEPALIVE
int MultiplyAndLog(int a, int b)
{
    int r = a * b;
    LogInt(r);          // 直接同步 console.log
    return r + Rand100();   // 同步拿 JS 返回值
}
}
```

* 任何 **参数 / 返回值** 只能是 `int | double | float | pointer`；
  要传字符串，用指针＋`UTF8ToString()` 转码（见下表）。

| C++ ↔ JS             | 转换示例                                  |
| -------------------- | ------------------------------------- |
| `const char* s` → JS | `UTF8ToString(s)`                     |
| JS 字符串 → `int ptr`   | `stringToUTF8(str, ptr, len)` + 把指针回传 |

## EM\_ASM：在 C 里“插一行 JS”

```cpp
#include <emscripten/emscripten.h>
extern "C" {

EMSCRIPTEN_KEEPALIVE
int Clamp(int x)
{
    // 把 x 发到 JS，返回 min(max(x,0),100)
    return EM_ASM_INT({
        const v = $0;                 // $0 对应第 1 个参数
        return Math.min(Math.max(v,0), 100);
    }, x);
}
}
```

* `EM_ASM_VOID / INT / DOUBLE` 系列根据返回类型不同。
* 更像“内联汇编”，适合一两行 JS。

---

## JS-Library 文件：模块化 + Tree-Shaking 友好

### 写一个 `library_native.js`

```js
// 必须用 `mergeInto(LibraryManager.library, {...})`
mergeInto(LibraryManager.library, {
  // 函数名随意，但最终导入到 wasm 的名字要与 C 声明一致
  js_sin: function (d) {           // d: double
    return Math.sin(d);
  },

  js_log_int: function (v) {
    console.log('log_int from library: ' + v);
  }
});
```

### C/C++ 声明并调用

```cpp
extern "C" {
  extern double js_sin(double d);   // 没有实现体，链接时会去 JS 找

  EMSCRIPTEN_KEEPALIVE
  double SinTimes2(double v) {
      double s = js_sin(v);
      return 2.0 * s;
  }
}
```

### 编译

Unity3D Engine 构建WebGL应用时, 会自动处理该过程.

```bash
emcc -c native_lib.cpp -O3
emcc native_lib.o \
     --js-library library_native.js \
     -sEXPORTED_FUNCTIONS="['_SinTimes2']" \
     -sSIDE_MODULE=0 -sWASM=1 -O3 \
     -o native_lib.a       # 最终静态库给 Unity
```

> `--js-library` 里的每个函数都会成为 **Wasm 的导入(import)**，
> 链接到最终 `build.framework.js / build.loader.js` 的作用域中。

## 把“Native → JS” 再传回 C#？

已经同步返回就可以直接带回 C#：

```csharp
[DllImport("__Internal")] private static extern int MultiplyAndLog(int a, int b);
// C# 调用会在一帧内完成；MultiplyAndLog 内部同步进出 JS。
```

若 JS 异步（例如 `fetch()`），就必须 **回调或 Asyncify**——
纯同步桥只有以上三种做法，无法跨线程等待浏览器异步操作。

## 常见注意点

| ⚠️ 场景      | 处理办法                                                                                             |
| ---------- | ------------------------------------------------------------------------------------------------ |
| **字符串/数组** | 在 C/C++ 开 malloc，JS 用 `UTF8ToString(ptr)` 读；写反向则 `stringToUTF8`。                                 |
| **双端同名冲突** | JS-Library 函数内部务必用普通 `function`，不要使用箭头函数保存 `this`。                                               |
| **优化体积**   | 用 `-O3 -sELIMINATE_DUPLICATE_FUNCTIONS=1 -sENVIRONMENT=web`，并删除未引用的 JS-Library 函数（tree-shaking）。 |
| **多文件链接**  | 所有 `.a` 给 Unity，一次性由 emscripten‐clang 链到主 wasm。                                                  |


## 调用JS异步函数并回调

```text
C#(Unity) ──► Wasm(C++) 同步函数 StartWxRequest
                   │
                   ▼
            EM_JS 里调 wx.request  (异步)
                   │
                   ▼
      wx.request success / fail 回调
                   │
                   ▼
      JS 用 unityInstance.SendMessage(...)
         把结果推回指定的 C# MonoBehaviour
```

---

C/C++（native_lib.cpp）

```cpp
// Assets/Plugins/WebGL/native_lib.cpp
#include <emscripten/emscripten.h>
#include <cstdlib>

extern "C" {

// ★ ① 供 JS 回调的函数（必须导出）
EMSCRIPTEN_KEEPALIVE                // 或写进 -sEXPORTED_FUNCTIONS
void OnWxResponse(int ok, const char* jsonPtr)
{
    const char* json = jsonPtr ? jsonPtr : "{}";
    printf("[Native] wx.request %s : %s\n", ok ? "SUCCESS" : "FAIL", json);
    free((void*)jsonPtr);           // ccall/allocateUTF8 用 malloc，记得 free
}

// ★ ② C# 同步调用的入口：只转去 JS 做异步
EMSCRIPTEN_KEEPALIVE
void StartWxRequest(const char* urlPtr)
{
    EM_ASM({                         // urlPtr = $0
        const url = UTF8ToString($0);

        // 容错：非微信环境直接回调失败
        if (typeof wx === 'undefined' || !wx.request) {
            Module.ccall('OnWxResponse', 'void',
                         ['number','string'], [0, allocateUTF8('no wx')]);
            return;
        }

        wx.request({
            url,
            success: res => {
                const json = JSON.stringify(res.data ?? {});
                Module.ccall('OnWxResponse', 'void',
                             ['number','string'],
                             [1, allocateUTF8(json)]);
            },
            fail: err => {
                Module.ccall('OnWxResponse', 'void',
                             ['number','string'],
                             [0, allocateUTF8(err.errMsg)]);
            }
        });
    }, urlPtr);
}

} // extern "C"

```

- `OnWxResponse(int ok, const char* json)` 就是**同步**地被 JS 调用。
- `allocateUTF8` 用 `_malloc`，因此 C 里可 `free`。
- 编译时把 `OnWxResponse` / `StartWxRequest` 写进 `-sEXPORTED_FUNCTIONS`（或用 `EMSCRIPTEN_KEEPALIVE`）。

```bash
emcc -O3 -c native_lib.cpp --no-entry -sWASM=1
emar rcs native_lib.a native_lib.o           # 给 Unity 使用
```
