// Copyright 2018-present 650 Industries. All rights reserved. #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import static const NSString *exportedMethodsNamesKeyPath = @"exportedMethods"; static const NSString *viewManagersNamesKeyPath = @"viewManagersNames"; static const NSString *exportedConstantsKeyPath = @"modulesConstants"; static const NSString *methodInfoKeyKey = @"key"; static const NSString *methodInfoNameKey = @"name"; static const NSString *methodInfoArgumentsCountKey = @"argumentsCount"; @interface RCTBridge (RegisterAdditionalModuleClasses) - (NSArray *)registerModulesForClasses:(NSArray *)moduleClasses; - (void)registerAdditionalModuleClasses:(NSArray *)modules; @end @interface RCTBridge (JSIRuntime) - (void *)runtime; @end @interface RCTComponentData (EXNativeModulesProxy) - (instancetype)initWithManagerClass:(Class)managerClass bridge:(RCTBridge *)bridge eventDispatcher:(id) eventDispatcher; // available in RN 0.65+ - (instancetype)initWithManagerClass:(Class)managerClass bridge:(RCTBridge *)bridge; @end @interface EXNativeModulesProxy () @property (nonatomic, strong) NSRegularExpression *regexp; @property (nonatomic, strong) EXModuleRegistry *exModuleRegistry; @property (nonatomic, strong) NSMutableDictionary *> *exportedMethodsKeys; @property (nonatomic, strong) NSMutableDictionary *> *exportedMethodsReverseKeys; @property (nonatomic) BOOL ownsModuleRegistry; @end @implementation EXNativeModulesProxy @synthesize bridge = _bridge; RCT_EXPORT_MODULE(NativeUnimoduleProxy) /** The designated initializer. It's used in the old setup where the native modules proxy is registered in `extraModulesForBridge:` by the bridge delegate. */ - (instancetype)initWithModuleRegistry:(nullable EXModuleRegistry *)moduleRegistry { if (self = [super init]) { _exModuleRegistry = moduleRegistry != nil ? moduleRegistry : [[EXModuleRegistryProvider new] moduleRegistry]; _swiftInteropBridge = [[SwiftInteropBridge alloc] initWithModulesProvider:[EXNativeModulesProxy getExpoModulesProvider] legacyModuleRegistry:_exModuleRegistry]; _exportedMethodsKeys = [NSMutableDictionary dictionary]; _exportedMethodsReverseKeys = [NSMutableDictionary dictionary]; _ownsModuleRegistry = moduleRegistry == nil; } return self; } /** Convenience initializer used by React Native in the new setup, where the modules are registered automatically. */ - (instancetype)init { return [self initWithModuleRegistry:nil]; } # pragma mark - React API + (BOOL)requiresMainQueueSetup { return YES; } - (NSDictionary *)constantsToExport { // Install the TurboModule implementation of the proxy. [self installExpoTurboModules]; NSMutableDictionary *exportedModulesConstants = [NSMutableDictionary dictionary]; // Grab all the constants exported by modules for (EXExportedModule *exportedModule in [_exModuleRegistry getAllExportedModules]) { @try { exportedModulesConstants[[[exportedModule class] exportedModuleName]] = [exportedModule constantsToExport] ?: [NSNull null]; } @catch (NSException *exception) { continue; } } [exportedModulesConstants addEntriesFromDictionary:[_swiftInteropBridge exportedModulesConstants]]; // Also add `exportedMethodsNames` NSMutableDictionary *> *> *exportedMethodsNamesAccumulator = [NSMutableDictionary dictionary]; for (EXExportedModule *exportedModule in [_exModuleRegistry getAllExportedModules]) { const NSString *exportedModuleName = [[exportedModule class] exportedModuleName]; exportedMethodsNamesAccumulator[exportedModuleName] = [NSMutableArray array]; [[exportedModule getExportedMethods] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull exportedName, NSString * _Nonnull selectorName, BOOL * _Nonnull stop) { NSMutableDictionary *methodInfo = [NSMutableDictionary dictionaryWithDictionary:@{ methodInfoNameKey: exportedName, // - 3 is for resolver and rejecter of the promise and the last, empty component methodInfoArgumentsCountKey: @([[selectorName componentsSeparatedByString:@":"] count] - 3) }]; [exportedMethodsNamesAccumulator[exportedModuleName] addObject:methodInfo]; }]; [self assignExportedMethodsKeys:exportedMethodsNamesAccumulator[exportedModuleName] forModuleName:exportedModuleName]; } // Add entries from Swift modules [exportedMethodsNamesAccumulator addEntriesFromDictionary:[_swiftInteropBridge exportedFunctionNames]]; // Also, add `viewManagersNames` for sanity check and testing purposes -- with names we know what managers to mock on UIManager NSArray *viewManagers = [_exModuleRegistry getAllViewManagers]; NSMutableArray *viewManagersNames = [NSMutableArray arrayWithCapacity:[viewManagers count]]; for (EXViewManager *viewManager in viewManagers) { [viewManagersNames addObject:[viewManager viewName]]; } [viewManagersNames addObjectsFromArray:[_swiftInteropBridge exportedViewManagersNames]]; NSMutableDictionary *constantsAccumulator = [NSMutableDictionary dictionary]; constantsAccumulator[viewManagersNamesKeyPath] = viewManagersNames; constantsAccumulator[exportedConstantsKeyPath] = exportedModulesConstants; constantsAccumulator[exportedMethodsNamesKeyPath] = exportedMethodsNamesAccumulator; return constantsAccumulator; } - (void)setBridge:(RCTBridge *)bridge { if (!_bridge) { [self registerExpoModulesInBridge:bridge]; } _bridge = bridge; } RCT_EXPORT_METHOD(callMethod:(NSString *)moduleName methodNameOrKey:(id)methodNameOrKey arguments:(NSArray *)arguments resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if ([_swiftInteropBridge hasModule:moduleName]) { [_swiftInteropBridge callFunction:methodNameOrKey onModule:moduleName withArgs:arguments resolve:resolve reject:reject]; return; } EXExportedModule *module = [_exModuleRegistry getExportedModuleForName:moduleName]; if (module == nil) { NSString *reason = [NSString stringWithFormat:@"No exported module was found for name '%@'. Are you sure all the packages are linked correctly?", moduleName]; reject(@"E_NO_MODULE", reason, nil); return; } if (!methodNameOrKey) { reject(@"E_NO_METHOD", @"No method key or name provided", nil); return; } NSString *methodName; if ([methodNameOrKey isKindOfClass:[NSString class]]) { methodName = (NSString *)methodNameOrKey; } else if ([methodNameOrKey isKindOfClass:[NSNumber class]]) { methodName = _exportedMethodsReverseKeys[moduleName][(NSNumber *)methodNameOrKey]; } else { reject(@"E_INV_MKEY", @"Method key is neither a String nor an Integer -- don't know how to map it to method name.", nil); return; } dispatch_async([module methodQueue], ^{ @try { [module callExportedMethod:methodName withArguments:arguments resolver:resolve rejecter:reject]; } @catch (NSException *e) { NSString *message = [NSString stringWithFormat:@"An exception was thrown while calling `%@.%@` with arguments `%@`: %@", moduleName, methodName, arguments, e]; reject(@"E_EXC", message, nil); } }); } - (id)callMethodSync:(NSString *)moduleName methodName:(NSString *)methodName arguments:(NSArray *)arguments { if ([_swiftInteropBridge hasModule:moduleName]) { return [_swiftInteropBridge callFunctionSync:methodName onModule:moduleName withArgs:arguments]; } return (id)kCFNull; } #pragma mark - Statics + (id)getExpoModulesProvider { // Dynamically gets the modules provider class. // NOTE: This needs to be versioned in Expo Go. Class generatedExpoModulesProvider; // [0] When ExpoModulesCore is built as separated framework/module, // we should explicitly load main bundle's `ExpoModulesProvider` class. NSString *bundleName = NSBundle.mainBundle.infoDictionary[@"CFBundleName"]; if (bundleName != nil) { generatedExpoModulesProvider = NSClassFromString([NSString stringWithFormat:@"%@.ExpoModulesProvider", bundleName]); if (generatedExpoModulesProvider != nil) { return [generatedExpoModulesProvider new]; } } // [1] Fallback to load `ExpoModulesProvider` class from the current module. generatedExpoModulesProvider = NSClassFromString(@"ExpoModulesProvider"); if (generatedExpoModulesProvider != nil) { return [generatedExpoModulesProvider new]; } // [2] Fallback to load `ModulesProvider` if `ExpoModulesProvider` was not generated return [ModulesProvider new]; } #pragma mark - Privates - (void)registerExpoModulesInBridge:(RCTBridge *)bridge { // Registering expo modules in bridge is needed only when the proxy module owns the registry // (was autoinitialized by React Native). Otherwise they're registered by the registry adapter. if (!_ownsModuleRegistry || [bridge moduleIsInitialized:[EXReactNativeEventEmitter class]]) { return; } // An array of `RCTBridgeModule` classes to register. NSMutableArray> *additionalModuleClasses = [NSMutableArray new]; NSMutableSet *visitedSweetModules = [NSMutableSet new]; // Event emitter is a bridge module, however it's also needed by expo modules, // so later we'll register an instance created by React Native as expo module. [additionalModuleClasses addObject:[EXReactNativeEventEmitter class]]; // Add dynamic wrappers for view modules written in Sweet API. for (ViewModuleWrapper *swiftViewModule in [_swiftInteropBridge getViewManagers]) { Class wrappedViewModuleClass = [ViewModuleWrapper createViewModuleWrapperClassWithModule:swiftViewModule]; [additionalModuleClasses addObject:wrappedViewModuleClass]; [visitedSweetModules addObject:swiftViewModule.name]; } // Add dynamic wrappers for the classic view managers. for (EXViewManager *viewManager in [_exModuleRegistry getAllViewManagers]) { if (![visitedSweetModules containsObject:viewManager.viewName]) { Class viewManagerWrapperClass = [EXViewManagerAdapterClassesRegistry createViewManagerAdapterClassForViewManager:viewManager]; [additionalModuleClasses addObject:viewManagerWrapperClass]; } } // View manager wrappers don't have their own prop configs, so we must register // their base view managers that provides common props such as `proxiedProperties`. // Otherwise, React Native may treat these props as invalid in subclassing views. [additionalModuleClasses addObject:[EXViewManagerAdapter class]]; [additionalModuleClasses addObject:[ViewModuleWrapper class]]; // Some modules might need access to the bridge. for (id module in [_exModuleRegistry getAllInternalModules]) { if ([module conformsToProtocol:@protocol(RCTBridgeModule)]) { [module setValue:bridge forKey:@"bridge"]; } } // `registerAdditionalModuleClasses:` call below is not thread-safe if RCTUIManager is not initialized. // The case happens especially with reanimated which accesses `bridge.uiManager` and initialize bridge in js thread. // Accessing uiManager here, we try to make sure RCTUIManager is initialized. [bridge uiManager]; // Register the view managers as additional modules. [self registerAdditionalModuleClasses:additionalModuleClasses inBridge:bridge]; // Bridge's `registerAdditionalModuleClasses:` method doesn't register // components in UIManager — we need to register them on our own. [self registerComponentDataForModuleClasses:additionalModuleClasses inBridge:bridge]; // Get the newly created instance of `EXReactEventEmitter` bridge module, // pass event names supported by Swift modules and register it in legacy modules registry. EXReactNativeEventEmitter *eventEmitter = [bridge moduleForClass:[EXReactNativeEventEmitter class]]; [eventEmitter setSwiftInteropBridge:_swiftInteropBridge]; [_exModuleRegistry registerInternalModule:eventEmitter]; // Let the modules consume the registry :) // It calls `setModuleRegistry:` on all `EXModuleRegistryConsumer`s. [_exModuleRegistry initialize]; } - (void)registerAdditionalModuleClasses:(NSArray *)moduleClasses inBridge:(RCTBridge *)bridge { // In remote debugging mode, i.e. executorClass is `RCTWebSocketExecutor`, // there is a deadlock issue in `registerAdditionalModuleClasses:` and causes app freezed. // - The JS thread acquired the `RCTCxxBridge._moduleRegistryLock` lock in `RCTCxxBridge._initializeBridgeLocked` // = it further goes into RCTObjcExecutor and tries to get module config from main thread // - The main thread is pending in `RCTCxxBridge.registerAdditionalModuleClasses` where trying to acquire the same lock. // To workaround the deadlock, we tend to use the non-locked registration and mutate the bridge internal module data. // Since JS thread in this situation is waiting for main thread, it's safe to mutate module data without lock. // The only risk should be the internal `_moduleRegistryCreated` flag without lock protection. // As we just workaround in `RCTWebSocketExecutor` case, the risk of `_moduleRegistryCreated` race condition should be lower. // // Learn more about the non-locked initialization: // https://github.com/facebook/react-native/blob/757bb75fbf837714725d7b2af62149e8e2a7ee51/React/CxxBridge/RCTCxxBridge.mm#L922-L935 // See the `_moduleRegistryCreated` false case if ([NSStringFromClass([bridge executorClass]) isEqualToString:@"RCTWebSocketExecutor"]) { NSNumber *moduleRegistryCreated = [bridge valueForKey:@"_moduleRegistryCreated"]; if (![moduleRegistryCreated boolValue]) { [bridge registerModulesForClasses:moduleClasses]; return; } } [bridge registerAdditionalModuleClasses:moduleClasses]; } - (void)registerComponentDataForModuleClasses:(NSArray *)moduleClasses inBridge:(RCTBridge *)bridge { // Hacky way to get a dictionary with `RCTComponentData` from UIManager. NSMutableDictionary *componentDataByName = [bridge.uiManager valueForKey:@"_componentDataByName"]; // Register missing components data for all view managers. for (Class moduleClass in moduleClasses) { NSString *className = NSStringFromClass(moduleClass); if ([moduleClass isSubclassOfClass:[RCTViewManager class]] && !componentDataByName[className]) { RCTComponentData *componentData = [RCTComponentData alloc]; if ([componentData respondsToSelector:@selector(initWithManagerClass:bridge:eventDispatcher:)]) { // Init method was changed in RN 0.65 [componentData initWithManagerClass:moduleClass bridge:bridge eventDispatcher:bridge.eventDispatcher]; } else { // fallback for older RNs [componentData initWithManagerClass:moduleClass bridge:bridge]; } componentDataByName[className] = componentData; } } } - (void)assignExportedMethodsKeys:(NSMutableArray *> *)exportedMethods forModuleName:(const NSString *)moduleName { if (!_exportedMethodsKeys[moduleName]) { _exportedMethodsKeys[moduleName] = [NSMutableDictionary dictionary]; } if (!_exportedMethodsReverseKeys[moduleName]) { _exportedMethodsReverseKeys[moduleName] = [NSMutableDictionary dictionary]; } for (int i = 0; i < [exportedMethods count]; i++) { NSMutableDictionary *methodInfo = exportedMethods[i]; if (!methodInfo[(NSString *)methodInfoNameKey] || ![methodInfo[methodInfoNameKey] isKindOfClass:[NSString class]]) { NSString *reason = [NSString stringWithFormat:@"Method info of a method of module %@ has no method name.", moduleName]; @throw [NSException exceptionWithName:@"Empty method name in method info" reason:reason userInfo:nil]; } NSString *methodName = methodInfo[(NSString *)methodInfoNameKey]; NSNumber *previousMethodKey = _exportedMethodsKeys[moduleName][methodName]; if (previousMethodKey) { methodInfo[methodInfoKeyKey] = previousMethodKey; } else { NSNumber *newKey = @([[_exportedMethodsKeys[moduleName] allValues] count]); methodInfo[methodInfoKeyKey] = newKey; _exportedMethodsKeys[moduleName][methodName] = newKey; _exportedMethodsReverseKeys[moduleName][newKey] = methodName; } } } /** Installs expo modules in JSI runtime. */ - (void)installExpoTurboModules { facebook::jsi::Runtime *runtime = [_bridge respondsToSelector:@selector(runtime)] ? reinterpret_cast(_bridge.runtime) : NULL; if (runtime) { expo::installRuntimeObjects(*runtime, _bridge.jsCallInvoker, self); } } @end