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