本文是 unit_rc 系列第四篇。对象公共模型见《BaseRC 如何统一对象身份与跨语言生命周期》。
跨语言框架很容易为了“统一”而做过头:给所有平台套同样的接口,然后假设 JNI global reference、Objective-C ARC、Dart persistent handle 和 N-API reference 只是名字不同。它们的目标确实相似,但创建、释放、线程和运行时约束并不相同。
unit_rc 的做法是让四个平台实现共同的对象协议,同时保留宿主差异:
BaseRC public semantics
├─ JniBaseRC -> Java object / JNI reference
├─ ObjcBaseRC -> Objective-C object / ARC relation
├─ DartBaseRC -> Dart handle / Isolate
└─ ArkTsBaseRC -> napi_value / napi_env
本文不逐行解释所有 bridge,而是沿同一组问题横向对照:对象怎样进入 C++,怎样回到宿主,强弱引用如何 attach/detach,哪些操作必须在特定运行时执行。
一、四个平台都需要完成的闭环
每个平台桥都要实现这四类操作:
1. From host
把宿主对象转换为 std::shared_ptr<T>:
Java jobject / ObjC id / Dart_Handle / napi_value
-> read attached identity
-> recover existing BaseRC
-> or create host-language proxy
2. To host
把 C++ 对象转换成宿主 wrapper:
std::shared_ptr<T>
-> reuse cached host object if valid
-> otherwise invoke generated creator
-> attach identity and reference relation
3. Reference bridge
根据 ProxyRCType 建立强或弱关系:
attachStrong / attachWeak
detachStrong / detachWeak
4. Proxy semantics
如果真实对象由宿主语言实现,C++ 侧 proxy 要覆盖:
isXxxProxy();strongBaseRC();weakBaseRC();baseRCFromXxx();- 生成接口中的所有虚方法。
这组共同操作是运行时契约。至于怎样保存对象,则由平台选择正确 API。
二、Java/JNI:GlobalRef、WeakGlobalRef 与线程环境
Java 桥主要位于 unit_rc/jni/JniBaseRC.*。
JNI 的本地引用只在当前 native 调用和当前线程环境里有效,不能被对象长期保存。因此跨调用持有必须使用:
NewGlobalRef:强持有 Java 对象;NewWeakGlobalRef:不阻止 GC;DeleteGlobalRef/DeleteWeakGlobalRef:成对释放。
Java 对象进入 C++
生成的 Java wrapper 保存两类 native identity 字段。baseRCFromJava() 会结合对象是否为 proxy 和请求的引用类型,优先尝试恢复已经存在的 C++ 对象。
Java wrapper
├─ strongPointer
└─ weakPointer
│
▼
baseRCFromId(pointer)
│
▼
existing BaseRC or new Jni proxy
如果 Java 对象是 C++ wrapper,通常可以直接回到原对象。如果 Java 对象由 Java 实现,则生成的 JniBaseRCProxy 负责后续 C++ → Java 的虚方法调用。
强弱 attach
强 attach 使用 GlobalRef,并把反向 identity 设为弱向;弱 attach 使用 WeakGlobalRef,同时通过 C++ addRC() 保留 proxy 通道。
detach 时除了删除 JNI reference,还会验证 wrapper 中的 pointer 是否仍等于 _preferRcId,避免旧 bridge 清掉已经重新绑定的对象。
JNIEnv 不是全局指针
JNI 操作需要当前线程拥有合法 JNIEnv。业务线程从 C++ 回调 Java 时,通常要 attach 到 JVM;线程退出时再 detach。桥接工具层应把这个过程封装起来,生成代码不应到处缓存 JNIEnv*。
另外,JNI local reference 有容量和作用域。批量转换列表、循环回调时必须及时释放 local ref,不能只依赖 native 方法返回时统一清理。
三、Objective-C:ARC 仍需要显式桥接关系
Objective-C 桥位于 unit_rc/objc/ObjcBaseRC.*,使用 Objective-C++ 同时访问 C++ shared_ptr 和 ObjC 对象。
ARC 会自动管理 Objective-C 强弱引用,但它不知道 C++ 对象图。桥接层仍需明确:
- ObjC wrapper 对应哪个 BaseRC;
- C++ 是强持有还是弱持有 ObjC;
- ObjC 对象释放时怎样解除 C++ 外部票据;
- 同一 ObjC 对象再次传回时怎样恢复原 BaseRC。
源码中的 StrongObjcId、WeakObjcId 和关联的 C++ 指针共同完成这件事。
ObjC 与 C++ 对象互转
ObjC protocol object
-> associated strong/weak C++ reference
-> existing BaseRC or Objc proxy
C++ BaseRC
-> cached ObjC object?
-> generated ObjC creator
-> attach relation
Objective-C 的便利之处在于 id 和 protocol 能较自然地表达接口对象,Objective-C++ 也能直接调用 C++。但 ARC 释放和 C++ 析构并不共享同一时钟,跨边界环仍需桥接协议主动打破。
no-copy 数据的生命周期
Objective-C 的 NSData dataWithBytesNoCopy:length:freeWhenDone: 能直接包装 C++ buffer。freeWhenDone:NO 只表示 NSData 不释放内存,不代表内存自动安全。对应 BaseRC/ByteBuffer 必须活得比 NSData 读者更久,或使用明确的 owner/finalizer。
四、Dart:PersistentHandle 必须带 Isolate 上下文
Dart 桥位于 unit_rc/dart/。它与 JNI 最大的差异是:handle 不只是“属于 Dart VM”,还属于具体 Isolate 和执行上下文。
主要持有方式包括:
- persistent handle:强持有 Dart 对象;
- weak persistent handle:允许 GC,并在回收时执行 finalizer;
- 普通
Dart_Handle:只在当前 Dart scope 中短期有效。
DartBaseRC 保存 _isolateInfo。attach 时记录当前 Isolate;to/from 或 detach 时检查该信息是否仍有效、是否为当前运行时。
为什么不能跨 Isolate 复用 handle
一个 Dart 对象只属于创建它的 Isolate。即使 native 侧仍保存旧 handle 的数值,也不能在新 Engine/Isolate 中调用它。
Isolate A: Dart object A <-> C++ proxy A
Isolate A exits
Isolate B starts
proxy A cannot become Dart object B
如果真实对象位于 C++,则可以丢弃旧 Dart wrapper,并在 Isolate B 创建新 wrapper;如果真实对象位于 Dart,proxy 应随 A 失效。
Dart proxy creator
宿主对象进入 C++ 时,运行时根据 Dart 对象的类名查找生成代码注册的 creator。creator 创建对应的 C++ proxy,并 attach Dart handle。
反向转换时,如果派生类 creator 未在当前 Dart 模块注册,当前版本通过 DartProxyChain 沿父类 DFS 回退,找到第一个可创建的父类 wrapper。它解决了“C++ 拿到派生对象,但 Dart 只依赖基础接口”的模块化问题。
五、ArkTS/N-API:napi_ref 与 env 同样属于运行时
ArkTS 桥位于 unit_rc/arkts/,通过 N-API 访问对象。
N-API reference 用引用计数区分强弱:
napi_create_reference(env, value, 1, &ref):初始引用计数为 1,强持有;napi_create_reference(env, value, 0, &ref):不阻止 GC,可作为弱引用;napi_delete_reference:释放引用;napi_wrap/finalizer:对象回收时通知 native。
和 Dart 类似,napi_ref 依赖创建它的 napi_env。ArkTsBaseRC 保存 isolate/env 信息,删除 reference 或调用函数前检查运行时与线程。
ArkTS identity
ArkTS 侧可以用 bigint 承载 rcId,wrapper 还保存强弱 pointer 语义。fromArkTs() 先尝试按 identity 取得原 BaseRC,否则根据类信息创建 proxy。
当前版本也有 ArkTsProxyChain,与 Dart 一样支持派生类型 creator 缺失时沿继承链回退。
External ArrayBuffer
ByteBuffer 到 ArkTS 时,native 可以用 napi_create_external_arraybuffer 包装 C++ 内存,再创建 Uint8Array。这能避免复制,但 finalizer 和 BaseRC 必须共同保证内存在 typed array 使用期间有效。
六、四个平台的对照表
| 问题 | Java/JNI | Objective-C | Dart | ArkTS |
|---|---|---|---|---|
| 长期强引用 | GlobalRef | ARC strong | PersistentHandle | napi_ref(1) |
| 长期弱引用 | WeakGlobalRef | ARC weak | WeakPersistentHandle | napi_ref(0) |
| 回收通知 | Java wrapper/finalizer | dealloc/关联对象 | weak finalizer | napi_wrap finalizer |
| 运行时身份 | JVM | ObjC runtime/process | Dart Isolate | napi_env/isolate |
| 线程约束 | 合法 JNIEnv | UI/业务队列约束 | Dart thread/Isolate | N-API runtime thread |
| 对象 ID | long 字段 | 关联 C++ 引用/ID | handle info + pointer | bigint/property |
| 派生回退链 | 主要依赖生成类 | 依赖 protocol/class | ProxyChain DFS | ProxyChain DFS |
这个表展示了公共契约与平台机制的边界:attachWeak 是共同语义,但具体 API、finalizer 和线程条件完全不同。
七、toXxx 的通用决策流程
各平台的 toXxx() 大体遵循相同流程:
input shared_ptr<BaseRC>
│ null?
├─ yes -> host null
▼
cached host handle valid for current runtime?
├─ yes -> return cached handle
▼
is this already a proxy owned by same host language?
├─ yes but handle invalid -> report error
▼
find generated creator using runtime type
│
▼
create host wrapper
│
▼
attach strong/weak relation
│
▼
return handle
“同语言 proxy 但 handle 已失效”不能随意重建,因为真实对象就在已失效的宿主运行时;重新创建 wrapper 只会制造一个没有真实实现的壳。
八、fromXxx 的通用决策流程
input host object
│ null?
├─ yes -> nullptr
▼
read attached strong/weak identity
│
▼
existing BaseRC found?
├─ yes -> type cast and return
▼
is creating proxy allowed?
├─ no -> conversion error
▼
resolve runtime class -> generated proxy creator
│
▼
create proxy + attach host object
类型转换失败需要被报告,而不是直接 reinterpret 成目标接口。一个 wrapper 携带有效 rcId,并不保证对应 BaseRC 一定实现当前请求的接口;生成代码仍要做动态转换。
九、释放为什么经常需要调度
宿主 reference 的删除不一定能在任意线程执行:
- JNI 线程需要合法环境;
- Dart persistent handle 操作要求有效 VM/Isolate 上下文;
- N-API reference 依赖原
napi_env; - Objective-C 对象的业务析构可能要求特定队列。
因此 detach 可能需要:
- 先原子地从 bridge 取走 reference,防止重复使用;
- 判断当前线程是否为宿主线程;
- 是则立即删除;
- 否则投递到运行时消息队列;
- 如果运行时已退出,进入专门的失效清理和错误上报。
把删除操作简单丢给任意后台线程,通常会把泄漏修成 crash。
十、怎样为框架新增第五种宿主语言
C++ 中转架构把工作量压缩为一条新桥,但仍不是只写几份模板。至少需要:
生成器层
- 在 Config 增加语言方向、路径和 enable;
- 定义统一类型到目标语言/ABI 的映射;
- 定义 prepare/name/release 转换;
- 生成接口、实现类、proxy、静态注册和初始化入口;
- 加入继承、空值、容器、回调的测试矩阵。
运行时层
- 新的
XxxBaseRC与XxxBaseRCProxy; - 强弱 reference 与 finalizer;
- from/to 和 identity 恢复;
- 运行时/线程上下文;
- native 到宿主的消息通知;
- ByteBuffer 或二进制载体;
- LanguageType、错误和 profile 维度。
集成层
- 动态库加载和注册;
- 包/模块脚手架;
- 构建系统与导出符号;
- 最小 demo 和端到端测试。
如果只做“目标语言调用 C++”,很容易误以为适配已经完成。真正困难的部分是让目标语言实现 interface,再由 C++ 和其他语言长期持有并回调。
十一、平台分叉的判断原则
遇到某个平台 bug 时,可以问三个问题:
- 这是共同对象语义错误,还是宿主 API 限制?
- 其他平台如何实现同一 attach/detach 或 from/to?
- 修复后是否仍满足 BaseRC 的强弱引用图?
例如“Dart handle 不能跨 Isolate”属于宿主限制,应留在 Dart bridge;“proxy 的 weak lock 需要保留通道”属于公共语义,应由 BaseRC/WeakPtr 表达。把前者塞进通用层会让所有平台背负无关复杂度,把后者只修在 Dart 又会导致平台行为分叉。
十二、小结
四个平台桥真正统一的不是 API,而是这些不变量:
- 同一个宿主对象往返后尽量恢复同一个 BaseRC;
- 原对象与 proxy 的实现端不会混淆;
- 强弱 attach 有对称 detach;
- 宿主对象和 C++ 通道不会同时被错误地全强或全弱持有;
- reference 只在所属运行时和合法线程使用;
- 当前语言没有派生 creator 时,可以按契约退化到父接口;
- 任何转换失败都进入统一错误模型。
下一篇将集中到最复杂的运行时之一:Dart 多 Isolate 注册表,以及 native 消息如何通过带序号的 MessageQueue 投递、重试、确认和退出。