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

Unity WebGL 中 CSharp ↔ JavaScript ↔ Native 互操作编写方法

更多
Markdown 结构化数据
本文目录 24 个章节

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)
内存模型 线性内存 = 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

编写 .jslib

// 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#声明调用

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,只需:

// 在 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代码

// 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代码

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声明调用

// 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. 加载阶段
UnityLoader.js
  └─ fetch("build.wasm")         (XHR/”小游戏”本地FS)
	  └─ WebAssembly.instantiateStreaming()
		  → Instance.exports = { …, _Multiply, … }
  1. IL2CPP 生成的 JS stub Unity 构建流程在 wasm.framework.js 里插入一段:
// 自动执行的 init code(节选)
var _Multiply = Module.cwrap(
	  'Multiply',       // C 符号(无下划线)
	  'number',         // 返回值类型
	  ['number','number']);   // 参数类型数组

cwrap 来自 Emscripten 运行时,会:

  • instance.exports['_Multiply']
  • 生成 JS 函数:
function js_Multiply(a,b){
  // ① 把 JS number 写进栈
  // ② 调 Wasm _Multiply(a,b)
  // ③ 把返回值 rewrap 成 JS number
}
  • 把它挂到 _Multiply 变量,供 IL2CPP 调用。
  1. 托管端 (C#) 调用 IL2CPP 在 Native.Demo() 里生成如下 JS 调用:

    var ret = _Multiply(a,b);         // a,b 已转成 JS number
    // ret 仍是 JS number,IL2CPP 再转成 C# int 提交给脚本层
    
  2. 数据在内存里的流向

C# int       ─┐ (P/Invoke marshaller)
			  ▼
JS number    ─┐ (cwrap 填栈,HEAP32)
			  ▼
Wasm int32   ←┘ (真正执行 a*b)
	 ▲
	 └─────────  返回值路径同理

CSharp通过 js glue 调用native

编写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代码

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)

// 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声明调用

[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)

#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)

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:最直接的“一键同步调用”

#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”

#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

// 必须用 `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++ 声明并调用

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应用时, 会自动处理该过程.

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#:

[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异步函数并回调

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

C/C++(native_lib.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)。
emcc -O3 -c native_lib.cpp --no-entry -sWASM=1
emar rcs native_lib.a native_lib.o           # 给 Unity 使用