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#ifdef RN_FABRIC_ENABLED 12#import <React/RCTComponentViewFactory.h> 13#endif 14 15#import <jsi/jsi.h> 16 17#import <ExpoModulesCore/EXNativeModulesProxy.h> 18#import <ExpoModulesCore/EXEventEmitter.h> 19#import <ExpoModulesCore/EXViewManager.h> 20#import <ExpoModulesCore/EXViewManagerAdapter.h> 21#import <ExpoModulesCore/EXViewManagerAdapterClassesRegistry.h> 22#import <ExpoModulesCore/EXModuleRegistryProvider.h> 23#import <ExpoModulesCore/EXReactNativeEventEmitter.h> 24#import <ExpoModulesCore/EXJSIInstaller.h> 25#import <ExpoModulesCore/Swift.h> 26 27static const NSString *exportedMethodsNamesKeyPath = @"exportedMethods"; 28static const NSString *viewManagersMetadataKeyPath = @"viewManagersMetadata"; 29static const NSString *exportedConstantsKeyPath = @"modulesConstants"; 30 31static const NSString *methodInfoKeyKey = @"key"; 32static const NSString *methodInfoNameKey = @"name"; 33static const NSString *methodInfoArgumentsCountKey = @"argumentsCount"; 34 35@interface EXModulesProxyConfig () 36 37@property (readonly) NSMutableDictionary *exportedConstants; 38@property (readonly) NSMutableDictionary *methodNames; 39@property (readonly) NSMutableDictionary *viewManagerMetadata; 40 41@end 42 43@implementation EXModulesProxyConfig 44 45- (instancetype)initWithConstants:(nonnull NSDictionary *)constants 46 methodNames:(nonnull NSDictionary *)methodNames 47 viewManagers:(nonnull NSDictionary *)viewManagerMetadata 48{ 49 if (self = [super init]) { 50 _exportedConstants = constants; 51 _methodNames = methodNames; 52 _viewManagerMetadata = viewManagerMetadata; 53 } 54 return self; 55} 56 57- (void)addEntriesFromConfig:(nonnull const EXModulesProxyConfig*)config 58{ 59 [_exportedConstants addEntriesFromDictionary:config.exportedConstants]; 60 [_methodNames addEntriesFromDictionary:config.methodNames]; 61 [_viewManagerMetadata addEntriesFromDictionary:config.viewManagerMetadata]; 62} 63 64- (nonnull NSDictionary<NSString *, id> *)toDictionary 65{ 66 NSMutableDictionary <NSString *, id> *constantsAccumulator = [NSMutableDictionary dictionary]; 67 constantsAccumulator[viewManagersMetadataKeyPath] = _viewManagerMetadata; 68 constantsAccumulator[exportedConstantsKeyPath] = _exportedConstants; 69 constantsAccumulator[exportedMethodsNamesKeyPath] = _methodNames; 70 return constantsAccumulator; 71} 72 73@end 74 75@interface RCTBridge (RegisterAdditionalModuleClasses) 76 77- (NSArray<RCTModuleData *> *)registerModulesForClasses:(NSArray<Class> *)moduleClasses; 78- (void)registerAdditionalModuleClasses:(NSArray<Class> *)modules; 79 80@end 81 82@interface RCTBridge (JSIRuntime) 83 84- (void *)runtime; 85 86@end 87 88@interface EXNativeModulesProxy () 89 90@property (nonatomic, strong) NSRegularExpression *regexp; 91@property (nonatomic, strong) EXModuleRegistry *exModuleRegistry; 92@property (nonatomic, strong) NSMutableDictionary<const NSString *, NSMutableDictionary<NSString *, NSNumber *> *> *exportedMethodsKeys; 93@property (nonatomic, strong) NSMutableDictionary<const NSString *, NSMutableDictionary<NSNumber *, NSString *> *> *exportedMethodsReverseKeys; 94@property (nonatomic) BOOL ownsModuleRegistry; 95 96@end 97 98@implementation EXNativeModulesProxy { 99 __weak EXAppContext * _Nullable _appContext; 100} 101 102@synthesize bridge = _bridge; 103@synthesize nativeModulesConfig = _nativeModulesConfig; 104 105RCT_EXPORT_MODULE(NativeUnimoduleProxy) 106 107/** 108 The designated initializer. It's used in the old setup where the native modules proxy 109 is registered in `extraModulesForBridge:` by the bridge delegate. 110 */ 111- (instancetype)initWithModuleRegistry:(nullable EXModuleRegistry *)moduleRegistry 112{ 113 if (self = [super init]) { 114 _exModuleRegistry = moduleRegistry != nil ? moduleRegistry : [[EXModuleRegistryProvider new] moduleRegistry]; 115 _exportedMethodsKeys = [NSMutableDictionary dictionary]; 116 _exportedMethodsReverseKeys = [NSMutableDictionary dictionary]; 117 _ownsModuleRegistry = moduleRegistry == nil; 118 } 119 return self; 120} 121 122/** 123 The initializer for Expo Go to pass a custom `EXModuleRegistry` 124 other than the default one from `EXModuleRegistryProvider`. 125 The `EXModuleRegistry` is still owned by this class. 126 */ 127- (instancetype)initWithCustomModuleRegistry:(nonnull EXModuleRegistry *)moduleRegistry 128{ 129 self = [self initWithModuleRegistry:moduleRegistry]; 130 self.ownsModuleRegistry = YES; 131 return self; 132} 133 134/** 135 Convenience initializer used by React Native in the new setup, where the modules are registered automatically. 136 */ 137- (instancetype)init 138{ 139 return [self initWithModuleRegistry:nil]; 140} 141 142# pragma mark - React API 143 144+ (BOOL)requiresMainQueueSetup 145{ 146 return YES; 147} 148 149- (nonnull EXModulesProxyConfig *)nativeModulesConfig 150{ 151 if (_nativeModulesConfig) { 152 return _nativeModulesConfig; 153 } 154 155 NSMutableDictionary <NSString *, id> *exportedModulesConstants = [NSMutableDictionary dictionary]; 156 // Grab all the constants exported by modules 157 for (EXExportedModule *exportedModule in [_exModuleRegistry getAllExportedModules]) { 158 @try { 159 exportedModulesConstants[[[exportedModule class] exportedModuleName]] = [exportedModule constantsToExport] ?: [NSNull null]; 160 } @catch (NSException *exception) { 161 continue; 162 } 163 } 164 165 // Also add `exportedMethodsNames` 166 NSMutableDictionary<const NSString *, NSMutableArray<NSMutableDictionary<const NSString *, id> *> *> *exportedMethodsNamesAccumulator = [NSMutableDictionary dictionary]; 167 for (EXExportedModule *exportedModule in [_exModuleRegistry getAllExportedModules]) { 168 const NSString *exportedModuleName = [[exportedModule class] exportedModuleName]; 169 exportedMethodsNamesAccumulator[exportedModuleName] = [NSMutableArray array]; 170 [[exportedModule getExportedMethods] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull exportedName, NSString * _Nonnull selectorName, BOOL * _Nonnull stop) { 171 NSMutableDictionary<const NSString *, id> *methodInfo = [NSMutableDictionary dictionaryWithDictionary:@{ 172 methodInfoNameKey: exportedName, 173 // - 3 is for resolver and rejecter of the promise and the last, empty component 174 methodInfoArgumentsCountKey: @([[selectorName componentsSeparatedByString:@":"] count] - 3) 175 }]; 176 [exportedMethodsNamesAccumulator[exportedModuleName] addObject:methodInfo]; 177 }]; 178 [self assignExportedMethodsKeys:exportedMethodsNamesAccumulator[exportedModuleName] forModuleName:exportedModuleName]; 179 } 180 181 // Also, add `viewManagersMetadata` for sanity check and testing purposes -- with names we know what managers to mock on UIManager 182 NSArray<EXViewManager *> *viewManagers = [_exModuleRegistry getAllViewManagers]; 183 NSMutableDictionary<NSString *, NSDictionary *> *viewManagersMetadata = [[NSMutableDictionary alloc] initWithCapacity:[viewManagers count]]; 184 185 for (EXViewManager *viewManager in viewManagers) { 186 viewManagersMetadata[viewManager.viewName] = @{ 187 @"propsNames": [[viewManager getPropsNames] allKeys] 188 }; 189 } 190 191 EXModulesProxyConfig *config = [[EXModulesProxyConfig alloc] initWithConstants:exportedModulesConstants 192 methodNames:exportedMethodsNamesAccumulator 193 viewManagers:viewManagersMetadata]; 194 // decorate legacy config with sweet expo-modules config 195 [config addEntriesFromConfig:[_appContext expoModulesConfig]]; 196 197 _nativeModulesConfig = config; 198 return config; 199} 200 201- (nonnull NSDictionary *)constantsToExport 202{ 203 return [self.nativeModulesConfig toDictionary]; 204} 205 206- (void)setBridge:(RCTBridge *)bridge 207{ 208 ExpoBridgeModule* expoBridgeModule = [bridge moduleForClass:ExpoBridgeModule.class]; 209 [expoBridgeModule legacyProxyDidSetBridgeWithLegacyModulesProxy:self 210 legacyModuleRegistry:_exModuleRegistry]; 211 _appContext = [expoBridgeModule appContext]; 212 213 if (!_bridge) { 214 // The `setBridge` can be called during module setup or after. Registering more modules 215 // during setup causes a crash due to mutating `_moduleDataByID` while it's being enumerated. 216 // In that case we register them asynchronously. 217 if ([[bridge valueForKey:@"_moduleSetupComplete"] boolValue]) { 218 [self registerExpoModulesInBridge:bridge]; 219 } else { 220 dispatch_async(dispatch_get_main_queue(), ^{ 221 [self registerExpoModulesInBridge:bridge]; 222 }); 223 } 224 } 225 _bridge = bridge; 226} 227 228RCT_EXPORT_METHOD(callMethod:(NSString *)moduleName methodNameOrKey:(id)methodNameOrKey arguments:(NSArray *)arguments resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 229{ 230 // Backwards compatibility for the new architecture 231 if ([_appContext hasModule:moduleName]) { 232 [_appContext callFunction:methodNameOrKey onModule:moduleName withArgs:arguments resolve:resolve reject:reject]; 233 return; 234 } 235 236 EXExportedModule *module = [_exModuleRegistry getExportedModuleForName:moduleName]; 237 if (module == nil) { 238 NSString *reason = [NSString stringWithFormat:@"No exported module was found for name '%@'. Are you sure all the packages are linked correctly?", moduleName]; 239 reject(@"E_NO_MODULE", reason, nil); 240 return; 241 } 242 243 if (!methodNameOrKey) { 244 reject(@"E_NO_METHOD", @"No method key or name provided", nil); 245 return; 246 } 247 248 NSString *methodName; 249 if ([methodNameOrKey isKindOfClass:[NSString class]]) { 250 methodName = (NSString *)methodNameOrKey; 251 } else if ([methodNameOrKey isKindOfClass:[NSNumber class]]) { 252 methodName = _exportedMethodsReverseKeys[moduleName][(NSNumber *)methodNameOrKey]; 253 } else { 254 reject(@"E_INV_MKEY", @"Method key is neither a String nor an Integer -- don't know how to map it to method name.", nil); 255 return; 256 } 257 258 dispatch_async([module methodQueue], ^{ 259 @try { 260 [module callExportedMethod:methodName withArguments:arguments resolver:resolve rejecter:reject]; 261 } @catch (NSException *e) { 262 NSString *message = [NSString stringWithFormat:@"An exception was thrown while calling `%@.%@` with arguments `%@`: %@", moduleName, methodName, arguments, e]; 263 reject(@"E_EXC", message, nil); 264 } 265 }); 266} 267 268#pragma mark - Privates 269 270- (void)registerExpoModulesInBridge:(RCTBridge *)bridge 271{ 272 // Registering expo modules (excluding Swifty view managers!) in bridge is needed only when the proxy module owns 273 // the registry (was autoinitialized by React Native). Otherwise they're registered by the registry adapter. 274 BOOL ownsModuleRegistry = _ownsModuleRegistry && ![bridge moduleIsInitialized:[EXReactNativeEventEmitter class]]; 275 276 // An array of `RCTBridgeModule` classes to register. 277 NSMutableArray<Class<RCTBridgeModule>> *additionalModuleClasses = [NSMutableArray new]; 278 NSMutableSet *visitedSweetModules = [NSMutableSet new]; 279 280 // Add dynamic wrappers for view modules written in Sweet API. 281 for (ViewModuleWrapper *swiftViewModule in [_appContext getViewManagers]) { 282 Class wrappedViewModuleClass = [self registerComponentData:swiftViewModule inBridge:bridge]; 283 [additionalModuleClasses addObject:wrappedViewModuleClass]; 284 [visitedSweetModules addObject:swiftViewModule.name]; 285 } 286 287 [additionalModuleClasses addObject:[ViewModuleWrapper class]]; 288 [self registerLegacyComponentData:[ViewModuleWrapper class] inBridge:bridge]; 289 290 // Add modules from legacy module registry only when the NativeModulesProxy owns the registry. 291 if (ownsModuleRegistry) { 292 // Event emitter is a bridge module, however it's also needed by expo modules, 293 // so later we'll register an instance created by React Native as expo module. 294 [additionalModuleClasses addObject:[EXReactNativeEventEmitter class]]; 295 296 // Add dynamic wrappers for the classic view managers. 297 for (EXViewManager *viewManager in [_exModuleRegistry getAllViewManagers]) { 298 if (![visitedSweetModules containsObject:viewManager.viewName]) { 299 Class viewManagerWrapperClass = [EXViewManagerAdapterClassesRegistry createViewManagerAdapterClassForViewManager:viewManager]; 300 [additionalModuleClasses addObject:viewManagerWrapperClass]; 301 [self registerLegacyComponentData:viewManagerWrapperClass inBridge:bridge]; 302 } 303 } 304 305 // View manager wrappers don't have their own prop configs, so we must register 306 // their base view managers that provides common props such as `proxiedProperties`. 307 // Otherwise, React Native may treat these props as invalid in subclassing views. 308 [additionalModuleClasses addObject:[EXViewManagerAdapter class]]; 309 // Also, we have to register component data for the View Adapter. 310 // Otherwise, it won't be recognized by the UIManager. 311 [self registerLegacyComponentData:[EXViewManagerAdapter class] inBridge:bridge]; 312 313 // Some modules might need access to the bridge. 314 for (id module in [_exModuleRegistry getAllInternalModules]) { 315 if ([module conformsToProtocol:@protocol(RCTBridgeModule)]) { 316 [module setValue:bridge forKey:@"bridge"]; 317 } 318 } 319 } 320 321 // `registerAdditionalModuleClasses:` call below is not thread-safe if RCTUIManager is not initialized. 322 // The case happens especially with reanimated which accesses `bridge.uiManager` and initialize bridge in js thread. 323 // Accessing uiManager here, we try to make sure RCTUIManager is initialized. 324 [bridge uiManager]; 325 326 // Register the view managers as additional modules. 327 [self registerAdditionalModuleClasses:additionalModuleClasses inBridge:bridge]; 328 329 // Get the instance of `EXReactEventEmitter` bridge module and give it access to the interop bridge. 330 EXReactNativeEventEmitter *eventEmitter = [bridge moduleForClass:[EXReactNativeEventEmitter class]]; 331 [eventEmitter setAppContext:_appContext]; 332 333 // As the last step, when the registry is owned, 334 // register the event emitter and initialize the registry. 335 if (ownsModuleRegistry) { 336 [_exModuleRegistry registerInternalModule:eventEmitter]; 337 338 // Let the modules consume the registry :) 339 // It calls `setModuleRegistry:` on all `EXModuleRegistryConsumer`s. 340 [_exModuleRegistry initialize]; 341 } 342} 343 344- (void)registerAdditionalModuleClasses:(NSArray<Class> *)moduleClasses inBridge:(RCTBridge *)bridge 345{ 346 // In remote debugging mode, i.e. executorClass is `RCTWebSocketExecutor`, 347 // there is a deadlock issue in `registerAdditionalModuleClasses:` and causes app freezed. 348 // - The JS thread acquired the `RCTCxxBridge._moduleRegistryLock` lock in `RCTCxxBridge._initializeBridgeLocked` 349 // = it further goes into RCTObjcExecutor and tries to get module config from main thread 350 // - The main thread is pending in `RCTCxxBridge.registerAdditionalModuleClasses` where trying to acquire the same lock. 351 // To workaround the deadlock, we tend to use the non-locked registration and mutate the bridge internal module data. 352 // Since JS thread in this situation is waiting for main thread, it's safe to mutate module data without lock. 353 // The only risk should be the internal `_moduleRegistryCreated` flag without lock protection. 354 // As we just workaround in `RCTWebSocketExecutor` case, the risk of `_moduleRegistryCreated` race condition should be lower. 355 // 356 // Learn more about the non-locked initialization: 357 // https://github.com/facebook/react-native/blob/757bb75fbf837714725d7b2af62149e8e2a7ee51/React/CxxBridge/RCTCxxBridge.mm#L922-L935 358 // See the `_moduleRegistryCreated` false case 359 if ([NSStringFromClass([bridge executorClass]) isEqualToString:@"RCTWebSocketExecutor"]) { 360 NSNumber *moduleRegistryCreated = [bridge valueForKey:@"_moduleRegistryCreated"]; 361 if (![moduleRegistryCreated boolValue]) { 362 [bridge registerModulesForClasses:moduleClasses]; 363 return; 364 } 365 } 366 367 [bridge registerAdditionalModuleClasses:moduleClasses]; 368} 369 370- (Class)registerComponentData:(ViewModuleWrapper *)viewModule inBridge:(RCTBridge *)bridge 371{ 372 // Hacky way to get a dictionary with `RCTComponentData` from UIManager. 373 NSMutableDictionary<NSString *, RCTComponentData *> *componentDataByName = [bridge.uiManager valueForKey:@"_componentDataByName"]; 374 Class wrappedViewModuleClass = [ViewModuleWrapper createViewModuleWrapperClassWithModule:viewModule]; 375 NSString *className = NSStringFromClass(wrappedViewModuleClass); 376 377 if (componentDataByName[className]) { 378 // Just in case the component was already registered, let's leave a log that we're overriding it. 379 NSLog(@"Overriding ComponentData for view %@", className); 380 } 381 382 EXComponentData *componentData = [[EXComponentData alloc] initWithViewModule:viewModule 383 managerClass:wrappedViewModuleClass 384 bridge:bridge]; 385 componentDataByName[className] = componentData; 386 387#ifdef RN_FABRIC_ENABLED 388 Class viewClass = [ExpoFabricView makeClassForAppContext:_appContext className:className]; 389 [[RCTComponentViewFactory currentComponentViewFactory] registerComponentViewClass:viewClass]; 390#endif 391 392 return wrappedViewModuleClass; 393} 394 395/** 396 Bridge's `registerAdditionalModuleClasses:` method doesn't register 397 components in UIManager — we need to register them on our own. 398 */ 399- (void)registerLegacyComponentData:(Class)moduleClass inBridge:(RCTBridge *)bridge 400{ 401 // Hacky way to get a dictionary with `RCTComponentData` from UIManager. 402 NSMutableDictionary<NSString *, RCTComponentData *> *componentDataByName = [bridge.uiManager valueForKey:@"_componentDataByName"]; 403 NSString *className = [moduleClass moduleName] ?: NSStringFromClass(moduleClass); 404 405 if ([moduleClass isSubclassOfClass:[RCTViewManager class]] && !componentDataByName[className]) { 406 RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass bridge:bridge eventDispatcher:bridge.eventDispatcher]; 407 componentDataByName[className] = componentData; 408 } 409 410#ifdef RN_FABRIC_ENABLED 411 if ([className hasPrefix:@"ViewManagerAdapter_"]) { 412 Class viewClass = [ExpoFabricView makeClassForAppContext:_appContext className:className]; 413 [[RCTComponentViewFactory currentComponentViewFactory] registerComponentViewClass:viewClass]; 414 } 415#endif 416} 417 418- (void)assignExportedMethodsKeys:(NSMutableArray<NSMutableDictionary<const NSString *, id> *> *)exportedMethods forModuleName:(const NSString *)moduleName 419{ 420 if (!_exportedMethodsKeys[moduleName]) { 421 _exportedMethodsKeys[moduleName] = [NSMutableDictionary dictionary]; 422 } 423 424 if (!_exportedMethodsReverseKeys[moduleName]) { 425 _exportedMethodsReverseKeys[moduleName] = [NSMutableDictionary dictionary]; 426 } 427 428 for (int i = 0; i < [exportedMethods count]; i++) { 429 NSMutableDictionary<const NSString *, id> *methodInfo = exportedMethods[i]; 430 431 if (!methodInfo[(NSString *)methodInfoNameKey] || ![methodInfo[methodInfoNameKey] isKindOfClass:[NSString class]]) { 432 NSString *reason = [NSString stringWithFormat:@"Method info of a method of module %@ has no method name.", moduleName]; 433 @throw [NSException exceptionWithName:@"Empty method name in method info" reason:reason userInfo:nil]; 434 } 435 436 NSString *methodName = methodInfo[(NSString *)methodInfoNameKey]; 437 NSNumber *previousMethodKey = _exportedMethodsKeys[moduleName][methodName]; 438 if (previousMethodKey) { 439 methodInfo[methodInfoKeyKey] = previousMethodKey; 440 } else { 441 NSNumber *newKey = @([[_exportedMethodsKeys[moduleName] allValues] count]); 442 methodInfo[methodInfoKeyKey] = newKey; 443 _exportedMethodsKeys[moduleName][methodName] = newKey; 444 _exportedMethodsReverseKeys[moduleName][newKey] = methodName; 445 } 446 } 447} 448 449@end 450