Unity WebGL 中 JavaScript层调用 C++ 接口
本文目录 11 个章节
Unity WebGL 中 JavaScript层调用 C++ 接口
C++ 接口编写方式 1
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void func1(const char* a, const char* b, const char* c) {
// captureException(errorType, errorMessage, stackTrace);
LOGI("func1 called!!!!!!!!!!!!!!!!!!!!!!!!!");
}
}
Unity3D->WebGL C++生成JS Stub
EMSCRIPTEN_KEEPALIVE只能保证符号在.a文件里保留,不能保证该静态库在 Unity3D 面向 WebGL 平台最终链接时会被拉入并写入导出表。
生成的启动脚本里需要出现
var _func1 = Module["_func1"] = createExportWrapper("func1");
只有当 对应
*.cpp里的接口被 C# 代码显式引用 时,该对象文件才会在 Unity 链接阶段被拉入,并写入导出表,生成同样的var _func1 = Module["_func1"] = createExportWrapper("func1");因此,C# 侧需显式声明:
[DllImport("__Internal")]
internal static extern void func1(string errorType,
string errorMessage,
string stackTrace);
TypeScript / JavaScript 调用方式
在 TypeScript 脚本中先声明接口:
interface EmscriptenModule {
ccall: (funcName: string,
returnType: string,
argTypes: string[],
args: any[]) => any;
cwrap: (funcName: string,
returnType: string,
argTypes: string[]) => (...args: any[]) => any;
_malloc: (size: number) => number;
_free: (ptr: number) => void;
_func1: (errorType: string,
errorMessage: string,
stackTrace: string) => void;
}
declare var Module: EmscriptenModule;
调用方案与测试结果
Module._functionName: Direct pointers to exported C functions (with underscore)
Module._func1(a, b, c);
调用成功,但字符串参数传递失败
Module._func1(
allocateUTF8OnStack(a),
allocateUTF8OnStack(b),
allocateUTF8OnStack(c));
(该方法未测试)
- Module.ccall: Helper to call C functions with automatic type conversion
Module.ccall('func1', 'void', ['string','string','string'], [a, b, c]);
调用成功, 字符串参数传递成功.
以上方案直接在 TypeScript 内调用;脚本经 rollup 打包后,通过
mergeInto(LibraryManager.library, { InitCrashReport: function() { … } })注册到globalThis。
另一种调用方案
在 mergeInto(LibraryManager.library, { InitCrashReport: function() { … } })
中注册
globalThis.CrashReport.native.func1,再内部调用:
| 调用方式 | 结果 |
|---|---|
_func1 |
调用成功,参数传递失败 |
Module._func1(...) |
同上,字符串仍为 null |
Module.ccall('func1', … ) |
调用成功,且参数传递成功 |
C++ 暴露接口的方案 2(Embind)
EMSCRIPTEN_BINDINGS(crash_reporter) {
emscripten::function("func3",
&func3,
emscripten::allow_raw_pointers());
}
对应 JavaScript 调用:
Module.func3(param1, param2, param3);
Unity3D 编译阶段未加入
--bind或-lembind,将报 missing function: _embind_register_function 因此此方案尚未测试。
C++ 导出接口的方案 3(EXPORTED_FUNCTIONS)
不使用
EMSCRIPTEN_KEEPALIVE,而在编译配置中通过-s EXPORTED_FUNCTIONS=['_func1', ...]显式导出(未测试)。
Module的概念
从编译到运行的视角快速梳理
编译 (
em++)- 生成二进制 Wasm (
*.wasm) + JS glue (*.framework.js) Module对象的定义就在 glue 里——它负责加载 Wasm、提供 runtime API(ccall、_malloc…)。
- 生成二进制 Wasm (
链接 & 导出表
- 所有写在
EXPORTED_FUNCTIONS列表里的符号,或打了EMSCRIPTEN_KEEPALIVE的符号,在 Wasm 里生成(export "_foo" ...)。 - glue 文件为每个导出插入
var _foo = Module["_foo"] = createExportWrapper("foo");
- 所有写在
运行时
- HTML 模板先加载
UnityLoader.js→ 再加载*.framework.js. - glue 创建
Module(或Module()Promise),拉取*.wasm, 完成实例化。 Module.ccall()内部:查表找Module["_foo"]→ 把 JS 参数转成底层指针 → 调用wasmInstance.exports["_foo"]. 下面按 编译—链接—运行时导出 的顺序,把上面三条要点扩展成更“工程师/工具链”视角的专业说明,帮助你彻底理解EMSCRIPTEN_KEEPALIVE、Unity WebGL 链接流程与 C# 显式引用 之间的关系。
- HTML 模板先加载
从C++到wasm的过程
EMSCRIPTEN_KEEPALIVE 的本质:仅阻止 编译期/链接后优化 删除符号
| 阶段 | Emscripten / LLVM 所做的事 | EMSCRIPTEN_KEEPALIVE 能力 |
|---|---|---|
Clang ➜ LLVM IR(源→.o) |
把 C++ 转成 LLVM IR;前端优化 (inline, DCE)。 | 把目标函数/变量打上 __attribute__((used)) 与 __attribute__((visibility("default"))),强制保留 IR 定义并设为“可导出” |
lld (wasm-ld) 全局链接(.o + .a ➜ .wasm) |
1️⃣ 解析静态库 (.a) 按需抽取 object file;2️⃣ 合并符号表;3️⃣ LTO / GC dead code。 |
仅对已被抽取进来的 object file 生效:符号不被 LTO / GC 删除。但如果 object 根本没被抽取,KEEPALIVE 也救不了。 |
🔑 关键点:
archive (.a)的抽取规则与 ELF/COFF 完全一致——只有当链接器在解析“未定义符号”时看到需要的符号,才会把对应.o拉进来。KEEPALIVE并不会自动把整个.o标记成“必须抽取”。
Unity WebGL 的静态库归档与最终链接
crash.a (包含 func1.o)
│
(归档后放入 Assets/Plugins/WebGL/)
│
┌──────────▼───────────┐
│ il2cpp.exe │ ↩️ 生成 .cpp stub,记录所有 [DllImport] 符号
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ em++(编译 stub) │ ↩️ 产生 undefined symbol: func1
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ wasm-ld / lld │ ↩️ 解析 undefined symbol → 抽取 func1.o
└──────────┬───────────┘
│
【Link & LTO】
│
crash.wasm
crash.js
C#
[DllImport("__Internal")]⇒ IL2CPP 生成一个 裸外部符号func1,在 stub C++ 中以extern "C"声明; 这让 wasm-ld 在解析时发现 “func1 未定义”,于是从crash.a抽取func1.o。 (没有这个未定义引用就不会抽取,导致func1被完全略过。)Unity 的 emscripten 参数默认不开
--whole-archive,因此.a里的 object 必须“用到”才能被拉入。一旦
func1.o被抽取,EMSCRIPTEN_KEEPALIVE才能阻止后续 LTO/DCE 把func1优化掉。
3 导出表与 createExportWrapper
生成的启动脚本需出现
var _func1 = Module["_func1"] = createExportWrapper("func1");
- 生成时机:
emcc在最终 JS shell 阶段把EXPORTED_FUNCTIONS数组写进wasmExports,再用createExportWrapper生成 JS stub。 - 组成来源
- 自动扫描
EMSCRIPTEN_KEEPALIVE符号; - 手动
-s EXPORTED_FUNCTIONS=['_func1']; - 默认运行时方法列表(
_malloc等)。
- 自动扫描
- 若
func1没有进入最终.wasm,则不会出现在EXPORTED_FUNCTIONS,对应 JS stub 也不会生成 ⇒ 浏览器侧报 “missing function”。