Skip to content

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 通常有线程约束,需要切换到允许执行脚本的线程。

误区四:自动绑定生成后不需要测试 ​

复杂类型和生命周期仍可能出错。


十、练习与答案 ​

  1. 为什么 JS 对象和 C++ 对象需要身份映射?
  2. 复杂参数转换为什么可能较贵?
  3. C++ 回调 JS 时要检查哪些条件?
  4. 自动绑定和手动绑定的主要区别是什么?

答案:

  1. 保证同一逻辑对象的状态、一致性和释放行为可追踪。
  2. 需要遍历、复制、分配和类型检查。
  3. 对象是否有效、ScriptEngine 是否运行、线程是否正确、回调是否仍被保存。
  4. 自动绑定生成规则化 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 对象;即使相同,也不代表底层生命周期永久。

实验步骤 ​

  1. 创建/获取对象。
  2. 调用公开方法并记录 Native 日志。
  3. 关闭页面但保留/清除 JS 字段分别测试。
  4. 触发 Native 销毁或连接关闭。
  5. 再调用方法,确认返回错误而不是继续使用。
  6. 恢复正确释放。

实验中不要以崩溃证明逻辑;优先使用公开有效性状态或错误回调。

参数转换也会复制 ​

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、引用计数和析构策略需按当前源码验证。

  1. Proxy 为什么不能代表 C++ 一定存活? Native 对象可能已销毁,代理只是 JS 可达对象。
  2. JS 全等比较能判断两个 Native 对象相同吗? 不能,包装层可能不同;应使用 API 提供的 ID/所有权语义。
  3. 为什么跨边界数组调用贵? 参数校验、转换、分配和复制会随元素和频率放大。
  4. 回调应直接持有 UI 吗? 长生命周期 Native 服务应发送数据,由页面按生命周期订阅。

十一、本课总结 ​

text
绑定对象是两套对象系统之间的身份和生命周期协议。
方法调用只是表面,映射、转换、线程和释放才是稳定性的关键。

十二、下一课预告 ​

text
Lesson076|为什么频繁跨 JSB 调用可能很贵