文章主旨
前面几篇已经回答了:
- JSI 为什么快;
- host function 和 HostObject 怎么写;
- 内存所有权怎么理解。
但这些代码都还停留在"概念上可运行"。Part 7 解决的是更现实的问题:
你的 C++ 代码怎么真的接进 React Native 的 iOS 和 Android 工程里?
原文把它叫作 plumbing,很贴切。因为这一篇主要不是算法,也不是架构抽象,而是"平台装配"。
这篇要解决的不是 C++,而是两套平台语言边界
一个很关键的判断是:
- 你的业务核心可以写成跨平台共享 C++;
- 但 iOS 和 Android 并不会原生"直接理解"这段 C++;
- 你必须分别做一层平台侧 wiring。
所以真正的结构是:
- 中间是一套共享 C++ engine;
- 左边用 Objective-C++ 接 iOS;
- 右边用 JNI + CMake 接 Android。
这也是原文"一个发动机,两条装配线"的类比含义。
一个标准 JSI 模块的目录结构应该怎么想
原文先给出了典型文件布局,这其实非常有参考价值。一个生产级 JSI 模块通常会有三层:
1. 共享 C++ 层
放:
install()入口;- host function / HostObject 实现;
- 共享数据结构;
- 通用算法与内存管理逻辑。
2. iOS wiring 层
放:
.mmObjective-C++ 文件;- 与 RN iOS runtime 对接的安装逻辑;
- podspec / CocoaPods 配置。
3. Android wiring 层
放:
- Kotlin / Java 的 module 壳;
- JNI glue code;
CMakeLists.txt。
这个结构很重要,因为它说明一件事:
真正应尽量跨平台共享的是 C++ 核心,而不是整套平台集成代码。
为什么 iOS 要用 Objective-C++
原文在 iOS 部分讲得很清楚:问题不只是"语言喜好",而是调用链兼容性。
iOS 侧的现实是:
- React Native 的 iOS 内部层大量是 Objective-C / Objective-C++;
- 你的 JSI 实现是 C++;
- 普通 Objective-C 不会直接调用 C++;
- Swift 虽然近年有 C++ interop,但在 RN 生态里还不够成熟和主流。
所以最务实的桥梁语言就是 .mm:
Objective-C++ 可以在同一个文件里同时理解 Objective-C 和 C++。
这也是为什么很多 RN JSI 库的 iOS 入口文件都是 .mm 而不是 .m 或 .swift。
iOS 侧真正做了什么
iOS wiring 的任务可以压缩成一句话:
在正确的时机拿到 jsi::Runtime,然后调用你的 install(runtime)。
围绕这句话,iOS 侧通常要做的事包括:
- 在原生模块初始化时接入 RN bridge;
- 获取底层 runtime;
- 进入 Objective-C++ 层;
- 调你的共享 C++ 安装函数;
- 把能力注入到 JS runtime。
Podspec 的作用则不是"注册 JSI 功能本身",而是确保:
- 这些源文件被编译;
- C++ 标准配置正确;
- 头文件路径和依赖关系正确。
所以 podspec 更像 build plumbing,而不是逻辑 plumbing。
Android 比 iOS 更分层:Kotlin、JNI、CMake 三层配合
Android 部分是整篇最值得整理的地方。原文把它拆成三层,我觉得这个拆法非常清楚。
第 1 层:Kotlin / Java 模块声明
这一层负责:
- 让 React Native 知道有这个模块;
- 在合适时机暴露原生入口;
- 把 runtime 指针或上下文继续传下去。
它是 RN 世界和 Android App 世界的连接点。
第 2 层:JNI glue
这一层负责:
- 让 JVM 世界和 C++ 世界对接;
- 把 Java/Kotlin 调用映射到 C++ 函数;
- 处理
jlong等桥接参数; - 最终触达
install()。
这一步的本质不是"业务逻辑",而是 ABI 级别的语言边界适配。
第 3 层:CMake
这一层负责:
- 告诉 Android NDK 如何编译你的 C++;
- 链接 RN / JSI 相关依赖;
- 产出
.so。
所以 Android wiring 的完整心智模型不是"写一个 native module 就完了",而是:
Kotlin 宣告入口,JNI 负责语言桥接,CMake 负责把 C++ 真正编出来。
为什么 Android 经常把 runtime 当作 jlong 传
原文 FAQ 里提到一个很常见的疑问:为什么不是传"一个 runtime 对象",而是把 runtime 指针当 long / jlong 传递。
本质原因很简单:
- JVM 不理解 C++ 对象模型;
- JNI 边界上最现实的通道通常是原始地址值;
- 所以你传的不是"高层语义对象",而是"底层指针句柄"。
这虽然不优雅,但很常见,也符合 JNI 这类边界的真实工作方式。
平台 wiring 的关键不是"怎么接",而是"什么时候接"
原文专门讲了 initialization timeline,这一点很重要。
JSI 安装不是随便找个模块构造时机就行,而是必须满足:
- runtime 已经存在;
- bridge / engine 已经初始化;
- 你注入的函数或对象不会错过 JS 侧使用时机;
- 也不会早到访问空 runtime。
所以 platform wiring 里最容易出的问题不是代码写不出来,而是:
- 装得太早;
- 装得太晚;
- runtime 生命周期判断错误;
- 热重载、重建 bridge 后没有重新安装。
这也是为什么"能编过"和"能稳定运行"之间还有一大段距离。
手工 wiring 的主要代价
原文没有回避这个方案的成本,主要有三类。
1. 依赖内部实现细节
无论 iOS 还是 Android,很多 runtime 获取路径都不是特别稳定、公开、长期承诺不变的高层 API。它们能用,但会受 RN 内部演进影响。
2. 双平台构建维护成本
你不只是维护一份 C++ 代码,还要维护:
- Podspec
- Objective-C++
- Kotlin / Java 壳
- JNI glue
- CMake
3. 调试链路变长
出问题时,可能卡在:
- JS 没调用到;
- 调到了原生模块但没进 JNI;
- JNI 进了但
.so没正确导出; - iOS / Android 某一侧运行时没正确初始化。
这意味着平台 wiring 不是"写一次就忘",而是生产维护成本的一部分。
我的补充理解
1. Part 7 本质上是在教你把"JSI demo"变成"可交付模块"
前几篇你学到的是机制;这一篇你才真正开始进入"库作者"视角。因为到了这里,你面对的已不是 createFromHostFunction 本身,而是:
- 包结构怎么分;
- 双平台怎么编;
- runtime 何时拿;
- 安装时机如何稳定。
2. 共享 C++ 是价值,平台 wiring 是成本
这是所有 JSI 模块都绕不开的基本面。你之所以愿意承担 iOS / Android 各自的一层集成成本,是因为:
- 核心算法只写一份;
- 生命周期和性能策略也能共享;
- 高性能能力不必双写。
如果共享层很薄,这套成本未必划算;如果共享层很厚,这套成本就很值得。
3. 手工 wiring 也解释了为什么后面会讨论 TurboModules
Part 11 会回头比较 TurboModules、pure JSI、pure C++。Part 7 提前暴露了 pure JSI 的一个现实代价:
你拥有最大控制权,同时也承担最多平台装配细节。
关键结论
- JSI 模块的核心逻辑可以写成共享 C++,但 iOS 和 Android 都需要各自平台 wiring。
- iOS 的现实桥梁语言是 Objective-C++,Android 的现实路径是 Kotlin/Java + JNI + CMake。
- 平台集成的目标都是一样的:在正确时机拿到
jsi::Runtime,然后调用共享的install()。 - build 配置、初始化时机、runtime 生命周期判断,是平台 wiring 里最常见的稳定性来源。
- pure JSI 的高控制权,直接对应更高的平台维护成本。
下一步适合看什么
紧接着看的应该是 Part 8,因为到这里虽然"能接进来了",但所有调用仍然是同步阻塞 JS 线程的。
下一篇真正回答的是:
重活怎么放到后台线程,还能安全地把结果送回 JS?