1// Copyright 2018-present 650 Industries. All rights reserved. 2 3#import <objc/runtime.h> 4 5#import <React/RCTLog.h> 6#import <React/RCTUIManager.h> 7#import <React/RCTComponentData.h> 8#import <React/RCTModuleData.h> 9#import <React/RCTEventDispatcherProtocol.h> 10 11#import <jsi/jsi.h> 12 13#import <ExpoModulesCore/EXNativeModulesProxy.h> 14#import <ExpoModulesCore/EXEventEmitter.h> 15#import <ExpoModulesCore/EXViewManager.h> 16#import <ExpoModulesCore/EXViewManagerAdapter.h> 17#import <ExpoModulesCore/EXViewManagerAdapterClassesRegistry.h> 18#import <ExpoModulesCore/EXModuleRegistryProvider.h> 19#import <ExpoModulesCore/EXReactNativeEventEmitter.h> 20#import <ExpoModulesCore/JSIInstaller.h> 21#import <ExpoModulesCore/Swift.h> 22 23static const NSString *exportedMethodsNamesKeyPath = @"exportedMethods"; 24static const NSString *viewManagersNamesKeyPath = @"viewManagersNames"; 25static const NSString *exportedConstantsKeyPath = @"modulesConstants"; 26 27static const NSString *methodInfoKeyKey = @"key"; 28static const NSString *methodInfoNameKey = @"name"; 29static const NSString *methodInfoArgumentsCountKey = @"argumentsCount"; 30 31@interface RCTBridge (RegisterAdditionalModuleClasses) 32 33- (NSArray<RCTModuleData *> *)registerModulesForClasses:(NSArray<Class> *)moduleClasses; 34- (void)registerAdditionalModuleClasses:(NSArray<Class> *)modules; 35 36@end 37 38@interface RCTBridge (JSIRuntime) 39 40- (void *)runtime; 41 42@end 43 44@interface RCTComponentData (EXNativeModulesProxy) 45 46- (instancetype)initWithManagerClass:(Class)managerClass bridge:(RCTBridge *)bridge eventDispatcher:(id<RCTEventDispatcherProtocol>) eventDispatcher; // available in RN 0.65+ 47- (instancetype)initWithManagerClass:(Class)managerClass bridge:(RCTBridge *)bridge; 48 49@end 50 51@interface EXNativeModulesProxy () 52 53@property (nonatomic, strong) NSRegularExpression *regexp; 54@property (nonatomic, strong) EXModuleRegistry *exModuleRegistry; 55@property (nonatomic, strong) NSMutableDictionary<const NSString *, NSMutableDictionary<NSString *, NSNumber *> *> *exportedMethodsKeys; 56@property (nonatomic, strong) NSMutableDictionary<const NSString *, NSMutableDictionary<NSNumber *, NSString *> *> *exportedMethodsReverseKeys; 57@property (nonatomic) BOOL ownsModuleRegistry; 58 59@end 60 61@implementation EXNativeModulesProxy 62 63@synthesize bridge = _bridge; 64 65RCT_EXPORT_MODULE(NativeUnimoduleProxy) 66 67/** 68 The designated initializer. It's used in the old setup where the native modules proxy 69 is registered in `extraModulesForBridge:` by the bridge delegate. 70 */ 71- (instancetype)initWithModuleRegistry:(nullable EXModuleRegistry *)moduleRegistry 72{ 73 if (self = [super init]) { 74 _exModuleRegistry = moduleRegistry != nil ? moduleRegistry : [[EXModuleRegistryProvider new] moduleRegistry]; 75 _swiftInteropBridge = [[SwiftInteropBridge alloc] initWithModulesProvider:[EXNativeModulesProxy getExpoModulesProvider] legacyModuleRegistry:_exModuleRegistry]; 76 _exportedMethodsKeys = [NSMutableDictionary dictionary]; 77 _exportedMethodsReverseKeys = [NSMutableDictionary dictionary]; 78 _ownsModuleRegistry = moduleRegistry == nil; 79 } 80 return self; 81} 82 83/** 84 Convenience initializer used by React Native in the new setup, where the modules are registered automatically. 85 */ 86- (instancetype)init 87{ 88 return [self initWithModuleRegistry:nil]; 89} 90 91# pragma mark - React API 92 93+ (BOOL)requiresMainQueueSetup 94{ 95 return YES; 96} 97 98- (NSDictionary *)constantsToExport 99{ 100 // Install the TurboModule implementation of the proxy. 101 [self installExpoTurboModules]; 102 103 NSMutableDictionary <NSString *, id> *exportedModulesConstants = [NSMutableDictionary dictionary]; 104 // Grab all the constants exported by modules 105 for (EXExportedModule *exportedModule in [_exModuleRegistry getAllExportedModules]) { 106 @try { 107 exportedModulesConstants[[[exportedModule class] exportedModuleName]] = [exportedModule constantsToExport] ?: [NSNull null]; 108 } @catch (NSException *exception) { 109 continue; 110 } 111 } 112 [exportedModulesConstants addEntriesFromDictionary:[_swiftInteropBridge exportedModulesConstants]]; 113 114 // Also add `exportedMethodsNames` 115 NSMutableDictionary<const NSString *, NSMutableArray<NSMutableDictionary<const NSString *, id> *> *> *exportedMethodsNamesAccumulator = [NSMutableDictionary dictionary]; 116 for (EXExportedModule *exportedModule in [_exModuleRegistry getAllExportedModules]) { 117 const NSString *exportedModuleName = [[exportedModule class] exportedModuleName]; 118 exportedMethodsNamesAccumulator[exportedModuleName] = [NSMutableArray array]; 119 [[exportedModule getExportedMethods] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull exportedName, NSString * _Nonnull selectorName, BOOL * _Nonnull stop) { 120 NSMutableDictionary<const NSString *, id> *methodInfo = [NSMutableDictionary dictionaryWithDictionary:@{ 121 methodInfoNameKey: exportedName, 122 // - 3 is for resolver and rejecter of the promise and the last, empty component 123 methodInfoArgumentsCountKey: @([[selectorName componentsSeparatedByString:@":"] count] - 3) 124 }]; 125 [exportedMethodsNamesAccumulator[exportedModuleName] addObject:methodInfo]; 126 }]; 127 [self assignExportedMethodsKeys:exportedMethodsNamesAccumulator[exportedModuleName] forModuleName:exportedModuleName]; 128 } 129 130 // Add entries from Swift modules 131 [exportedMethodsNamesAccumulator addEntriesFromDictionary:[_swiftInteropBridge exportedFunctionNames]]; 132 133 // Also, add `viewManagersNames` for sanity check and testing purposes -- with names we know what managers to mock on UIManager 134 NSArray<EXViewManager *> *viewManagers = [_exModuleRegistry getAllViewManagers]; 135 NSMutableArray<NSString *> *viewManagersNames = [NSMutableArray arrayWithCapacity:[viewManagers count]]; 136 for (EXViewManager *viewManager in viewManagers) { 137 [viewManagersNames addObject:[viewManager viewName]]; 138 } 139 140 [viewManagersNames addObjectsFromArray:[_swiftInteropBridge exportedViewManagersNames]]; 141 142 NSMutableDictionary <NSString *, id> *constantsAccumulator = [NSMutableDictionary dictionary]; 143 constantsAccumulator[viewManagersNamesKeyPath] = viewManagersNames; 144 constantsAccumulator[exportedConstantsKeyPath] = exportedModulesConstants; 145 constantsAccumulator[exportedMethodsNamesKeyPath] = exportedMethodsNamesAccumulator; 146 147 return constantsAccumulator; 148} 149 150- (void)setBridge:(RCTBridge *)bridge 151{ 152 if (!_bridge) { 153 [self registerExpoModulesInBridge:bridge]; 154 } 155 _bridge = bridge; 156} 157 158RCT_EXPORT_METHOD(callMethod:(NSString *)moduleName methodNameOrKey:(id)methodNameOrKey arguments:(NSArray *)arguments resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 159{ 160 if ([_swiftInteropBridge hasModule:moduleName]) { 161 [_swiftInteropBridge callFunction:methodNameOrKey onModule:moduleName withArgs:arguments resolve:resolve reject:reject]; 162 return; 163 } 164 EXExportedModule *module = [_exModuleRegistry getExportedModuleForName:moduleName]; 165 if (module == nil) { 166 NSString *reason = [NSString stringWithFormat:@"No exported module was found for name '%@'. Are you sure all the packages are linked correctly?", moduleName]; 167 reject(@"E_NO_MODULE", reason, nil); 168 return; 169 } 170 171 if (!methodNameOrKey) { 172 reject(@"E_NO_METHOD", @"No method key or name provided", nil); 173 return; 174 } 175 176 NSString *methodName; 177 if ([methodNameOrKey isKindOfClass:[NSString class]]) { 178 methodName = (NSString *)methodNameOrKey; 179 } else if ([methodNameOrKey isKindOfClass:[NSNumber class]]) { 180 methodName = _exportedMethodsReverseKeys[moduleName][(NSNumber *)methodNameOrKey]; 181 } else { 182 reject(@"E_INV_MKEY", @"Method key is neither a String nor an Integer -- don't know how to map it to method name.", nil); 183 return; 184 } 185 186 dispatch_async([module methodQueue], ^{ 187 @try { 188 [module callExportedMethod:methodName withArguments:arguments resolver:resolve rejecter:reject]; 189 } @catch (NSException *e) { 190 NSString *message = [NSString stringWithFormat:@"An exception was thrown while calling `%@.%@` with arguments `%@`: %@", moduleName, methodName, arguments, e]; 191 reject(@"E_EXC", message, nil); 192 } 193 }); 194} 195 196- (id)callMethodSync:(NSString *)moduleName methodName:(NSString *)methodName arguments:(NSArray *)arguments 197{ 198 if ([_swiftInteropBridge hasModule:moduleName]) { 199 return [_swiftInteropBridge callFunctionSync:methodName onModule:moduleName withArgs:arguments]; 200 } 201 return (id)kCFNull; 202} 203 204#pragma mark - Statics 205 206+ (id<ModulesProviderObjCProtocol>)getExpoModulesProvider 207{ 208 // Dynamically gets the modules provider class. 209 // NOTE: This needs to be versioned in Expo Go. 210 Class generatedExpoModulesProvider; 211 212 // [0] When ExpoModulesCore is built as separated framework/module, 213 // we should explicitly load main bundle's `ExpoModulesProvider` class. 214 NSString *bundleName = NSBundle.mainBundle.infoDictionary[@"CFBundleName"]; 215 if (bundleName != nil) { 216 generatedExpoModulesProvider = NSClassFromString([NSString stringWithFormat:@"%@.ExpoModulesProvider", bundleName]); 217 if (generatedExpoModulesProvider != nil) { 218 return [generatedExpoModulesProvider new]; 219 } 220 } 221 222 // [1] Fallback to load `ExpoModulesProvider` class from the current module. 223 generatedExpoModulesProvider = NSClassFromString(@"ExpoModulesProvider"); 224 if (generatedExpoModulesProvider != nil) { 225 return [generatedExpoModulesProvider new]; 226 } 227 228 // [2] Fallback to load `ModulesProvider` if `ExpoModulesProvider` was not generated 229 return [ModulesProvider new]; 230} 231 232#pragma mark - Privates 233 234- (void)registerExpoModulesInBridge:(RCTBridge *)bridge 235{ 236 // Registering expo modules in bridge is needed only when the proxy module owns the registry 237 // (was autoinitialized by React Native). Otherwise they're registered by the registry adapter. 238 if (!_ownsModuleRegistry || [bridge moduleIsInitialized:[EXReactNativeEventEmitter class]]) { 239 return; 240 } 241 242 // An array of `RCTBridgeModule` classes to register. 243 NSMutableArray<Class<RCTBridgeModule>> *additionalModuleClasses = [NSMutableArray new]; 244 NSMutableSet *visitedSweetModules = [NSMutableSet new]; 245 246 // Event emitter is a bridge module, however it's also needed by expo modules, 247 // so later we'll register an instance created by React Native as expo module. 248 [additionalModuleClasses addObject:[EXReactNativeEventEmitter class]]; 249 250 // Add dynamic wrappers for view modules written in Sweet API. 251 for (ViewModuleWrapper *swiftViewModule in [_swiftInteropBridge getViewManagers]) { 252 Class wrappedViewModuleClass = [ViewModuleWrapper createViewModuleWrapperClassWithModule:swiftViewModule]; 253 [additionalModuleClasses addObject:wrappedViewModuleClass]; 254 [visitedSweetModules addObject:swiftViewModule.name]; 255 } 256 257 // Add dynamic wrappers for the classic view managers. 258 for (EXViewManager *viewManager in [_exModuleRegistry getAllViewManagers]) { 259 if (![visitedSweetModules containsObject:viewManager.viewName]) { 260 Class viewManagerWrapperClass = [EXViewManagerAdapterClassesRegistry createViewManagerAdapterClassForViewManager:viewManager]; 261 [additionalModuleClasses addObject:viewManagerWrapperClass]; 262 } 263 } 264 265 // View manager wrappers don't have their own prop configs, so we must register 266 // their base view managers that provides common props such as `proxiedProperties`. 267 // Otherwise, React Native may treat these props as invalid in subclassing views. 268 [additionalModuleClasses addObject:[EXViewManagerAdapter class]]; 269 [additionalModuleClasses addObject:[ViewModuleWrapper class]]; 270 271 // Some modules might need access to the bridge. 272 for (id module in [_exModuleRegistry getAllInternalModules]) { 273 if ([module conformsToProtocol:@protocol(RCTBridgeModule)]) { 274 [module setValue:bridge forKey:@"bridge"]; 275 } 276 } 277 278 // `registerAdditionalModuleClasses:` call below is not thread-safe if RCTUIManager is not initialized. 279 // The case happens especially with reanimated which accesses `bridge.uiManager` and initialize bridge in js thread. 280 // Accessing uiManager here, we try to make sure RCTUIManager is initialized. 281 [bridge uiManager]; 282 283 // Register the view managers as additional modules. 284 [self registerAdditionalModuleClasses:additionalModuleClasses inBridge:bridge]; 285 286 // Bridge's `registerAdditionalModuleClasses:` method doesn't register 287 // components in UIManager — we need to register them on our own. 288 [self registerComponentDataForModuleClasses:additionalModuleClasses inBridge:bridge]; 289 290 // Get the newly created instance of `EXReactEventEmitter` bridge module, 291 // pass event names supported by Swift modules and register it in legacy modules registry. 292 EXReactNativeEventEmitter *eventEmitter = [bridge moduleForClass:[EXReactNativeEventEmitter class]]; 293 [eventEmitter setSwiftInteropBridge:_swiftInteropBridge]; 294 [_exModuleRegistry registerInternalModule:eventEmitter]; 295 296 // Let the modules consume the registry :) 297 // It calls `setModuleRegistry:` on all `EXModuleRegistryConsumer`s. 298 [_exModuleRegistry initialize]; 299} 300 301- (void)registerAdditionalModuleClasses:(NSArray<Class> *)moduleClasses inBridge:(RCTBridge *)bridge 302{ 303 // In remote debugging mode, i.e. executorClass is `RCTWebSocketExecutor`, 304 // there is a deadlock issue in `registerAdditionalModuleClasses:` and causes app freezed. 305 // - The JS thread acquired the `RCTCxxBridge._moduleRegistryLock` lock in `RCTCxxBridge._initializeBridgeLocked` 306 // = it further goes into RCTObjcExecutor and tries to get module config from main thread 307 // - The main thread is pending in `RCTCxxBridge.registerAdditionalModuleClasses` where trying to acquire the same lock. 308 // To workaround the deadlock, we tend to use the non-locked registration and mutate the bridge internal module data. 309 // Since JS thread in this situation is waiting for main thread, it's safe to mutate module data without lock. 310 // The only risk should be the internal `_moduleRegistryCreated` flag without lock protection. 311 // As we just workaround in `RCTWebSocketExecutor` case, the risk of `_moduleRegistryCreated` race condition should be lower. 312 // 313 // Learn more about the non-locked initialization: 314 // https://github.com/facebook/react-native/blob/757bb75fbf837714725d7b2af62149e8e2a7ee51/React/CxxBridge/RCTCxxBridge.mm#L922-L935 315 // See the `_moduleRegistryCreated` false case 316 if ([NSStringFromClass([bridge executorClass]) isEqualToString:@"RCTWebSocketExecutor"]) { 317 NSNumber *moduleRegistryCreated = [bridge valueForKey:@"_moduleRegistryCreated"]; 318 if (![moduleRegistryCreated boolValue]) { 319 [bridge registerModulesForClasses:moduleClasses]; 320 return; 321 } 322 } 323 324 [bridge registerAdditionalModuleClasses:moduleClasses]; 325} 326 327- (void)registerComponentDataForModuleClasses:(NSArray<Class> *)moduleClasses inBridge:(RCTBridge *)bridge 328{ 329 // Hacky way to get a dictionary with `RCTComponentData` from UIManager. 330 NSMutableDictionary<NSString *, RCTComponentData *> *componentDataByName = [bridge.uiManager valueForKey:@"_componentDataByName"]; 331 332 // Register missing components data for all view managers. 333 for (Class moduleClass in moduleClasses) { 334 NSString *className = NSStringFromClass(moduleClass); 335 336 if ([moduleClass isSubclassOfClass:[RCTViewManager class]] && !componentDataByName[className]) { 337 RCTComponentData *componentData = [RCTComponentData alloc]; 338 if ([componentData respondsToSelector:@selector(initWithManagerClass:bridge:eventDispatcher:)]) { 339 // Init method was changed in RN 0.65 340 [componentData initWithManagerClass:moduleClass bridge:bridge eventDispatcher:bridge.eventDispatcher]; 341 } else { 342 // fallback for older RNs 343 [componentData initWithManagerClass:moduleClass bridge:bridge]; 344 } 345 346 componentDataByName[className] = componentData; 347 } 348 } 349} 350 351- (void)assignExportedMethodsKeys:(NSMutableArray<NSMutableDictionary<const NSString *, id> *> *)exportedMethods forModuleName:(const NSString *)moduleName 352{ 353 if (!_exportedMethodsKeys[moduleName]) { 354 _exportedMethodsKeys[moduleName] = [NSMutableDictionary dictionary]; 355 } 356 357 if (!_exportedMethodsReverseKeys[moduleName]) { 358 _exportedMethodsReverseKeys[moduleName] = [NSMutableDictionary dictionary]; 359 } 360 361 for (int i = 0; i < [exportedMethods count]; i++) { 362 NSMutableDictionary<const NSString *, id> *methodInfo = exportedMethods[i]; 363 364 if (!methodInfo[(NSString *)methodInfoNameKey] || ![methodInfo[methodInfoNameKey] isKindOfClass:[NSString class]]) { 365 NSString *reason = [NSString stringWithFormat:@"Method info of a method of module %@ has no method name.", moduleName]; 366 @throw [NSException exceptionWithName:@"Empty method name in method info" reason:reason userInfo:nil]; 367 } 368 369 NSString *methodName = methodInfo[(NSString *)methodInfoNameKey]; 370 NSNumber *previousMethodKey = _exportedMethodsKeys[moduleName][methodName]; 371 if (previousMethodKey) { 372 methodInfo[methodInfoKeyKey] = previousMethodKey; 373 } else { 374 NSNumber *newKey = @([[_exportedMethodsKeys[moduleName] allValues] count]); 375 methodInfo[methodInfoKeyKey] = newKey; 376 _exportedMethodsKeys[moduleName][methodName] = newKey; 377 _exportedMethodsReverseKeys[moduleName][newKey] = methodName; 378 } 379 } 380} 381 382/** 383 Installs expo modules in JSI runtime. 384 */ 385- (void)installExpoTurboModules 386{ 387 facebook::jsi::Runtime *runtime = [_bridge respondsToSelector:@selector(runtime)] ? reinterpret_cast<facebook::jsi::Runtime *>(_bridge.runtime) : NULL; 388 389 if (runtime) { 390 expo::installRuntimeObjects(*runtime, _bridge.jsCallInvoker, self); 391 } 392} 393 394@end 395