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