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