本文是 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。

源码中的 StrongObjcIdWeakObjcId 和关联的 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_envArkTsBaseRC 保存 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/JNIObjective-CDartArkTS
长期强引用GlobalRefARC strongPersistentHandlenapi_ref(1)
长期弱引用WeakGlobalRefARC weakWeakPersistentHandlenapi_ref(0)
回收通知Java wrapper/finalizerdealloc/关联对象weak finalizernapi_wrap finalizer
运行时身份JVMObjC runtime/processDart Isolatenapi_env/isolate
线程约束合法 JNIEnvUI/业务队列约束Dart thread/IsolateN-API runtime thread
对象 IDlong 字段关联 C++ 引用/IDhandle info + pointerbigint/property
派生回退链主要依赖生成类依赖 protocol/classProxyChain DFSProxyChain 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 可能需要:

  1. 先原子地从 bridge 取走 reference,防止重复使用;
  2. 判断当前线程是否为宿主线程;
  3. 是则立即删除;
  4. 否则投递到运行时消息队列;
  5. 如果运行时已退出,进入专门的失效清理和错误上报。

把删除操作简单丢给任意后台线程,通常会把泄漏修成 crash。

十、怎样为框架新增第五种宿主语言

C++ 中转架构把工作量压缩为一条新桥,但仍不是只写几份模板。至少需要:

生成器层

  • 在 Config 增加语言方向、路径和 enable;
  • 定义统一类型到目标语言/ABI 的映射;
  • 定义 prepare/name/release 转换;
  • 生成接口、实现类、proxy、静态注册和初始化入口;
  • 加入继承、空值、容器、回调的测试矩阵。

运行时层

  • 新的 XxxBaseRCXxxBaseRCProxy
  • 强弱 reference 与 finalizer;
  • from/to 和 identity 恢复;
  • 运行时/线程上下文;
  • native 到宿主的消息通知;
  • ByteBuffer 或二进制载体;
  • LanguageType、错误和 profile 维度。

集成层

  • 动态库加载和注册;
  • 包/模块脚手架;
  • 构建系统与导出符号;
  • 最小 demo 和端到端测试。

如果只做“目标语言调用 C++”,很容易误以为适配已经完成。真正困难的部分是让目标语言实现 interface,再由 C++ 和其他语言长期持有并回调。

十一、平台分叉的判断原则

遇到某个平台 bug 时,可以问三个问题:

  1. 这是共同对象语义错误,还是宿主 API 限制?
  2. 其他平台如何实现同一 attach/detach 或 from/to?
  3. 修复后是否仍满足 BaseRC 的强弱引用图?

例如“Dart handle 不能跨 Isolate”属于宿主限制,应留在 Dart bridge;“proxy 的 weak lock 需要保留通道”属于公共语义,应由 BaseRC/WeakPtr 表达。把前者塞进通用层会让所有平台背负无关复杂度,把后者只修在 Dart 又会导致平台行为分叉。

十二、小结

四个平台桥真正统一的不是 API,而是这些不变量:

  • 同一个宿主对象往返后尽量恢复同一个 BaseRC;
  • 原对象与 proxy 的实现端不会混淆;
  • 强弱 attach 有对称 detach;
  • 宿主对象和 C++ 通道不会同时被错误地全强或全弱持有;
  • reference 只在所属运行时和合法线程使用;
  • 当前语言没有派生 creator 时,可以按契约退化到父接口;
  • 任何转换失败都进入统一错误模型。

下一篇将集中到最复杂的运行时之一:Dart 多 Isolate 注册表,以及 native 消息如何通过带序号的 MessageQueue 投递、重试、确认和退出。