codegraph 跨语言桥接设计:打通混合 iOS(Swift/ObjC)与 React Native JS↔Native 的完整调用流
本文基于 codegraph 仓库中的桥接覆盖设计文档 mixed-ios-and-react-native-bridging.md,系统讲解该项目如何为「跨语言运行时分发边界」建立确定性的命名解析规则:Swift ↔ Objective-C 的自动桥接选择器映射、React Native 旧桥(RCT_EXPORT_METHOD / @ReactMethod)、TurboModules、Expo Modules 与 Fabric 视图组件。读完后,你将理解 codegraph 如何用「resolver(解析器)」与「synthesizer(合成器)」两种机制,让 trace / callers / callees / impact / explore 流程在语言边界上端到端连通,并能对照仓库源码验证每一条设计决策的实际落地。
1. 背景与问题:静态索引在哪里断流
codegraph 是一个预索引的代码知识图谱工具:它先把源码解析为节点(函数、方法、类、属性等)与边(调用、引用),再由 Agent 通过 MCP 工具查询结构/流程问题。在纯 Objective-C 支持落地之后(#165),codegraph 对 Swift、Objective-C、JavaScript/TypeScript 各自单独索引都是正确的,但价值恰恰在跨语言流程——也就是 iOS 应用与 React Native 应用实际所在的地方。设计文档指出了三类典型断流:
- 混合 iOS 应用:
MyViewController.swift调用imageDownloader.download(url:completion:),其实现是ImageDownloader.m中的-[ImageDownloader downloadURL:completion:]。此时 Swift 调用点被 tree-sitter 解析为一个选择器「无处可去」的call_expression,ObjC 方法存在但没有任何入边。Agent 只能把两个文件都读出来手工重建桥接。 - React Native 应用:
App.js中useEffect(() => NativeModules.Geolocation.getCurrentPosition(cb))应当到达RNCGeolocation.m中的RCT_EXPORT_METHOD(getCurrentPosition:(RCTResponseSenderBlock)cb)。断流表现为:JS 调用点没有指向 ObjC 实现的出边,ObjC 处理器没有来自 JS 的入边,impact(getCurrentPosition)(ObjC 侧)看不到任何 JS 调用者。 - Expo Module:JS 的
await ExpoCamera.takePictureAsync(options)应当到达ExpoCamera.swift中的AsyncFunction("takePictureAsync") { ... }(Expo Modules API)。同样是断流。
这三类的共同点是:边界两侧都存在一个名字——Swift 自动桥接产生的 ObjC 选择器、RCT_EXPORT_METHOD 的字面量第一个参数、Expo Function("name") 的字面量字符串——任何命名匹配器都能关联它们。因此修复方式是一个知道每条通道桥接规则的 resolver,为调用点发出引用边(在实现中记为 framework-resolved 的 references 边,并带有 metadata.synthesizedBy 之类的溯源标记)。
这正是该文档在 动态分发覆盖手册 §6 矩阵中的位置:「Swift × Objective-C bridging」一行和新增的「React Native bridge」一行。两者都属于 resolver 模式——两侧都存在命名字面量,桥接规则是确定性的——而不是 synthesizer 模式(可以对照手册 §3a 的 Django ORM resolver 参考实现)。手册中那条「承重的警告」在这里比平时更严格:
部分覆盖比没有覆盖更糟。 只桥接了一个边界而没桥接下一个,会暴露一个跳点,Agent 转而深入 + 读文件来完成它。必须把流程端到端闭合后再重新测量——永远不要发布一个半桥接的流程。
具体到混合 iOS,意味着两个方向(Swift→ObjC 与 ObjC→Swift)和所有被桥接的种类(方法、属性、init/初始化器、协议)都必须闭合后才能测量。对 React Native 而言,JS→native 与 native→JS(RCTEventEmitter、sendEvent)都必须闭合,且在旧桥与 TurboModules 上都要闭合,否则混用两者的应用会半桥接。
2. 需要建模的 11 条分发通道
设计文档把跨语言机制拆成 11 行分发通道(dispatch channel)——每一行是 playbook 词汇中独立的一条通道,各自有自己的 resolver(或 synthesizer,若不存在静态 ref)、自己的验证、自己在 §6 矩阵中的一行:
| # | 方向 | 通道 | 映射规则 | 实现位置 | 难度 |
|---|---|---|---|---|---|
| 1 | Swift → ObjC | 直接调用,ObjC 类经 -Bridging-Header.h 导入 |
Swift 调用 obj.x(y:z:) ↔ ObjC 选择器 -x:z:(字面映射,见 §3) |
frameworks/swift-objc.ts 中的 resolver |
中 |
| 2 | ObjC → Swift | @objc 暴露 |
Swift @objc func foo(bar:) ↔ ObjC -fooWithBar:(自动命名);@objc(custom:) 可覆盖 |
frameworks/swift-objc.ts |
中 |
| 3 | Swift ↔ ObjC | 属性/getter/setter 桥接 | Swift var name: String ↔ ObjC -name / -setName: |
frameworks/swift-objc.ts |
低 |
| 4 | Swift ↔ ObjC | 初始化器桥接 | Swift init(name:age:) ↔ ObjC -initWithName:age: |
frameworks/swift-objc.ts |
低 |
| 5 | Swift ↔ ObjC | 协议桥接(@objc protocol) |
跨语言的 conformance 边 | frameworks/swift-objc.ts |
中 |
| 6 | JS → ObjC(RN 旧桥) | NativeModules.<Mod>.<fn> ↔ RCT_EXPORT_METHOD(<fn>:...) 或 RCT_REMAP_METHOD(<jsName>, <selector>:...) |
以 ObjC 侧 RCT_EXPORT_MODULE() 字面量为键做名字匹配 |
frameworks/react-native.ts |
中 |
| 7 | JS → Java/Kotlin(RN 旧桥,Android) | NativeModules.<Mod>.<fn> ↔ ReactContextBaseJavaModule 子类上带 @ReactMethod 注解、且 getName() 返回 <Mod> 的方法 |
同 #6 的形状,JVM 侧 | 中 | |
| 8 | JS ↔ native(RN TurboModules / Codegen) | TurboModuleRegistry.get('Mod') ↔ 生成的 spec 接口(NativeMod TS 类型)↔ 匹配 spec 的 ObjC++/Kotlin 实现 |
以 spec 文件为 ground truth 的 resolver | 难 | |
| 9 | Native → JS(事件) | ObjC [self sendEventWithName:@"x" body:b](继承 RCTEventEmitter)↔ JS new NativeEventEmitter(NativeModules.Mod).addListener('x', cb) |
EventEmitter 风格 synthesizer(与既有语言内 callback-synthesizer.ts 同族) |
中 | |
| 10 | JS → native(Expo modules) | JS ExpoX.fn(args) ↔ Swift Function("fn") { ... } 或 AsyncFunction("fn") { ... }(位于带 Name("ExpoX") 的 Module 子类内) |
frameworks/expo-modules.ts 中的 resolver |
中 | |
| 11 | JS → native(Fabric 视图组件) | JS <MyView prop={v}/> ↔ ObjC/Swift RCT_EXPORT_VIEW_PROPERTY(prop, ...) 或 Codegen 视图 spec |
resolver + JSX 跳点(与既有 JSX synthesizer 组合) | 难(延期) |
「难度」列驱动了分阶段实施——见 §12。
2a. 为什么这些是 resolver 而不是 synthesizer
每一行中,桥接规则都可以从一个名字确定性地推导出来:
- Swift 的
@objc暴露是有文档的自动映射;@objc(custom:)是显式覆盖;两者都是可静态提取的。 RCT_EXPORT_METHOD接受字面量选择器;RCT_EXPORT_MODULE()接受可选的字面量模块名(默认:类名去掉RCT前缀);NativeModules.Mod.fn是对已知全局的字面量属性访问。- Expo Modules 的
Function("name") { ... }与Module { Name("ExpoX"); ... }是Module定义内的字面量字符串。 - TurboModules 的 spec 接口是带
TurboModuleRegistry.get<...>('<Name>')的字面量Native<Name>导出。
因此工作是:提取桥接侧的名字 → 让 resolver 去匹配它们,形状与 djangoResolver 解析 _iterable_class 到 ModelIterable 相同——不需要全图关联遍历。
唯一例外是 #9 native→JS 事件:其注册站点看起来与既有 callback synthesizer 已处理的语言内 EventEmitter 模式几乎一样,扩展该 synthesizer 加入跨语言通道是最自然的落法(§8 有实测)。
2b. 源码印证:四个桥接组件均已注册进框架解析器
仓库中上述通道对应的实现都已存在,并统一注册在框架解析器索引 src/resolution/frameworks/index.ts 中(swiftObjcBridgeResolver、reactNativeBridgeResolver、expoModulesResolver、fabricViewResolver 依次注册),各自配有测试:tests/swift-objc-bridge.test.ts、tests/react-native-bridge.test.ts、tests/expo-modules.test.ts、tests/fabric-view.test.ts、tests/rn-event-channel.test.ts。
3. Phase 1:Swift ↔ ObjC 的命名换算规则
3a. Swift → ObjC 选择器自动映射(参考表)
Swift 用一套标准规则从 Swift 方法推导 ObjC 选择器:
| Swift 声明 | ObjC 选择器 |
|---|---|
func greet() |
greet |
func say(_ msg: String) |
say: |
func set(name: String) |
setWithName: |
func setName(_ name: String) |
setName: |
func move(to point: CGPoint) |
moveTo: |
func move(from a: CGPoint, to b: CGPoint) |
moveFrom:to: |
init(name: String) |
initWithName: |
init(name: String, age: Int) |
initWithName:age: |
var name: String(getter) |
name |
var name: String(setter) |
setName: |
@objc(customSel:) func f(...) |
customSel:(显式覆盖) |
完整的规则集来自 Apple 官方文档「Importing Swift into Objective-C」中的 "method name translation" 与 "initializer name translation" 两节。设计决策是:resolver 在提取阶段单向实现这套映射(Swift 声明产出桥接后的 ObjC 名,作为别名挂在 Swift 方法节点上),ObjC 侧的名字解析通过常规 name-matching 就能找到 Swift 方法。
仓库中这套「纯名字换算」被单独抽到一个无图/无 DB 依赖的纯函数模块 src/resolution/swift-objc-bridge.ts,这正是设计文档 §11 开放问题 3 的落地——把映射规则放在一处,供 Swift 提取器(提取时计算别名)与测试共同导入:
objcSelectorForSwiftMethod(baseName, externalLabels, explicitObjcName):实现上表的方法规则。核心逻辑是:无参数返回基名;单参数_标签产出baseName:;单参数显式标签L产出baseNameWithL:;多参数则首关键词按同样规则成形后,每个后续标签各自成为关键词。@objc(custom:)的字面量直接短路规则。objcSelectorForSwiftInit(externalLabels, internalNames, ...):实现初始化器规则,注意 Apple 的特殊约定——即使外部标签是_,也总是initWith,且此时使用参数的内部名作为关键词(init(_ name: String)→initWithName:)。objcAccessorsForSwiftProperty(swiftName, ...):属性 getter/setter 换算,var name: String→ gettername/ settersetName:。swiftBaseNamesForObjcSelector(selector):反向换算,从 ObjC 选择器产出候选 Swift 基名。例如playWithSong:→['playWithSong', 'play'](因为func play(song:)和字面叫playWithSong的方法都可能是它);initWithName:age:→['init'];setName:→['name', 'setName'](可能是属性 setter 也可能是普通函数)。它还会剥离With/For/By/In/On/At/From/To/Of/As等前置词(objectForKey:→object)以兼容 Cocoa 导入侧的命名习惯。返回多个候选是因为裸基名本质上是二义的,resolver 会逐一尝试。detectExplicitObjcName(sourceSlice)/isObjcExposed(sourceSlice):在声明前的小窗口源码上检测@objc(custom:)覆盖与@objc暴露(@nonobjc优先判否,覆盖@objcMembers类级默认)。
3b. 两个方向的解析实现(src/resolution/frameworks/swift-objc.ts)
框架 resolver src/resolution/frameworks/swift-objc.ts 把上述纯函数接进解析流水线,两个方向实现不同:
方向 1:Swift 调用 → ObjC 方法。 Swift 调用点 obj.foo(bar:) 到 resolver 时只有裸名 foo(tree-sitter-swift 把参数标签留在参数列表里,不带进 callee 名)。解析策略是维护一个反向桥接映射:遍历图中所有 ObjC 方法节点,用 swiftBaseNamesForObjcSelector 算出每个方法自动桥接到的 Swift 基名,把节点登记在每个候选名下(buildObjcMap,按 ResolutionContext 身份用 WeakMap 惰性记忆化,索引重建时自然失效)。resolver 收到未解析的 Swift 裸名后直接查表。由于该 resolver 在精确匹配之后运行,能到达这里的候选就是合法的跨语言命中。
方向 2:ObjC 调用 → Swift 方法。 ObjC 调用点 [swiftThing fooWithBar:42] 携带选择器 fooWithBar:——Swift 节点名字里没有冒号,常规匹配必然失败。resolver 从选择器派生候选 Swift 基名,查同名 Swift 方法,再读声明前 3 行源码窗口(SOURCE_PROBE_LINES)用 isObjcExposed 验证该声明确实 @objc 暴露,滤掉「恰好同名但并未桥接」的 Swift 函数。
两条方向都返回 resolvedBy: 'framework'、置信度 0.6 的解析结果(文档表述为「不精确但由桥接规则确定性地推出」,与 Django ORM 动态分发先例一致)。另外 claimsReference(name) 对所有含 : 的选择器形名字放行:没有 Swift 节点名带冒号,若不做这个 opt-in,这些 ref 会在 name-exists 预过滤阶段就被丢弃。
精度护栏:通用名黑名单。 源码中的 GENERIC_NAMES 集合(init、description、hash、isEqual、copy、count、value、data、string、object、add、remove、load、save、reload、cancel、start、stop、close、open、show、hide、dealloc、release、retain、autorelease 等)正是 §8d 记录的设计决策:每个 NSObject 子类都实现这些方法,把它们桥接到项目内任意同名 ObjC 方法只会产生噪音;这些名字的 ref 几乎总能被常规 name-matcher 解析(每个项目里都有大量 init 节点),跳过它们只是让桥接不抢答。
detect() 的启用条件也很克制:只有当项目中同时存在 .swift 与 .m/.mm 文件时该 resolver 才生效——单边项目不需要桥接,空反向映射也只是空转。
4. Phase 2:React Native 旧桥——RCT_EXPORT_METHOD 与 @ReactMethod
4a. 命名规则(文档 §3b)
// Native side (ObjC)
@implementation RCTGeolocation
RCT_EXPORT_MODULE(); // module name: "Geolocation" (RCT prefix stripped)
RCT_EXPORT_METHOD(getCurrentPosition:(RCTResponseSenderBlock)cb) { ... }
@end
// JS side
import { NativeModules } from 'react-native';
NativeModules.Geolocation.getCurrentPosition(cb); // resolves to the ObjC method above
规则四条:
- 原生侧对每个含
RCT_EXPORT_MODULE()的类提取一个合成module节点。名字 = 显式字符串参数(若有),否则类名去掉RCT前缀。 - 每个
RCT_EXPORT_METHOD(<sel>)与RCT_REMAP_METHOD(<jsName>, <sel>)成为挂在模块节点上的方法节点,JS 可见名分别为<sel>的首关键词与<jsName>。 - JS 侧解析器把字面量属性链
NativeModules.<Mod>.<fn>与原生侧(module, jsName)对匹配。 - 从 JS 调用点向原生方法发出
references边(provenance: 'heuristic',synthesizedBy: 'rn-bridge')。
4b. 源码实现(src/resolution/frameworks/react-native.ts)
src/resolution/frameworks/react-native.ts 完整实现了该规则,且有一个文档未展开的工程细节:RCT_EXPORT_METHOD 宏在 tree-sitter ObjC 语法中解析为宏表达式(ERROR 节点)而不是 method_definition,ObjC 提取器根本不会为它建节点——iOS 侧的 native module 对图是不可见的。因此 resolver 通过 extract() 直接对 .m/.mm 文件做正则扫描:
parseObjcRNExports:匹配RCT_EXPORT_MODULE(可选显式名)、RCT_EXPORT_METHOD(首个标识符即 JS 名)、RCT_REMAP_METHOD(第一个参数为 JS 名);findObjcClassName从@implementation提取类名作为无参模块名的回退(defaultObjcModuleName实现去RCT前缀:RCTGeolocation→Geolocation)。parseJvmRNExports:Android 侧同形状——从getName()的字面量返回值取模块名(回退为类名去Module后缀),从@ReactMethod注解后的fun <name>((Kotlin)/void <name>((Java)取 JS 可见名。- 所有导出汇入一张
(jsName) → 原生方法节点的懒建映射(buildRNMaps,按 context 记忆化)。resolve()只重定向 JS 系语言(js/ts/tsx/jsx)的调用点:剥掉一个点前缀得到 JS 方法名后查表,且在 iOS 与 Android 目标并存时优先 ObjC(RN 库文档的惯用一等平台),返回resolvedBy: 'framework'、置信度 0.6 的单目标解析。
detect() 用任一信号即启用:根 package.json 依赖 react-native,或前 200 个文件里出现 RCT_EXPORT_MODULE / TurboModuleRegistry.get*< 标记——因为不少库把 JS 包与原生代码分目录,不要求两侧同时出现。
精度护栏:emitter 内建方法黑名单。 RN_EMITTER_BUILTINS(addListener、removeListeners、remove、invalidate、startObserving、stopObserving)是每个 RCTEventEmitter 子类都经 RCT_EXPORT_METHOD 声明的继承内建。JS 代码并不直接调用它们——它们经由 JS 抽象 NativeEventEmitter 走事件通道。若留在桥接映射里,每个 JS addListener / remove 调用(Firestore 订阅、RxJS 管道、甚至数组的 remove)都会被错误桥接到恰好声明了它们的 emitter。这条黑名单正是 §8b 中 react-native-firebase 从 78 条边(含 60 条假阳性)收敛到 18 条全部精确边的关键措施。
5. Phase 5:TurboModules——以 TS spec 文件为 ground truth
5a. 命名规则(文档 §3c)
// Spec (TS) — codegen ground truth
export interface Spec extends TurboModule {
getCurrentPosition(cb: (loc: Location) => void): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('Geolocation');
// ObjC++ impl
@implementation RCTGeolocation
- (void)getCurrentPosition:(RCTResponseSenderBlock)cb { ... }
@end
import Geolocation from './NativeGeolocation';
Geolocation.getCurrentPosition(cb); // resolves to the ObjC method via the spec
规则:spec 文件是唯一事实源——解析 TurboModuleRegistry.get*<Spec>('<Name>') 得到模块名,读取 Spec 接口的方法列表;每个 spec 方法与同名原生实现匹配(ObjC 按选择器首关键词;类名由命名约定或 JSI_EXPORT_MODULE 宏确定);JS 对 spec 文件的 import 经由 spec 完成命名解析;发出的边与旧桥相同,仅 synthesizedBy 标记为 'rn-turbomodule'。
5b. 源码实现
parseTurboModuleSpec(react-native.ts)用两条正则完成:TurboModuleRegistry.(getEnforcing|get)\s*<...>\s*\(\s*'Name' 提取模块名,export interface Spec ... { ... } 体内以「标识符后跟 (」的形状挑出方法名(属性无括号,自动跳过)。匹配时不要求 ObjC 侧模块名一致——spec 的模块名并不决定原生文件路径(Codegen 靠命名约定接线),所以在全部同名原生方法(ObjC 首关键词 + JVM 裸名)中建立映射。
一个实际观察值得注意:真实 JS 代码中最常见的形态不是 NativeModules.<Mod>.fn(),而是 import Geo from './NativeGeolocation'; Geo.getPosition()——接收者是 default export 而非字面的 NativeModules。因此映射实际以 JS 方法名为键(byJsName),模块限定符只是附带信息,这也解释了为什么 react-native-svg 的 9 条 TurboModule 解析全部精确命中 isPointInStroke、getTotalLength、getPointAtLength 等方法名。
6. Phase 4:Expo Modules——DSL 字面量变图谱节点
6a. 命名规则(文档 §3d)
// Native (Swift, expo-modules-core API)
public class ExpoCameraModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoCamera")
AsyncFunction("takePictureAsync") { (options: CameraOptions) in /* ... */ }
View(ExpoCameraView.self) {
Prop("type") { (view: ExpoCameraView, type: String) in /* ... */ }
}
}
}
import { requireNativeModule } from 'expo-modules-core';
const ExpoCamera = requireNativeModule('ExpoCamera');
await ExpoCamera.takePictureAsync({ quality: 1 });
规则:继承 Module 且 definition()(或新 API 的 init { /* DSL */ })含 Name("X") 的类定义模块;每个 Function("y") / AsyncFunction("y") 字面量定义一个方法,尾随闭包是方法体——提取为挂在模块 X 下、名为 y 的方法节点。JS 侧 requireNativeModule('X') 产生绑定,对其属性访问解析到命名方法。Prop("name")(视图模块)行为类似 RN 的 RCT_EXPORT_VIEW_PROPERTY,与视图组件前线一起延期。
6b. 源码实现(src/resolution/frameworks/expo-modules.ts)
expo-modules.ts 的巧妙之处在于不需要自定义 resolve():extract() 对每个 .swift/.kt 文件扫描 Expo Module 声明(EXPO_DECL_RE 匹配 Function / AsyncFunction / Property / Constants 后的字面量名,Name("X") 字面量优先、类名回退作为模块名),为每个字面量发出一个 method 节点。之后 JS 调用点 Foo.takePictureAsync(...) 被标准 name-matcher 经由既有的 obj.method → 方法名路径解析到这些合成节点,resolve() 直接返回 null 就是正确行为。
两个实现细节:其一,EXPO_DECL_RE 中可选的 <…> 泛型段是事后修复——Kotlin 的 AsyncFunction<Float>("getBatteryLevelAsync") 形式若不被匹配,所有 Android 侧 Expo Module 方法都会被静默丢弃,JS 调用点只能解析到 iOS Swift 实现;其二,detect() 要求 class X: Module 继承与至少一条声明字面量同时出现,单独任一信号假阳性太多(isExpoModuleSource)。
实测(文档 §8f):expo-haptics 14 个文件提取出 6 个方法节点;expo-camera 72 个文件提取出 41 个(takePictureAsync、record、resumePreview 及视图侧 width/height 属性等)。一个有意思的精度案例:包内 JS 侧调用点被 TS wrapper 遮蔽(CameraView.tsx 上定义了 pausePreview()),name-matcher 正确地优先了本地 TS 方法——而外部消费应用调用 Camera.takePictureAsync() 时则直接落到原生方法。五个测试覆盖提取器与端到端 fixture(JS callsite of literal AsyncFunction("uniqueExpoHapticCall") resolves to the native impl node)。
7. Phase 3:Native → JS 跨语言事件通道
这是 11 条通道中唯一的 synthesizer 形态,扩展了既有 callback synthesizer。实现是 src/resolution/callback-synthesizer.ts 中的 rnEventEdges(约 L1393 起),形状与语言内的 eventEmitterEdges 相同但跨语言:
Native (ObjC, on RCTEventEmitter subclass):
[self sendEventWithName:@"locationUpdate" body:@{...}];
Native (Java/Kotlin):
emitter.emit("locationUpdate", body);
JS (subscriber):
new NativeEventEmitter(NativeModules.Geo).addListener("locationUpdate", handler);
按字面量事件名把 native 分发站点关联到 JS handler。扫描模式覆盖各平台变体:ObjC 的 sendEventWithName:@"X"、Swift 的 sendEvent(withName: "X", body: ...)(RNFusedLocation.swift 这类 Swift 侧 RCTEventEmitter 子类同样被捕获)、JVM 的 .emit("X", …) 与常见的手写包装器 sendEvent(ctx, "X", body)(字面量在包装器调用里而非定义里)。JS 订阅侧限定在 JS 系文件——避免把 ObjC 方法 addListener: 误认为 JS 订阅。
两条精度策略与文档一致:
- 扇出上限:
EVENT_FANOUT_CAP = 6(callback-synthesizer.ts),分发器或 handler 超过该数量的通用事件名(无接收者类型信息时无法精确匹配)直接跳过而不是过度连边。 - subscribe-wrapper 模式:RN 库里常见
messaging().onMessage(listener)(listener是向上流入用户代码的参数)。当 handler 参数不是命名符号时,把订阅归属到包裹它的 JS 函数(抽象层),得到可达性正确的跳点;对对象字面量 API 形状(const Foo = { watchX(...) { ... } })再回退到最小包裹constant/variable节点。
每条边为 provenance: 'heuristic'、metadata.synthesizedBy: 'rn-event-channel'。RNFirebase 实测(§8e):messaging_message_received 通道 2 条边(application:didReceiveRemoteNotification:... → TS onMessage),messaging_notification_opened 1 条边。
8. Phase 6:Fabric / Codegen 视图组件
这是难度最高、最后落地的一条通道,采用提取器 + 合成器两段式设计(文档 §8g):
第一段:框架提取器 src/resolution/frameworks/fabric.ts。
- Codegen spec 侧(
.ts/.tsx):extractFabricNodes解析codegenNativeComponent<Props>('Name', ...)声明,每处声明发出一个component节点(以 JS 可见组件名命名,恰好与 JSX synthesizer 的 name+kind 过滤门吻合);再解析NativeProps接口体,每个 prop 字段发出一个property节点——让onTap、nativeContainerBackgroundColor这类 JSX 可调用 prop 成为可发现的图节点。 - 实现侧的约定匹配由合成器
fabricNativeImplEdges(callback-synthesizer.ts 中)完成:遍历每个 Fabric component 节点,查找名字匹配且带 RN 约定后缀(空 /View/ViewManager/ComponentView/Manager)的原生类,发出calls边(synthesizedBy: 'fabric-native-impl')。这个约定在规范 RN 库里足够精确、不会撞名。 - 提取器还额外覆盖了文档未规划的旧 Paper ViewManager(
RCT_EXPORT_VIEW_PROPERTY/RCT_CUSTOM_VIEW_PROPERTY/RCT_REMAP_VIEW_PROPERTY宏,类名去Manager/ViewManager后缀推导组件名)与 JVM 侧@ReactProp("name")注解,使旧桥视图库与新架构走同一 component → native-class 连线。
组合既有 reactJsxChildEdges JSX synthesizer 后,完整流程闭合:消费应用的 JSX <MyView prop=v/> → Fabric component 节点 MyView → 原生类 MyViewView(或 MyViewManager / MyViewComponentView 等)。
react-native-screens 复验数据(Phase 2 时因全 Fabric 而 0 桥接的语料库):54 个 codegenNativeComponent 声明、27 个 component 节点(*.web.ts 变体被 spec 有效性过滤)、272 个 prop 节点、68 条 fabric-native-impl 桥接边。四个测试覆盖提取器与完整端到端 fixture(App (TSX) → MyView (fabric-component) → MyViewView (ObjC class)),断言 JSX→component 边与 component→native-class 边都存在。
9. 边模型:每条通道需要哪些边
对每条通道,闭合后的流程由三段边构成:
- JS 调用点 → 被桥接方法节点(
references,heuristic,synthesizedBy: '<channel>') - 被桥接方法节点 → 原生实现方法(已提取;对 #6/#7 被桥接方法就是原生实现,对 #10 闭包体就是实现)
- 原生实现方法 → 它自己的被调(语言内已提取)
对 Swift ↔ ObjC,最干净的模型是声明节点上的别名:扩展 Swift 方法提取,计算自动桥接的 ObjC 名并作为备选名存储,让 resolver 考虑。Swift 与 ObjC 方法节点之间不需要新边——提取后两侧对被桥接选择器达成一致,常规名字解析足够。
MCP 读工具已经内联展示 heuristic 边(metadata.synthesizedBy 的管线),这些新边走同一条展示路径,无需额外布线。
10. 验证语料:小/中/大三档方法论
文档遵循仓库 CLAUDE.md 的验证方法论:small / medium / large 各仓库 ≥3 个流程 prompt,确定性探针 + Agent A/B,每臂 ≥2 次运行。候选语料与标准流程如下(仓库名此处按纯文本给出,避免外部链接)。
10a. 混合 iOS(Swift+ObjC)——选 3
| 档位 | 仓库 | 理由 | 标准流程 |
|---|---|---|---|
| Small | Charts(约 150 文件 Swift+ObjC) | Swift 优先库带 ObjC 兼容层,知名 | 「给 ChartView 设置 data 如何到达渲染器?」 |
| Small (备选) | Lottie-ios(约 300 文件,曾混合;当前可能纯 Swift——需验证) | 动画引擎,知名混合体 | 「AnimationView.play() 如何到达层合成器?」 |
| Medium | Realm-Cocoa(约 500 文件) | 重度 Swift-on-ObjC:Swift API 包 ObjC 核,ObjC 核包 C++ Realm Core | 「Realm.write { realm.add(obj) } 如何到达 ObjC 持久层?」 |
| Large | Wikipedia-iOS(约 2500 Swift+ObjC 文件) | 真实应用,深度混合,活跃开发 | 「点击搜索结果如何到达文章抓取的网络调用?」 |
| Large (备选) | WordPress-iOS | 更重的 ObjC 遗留 + Swift 增量 | 「新草稿保存如何到达 Core Data 持久化?」 |
每仓库标准:(1) 纯语言探针仍通过(Swift 内 trace、ObjC 内 trace)——相对 #165 纯 ObjC 基线无回归;(2) 跨语言探针通过:标准流程用 trace 端到端可追,语言边界无断裂;(3) Agent A/B(有/无 codegraph,每臂 ≥2 次):explore 预算内 Read = 0;比无 codegraph 快;纯 Swift 或纯 ObjC 对照仓库无回归;(4) 节点数不爆炸:桥接前后 select count(*) from nodes 对比。
10b. React Native——选 3
| 档位 | 仓库 | 理由 | 标准流程 |
|---|---|---|---|
| Small | react-native-svg(约 100 文件 JS+ObjC+Java) | 小而范围清晰的 native 模块集 | 「设置 <Path d=.../> 如何到达 iOS Core Graphics 调用?」 |
| Medium | react-native-screens(约 300 文件 JS+native) | 真实导航原语,旧桥与 Fabric 都有 | 「导航到新屏幕如何到达 UINavigationController?」 |
| Medium (备选) | react-native-firebase(跨包约 1000 文件) | 大量 native 模块、双平台——压测模块发现 | 「firestore().collection('x').get() 如何到达 iOS Firebase SDK 调用?」 |
| Large | facebook/react-native 的 RNTester 子集(约 3000 文件) | 框架本体 + 示例应用;标准桥接用法 | 「按下 RNTester GeolocationExample 的按钮如何到达 iOS Core Location 调用?」 |
每仓库标准:(1) 纯 JS 探针不变(useState → re-render 流程仍可解析——既有 react synthesizer 无回归);(2) JS → ObjC 桥探针通过:每仓库 ≥1 个已知 RCT_EXPORT_METHOD;(3) JS → TurboModule 探针通过(在用了 TurboModules 的仓库上;react-native 主线两者都有,各选一);(4) Native → JS 事件探针通过(≥1 个 emitter,NativeEventEmitter 模式);(5) Agent A/B:一个跨越桥的问题(如「按下按钮 X 如何到达网络调用」)在 ≥1 次有 codegraph 的运行中把 Read 降到 0;(6) 纯 JS 对照仓库无回归(既有 react-realworld / excalidraw 测量值不变)。
10c. Expo——选 2(范围更小,API 面更窄)
Small/Medium 档取 expo/expo 的单模块(如 expo-camera 或 expo-location)——最干净的 Expo Modules API 实例;Large 档取完整 expo/expo monorepo,压测跨多包的模块名解析。标准流程:「await Camera.takePictureAsync()(JS)如何到达原生相机 API 调用(Swift AVCaptureSession 或 Kotlin CameraDevice)?」
11. 实测数据与两条关键教训
11a. Phase 1 测量——Swift ↔ ObjC
| 仓库 | 源文件 | 桥接边(framework-resolved) | 示例边 |
|---|---|---|---|
| Charts(小) | 269(205 Swift + 59 ObjC/.h) | 28 objc→swift,1 swift→objc | handleOption:forChartView: → animate;setupPieChartView: → setExtraOffsets;setDataCount:range: → setColor |
| realm-swift(中) | 369(151 Swift + 218 ObjC 系) | 36 objc→swift,1185 swift→objc | valueForUndefinedKey: → get;setValue:forUndefinedKey: → set;promote:on: → initialize |
| wikipedia-ios(大) | 1734(1234 Swift + 500 ObjC/.h) | 52 objc→swift,983 swift→objc | 真实 iOS 应用跨多 feature 模块的桥接 |
三者均:语言内基线不变、节点数不爆炸、trace 跨边界连通标准流程(在 Charts 上验证:trace(handleOption:forChartView:, animate) 直接浮现桥接边)。注意 swift→objc 方向边数远多于反向——与源码中「Swift 裸名查反向映射」的不对称实现一致。
11b. Phase 2 + 5(部分)测量——React Native 桥
| 仓库 | 源文件 | 桥接边 | 备注 |
|---|---|---|---|
| react-native-svg(小/中) | 约 700(93 .mm + 115 .java + 6 .kt + 49 js + 92 ts + 154 tsx) | 9 条 tsx→java(经 TurboModule spec) | RNSvg 的 iOS 用 TurboModule 自动生成(无 RCT_EXPORT_METHOD),解析落在 Java 侧;9 条全精确 |
| AsyncStorage(小,纯旧桥) | 约 60(28 kt + 2 mm + 16 ts + 14 tsx …) | 8/8 精确 | 标准旧桥测试——Kotlin @ReactMethod + ObjC RCT_EXPORT_METHOD;JS setItem → Kotlin legacy_multiSet 等 |
| react-native-firebase(大) | 约 1100 | RCTEventEmitter 黑名单后 18 条(此前 78 条) | 初始 78 条含 60 条指向 addListener: / remove: 的假阳性;黑名单砍到 18 条且全部精确 |
| react-native-screens(中) | 1211 | 0——空 TurboModule spec、无 RCT_EXPORT_METHOD、全 Fabric/Codegen 视图侧 |
RNScreens 完全落在 Phase 6(Fabric)。桥接在此拒绝过度匹配正是正确行为 |
11c. 验证中发现的架构性修复(§8c)
resolver 的 initialize() 原本在 CodeGraph 构造时运行——早于任何文件被索引——因此凡 detect() 需要查询已索引文件列表的框架 resolver(UIKit/SwiftUI 的 import 扫描、swift-objc-bridge 的双语言文件探测、react-native-bridge 的 RN 标记探测)在首轮全部返回 false 并静默自我剔除。这影响了代码库中每一个读 context.getAllFiles() / context.readFile() 而非直接扫文件系统的框架 resolver——是一个既有潜伏 bug,并非桥接特有。修复:indexAll() 现在在提取完成后调用 resolver.initialize(),让 detect() 对着已填充的索引运行。
11d. 桥接精度黑名单(经验教训汇总,§8d)
| 桥 | 屏蔽名 | 原因 |
|---|---|---|
| swift-objc | init、description、hash、isEqual、copy、count、value、data、string、object、add、remove、update、load、save、reload、cancel、start、stop、pause、resume、close、open、show、hide、dealloc、release、retain、autorelease … |
每个 NSObject 子类都实现这些;桥接到任意项目内同名 ObjC 方法只会产出噪音,常规 name-matcher 自己能处理 |
| react-native | addListener、removeListeners、remove、invalidate、startObserving、stopObserving |
每个 RCTEventEmitter 子类都经 RCT_EXPORT_METHOD 声明它们;JS 的 .addListener(...) / .remove(...) 调用走 JS 抽象 NativeEventEmitter,不直接走原生桥 |
12. 反目标(明确不做什么)
- Android Kotlin/Java 提取质量——不在范围内。使用既有 Kotlin/Java 提取器的产出;若它们漏掉
@ReactMethod注解的字面量名,可以加一个微小的提取器修补,但不重新设计 JVM 提取。 - 动态/计算桥接键——
NativeModules[someVar]、name为参数的requireNativeModule(name)等。只解析字面量键访问(与 playbook 中「仅命名字符」反目标一致——仅匿名模式延期)。 - 桥接头文件内容解析——
.h文件会被索引(#165 的内容嗅探已做),但不会把桥接头里的#import列表当作「Swift 可见了什么」的特殊清单;它就是普通 ObjC 头。 performSelector:运行时分发——不在范围;匹配同样的「仅命名字符」反目标。- JSI(裸的、非 TurboModule)——不在范围。用裸 JSI 的应用通过自定义
Host*接口调原生,没有文档化的声明式 spec;等它们迁移到 TurboModules。 - Swift-only 泛型 over ObjC 协议 / Swift extensions on ObjC 类——扩展方法若
@objc在 ObjC 中仍可调,走同一 Phase 1 路径;泛型不行,会静默漏掉。可接受,与 Java/Kotlin 泛型前线一致。
13. 分阶段路线、开放问题与完成标准
13a. 六个阶段,顺序由「最小仓库最先闭合端到端流程」固定
按 playbook 的难度梯度与半桥接规则,顺序不可交换:
- Phase 1 — Swift ↔ ObjC 桥接(行 1–5):范围最小、命名映射确定、无 JS 参与。在 Charts/Realm/Wikipedia 语料上验证后再进入 Phase 2。
- Phase 2 — React Native 旧桥(行 6–7,ObjC + Java/Kotlin):iOS 与 Android 两侧必须在同一 PR 闭合——只桥一个平台会在另一侧暴露半覆盖跳点,Agent 会去读文件。
- Phase 3 — Native → JS 事件(行 9):扩展既有 callback synthesizer 的跨语言通道。
- Phase 4 — Expo Modules(行 10):叠加在 Phase 1 的 Swift 提取之上,语料更小。
- Phase 5 — RN TurboModules / Codegen(行 8):需要把 spec 文件作为跨语言 ground truth 读取。
- Phase 6 — Fabric 视图组件(行 11):延期——与既有 JSX synthesizer 及 TurboModules 视图侧组合;当 §5b 语料中某仓库桥接其余部分已闭合、但 Fabric 流程仍断时再处理。
13b. Phase 1 期间的四个开放问题
- 声明别名 vs 新桥接边:把自动桥接的 ObjC 选择器存为 Swift 方法节点的备选名更便宜、且与既有名字解析一致;替代方案(合成跨语言
references边)在trace输出中更显式,但每个@objc符号多 N 条边。默认:别名。 trace如何显示跨语言跳点:跨语言跳点应在渲染输出中显式标注(「Swiftfunc foo(bar:)→ 桥接为 ObjC 选择器-fooWithBar:→ ObjC-[ImageDownloader fooWithBar:]」)。- 桥接规则放哪里:
src/resolution/frameworks/swift-objc.ts(纯函数)供 Swift 提取器与测试共用——已按此落地为 swift-objc-bridge.ts + frameworks/swift-objc.ts 两层。 @objcMembers类级导出:对类的所有成员生效(除非@nonobjc)。通过在 Swift 提取器中检查类修饰符、默认各成员的@objc性来处理——isObjcExposed已实现@nonobjc判否优先的规则。
13c. Done-bar(何时算完成)
Phase 1 完成的条件:§5a 三个语料全部通过——纯语言探针不变、跨语言标准流程探针端到端找到路径、Agent A/B 显示有 codegraph 时 ≥1 次运行 Read = 0 且更快;playbook §6 覆盖矩阵行以数字填充;存在用户视角书写的 CHANGELOG [Unreleased] 条目。
后续每个 Phase 同形——自己的语料、自己的矩阵行、自己的 CHANGELOG 条目——且前一阶段不通过就不发布。半桥接在这里不是「可以避开的坑」而是硬约束:它会让 codegraph 在这些代码库上比完全没有桥接更差。
延伸阅读:本设计的上位方法论见 动态分发覆盖手册(resolver vs synthesizer 的判别标准与 Django ORM 参考实现),回调边的合成原理见 callback-edge-synthesis.md;框架解析器的注册入口在 src/resolution/frameworks/index.ts,跨语言事件合成器在 src/resolution/callback-synthesizer.ts。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00