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