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    // Add dynamic wrappers for the classic view managers.
293    for (EXViewManager *viewManager in [_exModuleRegistry getAllViewManagers]) {
294      if (![visitedSweetModules containsObject:viewManager.viewName]) {
295        Class viewManagerWrapperClass = [EXViewManagerAdapterClassesRegistry createViewManagerAdapterClassForViewManager:viewManager];
296        [additionalModuleClasses addObject:viewManagerWrapperClass];
297        [self registerLegacyComponentData:viewManagerWrapperClass inBridge:bridge];
298      }
299    }
300
301    // View manager wrappers don't have their own prop configs, so we must register
302    // their base view managers that provides common props such as `proxiedProperties`.
303    // Otherwise, React Native may treat these props as invalid in subclassing views.
304    [additionalModuleClasses addObject:[EXViewManagerAdapter class]];
305    // Also, we have to register component data for the View Adapter.
306    // Otherwise, it won't be recognized by the UIManager.
307    [self registerLegacyComponentData:[EXViewManagerAdapter class] inBridge:bridge];
308
309    // Some modules might need access to the bridge.
310    for (id module in [_exModuleRegistry getAllInternalModules]) {
311      if ([module conformsToProtocol:@protocol(RCTBridgeModule)]) {
312        [module setValue:bridge forKey:@"bridge"];
313      }
314    }
315  }
316
317  // `registerAdditionalModuleClasses:` call below is not thread-safe if RCTUIManager is not initialized.
318  // The case happens especially with reanimated which accesses `bridge.uiManager` and initialize bridge in js thread.
319  // Accessing uiManager here, we try to make sure RCTUIManager is initialized.
320  [bridge uiManager];
321
322  // Register the view managers as additional modules.
323  [self registerAdditionalModuleClasses:additionalModuleClasses inBridge:bridge];
324
325  // Get the instance of `EXReactEventEmitter` bridge module and give it access to the interop bridge.
326  EXReactNativeEventEmitter *eventEmitter = [bridge moduleForClass:[EXReactNativeEventEmitter class]];
327  [eventEmitter setAppContext:_appContext];
328
329  // As the last step, when the registry is owned,
330  // register the event emitter and initialize the registry.
331  if (ownsModuleRegistry) {
332    [_exModuleRegistry registerInternalModule:eventEmitter];
333
334    // Let the modules consume the registry :)
335    // It calls `setModuleRegistry:` on all `EXModuleRegistryConsumer`s.
336    [_exModuleRegistry initialize];
337  }
338}
339
340- (void)registerAdditionalModuleClasses:(NSArray<Class> *)moduleClasses inBridge:(RCTBridge *)bridge
341{
342  // In remote debugging mode, i.e. executorClass is `RCTWebSocketExecutor`,
343  // there is a deadlock issue in `registerAdditionalModuleClasses:` and causes app freezed.
344  //   - The JS thread acquired the `RCTCxxBridge._moduleRegistryLock` lock in `RCTCxxBridge._initializeBridgeLocked`
345  //      = it further goes into RCTObjcExecutor and tries to get module config from main thread
346  //   - The main thread is pending in `RCTCxxBridge.registerAdditionalModuleClasses` where trying to acquire the same lock.
347  // To workaround the deadlock, we tend to use the non-locked registration and mutate the bridge internal module data.
348  // Since JS thread in this situation is waiting for main thread, it's safe to mutate module data without lock.
349  // The only risk should be the internal `_moduleRegistryCreated` flag without lock protection.
350  // As we just workaround in `RCTWebSocketExecutor` case, the risk of `_moduleRegistryCreated` race condition should be lower.
351  //
352  // Learn more about the non-locked initialization:
353  // https://github.com/facebook/react-native/blob/757bb75fbf837714725d7b2af62149e8e2a7ee51/React/CxxBridge/RCTCxxBridge.mm#L922-L935
354  // See the `_moduleRegistryCreated` false case
355  if ([NSStringFromClass([bridge executorClass]) isEqualToString:@"RCTWebSocketExecutor"]) {
356    NSNumber *moduleRegistryCreated = [bridge valueForKey:@"_moduleRegistryCreated"];
357    if (![moduleRegistryCreated boolValue]) {
358      [bridge registerModulesForClasses:moduleClasses];
359      return;
360    }
361  }
362
363  [bridge registerAdditionalModuleClasses:moduleClasses];
364}
365
366- (Class)registerComponentData:(ViewModuleWrapper *)viewModule inBridge:(RCTBridge *)bridge
367{
368  // Hacky way to get a dictionary with `RCTComponentData` from UIManager.
369  NSMutableDictionary<NSString *, RCTComponentData *> *componentDataByName = [bridge.uiManager valueForKey:@"_componentDataByName"];
370  Class wrappedViewModuleClass = [ViewModuleWrapper createViewModuleWrapperClassWithModule:viewModule];
371  NSString *className = NSStringFromClass(wrappedViewModuleClass);
372
373  if (componentDataByName[className]) {
374    // Just in case the component was already registered, let's leave a log that we're overriding it.
375    NSLog(@"Overriding ComponentData for view %@", className);
376  }
377
378  EXComponentData *componentData = [[EXComponentData alloc] initWithViewModule:viewModule
379                                                                  managerClass:wrappedViewModuleClass
380                                                                        bridge:bridge];
381  componentDataByName[className] = componentData;
382
383#ifdef RN_FABRIC_ENABLED
384  Class viewClass = [ExpoFabricView makeViewClassForAppContext:_appContext className:className];
385  [[RCTComponentViewFactory currentComponentViewFactory] registerComponentViewClass:viewClass];
386#endif
387
388  return wrappedViewModuleClass;
389}
390
391/**
392 Bridge's `registerAdditionalModuleClasses:` method doesn't register
393 components in UIManager — we need to register them on our own.
394 */
395- (void)registerLegacyComponentData:(Class)moduleClass inBridge:(RCTBridge *)bridge
396{
397  // Hacky way to get a dictionary with `RCTComponentData` from UIManager.
398  NSMutableDictionary<NSString *, RCTComponentData *> *componentDataByName = [bridge.uiManager valueForKey:@"_componentDataByName"];
399  NSString *className = [moduleClass moduleName] ?: NSStringFromClass(moduleClass);
400
401  if ([moduleClass isSubclassOfClass:[RCTViewManager class]] && !componentDataByName[className]) {
402    RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass bridge:bridge eventDispatcher:bridge.eventDispatcher];
403    componentDataByName[className] = componentData;
404  }
405}
406
407- (void)assignExportedMethodsKeys:(NSMutableArray<NSMutableDictionary<const NSString *, id> *> *)exportedMethods forModuleName:(const NSString *)moduleName
408{
409  if (!_exportedMethodsKeys[moduleName]) {
410    _exportedMethodsKeys[moduleName] = [NSMutableDictionary dictionary];
411  }
412
413  if (!_exportedMethodsReverseKeys[moduleName]) {
414    _exportedMethodsReverseKeys[moduleName] = [NSMutableDictionary dictionary];
415  }
416
417  for (int i = 0; i < [exportedMethods count]; i++) {
418    NSMutableDictionary<const NSString *, id> *methodInfo = exportedMethods[i];
419
420    if (!methodInfo[(NSString *)methodInfoNameKey] || ![methodInfo[methodInfoNameKey] isKindOfClass:[NSString class]]) {
421      NSString *reason = [NSString stringWithFormat:@"Method info of a method of module %@ has no method name.", moduleName];
422      @throw [NSException exceptionWithName:@"Empty method name in method info" reason:reason userInfo:nil];
423    }
424
425    NSString *methodName = methodInfo[(NSString *)methodInfoNameKey];
426    NSNumber *previousMethodKey = _exportedMethodsKeys[moduleName][methodName];
427    if (previousMethodKey) {
428      methodInfo[methodInfoKeyKey] = previousMethodKey;
429    } else {
430      NSNumber *newKey = @([[_exportedMethodsKeys[moduleName] allValues] count]);
431      methodInfo[methodInfoKeyKey] = newKey;
432      _exportedMethodsKeys[moduleName][methodName] = newKey;
433      _exportedMethodsReverseKeys[moduleName][newKey] = methodName;
434    }
435  }
436}
437
438@end
439