外观
Stage06|JSB & Native Bridge
Lesson075|JavaScript 对象与 C++ 对象如何绑定
Tags: #Creator2.x #Stage06 #Binding #ObjectMapping #NativeObjectDifficulty: ⭐⭐⭐⭐☆
真实问题:JavaScript 变量还在,原生对象为什么已失效
Native 下载器返回一个 JavaScript 代理。页面关闭后,代理变量仍可访问;调用方法却崩溃或返回无效句柄。
跨语言对象至少有三种身份:
text
JavaScript wrapper/proxy
Native/C++ 实例
底层资源/平台句柄它们的创建、所有权和销毁时机可能不同。代理存在不等于 Native 实例仍活着。
一、本课目标
跨语言调用不仅要转换参数,还要回答:
JavaScript 中的对象,如何代表 C++ 中已经存在的对象?
本课理解对象代理、身份映射、方法调用和生命周期协调。
二、为什么需要对象映射
JavaScript 和 C++ 有各自的对象系统:
text
JavaScript Object
├── 属性
├── 原型
└── GC 生命周期
C++ Object
├── 内存地址
├── 类和方法
└── 手动或引用计数生命周期要让 JS 调用 C++ 对象方法,绑定层需要知道两边代表的是同一个逻辑对象。
三、代理对象模型
可以抽象为:
text
JavaScript Proxy Object
↓ 保存或关联 Native 身份
Binding Mapping
↓
C++ Native Object当 JS 调用:
ts
nativeObject.setValue(10);绑定层需要:
text
找到对应 C++ 对象
→ 转换参数 10
→ 调用 C++ setValue
→ 转换返回值或错误四、同一个 Native 对象不能随意产生多个身份
如果同一个 C++ 对象被包装成多个互不相知的 JS 对象,可能造成:
- 状态不一致。
- 重复释放。
- 比较结果错误。
- 回调落到过期代理。
绑定系统通常需要维护 Native 指针与 JS 对象之间的映射,确保身份和生命周期可追踪。
五、参数对象如何转换
简单类型:
text
number ↔ int / float / double
string ↔ std::string
boolean ↔ bool复杂类型:
text
JS Array ↔ C++ container
JS Object ↔ struct / map / Native object
Callback ↔ function object / saved JS function复杂转换可能涉及遍历、分配、复制和错误检查,因此成本高于普通 JS 属性读取。
六、方法、属性和回调
绑定通常需要处理三类方向:
text
JS → C++ 方法调用
JS → C++ 属性读写
C++ → JS 回调C++ 回调 JS 时风险更高,因为回调可能发生在:
- JS 对象已经被 GC 后。
- Scene 已切换后。
- 非 JavaScript 线程上。
- ScriptEngine 已退出后。
因此必须设计保存、释放、线程切换和有效性检查。
七、自动绑定和手动绑定
自动绑定
根据 C++ 接口描述生成大量重复 glue code,适合规则明确、接口稳定的类。
手动绑定
开发者直接编写注册、参数转换和调用逻辑,适合特殊类型、平台 API 或复杂生命周期。
两者都需要测试:
- 参数错误。
- 空对象。
- 生命周期结束。
- 异常和返回值。
- Web/Native 兼容分支。
八、案例:原生下载器回调
text
JS 创建下载任务
→ Binding 创建或获取 C++ Downloader
→ 保存 JS 回调
→ Native 线程下载
→ 切回可执行 JS 的线程
→ 检查 ScriptEngine 和 JS 对象
→ 调用 JS 回调
→ 任务结束后释放映射任何一步遗漏都可能造成崩溃、泄漏或回调丢失。
九、常见误区
误区一:JS 对象就是 C++ 指针
JS 对象通常是代理或包装,不能把任意数字当作安全指针使用。
误区二:绑定只需要把函数名暴露出来
还需要参数、返回值、对象身份、错误和生命周期处理。
误区三:C++ 可以从任意线程直接调用 JS
JavaScript Engine 通常有线程约束,需要切换到允许执行脚本的线程。
误区四:自动绑定生成后不需要测试
复杂类型和生命周期仍可能出错。
十、练习与答案
- 为什么 JS 对象和 C++ 对象需要身份映射?
- 复杂参数转换为什么可能较贵?
- C++ 回调 JS 时要检查哪些条件?
- 自动绑定和手动绑定的主要区别是什么?
答案:
- 保证同一逻辑对象的状态、一致性和释放行为可追踪。
- 需要遍历、复制、分配和类型检查。
- 对象是否有效、ScriptEngine 是否运行、线程是否正确、回调是否仍被保存。
- 自动绑定生成规则化 glue code,手动绑定直接处理特殊接口和生命周期。
代理对象的身份与所有权
text
JS Proxy ──关联──> C++ Object ──持有──> Platform Handle需要定义:谁创建 Native Object、谁保持它活着、JS Proxy 是拥有者还是观察者、Native 销毁如何通知 JS、回调是否持有 Proxy。
Creator 2.4.x 绑定身份实验
选择项目已有的 Native 可用组件/API,禁止手工伪造 C++ 绑定。创建两个 JS 层变量取得同一业务对象:
ts
const first = service.getCurrent();
const second = service.getCurrent();
cc.log(
'[binding] same JS identity=',
first === second
);即使 JS identity 不同,也可能代理同一个 Native 对象;即使相同,也不代表底层生命周期永久。
实验步骤
- 创建/获取对象。
- 调用公开方法并记录 Native 日志。
- 关闭页面但保留/清除 JS 字段分别测试。
- 触发 Native 销毁或连接关闭。
- 再调用方法,确认返回错误而不是继续使用。
- 恢复正确释放。
实验中不要以崩溃证明逻辑;优先使用公开有效性状态或错误回调。
参数转换也会复制
text
JS string/array/object
→ 绑定层校验与转换
→ C++ string/vector/struct
→ 平台类型大数组每帧传递会同时产生 JS 遍历、Native 容器分配、数据复制和平台调用。参数对象不是零成本引用。
真实项目故障:Native 回调带着旧代理更新 UI
下载器由 GameRoot 常驻,回调捕获旧页面代理。页面关闭后重新打开新页面,旧回调返回并调用旧组件。
修复:
- 页面订阅保存 session/generation;
- 关闭时取消回调或使代次失效;
- Native 层回调只发送数据事件,不直接持有 UI;
- JS 入口检查代理/Node 有效性;
- 常驻服务不保存短生命周期组件。
绑定故障诊断
JS 对象存在但方法无效
检查 Native handle、销毁通知、线程、模块初始化和版本 ABI。
同一 Native 对象出现多个 JS 包装
检查绑定层 identity map/缓存和 API 语义;业务不要用 JS 全等比较作为 Native 唯一性证明。
传数组后性能差
测量元素数、调用频率、复制/转换时间;设计批量 API 或减少跨边界频率。
版本边界与练习答案
Creator 2.4.x 绑定生成与 Native 运行时实现随平台构建,课程只使用公开接口。绑定内部 identity map、引用计数和析构策略需按当前源码验证。
- Proxy 为什么不能代表 C++ 一定存活? Native 对象可能已销毁,代理只是 JS 可达对象。
- JS 全等比较能判断两个 Native 对象相同吗? 不能,包装层可能不同;应使用 API 提供的 ID/所有权语义。
- 为什么跨边界数组调用贵? 参数校验、转换、分配和复制会随元素和频率放大。
- 回调应直接持有 UI 吗? 长生命周期 Native 服务应发送数据,由页面按生命周期订阅。
十一、本课总结
text
绑定对象是两套对象系统之间的身份和生命周期协议。
方法调用只是表面,映射、转换、线程和释放才是稳定性的关键。十二、下一课预告
text
Lesson076|为什么频繁跨 JSB 调用可能很贵