diff --git a/packages/core/platforms/ios/lib_common/RCTTurboModule.h b/packages/core/platforms/ios/lib_common/RCTTurboModule.h deleted file mode 100644 index ab041268..00000000 --- a/packages/core/platforms/ios/lib_common/RCTTurboModule.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import -#import "CallInvoker.h" -#import - -#define RCT_IS_TURBO_MODULE_CLASS(klass) \ - ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTTurboModule)])) -#define RCT_IS_TURBO_MODULE_INSTANCE(module) RCT_IS_TURBO_MODULE_CLASS([(module) class]) - -namespace facebook { -namespace react { - -class CallbackWrapper {}; - -typedef std::weak_ptr ( -^RCTRetainJSCallback)(jsi::Function &&callback, jsi::Runtime &runtime, std::shared_ptr jsInvoker); - -class TurboModule {}; -/** - * ObjC++ specific TurboModule base class. - */ -class JSI_EXPORT ObjCTurboModule : public TurboModule { -public: - struct InitParams { - std::string moduleName; - id instance; - std::shared_ptr jsInvoker; - std::shared_ptr nativeInvoker; - bool isSyncModule; - RCTRetainJSCallback retainJSCallback; - }; -}; - -} // namespace react -} // namespace facebook - -@protocol RCTTurboModule -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params; -@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeConstants.h b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeConstants.h new file mode 100644 index 00000000..13a1d0e9 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeConstants.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + * RCTBridgeConstants are constants that are only used in the legacy architecture. + * Please place constants used in the new architecture into RCTConstants. + */ +/** + * DEPRECATED - Use RCTReloadCommand instead. This notification fires just before the bridge starts + * processing a request to reload. + */ +RCT_EXTERN NSString *const RCTBridgeWillReloadNotification; + +/** + * This notification fires whenever a fast refresh happens. + */ +RCT_EXTERN NSString *const RCTBridgeFastRefreshNotification; + +/** + * This notification fires just before the bridge begins downloading a script + * from the packager. + */ +RCT_EXTERN NSString *const RCTBridgeWillDownloadScriptNotification; + +/** + * This notification fires just after the bridge finishes downloading a script + * from the packager. + */ +RCT_EXTERN NSString *const RCTBridgeDidDownloadScriptNotification; + +/** + * This notification fires right after the bridge is about to invalidate NativeModule + * instances during teardown. Handle this notification to perform additional invalidation. + */ +RCT_EXTERN NSString *const RCTBridgeWillInvalidateModulesNotification; + +/** + * This notification fires right after the bridge finishes invalidating NativeModule + * instances during teardown. Handle this notification to perform additional invalidation. + */ +RCT_EXTERN NSString *const RCTBridgeDidInvalidateModulesNotification; + +/** + * This notification fires right before the bridge starting invalidation process. + * Handle this notification to perform additional invalidation. + * The notification can be issued on any thread. + */ +RCT_EXTERN NSString *const RCTBridgeWillBeInvalidatedNotification; + +/** + * Key for the RCTSource object in the RCTBridgeDidDownloadScriptNotification + * userInfo dictionary. + */ +RCT_EXTERN NSString *const RCTBridgeDidDownloadScriptNotificationSourceKey; + +/** + * Key for the reload reason in the RCTBridgeWillReloadNotification userInfo dictionary. + */ +RCT_EXTERN NSString *const RCTBridgeDidDownloadScriptNotificationReasonKey; + +/** + * Key for the bridge description (NSString_ in the + * RCTBridgeDidDownloadScriptNotification userInfo dictionary. + */ +RCT_EXTERN NSString *const RCTBridgeDidDownloadScriptNotificationBridgeDescriptionKey; diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeConstants.m b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeConstants.m new file mode 100644 index 00000000..18080e0a --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeConstants.m @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTBridgeConstants.h" + +NSString *const RCTBridgeWillReloadNotification = @"RCTBridgeWillReloadNotification"; +NSString *const RCTBridgeFastRefreshNotification = @"RCTBridgeFastRefreshNotification"; +NSString *const RCTBridgeWillDownloadScriptNotification = @"RCTBridgeWillDownloadScriptNotification"; +NSString *const RCTBridgeDidDownloadScriptNotification = @"RCTBridgeDidDownloadScriptNotification"; +NSString *const RCTBridgeWillInvalidateModulesNotification = @"RCTBridgeWillInvalidateModulesNotification"; +NSString *const RCTBridgeDidInvalidateModulesNotification = @"RCTBridgeDidInvalidateModulesNotification"; +NSString *const RCTBridgeWillBeInvalidatedNotification = @"RCTBridgeWillBeInvalidatedNotification"; +NSString *const RCTBridgeDidDownloadScriptNotificationSourceKey = @"source"; +NSString *const RCTBridgeDidDownloadScriptNotificationBridgeDescriptionKey = @"bridgeDescription"; diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeMethod.h b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeMethod.h new file mode 100644 index 00000000..4311904b --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeMethod.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class RCTBridge; + +typedef NS_ENUM(NSInteger, RCTFunctionType) { + RCTFunctionTypeNormal, + RCTFunctionTypePromise, + RCTFunctionTypeSync, +}; + +static inline const char *RCTFunctionDescriptorFromType(RCTFunctionType type) +{ + switch (type) { + case RCTFunctionTypePromise: + return "promise"; + case RCTFunctionTypeSync: + return "sync"; + case RCTFunctionTypeNormal: + default: + return "async"; + } +} + +@protocol RCTBridgeMethod + +@property (nonatomic, readonly) const char *JSMethodName; +@property (nonatomic, readonly) RCTFunctionType functionType; + +- (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments; + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModule.h b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModule.h index 46231ea5..7c44265e 100644 --- a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModule.h +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModule.h @@ -11,10 +11,14 @@ #import #import +#import "RCTDeprecation.h" + #import "RCTBundleManager.h" @class RCTBridge; @protocol RCTBridgeMethod; +@protocol RCTTurboModule; +@protocol RCTTurboModuleRegistry; @class RCTModuleRegistry; @class RCTViewRegistry; @class RCTCallableJSModules; @@ -81,28 +85,15 @@ RCT_EXTERN_C_END * registration. Useful for registering swift classes that forbids use of load * Used in RCT_EXTERN_REMAP_MODULE */ -#define RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name) \ - RCT_EXTERN void RCTRegisterModule(Class); \ - +(NSString *)moduleName \ - { \ - return @ #js_name; \ - } \ - __attribute__((constructor)) static void RCT_CONCAT(initialize_, objc_name)() \ - { \ - RCTRegisterModule([objc_name class]); \ - } - -/** - * To improve startup performance users may want to generate their module lists - * at build time and hook the delegate to merge with the runtime list. This - * macro takes the place of the above for those cases by omitting the +load - * generation. - * - */ -#define RCT_EXPORT_PRE_REGISTERED_MODULE(js_name) \ - +(NSString *)moduleName \ - { \ - return @ #js_name; \ +#define RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name) \ + RCT_EXTERN void RCTRegisterModule(Class); \ + +(NSString *)moduleName \ + { \ + return @ #js_name; \ + } \ + __attribute__((constructor)) static void RCT_CONCAT(initialize_, objc_name)(void) \ + { \ + RCTRegisterModule([objc_name class]); \ } // Implemented by RCT_EXPORT_MODULE @@ -157,30 +148,20 @@ RCT_EXTERN_C_END * To implement this in your module, just add `@synthesize bridge = _bridge;` * If using Swift, add `@objc var bridge: RCTBridge!` to your module. */ -@property (nonatomic, weak, readonly) RCTBridge *bridge; +@property (nonatomic, weak, readonly) RCTBridge *bridge RCT_DEPRECATED; /** - * The queue that will be used to call all exported methods. If omitted, this - * will call on a default background queue, which is avoids blocking the main - * thread. - * - * If the methods in your module need to interact with UIKit methods, they will - * probably need to call those on the main thread, as most of UIKit is main- - * thread-only. You can tell React Native to call your module methods on the - * main thread by returning a reference to the main queue, like this: - * - * - (dispatch_queue_t)methodQueue - * { - * return dispatch_get_main_queue(); - * } - * - * If you don't want to specify the queue yourself, but you need to use it - * inside your class (e.g. if you have internal methods that need to dispatch - * onto that queue), you can just add `@synthesize methodQueue = _methodQueue;` - * and the bridge will populate the methodQueue property for you automatically - * when it initializes the module. + * This property is deprecated. This selector used to support two functionalities. + * + * 1) Providing a queue to do additional async work. + * Instead of synthesizing this selector, retrieve a queue from GCD to do any asynchronous work. + * Example: _myQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL); + * + * 2) Overriding this in order to run all the module's methods on a specific queue, usually main. + * Instead of overriding this, directly dispatch the code onto main queue when necessary. + * Example: dispatch_async(dispatch_get_main_queue, ^{ ... }); */ -@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue; +@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue RCT_DEPRECATED; /** * Wrap the parameter line of your method implementation with this macro to @@ -255,7 +236,7 @@ RCT_EXTERN_C_END */ #define RCT_REMAP_METHOD(js_name, method) \ _RCT_EXTERN_REMAP_METHOD(js_name, method, NO) \ - -(void)method RCT_DYNAMIC; + -(void)method RCT_DYNAMIC /** * Similar to RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD but lets you set @@ -267,7 +248,7 @@ RCT_EXTERN_C_END */ #define RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(js_name, returnType, method) \ _RCT_EXTERN_REMAP_METHOD(js_name, method, YES) \ - -(returnType)method RCT_DYNAMIC; + -(returnType)method RCT_DYNAMIC /** * Use this macro in a private Objective-C implementation file to automatically @@ -302,12 +283,10 @@ RCT_EXTERN_C_END /** * Like RCT_EXTERN_MODULE, but allows setting a custom JavaScript name. */ -#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \ - objc_name: \ - objc_supername @ \ - end @interface objc_name(RCTExternModule) \ - @end \ - @implementation objc_name (RCTExternModule) \ +#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \ + objc_name : objc_supername @end @interface objc_name(RCTExternModule) \ + @end \ + @implementation objc_name (RCTExternModule) \ RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name) /** @@ -367,58 +346,25 @@ RCT_EXTERN_C_END /** * Notifies the module that a batch of JS method invocations has just completed. */ -- (void)batchDidComplete; - -/** - * Notifies the module that the active batch of JS method invocations has been - * partially flushed. - * - * This occurs before -batchDidComplete, and more frequently. - */ -- (void)partialBatchDidFlush; - -@end - -/** - * A protocol that allows TurboModules to do lookup on other TurboModules. - * Calling these methods may cause a module to be synchronously instantiated. - */ -@protocol RCTTurboModuleRegistry -- (id)moduleForName:(const char *)moduleName; - -/** - * Rationale: - * When TurboModules lookup other modules by name, we first check the TurboModule - * registry to see if a TurboModule exists with the respective name. In this case, - * we don't want a RedBox to be raised if the TurboModule isn't found. - * - * This method is deprecated and will be deleted after the migration from - * TurboModules to TurboModules is complete. - */ -- (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure; -- (BOOL)moduleIsInitialized:(const char *)moduleName; +- (void)batchDidComplete RCT_DEPRECATED; -- (NSArray *)eagerInitModuleNames; -- (NSArray *)eagerInitMainQueueModuleNames; @end -/** - * Experimental. - * A protocol to declare that a class supports TurboModule. - * This may be removed in the future. - * See RCTTurboModule.h for actual signature. - */ -@protocol RCTTurboModule; - /** * A class that allows NativeModules and TurboModules to look up one another. */ @interface RCTModuleRegistry : NSObject +#ifndef RCT_FIT_RM_OLD_RUNTIME - (void)setBridge:(RCTBridge *)bridge; +#endif // RCT_FIT_RM_OLD_RUNTIME - (void)setTurboModuleRegistry:(id)turboModuleRegistry; - (id)moduleForName:(const char *)moduleName; - (id)moduleForName:(const char *)moduleName lazilyLoadIfNecessary:(BOOL)lazilyLoad; +- (BOOL)moduleIsInitialized:(Class)moduleClass; + +// Note: This method lazily load the module as necessary. +- (id)moduleForClass:(Class)moduleClass; @end typedef UIView * (^RCTBridgelessComponentViewProvider)(NSNumber *); @@ -429,7 +375,9 @@ typedef void (^RCTViewRegistryUIBlock)(RCTViewRegistry *viewRegistry); * A class that allows NativeModules to query for views, given React Tags. */ @interface RCTViewRegistry : NSObject +#ifndef RCT_FIT_RM_OLD_RUNTIME - (void)setBridge:(RCTBridge *)bridge; +#endif // RCT_FIT_RM_OLD_RUNTIME - (void)setBridgelessComponentViewProvider:(RCTBridgelessComponentViewProvider)bridgelessComponentViewProvider; - (UIView *)viewForReactTag:(NSNumber *)reactTag; @@ -447,7 +395,9 @@ typedef void (^RCTBridgelessJSModuleMethodInvoker)( * as callable with React Native. */ @interface RCTCallableJSModules : NSObject +#ifndef RCT_FIT_RM_OLD_RUNTIME - (void)setBridge:(RCTBridge *)bridge; +#endif // RCT_FIT_RM_OLD_RUNTIME - (void)setBridgelessJSModuleMethodInvoker:(RCTBridgelessJSModuleMethodInvoker)bridgelessJSModuleMethodInvoker; - (void)invokeModule:(NSString *)moduleName method:(NSString *)methodName withArgs:(NSArray *)args; diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModuleDecorator.h b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModuleDecorator.h new file mode 100644 index 00000000..e7860967 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModuleDecorator.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTBridgeModule.h" + +@class RCTBundleManager; +@class RCTCallableJSModules; +@class RCTModuleRegistry; +@class RCTViewRegistry; + +/** + RCTBridgeModuleDecorator contains instances that can be initialized with @synthesize + in RCTBridgeModules. For the Fabric interop layer. + + In Bridgeless, @synthesize ivars are passed from RCTBridgeModuleDecorator. + In Bridge, @synthesize ivars are passed from RCTModuleData. + */ +@interface RCTBridgeModuleDecorator : NSObject +@property (nonatomic, strong, readonly) RCTViewRegistry *viewRegistry_DEPRECATED; +@property (nonatomic, strong, readonly) RCTModuleRegistry *moduleRegistry; +@property (nonatomic, strong, readonly) RCTBundleManager *bundleManager; +@property (nonatomic, strong, readonly) RCTCallableJSModules *callableJSModules; + +- (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry + moduleRegistry:(RCTModuleRegistry *)moduleRegistry + bundleManager:(RCTBundleManager *)bundleManager + callableJSModules:(RCTCallableJSModules *)callableJSModules; + +- (void)attachInteropAPIsToModule:(id)bridgeModule; + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModuleDecorator.m b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModuleDecorator.m new file mode 100644 index 00000000..7f8e1541 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeModuleDecorator.m @@ -0,0 +1,73 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTBridgeModuleDecorator.h" + +@implementation RCTBridgeModuleDecorator + +- (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry + moduleRegistry:(RCTModuleRegistry *)moduleRegistry + bundleManager:(RCTBundleManager *)bundleManager + callableJSModules:(RCTCallableJSModules *)callableJSModules +{ + if (self = [super init]) { + _viewRegistry_DEPRECATED = viewRegistry; + _moduleRegistry = moduleRegistry; + _bundleManager = bundleManager; + _callableJSModules = callableJSModules; + } + return self; +} + +- (void)attachInteropAPIsToModule:(id)bridgeModule +{ + /** + * Attach the RCTViewRegistry to this TurboModule, which allows this TurboModule + * To query a React component's UIView, given its reactTag. + * + * Usage: In the TurboModule @implementation, include: + * `@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED` + */ + if ([bridgeModule respondsToSelector:@selector(setViewRegistry_DEPRECATED:)]) { + bridgeModule.viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; + } + + /** + * Attach the RCTBundleManager to this TurboModule, which allows this TurboModule to + * read from/write to the app's bundle URL. + * + * Usage: In the TurboModule @implementation, include: + * `@synthesize bundleManager = _bundleManager` + */ + if ([bridgeModule respondsToSelector:@selector(setBundleManager:)]) { + bridgeModule.bundleManager = _bundleManager; + } + + /** + * Attach the RCTCallableJSModules to this TurboModule, which allows this TurboModule + * to call JS Module methods. + * + * Usage: In the TurboModule @implementation, include: + * `@synthesize callableJSModules = _callableJSModules` + */ + if ([bridgeModule respondsToSelector:@selector(setCallableJSModules:)]) { + bridgeModule.callableJSModules = _callableJSModules; + } + + /** + * Attach the RCTModuleRegistry to this TurboModule, which allows this TurboModule + * to require other TurboModules/NativeModules. + * + * Usage: In the TurboModule @implementation, include: + * `@synthesize moduleRegistry = _moduleRegistry` + */ + if ([bridgeModule respondsToSelector:@selector(setModuleRegistry:)]) { + bridgeModule.moduleRegistry = _moduleRegistry; + } +} + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy+Cxx.h b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy+Cxx.h new file mode 100644 index 00000000..2748178a --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy+Cxx.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifdef __cplusplus +#import +#endif + +#import "RCTBridgeProxy.h" + +@interface RCTBridgeProxy (Cxx) + +#ifdef __cplusplus +@property (nonatomic, readwrite) std::shared_ptr jsCallInvoker; +#endif + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy.h b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy.h new file mode 100644 index 00000000..ef392aad --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "RCTBridgeModule.h" + +NS_ASSUME_NONNULL_BEGIN + +@class RCTBundleManager; +@class RCTCallableJSModules; +@class RCTModuleRegistry; +@class RCTViewRegistry; + +@interface RCTBridgeProxy : NSProxy + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +- (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry + moduleRegistry:(RCTModuleRegistry *)moduleRegistry + bundleManager:(RCTBundleManager *)bundleManager + callableJSModules:(RCTCallableJSModules *)callableJSModules + dispatchToJSThread:(void (^)(dispatch_block_t))dispatchToJSThread + registerSegmentWithId:(void (^)(NSNumber *, NSString *))registerSegmentWithId + runtime:(void *)runtime + launchOptions:(nullable NSDictionary *)launchOptions NS_DESIGNATED_INITIALIZER; + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; +- (void)forwardInvocation:(NSInvocation *)invocation; + +- (void)logWarning:(NSString *)message cmd:(SEL)cmd; +- (void)logError:(NSString *)message cmd:(SEL)cmd; + +/** + * Methods required for RCTBridge class extensions + */ +- (id)moduleForClass:(Class)moduleClass; +- (id)moduleForName:(NSString *)moduleName lazilyLoadIfNecessary:(BOOL)lazilyLoad; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy.mm b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy.mm new file mode 100644 index 00000000..9608f72b --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTBridgeProxy.mm @@ -0,0 +1,504 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTBridgeProxy.h" +#import "RCTBridgeProxy+Cxx.h" + +#import +#import +#import +#import +#import +#import + +using namespace facebook; + +@interface RCTUIManagerProxy : NSProxy +- (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry NS_DESIGNATED_INITIALIZER; + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; +- (void)forwardInvocation:(NSInvocation *)invocation; +@end + +@interface RCTBridgeProxy () + +@property (nonatomic, readwrite) std::shared_ptr jsCallInvoker; + +@end + +@implementation RCTBridgeProxy { + RCTUIManagerProxy *_uiManagerProxy; + RCTModuleRegistry *_moduleRegistry; + RCTBundleManager *_bundleManager; + RCTCallableJSModules *_callableJSModules; + NSDictionary *_launchOptions; + void (^_dispatchToJSThread)(dispatch_block_t); + void (^_registerSegmentWithId)(NSNumber *, NSString *); + void *_runtime; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-designated-initializers" +- (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry + moduleRegistry:(RCTModuleRegistry *)moduleRegistry + bundleManager:(RCTBundleManager *)bundleManager + callableJSModules:(RCTCallableJSModules *)callableJSModules + dispatchToJSThread:(void (^)(dispatch_block_t))dispatchToJSThread + registerSegmentWithId:(void (^)(NSNumber *, NSString *))registerSegmentWithId + runtime:(void *)runtime + launchOptions:(nullable NSDictionary *)launchOptions +{ + self = [super self]; + if (self != nullptr) { + _uiManagerProxy = [[RCTUIManagerProxy alloc] initWithViewRegistry:viewRegistry]; + _moduleRegistry = moduleRegistry; + _bundleManager = bundleManager; + _callableJSModules = callableJSModules; + _dispatchToJSThread = dispatchToJSThread; + _registerSegmentWithId = registerSegmentWithId; + _runtime = runtime; + _launchOptions = [launchOptions copy]; + } + + return self; +} +#pragma clang diagnostic pop + +- (void)dispatchBlock:(dispatch_block_t)block queue:(dispatch_queue_t)queue +{ + [self logWarning:@"Please migrate to dispatchToJSThread: @synthesize dispatchToJSThread = _dispatchToJSThread" + cmd:_cmd]; + + if (queue == RCTJSThread) { + _dispatchToJSThread(block); + } else if (queue != nullptr) { + dispatch_async(queue, block); + } +} + +/** + * Used By: + * - RCTDevSettings + */ +- (Class)executorClass +{ + [self logWarning:@"This method is unsupported. Returning nil." cmd:_cmd]; + return nil; +} + +/** + * Used By: + * - RCTBlobCollector + */ +- (void *)runtime +{ + [self logWarning:@"Please migrate to C++ TurboModule or RuntimeExecutor." cmd:_cmd]; + return _runtime; +} + +- (std::shared_ptr)jsCallInvoker +{ + [self logWarning:@"Please migrate to RuntimeExecutor" cmd:_cmd]; + return _jsCallInvoker; +} + +/** + * RCTModuleRegistry + */ +- (id)moduleForName:(NSString *)moduleName +{ + [self logWarning:@"Please migrate to RCTModuleRegistry: @synthesize moduleRegistry = _moduleRegistry." cmd:_cmd]; + return [_moduleRegistry moduleForName:[moduleName UTF8String]]; +} + +- (id)moduleForName:(NSString *)moduleName lazilyLoadIfNecessary:(BOOL)lazilyLoad +{ + [self logWarning:@"Please migrate to RCTModuleRegistry: @synthesize moduleRegistry = _moduleRegistry." cmd:_cmd]; + return [_moduleRegistry moduleForName:[moduleName UTF8String] lazilyLoadIfNecessary:lazilyLoad]; +} + +- (id)moduleForClass:(Class)moduleClass +{ + [self logWarning:@"Please migrate to RCTModuleRegistry: @synthesize moduleRegistry = _moduleRegistry." cmd:_cmd]; + NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); + return [_moduleRegistry moduleForName:[moduleName UTF8String] lazilyLoadIfNecessary:YES]; +} + +- (NSArray *)modulesConformingToProtocol:(Protocol *)protocol +{ + [self logError:@"The TurboModule system cannot load modules by protocol. Returning an empty NSArray*." cmd:_cmd]; + return @[]; +} + +- (BOOL)moduleIsInitialized:(Class)moduleClass +{ + [self logWarning:@"Please migrate to RCTModuleRegistry: @synthesize moduleRegistry = _moduleRegistry." cmd:_cmd]; + return [_moduleRegistry moduleIsInitialized:moduleClass]; +} + +- (NSArray *)moduleClasses +{ + [self logError:@"The TurboModuleManager does not implement this method. Returning an empty NSArray*." cmd:_cmd]; + return @[]; +} + +/** + * RCTBundleManager + */ +- (void)setBundleURL:(NSURL *)bundleURL +{ + [self logWarning:@"Please migrate to RCTBundleManager: @synthesize bundleManager = _bundleManager." cmd:_cmd]; + [_bundleManager setBundleURL:bundleURL]; +} + +- (NSURL *)bundleURL +{ + [self logWarning:@"Please migrate to RCTBundleManager: @synthesize bundleManager = _bundleManager." cmd:_cmd]; + return [_bundleManager bundleURL]; +} + +/** + * RCTCallableJSModules + */ +- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args +{ + [self logWarning:@"Please migrate to RCTCallableJSModules: @synthesize callableJSModules = _callableJSModules." + cmd:_cmd]; + + NSArray *ids = [moduleDotMethod componentsSeparatedByString:@"."]; + NSString *module = ids[0]; + NSString *method = ids[1]; + [_callableJSModules invokeModule:module method:method withArgs:args]; +} + +- (void)enqueueJSCall:(NSString *)module + method:(NSString *)method + args:(NSArray *)args + completion:(dispatch_block_t)completion +{ + [self logWarning:@"Please migrate to RCTCallableJSModules: @synthesize callableJSModules = _callableJSModules." + cmd:_cmd]; + [_callableJSModules invokeModule:module method:method withArgs:args onComplete:completion]; +} + +- (void)registerSegmentWithId:(NSUInteger)segmentId path:(NSString *)path +{ + _registerSegmentWithId(@(segmentId), path); +} + +- (id)delegate +{ + [self logError:@"This method is unsupported. Returning nil." cmd:_cmd]; + return nil; +} + +- (NSDictionary *)launchOptions +{ + return _launchOptions; +} + +- (BOOL)loading +{ + [self logWarning:@"This method is not implemented. Returning NO." cmd:_cmd]; + return NO; +} + +- (BOOL)valid +{ + [self logWarning:@"This method is not implemented. Returning NO." cmd:_cmd]; + return NO; +} + +- (RCTPerformanceLogger *)performanceLogger +{ + [self logWarning:@"This method is not supported. Returning nil." cmd:_cmd]; + return nil; +} + +- (void)reload +{ + [self logError:@"Please use RCTReloadCommand instead. Nooping." cmd:_cmd]; +} + +- (void)reloadWithReason:(NSString *)reason +{ + [self logError:@"Please use RCTReloadCommand instead. Nooping." cmd:_cmd]; +} + +- (void)onFastRefresh +{ + [[NSNotificationCenter defaultCenter] postNotificationName:RCTBridgeFastRefreshNotification object:self]; +} + +- (void)requestReload __deprecated_msg("Use RCTReloadCommand instead") +{ + [self logError:@"Please use RCTReloadCommand instead. Nooping." cmd:_cmd]; +} + +- (BOOL)isBatchActive +{ + [self logWarning:@"This method is not supported. Returning NO." cmd:_cmd]; + return NO; +} + +/** + * RCTBridge () + */ + +- (NSString *)bridgeDescription +{ + [self logWarning:@"This method is not supported. Returning \"BridgeProxy\"." cmd:_cmd]; + return @"BridgeProxy"; +} + +- (void)enqueueCallback:(NSNumber *)cbID args:(NSArray *)args +{ + [self logError:@"This method is not supported. No-oping." cmd:_cmd]; +} + +- (RCTBridge *)batchedBridge +{ + [self logWarning:@"This method is not supported. Returning bridge proxy." cmd:_cmd]; + return (RCTBridge *)self; +} + +- (void)setBatchedBridge +{ + [self logError:@"This method is not supported. No-oping." cmd:_cmd]; +} + +- (RCTBridgeModuleListProvider)moduleProvider +{ + [self logWarning:@"This method is not supported. Returning empty block" cmd:_cmd]; + return ^{ + return @[]; + }; +} + +- (RCTModuleRegistry *)moduleRegistry +{ + return _moduleRegistry; +} + +/** + * RCTBridge (RCTCxxBridge) + */ + +- (RCTBridge *)parentBridge +{ + [self logWarning:@"This method is not supported. Returning bridge proxy." cmd:_cmd]; + return (RCTBridge *)self; +} + +- (BOOL)moduleSetupComplete +{ + [self logWarning:@"This method is not supported. Returning YES." cmd:_cmd]; + return YES; +} + +- (void)start +{ + [self + logError: + @"Starting the bridge proxy does nothing. If you want to start React Native, please use RCTHost start. Nooping" + cmd:_cmd]; +} + +#ifndef RCT_FIT_RM_OLD_RUNTIME +- (void)registerModuleForFrameUpdates:(id)module withModuleData:(RCTModuleData *)moduleData +{ + [self logError:@"This method is not supported. Nooping" cmd:_cmd]; +} + +- (RCTModuleData *)moduleDataForName:(NSString *)moduleName +{ + [self logError:@"This method is not supported. Returning nil." cmd:_cmd]; + return nil; +} +#endif // RCT_FIT_RM_OLD_RUNTIME + +- (void)registerAdditionalModuleClasses:(NSArray *)newModules +{ + [self + logError: + @"This API is unsupported. Please return all module classes from your app's RCTTurboModuleManagerDelegate getModuleClassFromName:. Nooping." + cmd:_cmd]; +} + +- (void)updateModuleWithInstance:(id)instance +{ + [self logError:@"This method is not supported. Nooping." cmd:_cmd]; +} + +- (void)startProfiling +{ + [self logWarning:@"This method is not supported. Nooping." cmd:_cmd]; +} + +- (void)stopProfiling:(void (^)(NSData *))callback +{ + [self logWarning:@"This method is not supported. Nooping." cmd:_cmd]; +} + +- (id)callNativeModule:(NSUInteger)moduleID method:(NSUInteger)methodID params:(NSArray *)params +{ + [self logError:@"This method is not supported. Nooping and returning nil." cmd:_cmd]; + return nil; +} + +- (void)logMessage:(NSString *)message level:(NSString *)level +{ + [self logWarning:@"This method is not supported. Nooping." cmd:_cmd]; +} + +- (void)_immediatelyCallTimer:(NSNumber *)timer +{ + [self logWarning:@"This method is not supported. Nooping." cmd:_cmd]; +} + +/** + * RCTBridge (Inspector) + */ +- (BOOL)inspectable +{ + [self logWarning:@"This method is not supported. Returning NO." cmd:_cmd]; + return NO; +} + +/** + * RCTBridge (RCTUIManager) + */ +- (RCTUIManager *)uiManager +{ + return (RCTUIManager *)_uiManagerProxy; +} + +- (RCTBridgeProxy *)object +{ + return self; +} + +/** + * NSProxy setup + */ +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; +{ + return [RCTCxxBridge instanceMethodSignatureForSelector:sel]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + [self logError:@"This method is unsupported." cmd:invocation.selector]; +} + +/** + * Logging + * TODO(155977839): Add a means to configure/disable these logs, so people do not ignore all LogBoxes + */ +- (void)logWarning:(NSString *)message cmd:(SEL)cmd +{ + if (RCTTurboModuleInteropBridgeProxyLogLevel() == kRCTBridgeProxyLoggingLevelWarning) { + RCTLogWarn(@"RCTBridgeProxy: Calling [bridge %@]. %@", NSStringFromSelector(cmd), message); + } +} + +- (void)logError:(NSString *)message cmd:(SEL)cmd +{ + if (RCTTurboModuleInteropBridgeProxyLogLevel() == kRCTBridgeProxyLoggingLevelWarning || + RCTTurboModuleInteropBridgeProxyLogLevel() == kRCTBridgeProxyLoggingLevelError) { + RCTLogError(@"RCTBridgeProxy: Calling [bridge %@]. %@", NSStringFromSelector(cmd), message); + } +} + +@end + +@implementation RCTUIManagerProxy { + RCTViewRegistry *_viewRegistry; + NSMutableDictionary *_legacyViewRegistry; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-designated-initializers" + +- (instancetype)initWithViewRegistry:(RCTViewRegistry *)viewRegistry +{ + self = [super self]; + if (self != nullptr) { + _viewRegistry = viewRegistry; + _legacyViewRegistry = [NSMutableDictionary new]; + } + return self; +} + +#pragma clang diagnostic pop + +/** + * RCTViewRegistry + */ +- (UIView *)viewForReactTag:(NSNumber *)reactTag +{ + [self logWarning:@"Please migrate to RCTViewRegistry: @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED." + cmd:_cmd]; + UIView *view = ([_viewRegistry viewForReactTag:reactTag] != nullptr) ? [_viewRegistry viewForReactTag:reactTag] + : [_legacyViewRegistry objectForKey:reactTag]; + return RCTPaperViewOrCurrentView(view); +} + +- (void)addUIBlock:(RCTViewManagerUIBlock)block +{ + [self + logWarning: + @"This method isn't implemented faithfully. Please migrate to RCTViewRegistry if possible: @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED." + cmd:_cmd]; + __weak __typeof(self) weakSelf = self; + RCTExecuteOnMainQueue(^{ + __typeof(self) strongSelf = weakSelf; + if (strongSelf != nullptr) { + RCTUIManager *proxiedManager = (RCTUIManager *)strongSelf; + RCTComposedViewRegistry *composedViewRegistry = + [[RCTComposedViewRegistry alloc] initWithUIManager:proxiedManager + andRegistry:strongSelf->_legacyViewRegistry]; + block(proxiedManager, composedViewRegistry); + } + }); +} + +/** + * NSProxy setup + */ +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel +{ + return [RCTUIManager instanceMethodSignatureForSelector:sel]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + [self logError:@"This method is unsupported." cmd:invocation.selector]; +} + +/** + * Logging + * TODO(155977839): Add a means to configure/disable these logs, so people do not ignore all LogBoxes + */ +- (void)logWarning:(NSString *)message cmd:(SEL)cmd +{ + if (RCTTurboModuleInteropBridgeProxyLogLevel() == kRCTBridgeProxyLoggingLevelWarning) { + RCTLogWarn( + @"RCTBridgeProxy (RCTUIManagerProxy): Calling [bridge.uiManager %@]. %@", NSStringFromSelector(cmd), message); + } +} + +- (void)logError:(NSString *)message cmd:(SEL)cmd +{ + if (RCTTurboModuleInteropBridgeProxyLogLevel() == kRCTBridgeProxyLoggingLevelWarning || + RCTTurboModuleInteropBridgeProxyLogLevel() == kRCTBridgeProxyLoggingLevelError) { + RCTLogError( + @"RCTBridgeProxy (RCTUIManagerProxy): Calling [bridge.uiManager %@]. %@", NSStringFromSelector(cmd), message); + } +} + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvoker.h b/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvoker.h new file mode 100644 index 00000000..b6a3e753 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvoker.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#ifdef __cplusplus +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTCallInvoker : NSObject + +#ifdef __cplusplus +- (instancetype)initWithCallInvoker:(std::shared_ptr)callInvoker + NS_DESIGNATED_INITIALIZER; + +- (std::shared_ptr)callInvoker; +#endif + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvoker.mm b/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvoker.mm new file mode 100644 index 00000000..803bea43 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvoker.mm @@ -0,0 +1,33 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTCallInvoker.h" + +@implementation RCTCallInvoker { + std::shared_ptr _callInvoker; +} + +- (instancetype)init +{ + return [self initWithCallInvoker:nullptr]; +} + +- (instancetype)initWithCallInvoker:(std::shared_ptr)callInvoker +{ + if (self = [super init]) { + _callInvoker = callInvoker; + } + + return self; +} + +- (std::shared_ptr)callInvoker +{ + return _callInvoker; +} + +@end \ No newline at end of file diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvokerModule.h b/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvokerModule.h new file mode 100644 index 00000000..9b8fe6f7 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTCallInvokerModule.h @@ -0,0 +1,20 @@ + +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class RCTCallInvoker; + +/** + * Have your module conform to this protocol to access the CallInvoker. + */ +@protocol RCTCallInvokerModule + +@property (nonatomic, nullable, readwrite) RCTCallInvoker *callInvoker; + +@end \ No newline at end of file diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTConstants.h b/packages/core/platforms/ios/lib_core/React/Base/RCTConstants.h new file mode 100644 index 00000000..72bbb0f7 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTConstants.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +RCT_EXTERN NSString *const RCTPlatformName; + +RCT_EXTERN NSString *const RCTUserInterfaceStyleDidChangeNotification; +RCT_EXTERN NSString *const RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey; + +RCT_EXTERN NSString *const RCTWindowFrameDidChangeNotification; + +RCT_EXTERN NSString *const RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED; + +/** + * This notification fires when the bridge initializes. + */ +RCT_EXTERN NSString *const RCTJavaScriptWillStartLoadingNotification; + +/** + * This notification fires when the bridge starts executing the JS bundle. + */ +RCT_EXTERN NSString *const RCTJavaScriptWillStartExecutingNotification; + +/** + * This notification fires when the bridge has finished loading the JS bundle. + */ +RCT_EXTERN NSString *const RCTJavaScriptDidLoadNotification; + +/** + * This notification fires when the bridge failed to load the JS bundle. The + * `error` key can be used to determine the error that occurred. + */ +RCT_EXTERN NSString *const RCTJavaScriptDidFailToLoadNotification; + +/** + * This notification fires each time a native module is instantiated. The + * `module` key will contain a reference to the newly-created module instance. + * Note that this notification may be fired before the module is available via + * the `[bridge moduleForClass:]` method. + */ +RCT_EXTERN NSString *const RCTDidInitializeModuleNotification; + +/* + * W3C Pointer Events + */ +RCT_EXTERN BOOL RCTGetDispatchW3CPointerEvents(void); +RCT_EXTERN void RCTSetDispatchW3CPointerEvents(BOOL value); diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTConstants.m b/packages/core/platforms/ios/lib_core/React/Base/RCTConstants.m new file mode 100644 index 00000000..eb906407 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTConstants.m @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTConstants.h" + +NSString *const RCTPlatformName = @"ios"; + +NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification"; +NSString *const RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey = @"traitCollection"; + +NSString *const RCTWindowFrameDidChangeNotification = @"RCTWindowFrameDidChangeNotification"; + +NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; +NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTJavaScriptWillStartExecutingNotification = @"RCTJavaScriptWillStartExecutingNotification"; +NSString *const RCTJavaScriptWillStartLoadingNotification = @"RCTJavaScriptWillStartLoadingNotification"; + +NSString *const RCTDidInitializeModuleNotification = @"RCTDidInitializeModuleNotification"; + +NSString *const RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED = + @"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED"; + +/* + * W3C Pointer Events + */ +static BOOL RCTDispatchW3CPointerEvents = NO; + +BOOL RCTGetDispatchW3CPointerEvents(void) +{ + return RCTDispatchW3CPointerEvents; +} + +void RCTSetDispatchW3CPointerEvents(BOOL value) +{ + RCTDispatchW3CPointerEvents = value; +} diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTConvert.mm b/packages/core/platforms/ios/lib_core/React/Base/RCTConvert.mm new file mode 100644 index 00000000..95a4bdc6 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTConvert.mm @@ -0,0 +1,1376 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTConvert.h" + +#import + +#import + +#import "RCTDefines.h" +#import "RCTImageSource.h" +#import "RCTParserUtils.h" +#import "RCTUtils.h" + +@implementation RCTConvert + +RCT_CONVERTER(id, id, self) + +RCT_CONVERTER(BOOL, BOOL, boolValue) +RCT_NUMBER_CONVERTER(double, doubleValue) +RCT_NUMBER_CONVERTER(float, floatValue) +RCT_NUMBER_CONVERTER(int, intValue) + +RCT_NUMBER_CONVERTER(int64_t, longLongValue); +RCT_NUMBER_CONVERTER(uint64_t, unsignedLongLongValue); + +RCT_NUMBER_CONVERTER(NSInteger, integerValue) +RCT_NUMBER_CONVERTER(NSUInteger, unsignedIntegerValue) + +/** + * This macro is used for creating converter functions for directly + * representable json values that require no conversion. + */ +#if RCT_DEBUG +#define RCT_JSON_CONVERTER(type) \ + +(type *)type : (id)json \ + { \ + if ([json isKindOfClass:[type class]]) { \ + return json; \ + } else if (json) { \ + RCTLogConvertError(json, @ #type); \ + } \ + return nil; \ + } +#else +#define RCT_JSON_CONVERTER(type) \ + +(type *)type : (id)json \ + { \ + return json; \ + } +#endif + +RCT_JSON_CONVERTER(NSArray) +RCT_JSON_CONVERTER(NSDictionary) +RCT_JSON_CONVERTER(NSString) +RCT_JSON_CONVERTER(NSNumber) + +RCT_CUSTOM_CONVERTER(NSSet *, NSSet, [NSSet setWithArray:json]) +RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncoding]) + ++ (NSIndexSet *)NSIndexSet:(id)json +{ + json = [self NSNumberArray:json]; + NSMutableIndexSet *indexSet = [NSMutableIndexSet new]; + for (NSNumber *number in json) { + NSInteger index = number.integerValue; + if (RCT_DEBUG && index < 0) { + RCTLogInfo(@"Invalid index value %lld. Indices must be positive.", (long long)index); + } + [indexSet addIndex:index]; + } + return indexSet; +} + ++ (NSURL *)NSURL:(id)json +{ + NSString *path = [self NSString:RCTNilIfNull(json)]; + if (!path) { + return nil; + } + + @try { // NSURL has a history of crashing with bad input, so let's be safe + NSURL *URL = [NSURL URLWithString:path]; + if (URL.scheme) { // Was a well-formed absolute URL + return URL; + } + + // Check if it has a scheme + if ([path rangeOfString:@"://"].location != NSNotFound) { + NSMutableCharacterSet *urlAllowedCharacterSet = [NSMutableCharacterSet new]; + [urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLUserAllowedCharacterSet]]; + [urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLPasswordAllowedCharacterSet]]; + [urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLHostAllowedCharacterSet]]; + [urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLPathAllowedCharacterSet]]; + [urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLQueryAllowedCharacterSet]]; + [urlAllowedCharacterSet formUnionWithCharacterSet:[NSCharacterSet URLFragmentAllowedCharacterSet]]; + path = [path stringByAddingPercentEncodingWithAllowedCharacters:urlAllowedCharacterSet]; + URL = [NSURL URLWithString:path]; + if (URL) { + return URL; + } + } + + // Assume that it's a local path + path = path.stringByRemovingPercentEncoding; + if ([path hasPrefix:@"~"]) { + // Path is inside user directory + path = path.stringByExpandingTildeInPath; + } else if (!path.absolutePath) { + // Assume it's a resource path + path = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:path]; + } + if (!(URL = [NSURL fileURLWithPath:path])) { + RCTLogConvertError(json, @"a valid URL"); + } + return URL; + } @catch (__unused NSException *e) { + RCTLogConvertError(json, @"a valid URL"); + return nil; + } +} + +RCT_ENUM_CONVERTER( + NSURLRequestCachePolicy, + (@{ + @"default" : @(NSURLRequestUseProtocolCachePolicy), + @"reload" : @(NSURLRequestReloadIgnoringLocalCacheData), + @"force-cache" : @(NSURLRequestReturnCacheDataElseLoad), + @"only-if-cached" : @(NSURLRequestReturnCacheDataDontLoad), + }), + NSURLRequestUseProtocolCachePolicy, + integerValue) + ++ (NSURLRequest *)NSURLRequest:(id)json +{ + if ([json isKindOfClass:[NSString class]]) { + NSURL *URL = [self NSURL:json]; + return URL ? [NSURLRequest requestWithURL:URL] : nil; + } + if ([json isKindOfClass:[NSDictionary class]]) { + NSString *URLString = json[@"uri"] ?: json[@"url"]; + + NSURL *URL; + NSString *bundleName = json[@"bundle"]; + if (bundleName) { + URLString = [NSString stringWithFormat:@"%@.bundle/%@", bundleName, URLString]; + } + + URL = [self NSURL:URLString]; + if (!URL) { + return nil; + } + + NSData *body = [self NSData:json[@"body"]]; + NSString *method = [self NSString:json[@"method"]].uppercaseString ?: @"GET"; + NSURLRequestCachePolicy cachePolicy = [self NSURLRequestCachePolicy:json[@"cache"]]; + NSDictionary *headers = [self NSDictionary:json[@"headers"]]; + if ([method isEqualToString:@"GET"] && headers == nil && body == nil && + cachePolicy == NSURLRequestUseProtocolCachePolicy) { + return [NSURLRequest requestWithURL:URL]; + } + + if (headers) { + __block BOOL allHeadersAreStrings = YES; + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id header, BOOL *stop) { + if (![header isKindOfClass:[NSString class]]) { + RCTLogInfo( + @"Values of HTTP headers passed must be of type string. " + "Value of header '%@' is not a string.", + key); + allHeadersAreStrings = NO; + *stop = YES; + } + }]; + if (!allHeadersAreStrings) { + // Set headers to nil here to avoid crashing later. + headers = nil; + } + } + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; + request.HTTPBody = body; + request.HTTPMethod = method; + request.cachePolicy = cachePolicy; + request.allHTTPHeaderFields = headers; + return [request copy]; + } + if (json) { + RCTLogConvertError(json, @"a valid URLRequest"); + } + return nil; +} + ++ (RCTFileURL *)RCTFileURL:(id)json +{ + NSURL *fileURL = [self NSURL:json]; + if (!fileURL.fileURL) { + RCTLogInfo(@"URI must be a local file, '%@' isn't.", fileURL); + return nil; + } + if (![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]) { + RCTLogInfo(@"File '%@' could not be found.", fileURL); + return nil; + } + return fileURL; +} + ++ (NSDate *)NSDate:(id)json +{ + if ([json isKindOfClass:[NSNumber class]]) { + return [NSDate dateWithTimeIntervalSince1970:[self NSTimeInterval:json]]; + } else if ([json isKindOfClass:[NSString class]]) { + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [NSDateFormatter new]; + formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; + }); + NSDate *date = [formatter dateFromString:json]; + if (!date) { + RCTLogInfo( + @"JSON String '%@' could not be interpreted as a date. " + "Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", + json); + } + return date; + } else if (json) { + RCTLogConvertError(json, @"a date"); + } + return nil; +} + ++ (NSLocale *)NSLocale:(id)json +{ + if ([json isKindOfClass:[NSString class]]) { + NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:json]; + if (!locale) { + RCTLogInfo(@"JSON String '%@' could not be interpreted as a valid locale. ", json); + } + return locale; + } else if (json) { + RCTLogConvertError(json, @"a locale"); + } + return nil; +} + +// JS Standard for time is milliseconds +RCT_CUSTOM_CONVERTER(NSTimeInterval, NSTimeInterval, [self double:json] / 1000.0) + +// JS standard for time zones is minutes. +RCT_CUSTOM_CONVERTER(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[self double:json] * 60.0]) + +NSNumber *RCTConvertEnumValue(const char *typeName, NSDictionary *mapping, NSNumber *defaultValue, id json) +{ + if (!json) { + return defaultValue; + } + if ([json isKindOfClass:[NSNumber class]]) { + NSArray *allValues = mapping.allValues; + if ([allValues containsObject:json] || [json isEqual:defaultValue]) { + return json; + } + RCTLogInfo(@"Invalid %s '%@'. should be one of: %@", typeName, json, allValues); + return defaultValue; + } + if (RCT_DEBUG && ![json isKindOfClass:[NSString class]]) { + RCTLogInfo(@"Expected NSNumber or NSString for %s, received %@: %@", typeName, [json classForCoder], json); + } + id value = mapping[json]; + if (RCT_DEBUG && !value && [json description].length > 0) { + RCTLogInfo( + @"Invalid %s '%@'. should be one of: %@", + typeName, + json, + [[mapping allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]); + } + return value ?: defaultValue; +} + +NSNumber *RCTConvertMultiEnumValue(const char *typeName, NSDictionary *mapping, NSNumber *defaultValue, id json) +{ + if ([json isKindOfClass:[NSArray class]]) { + if ([json count] == 0) { + return defaultValue; + } + long long result = 0; + for (id arrayElement in json) { + NSNumber *value = RCTConvertEnumValue(typeName, mapping, defaultValue, arrayElement); + result |= value.longLongValue; + } + return @(result); + } + return RCTConvertEnumValue(typeName, mapping, defaultValue, json); +} + +RCT_ENUM_CONVERTER( + NSLineBreakMode, + (@{ + @"clip" : @(NSLineBreakByClipping), + @"head" : @(NSLineBreakByTruncatingHead), + @"tail" : @(NSLineBreakByTruncatingTail), + @"middle" : @(NSLineBreakByTruncatingMiddle), + @"wordWrapping" : @(NSLineBreakByWordWrapping), + @"char" : @(NSLineBreakByCharWrapping), + }), + NSLineBreakByTruncatingTail, + integerValue) + +RCT_ENUM_CONVERTER( + NSTextAlignment, + (@{ + @"auto" : @(NSTextAlignmentNatural), + @"left" : @(NSTextAlignmentLeft), + @"center" : @(NSTextAlignmentCenter), + @"right" : @(NSTextAlignmentRight), + @"justify" : @(NSTextAlignmentJustified), + }), + NSTextAlignmentNatural, + integerValue) + +RCT_ENUM_CONVERTER( + NSUnderlineStyle, + (@{ + @"solid" : @(NSUnderlineStyleSingle), + @"double" : @(NSUnderlineStyleDouble), + @"dotted" : @(NSUnderlinePatternDot | NSUnderlineStyleSingle), + @"dashed" : @(NSUnderlinePatternDash | NSUnderlineStyleSingle), + }), + NSUnderlineStyleSingle, + integerValue) + +RCT_ENUM_CONVERTER( + RCTBorderStyle, + (@{ + @"solid" : @(RCTBorderStyleSolid), + @"dotted" : @(RCTBorderStyleDotted), + @"dashed" : @(RCTBorderStyleDashed), + }), + RCTBorderStyleSolid, + integerValue) + +RCT_ENUM_CONVERTER( + RCTBorderCurve, + (@{ + @"circular" : @(RCTBorderCurveCircular), + @"continuous" : @(RCTBorderCurveContinuous), + }), + RCTBorderCurveCircular, + integerValue) + +RCT_ENUM_CONVERTER( + RCTTextDecorationLineType, + (@{ + @"none" : @(RCTTextDecorationLineTypeNone), + @"underline" : @(RCTTextDecorationLineTypeUnderline), + @"line-through" : @(RCTTextDecorationLineTypeStrikethrough), + @"underline line-through" : @(RCTTextDecorationLineTypeUnderlineStrikethrough), + }), + RCTTextDecorationLineTypeNone, + integerValue) + +RCT_ENUM_CONVERTER( + NSWritingDirection, + (@{ + @"auto" : @(NSWritingDirectionNatural), + @"ltr" : @(NSWritingDirectionLeftToRight), + @"rtl" : @(NSWritingDirectionRightToLeft), + }), + NSWritingDirectionNatural, + integerValue) + ++ (NSLineBreakStrategy)NSLineBreakStrategy:(id)json RCT_DYNAMIC +{ + if (@available(iOS 14.0, *)) { + static NSDictionary *mapping; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + mapping = @{ + @"none" : @(NSLineBreakStrategyNone), + @"standard" : @(NSLineBreakStrategyStandard), + @"hangul-word" : @(NSLineBreakStrategyHangulWordPriority), + @"push-out" : @(NSLineBreakStrategyPushOut) + }; + }); + return RCTConvertEnumValue("NSLineBreakStrategy", mapping, @(NSLineBreakStrategyNone), json).integerValue; + } else { + return NSLineBreakStrategyNone; + } +} + +RCT_ENUM_CONVERTER( + UITextAutocapitalizationType, + (@{ + @"none" : @(UITextAutocapitalizationTypeNone), + @"words" : @(UITextAutocapitalizationTypeWords), + @"sentences" : @(UITextAutocapitalizationTypeSentences), + @"characters" : @(UITextAutocapitalizationTypeAllCharacters) + }), + UITextAutocapitalizationTypeSentences, + integerValue) + +RCT_ENUM_CONVERTER( + UITextFieldViewMode, + (@{ + @"never" : @(UITextFieldViewModeNever), + @"while-editing" : @(UITextFieldViewModeWhileEditing), + @"unless-editing" : @(UITextFieldViewModeUnlessEditing), + @"always" : @(UITextFieldViewModeAlways), + }), + UITextFieldViewModeNever, + integerValue) + ++ (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC +{ + static NSDictionary *mapping; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *temporaryMapping = [NSMutableDictionary dictionaryWithDictionary:@{ + @"default" : @(UIKeyboardTypeDefault), + @"ascii-capable" : @(UIKeyboardTypeASCIICapable), + @"numbers-and-punctuation" : @(UIKeyboardTypeNumbersAndPunctuation), + @"url" : @(UIKeyboardTypeURL), + @"number-pad" : @(UIKeyboardTypeNumberPad), + @"phone-pad" : @(UIKeyboardTypePhonePad), + @"name-phone-pad" : @(UIKeyboardTypeNamePhonePad), + @"email-address" : @(UIKeyboardTypeEmailAddress), + @"decimal-pad" : @(UIKeyboardTypeDecimalPad), + @"twitter" : @(UIKeyboardTypeTwitter), + @"web-search" : @(UIKeyboardTypeWebSearch), + // Added for Android compatibility + @"numeric" : @(UIKeyboardTypeDecimalPad), + }]; + temporaryMapping[@"ascii-capable-number-pad"] = @(UIKeyboardTypeASCIICapableNumberPad); + mapping = temporaryMapping; + }); + + UIKeyboardType type = + (UIKeyboardType)RCTConvertEnumValue("UIKeyboardType", mapping, @(UIKeyboardTypeDefault), json).integerValue; + return type; +} + +RCT_MULTI_ENUM_CONVERTER( + UIDataDetectorTypes, + (@{ + @"phoneNumber" : @(UIDataDetectorTypePhoneNumber), + @"link" : @(UIDataDetectorTypeLink), + @"address" : @(UIDataDetectorTypeAddress), + @"calendarEvent" : @(UIDataDetectorTypeCalendarEvent), + @"trackingNumber" : @(UIDataDetectorTypeShipmentTrackingNumber), + @"flightNumber" : @(UIDataDetectorTypeFlightNumber), + @"lookupSuggestion" : @(UIDataDetectorTypeLookupSuggestion), + @"none" : @(UIDataDetectorTypeNone), + @"all" : @(UIDataDetectorTypeAll), + }), + UIDataDetectorTypePhoneNumber, + unsignedLongLongValue) + +RCT_ENUM_CONVERTER( + UIKeyboardAppearance, + (@{ + @"default" : @(UIKeyboardAppearanceDefault), + @"light" : @(UIKeyboardAppearanceLight), + @"dark" : @(UIKeyboardAppearanceDark), + }), + UIKeyboardAppearanceDefault, + integerValue) + +RCT_ENUM_CONVERTER( + UIReturnKeyType, + (@{ + @"default" : @(UIReturnKeyDefault), + @"go" : @(UIReturnKeyGo), + @"google" : @(UIReturnKeyGoogle), + @"join" : @(UIReturnKeyJoin), + @"next" : @(UIReturnKeyNext), + @"route" : @(UIReturnKeyRoute), + @"search" : @(UIReturnKeySearch), + @"send" : @(UIReturnKeySend), + @"yahoo" : @(UIReturnKeyYahoo), + @"done" : @(UIReturnKeyDone), + @"emergency-call" : @(UIReturnKeyEmergencyCall), + }), + UIReturnKeyDefault, + integerValue) + +RCT_ENUM_CONVERTER( + UIUserInterfaceStyle, + (@{ + @"unspecified" : @(UIUserInterfaceStyleUnspecified), + @"light" : @(UIUserInterfaceStyleLight), + @"dark" : @(UIUserInterfaceStyleDark), + }), + UIUserInterfaceStyleUnspecified, + integerValue) + +RCT_ENUM_CONVERTER( + UIInterfaceOrientationMask, + (@{ + @"ALL" : @(UIInterfaceOrientationMaskAll), + @"PORTRAIT" : @(UIInterfaceOrientationMaskPortrait), + @"LANDSCAPE" : @(UIInterfaceOrientationMaskLandscape), + @"LANDSCAPE_LEFT" : @(UIInterfaceOrientationMaskLandscapeLeft), + @"LANDSCAPE_RIGHT" : @(UIInterfaceOrientationMaskLandscapeRight), + }), + NSNotFound, + unsignedIntegerValue) + +RCT_ENUM_CONVERTER( + UIModalPresentationStyle, + (@{ + @"fullScreen" : @(UIModalPresentationFullScreen), + @"pageSheet" : @(UIModalPresentationPageSheet), + @"formSheet" : @(UIModalPresentationFormSheet), + @"overFullScreen" : @(UIModalPresentationOverFullScreen), + }), + UIModalPresentationFullScreen, + integerValue) + +RCT_ENUM_CONVERTER( + UIViewContentMode, + (@{ + @"scale-to-fill" : @(UIViewContentModeScaleToFill), + @"scale-aspect-fit" : @(UIViewContentModeScaleAspectFit), + @"scale-aspect-fill" : @(UIViewContentModeScaleAspectFill), + @"redraw" : @(UIViewContentModeRedraw), + @"center" : @(UIViewContentModeCenter), + @"top" : @(UIViewContentModeTop), + @"bottom" : @(UIViewContentModeBottom), + @"left" : @(UIViewContentModeLeft), + @"right" : @(UIViewContentModeRight), + @"top-left" : @(UIViewContentModeTopLeft), + @"top-right" : @(UIViewContentModeTopRight), + @"bottom-left" : @(UIViewContentModeBottomLeft), + @"bottom-right" : @(UIViewContentModeBottomRight), + // Cross-platform values + @"cover" : @(UIViewContentModeScaleAspectFill), + @"contain" : @(UIViewContentModeScaleAspectFit), + @"stretch" : @(UIViewContentModeScaleToFill), + }), + UIViewContentModeScaleAspectFill, + integerValue) + +RCT_ENUM_CONVERTER( + RCTCursor, + (@{ + @"auto" : @(RCTCursorAuto), + @"pointer" : @(RCTCursorPointer), + }), + RCTCursorAuto, + integerValue) + +static void convertCGStruct(const char *type, NSArray *fields, CGFloat *result, id json) +{ + NSUInteger count = fields.count; + if ([json isKindOfClass:[NSArray class]]) { + if (RCT_DEBUG && [json count] != count) { + RCTLogInfo( + @"Expected array with count %llu, but count is %llu: %@", + (unsigned long long)count, + (unsigned long long)[json count], + json); + } else { + for (NSUInteger i = 0; i < count; i++) { + result[i] = [RCTConvert CGFloat:RCTNilIfNull(json[i])]; + } + } + } else if ([json isKindOfClass:[NSDictionary class]]) { + for (NSUInteger i = 0; i < count; i++) { + result[i] = [RCTConvert CGFloat:RCTNilIfNull(json[fields[i]])]; + } + } else if (json) { + RCTLogConvertError(json, @(type)); + } +} + +/** + * This macro is used for creating converter functions for structs that consist + * of a number of CGFloat properties, such as CGPoint, CGRect, etc. + */ +#define RCT_CGSTRUCT_CONVERTER(type, values) \ + +(type)type : (id)json \ + { \ + static NSArray *fields; \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + fields = values; \ + }); \ + type result; \ + convertCGStruct(#type, fields, (CGFloat *)&result, json); \ + return result; \ + } + +RCT_CUSTOM_CONVERTER(CGFloat, CGFloat, [self double:json]) + +RCT_CGSTRUCT_CONVERTER(CGPoint, (@[ @"x", @"y" ])) +RCT_CGSTRUCT_CONVERTER(CGSize, (@[ @"width", @"height" ])) +RCT_CGSTRUCT_CONVERTER(CGRect, (@[ @"x", @"y", @"width", @"height" ])) + ++ (UIEdgeInsets)UIEdgeInsets:(id)json +{ + static NSArray *fields; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + fields = @[ @"top", @"left", @"bottom", @"right" ]; + }); + + if ([json isKindOfClass:[NSNumber class]]) { + CGFloat value = [json doubleValue]; + return UIEdgeInsetsMake(value, value, value, value); + } else { + UIEdgeInsets result; + convertCGStruct("UIEdgeInsets", fields, (CGFloat *)&result, json); + return result; + } +} + +RCT_ENUM_CONVERTER( + CGLineJoin, + (@{ + @"miter" : @(kCGLineJoinMiter), + @"round" : @(kCGLineJoinRound), + @"bevel" : @(kCGLineJoinBevel), + }), + kCGLineJoinMiter, + intValue) + +RCT_ENUM_CONVERTER( + CGLineCap, + (@{ + @"butt" : @(kCGLineCapButt), + @"round" : @(kCGLineCapRound), + @"square" : @(kCGLineCapSquare), + }), + kCGLineCapButt, + intValue) + +RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ @"a", @"b", @"c", @"d", @"tx", @"ty" ])) + +static NSString *const RCTFallback = @"fallback"; +static NSString *const RCTFallbackARGB = @"fallback-argb"; +static NSString *const RCTSelector = @"selector"; +static NSString *const RCTIndex = @"index"; + +/** The following dictionary defines the react-native semantic colors for ios. + * If the value for a given name is empty then the name itself + * is used as the UIColor selector. + * If the RCTSelector key is present then that value is used for a selector instead + * of the key name. + * If the given selector is not available on the running OS version then + * the RCTFallback selector is used instead. + * If the RCTIndex key is present then object returned from UIColor is an + * NSArray and the object at index RCTIndex is to be used. + */ +static NSDictionary *RCTSemanticColorsMap(void) +{ + static NSDictionary *colorMap = nil; + if (colorMap == nil) { + NSMutableDictionary *map = [@{ + // https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors + // Label Colors + @"labelColor" : @{ + // iOS 13.0 + RCTFallbackARGB : + @(0xFF000000) // fallback for iOS<=12: RGBA returned by this semantic color in light mode on iOS 13 + }, + @"secondaryLabelColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x993c3c43) + }, + @"tertiaryLabelColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x4c3c3c43) + }, + @"quaternaryLabelColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x2d3c3c43) + }, + // Fill Colors + @"systemFillColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x33787880) + }, + @"secondarySystemFillColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x28787880) + }, + @"tertiarySystemFillColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x1e767680) + }, + @"quaternarySystemFillColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x14747480) + }, + // Text Colors + @"placeholderTextColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x4c3c3c43) + }, + // Standard Content Background Colors + @"systemBackgroundColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFffffff) + }, + @"secondarySystemBackgroundColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFf2f2f7) + }, + @"tertiarySystemBackgroundColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFffffff) + }, + // Grouped Content Background Colors + @"systemGroupedBackgroundColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFf2f2f7) + }, + @"secondarySystemGroupedBackgroundColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFffffff) + }, + @"tertiarySystemGroupedBackgroundColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFf2f2f7) + }, + // Separator Colors + @"separatorColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x493c3c43) + }, + @"opaqueSeparatorColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFc6c6c8) + }, + // Link Color + @"linkColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFF007aff) + }, + // Nonadaptable Colors + @"darkTextColor" : @{}, + @"lightTextColor" : @{}, + // https://developer.apple.com/documentation/uikit/uicolor/standard_colors + // Adaptable Colors + @"systemBlueColor" : @{}, + @"systemBrownColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFa2845e) + }, + @"systemCyanColor" : @{}, + @"systemGreenColor" : @{}, + @"systemIndigoColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFF5856d6) + }, + @"systemMintColor" : @{}, + @"systemOrangeColor" : @{}, + @"systemPinkColor" : @{}, + @"systemPurpleColor" : @{}, + @"systemRedColor" : @{}, + @"systemTealColor" : @{}, + @"systemYellowColor" : @{}, + // Adaptable Gray Colors + @"systemGrayColor" : @{}, + @"systemGray2Color" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFaeaeb2) + }, + @"systemGray3Color" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFc7c7cc) + }, + @"systemGray4Color" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFd1d1d6) + }, + @"systemGray5Color" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFe5e5ea) + }, + @"systemGray6Color" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0xFFf2f2f7) + }, + // Transparent Color + @"clearColor" : @{ + // iOS 13.0 + RCTFallbackARGB : @(0x00000000) + }, + } mutableCopy]; + // The color names are the Objective-C UIColor selector names, + // but Swift selector names are valid as well, so make aliases. + static NSString *const RCTColorSuffix = @"Color"; + NSMutableDictionary *aliases = [NSMutableDictionary new]; + for (NSString *objcSelector in map) { + RCTAssert( + [objcSelector hasSuffix:RCTColorSuffix], @"A selector in the color map did not end with the suffix Color."); + NSMutableDictionary *entry = [map[objcSelector] mutableCopy]; + RCTAssert([entry objectForKey:RCTSelector] == nil, @"Entry should not already have an RCTSelector"); + NSString *swiftSelector = [objcSelector substringToIndex:[objcSelector length] - [RCTColorSuffix length]]; + entry[RCTSelector] = objcSelector; + aliases[swiftSelector] = entry; + } + [map addEntriesFromDictionary:aliases]; +#if DEBUG + [map addEntriesFromDictionary:@{ + // The follow exist for Unit Tests + @"unitTestFallbackColor" : @{RCTFallback : @"gridColor"}, + @"unitTestFallbackColorIOS" : @{RCTFallback : @"blueColor"}, + @"unitTestFallbackColorEven" : @{ + RCTSelector : @"unitTestFallbackColorEven", + RCTIndex : @0, + RCTFallback : @"controlAlternatingRowBackgroundColors" + }, + @"unitTestFallbackColorOdd" : @{ + RCTSelector : @"unitTestFallbackColorOdd", + RCTIndex : @1, + RCTFallback : @"controlAlternatingRowBackgroundColors" + }, + }]; +#endif + colorMap = [map copy]; + } + + return colorMap; +} + +/** Returns a UIColor based on a semantic color name. + * Returns nil if the semantic color name is invalid. + */ +static UIColor *RCTColorFromSemanticColorName(NSString *semanticColorName) +{ + NSDictionary *colorMap = RCTSemanticColorsMap(); + UIColor *color = nil; + NSDictionary *colorInfo = colorMap[semanticColorName]; + if (colorInfo) { + NSString *semanticColorSelector = colorInfo[RCTSelector]; + if (semanticColorSelector == nil) { + semanticColorSelector = semanticColorName; + } + SEL selector = NSSelectorFromString(semanticColorSelector); + if (![UIColor respondsToSelector:selector]) { + NSNumber *fallbackRGB = colorInfo[RCTFallbackARGB]; + if (fallbackRGB != nil) { + RCTAssert([fallbackRGB isKindOfClass:[NSNumber class]], @"fallback ARGB is not a number"); + return [RCTConvert UIColor:fallbackRGB]; + } + semanticColorSelector = colorInfo[RCTFallback]; + selector = NSSelectorFromString(semanticColorSelector); + } + RCTAssert([UIColor respondsToSelector:selector], @"RCTUIColor does not respond to a semantic color selector."); + Class klass = [UIColor class]; + IMP imp = [klass methodForSelector:selector]; + id (*getSemanticColorObject)(id, SEL) = (id(*)(id, SEL))imp; + id colorObject = getSemanticColorObject(klass, selector); + if ([colorObject isKindOfClass:[UIColor class]]) { + color = colorObject; + } else if ([colorObject isKindOfClass:[NSArray class]]) { + NSArray *colors = colorObject; + NSNumber *index = colorInfo[RCTIndex]; + RCTAssert(index, @"index should not be null"); + color = colors[[index unsignedIntegerValue]]; + } else { + RCTAssert(false, @"selector return an unknown object type"); + } + } + return color; +} + +/** Returns an alphabetically sorted comma separated list of the valid semantic color names + */ +static NSString *RCTSemanticColorNames(void) +{ + NSMutableString *names = [NSMutableString new]; + NSDictionary *colorMap = RCTSemanticColorsMap(); + NSArray *allKeys = + [[[colorMap allKeys] mutableCopy] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + + for (id key in allKeys) { + if ([names length]) { + [names appendString:@", "]; + } + [names appendString:key]; + } + return names; +} + +// The iOS side is kept in synch with the C++ side by using the +// RCTAppDelegate which, at startup, sets the default color space. +// The usage of dispatch_once and of once_flag ensoure that those are +// set only once when the app starts and that they can't change while +// the app is running. +static RCTColorSpace _defaultColorSpace = RCTColorSpaceSRGB; +RCTColorSpace RCTGetDefaultColorSpace(void) +{ + return _defaultColorSpace; +} +void RCTSetDefaultColorSpace(RCTColorSpace colorSpace) +{ + _defaultColorSpace = colorSpace; +} + ++ (UIColor *)UIColorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha +{ + RCTColorSpace space = RCTGetDefaultColorSpace(); + return [self UIColorWithRed:red green:green blue:blue alpha:alpha andColorSpace:space]; +} ++ (UIColor *)UIColorWithRed:(CGFloat)red + green:(CGFloat)green + blue:(CGFloat)blue + alpha:(CGFloat)alpha + andColorSpace:(RCTColorSpace)colorSpace +{ + if (colorSpace == RCTColorSpaceDisplayP3) { + return [UIColor colorWithDisplayP3Red:red green:green blue:blue alpha:alpha]; + } + return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; +} + ++ (RCTColorSpace)RCTColorSpaceFromString:(NSString *)colorSpace +{ + if ([colorSpace isEqualToString:@"display-p3"]) { + return RCTColorSpaceDisplayP3; + } else if ([colorSpace isEqualToString:@"srgb"]) { + return RCTColorSpaceSRGB; + } + return RCTGetDefaultColorSpace(); +} + ++ (UIColor *)UIColor:(id)json +{ + if (!json) { + return nil; + } + if ([json isKindOfClass:[NSArray class]]) { + NSArray *components = [self NSNumberArray:json]; + CGFloat alpha = components.count > 3 ? [self CGFloat:components[3]] : 1.0; + return [self UIColorWithRed:[self CGFloat:components[0]] + green:[self CGFloat:components[1]] + blue:[self CGFloat:components[2]] + alpha:alpha]; + } else if ([json isKindOfClass:[NSNumber class]]) { + NSUInteger argb = [self NSUInteger:json]; + CGFloat a = ((argb >> 24) & 0xFF) / 255.0; + CGFloat r = ((argb >> 16) & 0xFF) / 255.0; + CGFloat g = ((argb >> 8) & 0xFF) / 255.0; + CGFloat b = (argb & 0xFF) / 255.0; + return [self UIColorWithRed:r green:g blue:b alpha:a]; + } else if ([json isKindOfClass:[NSDictionary class]]) { + NSDictionary *dictionary = json; + id value = nil; + NSString *rawColorSpace = [dictionary objectForKey:@"space"]; + if ([rawColorSpace isEqualToString:@"display-p3"] || [rawColorSpace isEqualToString:@"srgb"]) { + CGFloat r = [[dictionary objectForKey:@"r"] floatValue]; + CGFloat g = [[dictionary objectForKey:@"g"] floatValue]; + CGFloat b = [[dictionary objectForKey:@"b"] floatValue]; + CGFloat a = [[dictionary objectForKey:@"a"] floatValue]; + RCTColorSpace colorSpace = [self RCTColorSpaceFromString:rawColorSpace]; + return [self UIColorWithRed:r green:g blue:b alpha:a andColorSpace:colorSpace]; + } else if ((value = [dictionary objectForKey:@"semantic"])) { + if ([value isKindOfClass:[NSString class]]) { + NSString *semanticName = value; + UIColor *color = [UIColor colorNamed:semanticName]; + if (color != nil) { + return color; + } + color = RCTColorFromSemanticColorName(semanticName); + if (color == nil) { + RCTLogConvertError( + json, + [@"a UIColor. Expected one of the following values: " stringByAppendingString:RCTSemanticColorNames()]); + } + return color; + } else if ([value isKindOfClass:[NSArray class]]) { + for (id name in value) { + UIColor *color = [UIColor colorNamed:name]; + if (color != nil) { + return color; + } + color = RCTColorFromSemanticColorName(name); + if (color != nil) { + return color; + } + } + RCTLogConvertError( + json, + [@"a UIColor. None of the names in the array were one of the following values: " + stringByAppendingString:RCTSemanticColorNames()]); + return nil; + } + RCTLogConvertError( + json, @"a UIColor. Expected either a single name or an array of names but got something else."); + return nil; + } else if ((value = [dictionary objectForKey:@"dynamic"])) { + NSDictionary *appearances = value; + id light = [appearances objectForKey:@"light"]; + UIColor *lightColor = [RCTConvert UIColor:light]; + id dark = [appearances objectForKey:@"dark"]; + UIColor *darkColor = [RCTConvert UIColor:dark]; + id highContrastLight = [appearances objectForKey:@"highContrastLight"]; + UIColor *highContrastLightColor = [RCTConvert UIColor:highContrastLight]; + id highContrastDark = [appearances objectForKey:@"highContrastDark"]; + UIColor *highContrastDarkColor = [RCTConvert UIColor:highContrastDark]; + if (lightColor != nil && darkColor != nil) { + UIColor *color = [UIColor colorWithDynamicProvider:^UIColor *_Nonnull(UITraitCollection *_Nonnull collection) { + if (collection.userInterfaceStyle == UIUserInterfaceStyleDark) { + if (collection.accessibilityContrast == UIAccessibilityContrastHigh && highContrastDarkColor != nil) { + return highContrastDarkColor; + } else { + return darkColor; + } + } else { + if (collection.accessibilityContrast == UIAccessibilityContrastHigh && highContrastLightColor != nil) { + return highContrastLightColor; + } else { + return lightColor; + } + } + }]; + return color; + + } else { + RCTLogConvertError(json, @"a UIColor. Expected an iOS dynamic appearance aware color."); + return nil; + } + } else { + RCTLogConvertError(json, @"a UIColor. Expected an iOS semantic color or dynamic appearance aware color."); + return nil; + } + } else { + RCTLogConvertError(json, @"a UIColor. Did you forget to call processColor() on the JS side?"); + return nil; + } +} + ++ (CGColorRef)CGColor:(id)json +{ + return [self UIColor:json].CGColor; +} + ++ (YGValue)YGValue:(id)json +{ + if (!json) { + return YGValueUndefined; + } else if ([json isKindOfClass:[NSNumber class]]) { + return (YGValue){[json floatValue], YGUnitPoint}; + } else if ([json isKindOfClass:[NSString class]]) { + NSString *s = (NSString *)json; + if ([s isEqualToString:@"auto"]) { + return (YGValue){YGUndefined, YGUnitAuto}; + } else if ([s hasSuffix:@"%"]) { + float floatValue; + if ([[NSScanner scannerWithString:s] scanFloat:&floatValue]) { + return (YGValue){floatValue, YGUnitPercent}; + } + } else { + RCTLogAdvice( + @"\"%@\" is not a valid dimension. Dimensions must be a number, \"auto\", or a string suffixed with \"%%\".", + s); + } + } + return YGValueUndefined; +} + +NSArray *RCTConvertArrayValue(SEL type, id json) +{ + __block BOOL copy = NO; + __block NSArray *values = json = [RCTConvert NSArray:json]; + [json enumerateObjectsUsingBlock:^(id jsonValue, NSUInteger idx, __unused BOOL *stop) { + id value = ((id(*)(Class, SEL, id))objc_msgSend)([RCTConvert class], type, jsonValue); + if (copy) { + if (value) { + [(NSMutableArray *)values addObject:value]; + } + } else if (value != jsonValue) { + // Converted value is different, so we'll need to copy the array + values = [[NSMutableArray alloc] initWithCapacity:values.count]; + for (NSUInteger i = 0; i < idx; i++) { + [(NSMutableArray *)values addObject:json[i]]; + } + if (value) { + [(NSMutableArray *)values addObject:value]; + } + copy = YES; + } + }]; + return values; +} + +RCT_ARRAY_CONVERTER(NSURL) +RCT_ARRAY_CONVERTER(RCTFileURL) +RCT_ARRAY_CONVERTER(UIColor) + +/** + * This macro is used for creating converter functions for directly + * representable json array values that require no conversion. + */ +#if RCT_DEBUG +#define RCT_JSON_ARRAY_CONVERTER_NAMED(type, name) RCT_ARRAY_CONVERTER_NAMED(type, name) +#else +#define RCT_JSON_ARRAY_CONVERTER_NAMED(type, name) \ + +(NSArray *)name##Array : (id)json \ + { \ + return json; \ + } +#endif +#define RCT_JSON_ARRAY_CONVERTER(type) RCT_JSON_ARRAY_CONVERTER_NAMED(type, type) + +RCT_JSON_ARRAY_CONVERTER(NSArray) +RCT_JSON_ARRAY_CONVERTER(NSString) +RCT_JSON_ARRAY_CONVERTER_NAMED(NSArray, NSStringArray) +RCT_JSON_ARRAY_CONVERTER(NSDictionary) +RCT_JSON_ARRAY_CONVERTER(NSNumber) + +// Can't use RCT_ARRAY_CONVERTER due to bridged cast ++ (NSArray *)CGColorArray:(id)json +{ + NSMutableArray *colors = [NSMutableArray new]; + for (id value in [self NSArray:json]) { + [colors addObject:(__bridge id)[self CGColor:value]]; + } + return colors; +} + +static id RCTConvertPropertyListValue(id json) +{ + if (!json || json == (id)kCFNull) { + return nil; + } + + if ([json isKindOfClass:[NSDictionary class]]) { + __block BOOL copy = NO; + NSMutableDictionary *values = [[NSMutableDictionary alloc] initWithCapacity:[json count]]; + [json enumerateKeysAndObjectsUsingBlock:^(NSString *key, id jsonValue, __unused BOOL *stop) { + id value = RCTConvertPropertyListValue(jsonValue); + if (value) { + values[key] = value; + } + copy |= value != jsonValue; + }]; + return copy ? values : json; + } + + if ([json isKindOfClass:[NSArray class]]) { + __block BOOL copy = NO; + __block NSArray *values = json; + [json enumerateObjectsUsingBlock:^(id jsonValue, NSUInteger idx, __unused BOOL *stop) { + id value = RCTConvertPropertyListValue(jsonValue); + if (copy) { + if (value) { + [(NSMutableArray *)values addObject:value]; + } + } else if (value != jsonValue) { + // Converted value is different, so we'll need to copy the array + values = [[NSMutableArray alloc] initWithCapacity:values.count]; + for (NSUInteger i = 0; i < idx; i++) { + [(NSMutableArray *)values addObject:json[i]]; + } + if (value) { + [(NSMutableArray *)values addObject:value]; + } + copy = YES; + } + }]; + return values; + } + + // All other JSON types are supported by property lists + return json; +} + ++ (NSPropertyList)NSPropertyList:(id)json +{ + return RCTConvertPropertyListValue(json); +} + +RCT_ENUM_CONVERTER(css_backface_visibility_t, (@{@"hidden" : @NO, @"visible" : @YES}), YES, boolValue) + +RCT_ENUM_CONVERTER( + YGOverflow, + (@{ + @"hidden" : @(YGOverflowHidden), + @"visible" : @(YGOverflowVisible), + @"scroll" : @(YGOverflowScroll), + }), + YGOverflowVisible, + intValue) + +RCT_ENUM_CONVERTER( + YGDisplay, + (@{ + @"flex" : @(YGDisplayFlex), + @"none" : @(YGDisplayNone), + }), + YGDisplayFlex, + intValue) + +RCT_ENUM_CONVERTER( + YGFlexDirection, + (@{ + @"row" : @(YGFlexDirectionRow), + @"row-reverse" : @(YGFlexDirectionRowReverse), + @"column" : @(YGFlexDirectionColumn), + @"column-reverse" : @(YGFlexDirectionColumnReverse) + }), + YGFlexDirectionColumn, + intValue) + +RCT_ENUM_CONVERTER( + YGJustify, + (@{ + @"flex-start" : @(YGJustifyFlexStart), + @"flex-end" : @(YGJustifyFlexEnd), + @"center" : @(YGJustifyCenter), + @"space-between" : @(YGJustifySpaceBetween), + @"space-around" : @(YGJustifySpaceAround), + @"space-evenly" : @(YGJustifySpaceEvenly) + }), + YGJustifyFlexStart, + intValue) + +RCT_ENUM_CONVERTER( + YGAlign, + (@{ + @"flex-start" : @(YGAlignFlexStart), + @"flex-end" : @(YGAlignFlexEnd), + @"center" : @(YGAlignCenter), + @"auto" : @(YGAlignAuto), + @"stretch" : @(YGAlignStretch), + @"baseline" : @(YGAlignBaseline), + @"space-between" : @(YGAlignSpaceBetween), + @"space-around" : @(YGAlignSpaceAround), + @"space-evenly" : @(YGAlignSpaceEvenly) + }), + YGAlignFlexStart, + intValue) + +RCT_ENUM_CONVERTER( + YGDirection, + (@{ + @"inherit" : @(YGDirectionInherit), + @"ltr" : @(YGDirectionLTR), + @"rtl" : @(YGDirectionRTL), + }), + YGDirectionInherit, + intValue) + +RCT_ENUM_CONVERTER( + YGPositionType, + (@{@"absolute" : @(YGPositionTypeAbsolute), @"relative" : @(YGPositionTypeRelative)}), + YGPositionTypeRelative, + intValue) + +RCT_ENUM_CONVERTER( + YGWrap, + (@{@"wrap" : @(YGWrapWrap), @"nowrap" : @(YGWrapNoWrap), @"wrap-reverse" : @(YGWrapWrapReverse)}), + YGWrapNoWrap, + intValue) + +RCT_ENUM_CONVERTER( + RCTPointerEvents, + (@{ + @"none" : @(RCTPointerEventsNone), + @"box-only" : @(RCTPointerEventsBoxOnly), + @"box-none" : @(RCTPointerEventsBoxNone), + @"auto" : @(RCTPointerEventsUnspecified) + }), + RCTPointerEventsUnspecified, + integerValue) + +RCT_ENUM_CONVERTER( + RCTAnimationType, + (@{ + @"spring" : @(RCTAnimationTypeSpring), + @"linear" : @(RCTAnimationTypeLinear), + @"easeIn" : @(RCTAnimationTypeEaseIn), + @"easeOut" : @(RCTAnimationTypeEaseOut), + @"easeInEaseOut" : @(RCTAnimationTypeEaseInEaseOut), + @"keyboard" : @(RCTAnimationTypeKeyboard), + }), + RCTAnimationTypeEaseInEaseOut, + integerValue) + +@end + +@interface RCTImageSource (Packager) + +@property (nonatomic, assign) BOOL packagerAsset; + +@end + +@implementation RCTConvert (Deprecated) + +/* This method is only used when loading images synchronously, e.g. for tabbar icons */ ++ (UIImage *)UIImage:(id)json +{ + if (!json) { + return nil; + } + + RCTImageSource *imageSource = [self RCTImageSource:json]; + if (!imageSource) { + return nil; + } + + __block UIImage *image; + if (!RCTIsMainQueue()) { + // It seems that none of the UIImage loading methods can be guaranteed + // thread safe, so we'll pick the lesser of two evils here and block rather + // than run the risk of crashing + RCTLogWarn(@"Calling [RCTConvert UIImage:] on a background thread is not recommended"); + RCTUnsafeExecuteOnMainQueueSync(^{ + image = [self UIImage:json]; + }); + return image; + } + + NSURL *URL = imageSource.request.URL; + NSString *scheme = URL.scheme.lowercaseString; + if ([scheme isEqualToString:@"file"]) { + image = RCTImageFromLocalAssetURL(URL); + // There is a case where this may fail when the image is at the bundle location. + // RCTImageFromLocalAssetURL only checks for the image in the same location as the jsbundle + // Hence, if the bundle is CodePush-ed, it will not be able to find the image. + // This check is added here instead of being inside RCTImageFromLocalAssetURL, since + // we don't want breaking changes to RCTImageFromLocalAssetURL, which is called in a lot of places + // This is a deprecated method, and hence has the least impact on existing code. Basically, + // instead of crashing the app, it tries one more location for the image. + if (!image) { + image = RCTImageFromLocalBundleAssetURL(URL); + } + if (!image) { + RCTLogConvertError(json, @"an image. File not found."); + } + } else if ([scheme isEqualToString:@"data"]) { + image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; + } else if ([scheme isEqualToString:@"http"] && imageSource.packagerAsset) { + image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; + } else { + RCTLogConvertError(json, @"an image. Only local files or data URIs are supported."); + return nil; + } + + CGFloat scale = imageSource.scale; + if (!scale && imageSource.size.width) { + // If no scale provided, set scale to image width / source width + scale = CGImageGetWidth(image.CGImage) / imageSource.size.width; + } + + if (scale) { + image = [UIImage imageWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; + } + + if (!CGSizeEqualToSize(imageSource.size, CGSizeZero) && !CGSizeEqualToSize(imageSource.size, image.size)) { + RCTLogInfo( + @"Image source %@ size %@ does not match loaded image size %@.", + URL.path.lastPathComponent, + NSStringFromCGSize(imageSource.size), + NSStringFromCGSize(image.size)); + } + + return image; +} + ++ (CGImageRef)CGImage:(id)json +{ + return [self UIImage:json].CGImage; +} + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTDeprecation.h b/packages/core/platforms/ios/lib_core/React/Base/RCTDeprecation.h new file mode 100644 index 00000000..dc7b92b3 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTDeprecation.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifndef RCT_DEPRECATED_DECLARATIONS +#define RCT_DEPRECATED_DECLARATIONS 0 +#endif + +#if RCT_DEPRECATED_DECLARATIONS +#define RCT_DEPRECATED __attribute__((deprecated)) +#else +#define RCT_DEPRECATED +#endif diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTDeprecation.m b/packages/core/platforms/ios/lib_core/React/Base/RCTDeprecation.m new file mode 100644 index 00000000..cc1cbc7c --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTDeprecation.m @@ -0,0 +1,8 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Dummy file to make RCTDeprecation a valid framework. diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTDisplayLink.h b/packages/core/platforms/ios/lib_core/React/Base/RCTDisplayLink.h new file mode 100644 index 00000000..636f3742 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTDisplayLink.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@protocol RCTBridgeModule; +@class RCTModuleData; + +@protocol RCTDisplayLinkModuleHolder +- (id)instance; +- (Class)moduleClass; +- (dispatch_queue_t)methodQueue; +@end + +@interface RCTDisplayLink : NSObject + +- (instancetype)init; +- (void)invalidate; +- (void)registerModuleForFrameUpdates:(id)module + withModuleHolder:(id)moduleHolder; +- (void)addToRunLoop:(NSRunLoop *)runLoop; + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTDisplayLink.m b/packages/core/platforms/ios/lib_core/React/Base/RCTDisplayLink.m new file mode 100644 index 00000000..de7c8700 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTDisplayLink.m @@ -0,0 +1,165 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTDisplayLink.h" + +#import +#import + +#import "RCTAssert.h" +#import "RCTBridgeModule.h" +#import "RCTFrameUpdate.h" +#import "RCTModuleData.h" +#import "RCTProfile.h" + +#define RCTAssertRunLoop() \ + RCTAssert(_runLoop == [NSRunLoop currentRunLoop], @"This method must be called on the CADisplayLink run loop") + +@implementation RCTDisplayLink { + CADisplayLink *_jsDisplayLink; + NSMutableSet> *_frameUpdateObservers; + NSRunLoop *_runLoop; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _frameUpdateObservers = [NSMutableSet new]; + _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + } + + return self; +} + +- (void)registerModuleForFrameUpdates:(id)module + withModuleHolder:(id)moduleHolder +{ + if (![moduleHolder.moduleClass conformsToProtocol:@protocol(RCTFrameUpdateObserver)] || + [_frameUpdateObservers containsObject:moduleHolder]) { + return; + } + + [_frameUpdateObservers addObject:moduleHolder]; + + // Don't access the module instance via moduleHolder, as this will cause deadlock + id observer = (id)module; + __weak typeof(self) weakSelf = self; + observer.pauseCallback = ^{ + typeof(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + CFRunLoopRef cfRunLoop = [strongSelf->_runLoop getCFRunLoop]; + if (!cfRunLoop) { + return; + } + + if ([NSRunLoop currentRunLoop] == strongSelf->_runLoop) { + [weakSelf updateJSDisplayLinkState]; + } else { + CFRunLoopPerformBlock(cfRunLoop, kCFRunLoopDefaultMode, ^{ + @autoreleasepool { + [weakSelf updateJSDisplayLinkState]; + } + }); + CFRunLoopWakeUp(cfRunLoop); + } + }; + + // Assuming we're paused right now, we only need to update the display link's state + // when the new observer is not paused. If it not paused, the observer will immediately + // start receiving updates anyway. + if (![observer isPaused] && _runLoop) { + CFRunLoopPerformBlock([_runLoop getCFRunLoop], kCFRunLoopDefaultMode, ^{ + @autoreleasepool { + [self updateJSDisplayLinkState]; + } + }); + } +} + +- (void)addToRunLoop:(NSRunLoop *)runLoop +{ + _runLoop = runLoop; + [_jsDisplayLink addToRunLoop:runLoop forMode:NSRunLoopCommonModes]; +} + +- (void)dealloc +{ + [self invalidate]; +} + +- (void)invalidate +{ + // ensure observer callbacks do not hold a reference to weak self via pauseCallback + for (id moduleHolder in _frameUpdateObservers) { + id observer = (id)moduleHolder.instance; + [observer setPauseCallback:nil]; + } + [_frameUpdateObservers removeAllObjects]; // just to be explicit + + [_jsDisplayLink invalidate]; +} + +- (void)dispatchBlock:(dispatch_block_t)block queue:(dispatch_queue_t)queue +{ + if (queue == RCTJSThread) { + block(); + } else if (queue) { + dispatch_async(queue, block); + } +} + +- (void)_jsThreadUpdate:(CADisplayLink *)displayLink +{ + RCTAssertRunLoop(); + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTDisplayLink _jsThreadUpdate:]", nil); + + RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; + for (id moduleHolder in _frameUpdateObservers) { + id observer = (id)moduleHolder.instance; + if (!observer.paused) { + if (moduleHolder.methodQueue) { + RCTProfileBeginFlowEvent(); + [self + dispatchBlock:^{ + RCTProfileEndFlowEvent(); + [observer didUpdateFrame:frameUpdate]; + } + queue:moduleHolder.methodQueue]; + } else { + [observer didUpdateFrame:frameUpdate]; + } + } + } + + [self updateJSDisplayLinkState]; + + RCTProfileImmediateEvent(RCTProfileTagAlways, @"JS Thread Tick", displayLink.timestamp, 'g'); + + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"objc_call"); +} + +- (void)updateJSDisplayLinkState +{ + RCTAssertRunLoop(); + + BOOL pauseDisplayLink = YES; + for (id moduleHolder in _frameUpdateObservers) { + id observer = (id)moduleHolder.instance; + if (!observer.paused) { + pauseDisplayLink = NO; + break; + } + } + + _jsDisplayLink.paused = pauseDisplayLink; +} + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTErrorCustomizer.h b/packages/core/platforms/ios/lib_core/React/Base/RCTErrorCustomizer.h new file mode 100644 index 00000000..d634b557 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTErrorCustomizer.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class RCTErrorInfo; + +/** + * Provides an interface to customize React Native error messages and stack + * traces from exceptions. + */ +@protocol RCTErrorCustomizer + +/** + * Customizes the given error, returning the passed info argument if no + * customization is required. + */ +- (nonnull RCTErrorInfo *)customizeErrorInfo:(nonnull RCTErrorInfo *)info; +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTErrorInfo.h b/packages/core/platforms/ios/lib_core/React/Base/RCTErrorInfo.h new file mode 100644 index 00000000..a388b781 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTErrorInfo.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class RCTJSStackFrame; + +/** + * An ObjC wrapper for React Native errors. + */ +@interface RCTErrorInfo : NSObject +@property (nonatomic, copy, readonly) NSString *errorMessage; +@property (nonatomic, copy, readonly) NSArray *stack; + +- (instancetype)initWithErrorMessage:(NSString *)errorMessage stack:(NSArray *)stack; + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTErrorInfo.m b/packages/core/platforms/ios/lib_core/React/Base/RCTErrorInfo.m new file mode 100644 index 00000000..1eb41fe4 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTErrorInfo.m @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTErrorInfo.h" + +#import "RCTJSStackFrame.h" + +@implementation RCTErrorInfo + +- (instancetype)initWithErrorMessage:(NSString *)errorMessage stack:(NSArray *)stack +{ + self = [super init]; + if (self) { + _errorMessage = [errorMessage copy]; + _stack = [stack copy]; + } + return self; +} + +@end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTModuleRegistry.m b/packages/core/platforms/ios/lib_core/React/Base/RCTModuleRegistry.m index f1c7e03c..7a1c3bb8 100644 --- a/packages/core/platforms/ios/lib_core/React/Base/RCTModuleRegistry.m +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTModuleRegistry.m @@ -6,17 +6,23 @@ */ #import "RCTBridge.h" -#import "RCTBridgeModule.h" +#import "RCTTurboModuleRegistry.h" + +@class RCTBridgeModule; @implementation RCTModuleRegistry { __weak id _turboModuleRegistry; +#ifndef RCT_FIT_RM_OLD_RUNTIME __weak RCTBridge *_bridge; +#endif // RCT_FIT_RM_OLD_RUNTIME } +#ifndef RCT_FIT_RM_OLD_RUNTIME - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; } +#endif // RCT_FIT_RM_OLD_RUNTIME - (void)setTurboModuleRegistry:(id)turboModuleRegistry { @@ -32,10 +38,12 @@ - (id)moduleForName:(const char *)moduleName lazilyLoadIfNecessary:(BOOL)lazilyL { id module = nil; +#ifndef RCT_FIT_RM_OLD_RUNTIME RCTBridge *bridge = _bridge; if (bridge) { module = [bridge moduleForName: [NSString stringWithUTF8String:moduleName] lazilyLoadIfNecessary:lazilyLoad]; } +#endif // RCT_FIT_RM_OLD_RUNTIME id turboModuleRegistry = _turboModuleRegistry; if (module == nil && turboModuleRegistry && (lazilyLoad || [turboModuleRegistry moduleIsInitialized:moduleName])) { @@ -45,4 +53,28 @@ - (id)moduleForName:(const char *)moduleName lazilyLoadIfNecessary:(BOOL)lazilyL return module; } +- (BOOL)moduleIsInitialized:(Class)moduleClass +{ +#ifndef RCT_FIT_RM_OLD_RUNTIME + RCTBridge *bridge = _bridge; + + if (bridge) { + return [bridge moduleIsInitialized:moduleClass]; + } +#endif // RCT_FIT_RM_OLD_RUNTIME + + id turboModuleRegistry = _turboModuleRegistry; + if (turboModuleRegistry) { + NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); + return [turboModuleRegistry moduleIsInitialized:[moduleName UTF8String]]; + } + + return NO; +} + +- (id)moduleForClass:(Class)moduleClass +{ + return [self moduleForName:RCTBridgeModuleNameForClass(moduleClass).UTF8String]; +} + @end diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTNullability.h b/packages/core/platforms/ios/lib_core/React/Base/RCTNullability.h new file mode 100644 index 00000000..266f3f53 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTNullability.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +typedef NS_ENUM(NSInteger, RCTNullability) { + RCTNullabilityUnspecified, + RCTNullable, + RCTNonnullable, +}; diff --git a/packages/core/platforms/ios/lib_core/React/Base/RCTTurboModuleRegistry.h b/packages/core/platforms/ios/lib_core/React/Base/RCTTurboModuleRegistry.h new file mode 100644 index 00000000..8ad5d06a --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/Base/RCTTurboModuleRegistry.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * A protocol that allows TurboModules to do lookup on other TurboModules. + * Calling these methods may cause a module to be synchronously instantiated. + */ +@protocol RCTTurboModuleRegistry +- (id)moduleForName:(const char *)moduleName; + +/** + * Rationale: + * When TurboModules lookup other modules by name, we first check the TurboModule + * registry to see if a TurboModule exists with the respective name. In this case, + * we don't want a RedBox to be raised if the TurboModule isn't found. + * + * This method is deprecated and will be deleted after the migration from + * TurboModules to TurboModules is complete. + */ +- (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure; +- (BOOL)moduleIsInitialized:(const char *)moduleName; +@end diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTInteropTurboModule.h b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTInteropTurboModule.h new file mode 100644 index 00000000..de805762 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTInteropTurboModule.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#import +#import + +#import +#import + +#import "RCTTurboModule.h" + +namespace facebook::react { + +class JSI_EXPORT ObjCInteropTurboModule : public ObjCTurboModule { + public: + struct MethodDescriptor { + std::string methodName; + SEL selector; + size_t jsArgCount; + TurboModuleMethodValueKind jsReturnKind; + }; + + ObjCInteropTurboModule(const ObjCTurboModule::InitParams ¶ms); + + std::vector getPropertyNames(facebook::jsi::Runtime &runtime) override; + + protected: + jsi::Value create(jsi::Runtime &runtime, const jsi::PropNameID &propName) override; + + /** + * Why is this overriden? + * + * Purpose: Converts native module method returns from Objective C values to JavaScript values. + * + * ObjCTurboModule converts returns by returnType. But, Legacy native modules convert returns by the Objective C type: + * React Native cannot infer a method's returnType from the RCT_EXPORT_METHOD annotations. + */ + jsi::Value convertReturnIdToJSIValue( + jsi::Runtime &runtime, + const char *methodName, + TurboModuleMethodValueKind returnType, + id result) override; + + /** + * Why is this overriden? + * + * Purpose: Get a native module method's argument's type, given the method name, and argument index. + * + * This override is meant to serve as a performance optimization. + * + * ObjCTurboModule computes the method argument types from the RCT_EXPORT_METHOD macros lazily. + * ObjCInteropTurboModule computes all the method argument types eagerly on module init. + * + * ObjCInteropTurboModule overrides getArgumentTypeName, so ObjCTurboModule doesn't end up re-computing the argument + * type names again. + */ + NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex) override; + + /** + * Why is this overriden? + * + * Purpose: Convert arguments from JavaScript values to Objective C values. Assign the Objective C argument to the + * method invocation. + * + * ObjCTurboModule tries to minimize reliance on RCTConvert for argument conversion. Why: RCTConvert relies on the + * RCT_EXPORT_METHOD macros, which we want to remove long term. But, Legacy native modules rely heavily on RCTConvert + * for argument conversion. + */ + void setInvocationArg( + jsi::Runtime &runtime, + const char *methodName, + const std::string &objCArgType, + const jsi::Value &arg, + size_t i, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation) override; + + private: + std::vector methodDescriptors_; + NSDictionary *> *methodArgumentTypeNames_; + jsi::Value constantsCache_; + std::unordered_set warnedModuleInvocation_; + + const jsi::Value &getConstants(jsi::Runtime &runtime); + bool exportsConstants(); + + void _logLegacyArchitectureWarning(NSString *moduleName, const std::string &methodName); +}; + +} // namespace facebook::react diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTInteropTurboModule.mm b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTInteropTurboModule.mm new file mode 100644 index 00000000..bdf4c397 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTInteropTurboModule.mm @@ -0,0 +1,698 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RCTInteropTurboModule.h" + +#import +#import + +#import +#import +#import +#import +#import +#import + +namespace facebook::react { + +namespace { + +// This is used for generating short exception strings. +std::string getType(jsi::Runtime &rt, const jsi::Value &v) +{ + if (v.isUndefined()) { + return "undefined"; + } else if (v.isNull()) { + return "null"; + } else if (v.isBool()) { + return v.getBool() ? "true" : "false"; + } else if (v.isNumber()) { + return "number"; + } else if (v.isString()) { + return "string"; + } else if (v.isSymbol()) { + return "symbol"; + } else if (v.isBigInt()) { + return "bigint"; + } else if (v.isObject()) { + jsi::Object vObj = v.getObject(rt); + return vObj.isFunction(rt) ? "function" : vObj.isArray(rt) ? "array" : "object"; + } else { + return "unknown"; + } +} + +std::vector getMethodInfos(Class moduleClass) +{ + std::vector methodInfos; + + Class cls = moduleClass; + while ((cls != nullptr) && cls != [NSObject class] && cls != [NSProxy class]) { + unsigned int methodCount; + Method *methods = class_copyMethodList(object_getClass(cls), &methodCount); + + for (unsigned int i = 0; i < methodCount; i++) { + Method method = methods[i]; + SEL selector = method_getName(method); + if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) { + IMP imp = method_getImplementation(method); + const RCTMethodInfo *methodInfo = ((const RCTMethodInfo *(*)(id, SEL))imp)(moduleClass, selector); + methodInfos.push_back(methodInfo); + } + } + + free(methods); + cls = class_getSuperclass(cls); + } + + return methodInfos; +} + +NSString *getJSMethodName(const RCTMethodInfo *methodInfo) +{ + std::string jsName = methodInfo->jsName; + if (!jsName.empty()) { + return @(jsName.c_str()); + } + + NSString *methodName = @(methodInfo->objcName); + NSRange colonRange = [methodName rangeOfString:@":"]; + if (colonRange.location != NSNotFound) { + methodName = [methodName substringToIndex:colonRange.location]; + } + methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + RCTAssert( + methodName.length, + @"%s is not a valid JS function name, please" + " supply an alternative using RCT_REMAP_METHOD()", + methodInfo->objcName); + + return methodName; +} + +class ObjCInteropTurboModuleParseException : public std::runtime_error { + public: + ObjCInteropTurboModuleParseException(std::string moduleName, std::string methodName, std::string message) + : std::runtime_error( + "Failed to create module \"" + moduleName + "\": Error while parsing method " + moduleName + "." + + methodName + ": " + message) + { + } +}; + +struct ExportedMethod { + NSString *methodName; + NSArray *argumentTypes; + std::string returnType; + SEL selector; +}; + +std::vector parseExportedMethods(std::string moduleName, Class moduleClass) +{ + std::vector methodInfos = getMethodInfos(moduleClass); + std::vector methods; + methods.reserve(methodInfos.size()); + + for (const RCTMethodInfo *methodInfo : methodInfos) { + NSString *jsMethodName = getJSMethodName(methodInfo); + NSArray *arguments; + SEL objCMethodSelector = NSSelectorFromString(RCTParseMethodSignature(methodInfo->objcName, &arguments)); + NSMethodSignature *objCMethodSignature = [moduleClass instanceMethodSignatureForSelector:objCMethodSelector]; + if (objCMethodSignature == nullptr) { + RCTLogWarn( + @"The Objective-C `%s` method signature for the JS method `%@` can not be found in the Objective-C definition of the %s module.\nThe `%@` JS method will not be available.", + methodInfo->objcName, + jsMethodName, + moduleName.c_str(), + jsMethodName); + continue; + } + std::string objCMethodReturnType = [objCMethodSignature methodReturnType]; + + if (objCMethodSignature.numberOfArguments - 2 != [arguments count]) { + std::string message = "Parsed argument count (i.e: " + std::to_string([arguments count]) + + ") != Objective C method signature argument count (i.e: " + + std::to_string(objCMethodSignature.numberOfArguments - 2) + ")."; + throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message); + } + + NSMutableArray *argumentTypes = [NSMutableArray new]; + for (NSUInteger i = 0; i < [arguments count]; i += 1) { + [argumentTypes addObject:arguments[i].type]; + } + + if ([argumentTypes count] == 1) { + std::string lastArgType = [argumentTypes[[argumentTypes count] - 1] UTF8String]; + if (lastArgType == "RCTPromiseResolveBlock" || lastArgType == "RCTPromiseRejectBlock") { + std::string message = + "Methods that return promises must accept a RCTPromiseResolveBlock followed by a RCTPromiseRejectBlock. This method just accepts a " + + lastArgType + "."; + throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message); + } + } else if ([argumentTypes count] > 1) { + std::string lastArgType = [argumentTypes[[argumentTypes count] - 1] UTF8String]; + std::string secondLastArgType = [argumentTypes[[argumentTypes count] - 2] UTF8String]; + if ((secondLastArgType == "RCTPromiseResolveBlock" && lastArgType != "RCTPromiseRejectBlock") || + (secondLastArgType != "RCTPromiseResolveBlock" && lastArgType == "RCTPromiseRejectBlock")) { + std::string message = + "Methods that return promises must accept a RCTPromiseResolveBlock followed by a RCTPromiseRejectBlock. This method accepts a " + + secondLastArgType + " followed by a " + lastArgType + "."; + throw ObjCInteropTurboModuleParseException(moduleName, [jsMethodName UTF8String], message); + } + } + + methods.push_back( + {.methodName = jsMethodName, + .argumentTypes = argumentTypes, + .returnType = objCMethodReturnType, + .selector = objCMethodSelector}); + } + + return methods; +} + +SEL selectorForType(NSString *type) +{ + const char *input = type.UTF8String; + return NSSelectorFromString([RCTParseType(&input) stringByAppendingString:@":"]); +} + +template +T RCTConvertTo(SEL selector, id json) +{ + T (*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; + return convert([RCTConvert class], selector, json); +} + +} // namespace + +ObjCInteropTurboModule::ObjCInteropTurboModule(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params), constantsCache_(jsi::Value::undefined()) +{ + std::vector methods = parseExportedMethods(name_, [params.instance class]); + methodDescriptors_.reserve(methods.size()); + + NSMutableDictionary *> *methodArgTypeNames = [NSMutableDictionary new]; + methodArgumentTypeNames_ = methodArgTypeNames; + + for (const ExportedMethod &method : methods) { + const size_t numArgs = [method.argumentTypes count]; + const bool isPromiseMethod = + numArgs >= 2 && [method.argumentTypes[numArgs - 1] isEqualToString:@"RCTPromiseRejectBlock"]; + + const size_t jsArgCount = isPromiseMethod ? numArgs - 2 : numArgs; + + /** + * In the TurboModule system, only promises and voids are special. So, set those. + * In the else case, just assume ObjectKind. This will be ignored by the interop layer. + * In the else case, the interop layer will just call into ::convertReturnIdToJSIValue() + */ + const TurboModuleMethodValueKind returnKind = isPromiseMethod ? PromiseKind + : method.returnType == @encode(void) ? VoidKind + : ObjectKind; + + methodMap_[[method.methodName UTF8String]] = + MethodMetadata{.argCount = static_cast(jsArgCount), .invoker = nullptr}; + + for (NSUInteger i = 0; i < numArgs; i += 1) { + NSString *typeName = method.argumentTypes[i]; + + if ([typeName hasPrefix:@"JS::"]) { + NSString *rctCxxConvertSelector = + [[typeName stringByReplacingOccurrencesOfString:@"::" withString:@"_"] stringByAppendingString:@":"]; + setMethodArgConversionSelector(method.methodName, i, rctCxxConvertSelector); + } + } + + methodArgTypeNames[method.methodName] = method.argumentTypes; + methodDescriptors_.push_back({ + .methodName = [method.methodName UTF8String], + .selector = method.selector, + .jsArgCount = jsArgCount, + .jsReturnKind = returnKind, + }); + } + + if ([params.instance respondsToSelector:@selector(constantsToExport)]) { + methodDescriptors_.push_back({ + .methodName = "getConstants", .selector = @selector(constantsToExport), .jsArgCount = 0, + .jsReturnKind = ObjectKind, + }); + } else { + static SEL getConstantsSelector = NSSelectorFromString(@"getConstants"); + if ([params.instance respondsToSelector:getConstantsSelector]) { + methodDescriptors_.push_back({ + .methodName = "getConstants", + .selector = getConstantsSelector, + .jsArgCount = 0, + .jsReturnKind = ObjectKind, + }); + } + } +} + +jsi::Value ObjCInteropTurboModule::create(jsi::Runtime &runtime, const jsi::PropNameID &propName) +{ + for (size_t i = 0; i < methodDescriptors_.size(); i += 1) { + if (methodDescriptors_[i].methodName == propName.utf8(runtime)) { + if (propName.utf8(runtime) == "getConstants") { + return jsi::Function::createFromHostFunction( + runtime, + propName, + static_cast(methodDescriptors_[i].jsArgCount), + [this, i](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) mutable { + if (!this->constantsCache_.isUndefined()) { + return jsi::Value(rt, this->constantsCache_); + } + const std::string &methodName = this->methodDescriptors_[i].methodName; + NSString *moduleName = [[this->instance_ class] description]; + this->_logLegacyArchitectureWarning(moduleName, methodName); + + // TODO: Dispatch getConstants to the main queue, if the module requires main queue setup + jsi::Value ret = this->invokeObjCMethod( + rt, + this->methodDescriptors_[i].jsReturnKind, + methodName, + this->methodDescriptors_[i].selector, + args, + count); + + bool isRetValid = ret.isUndefined() || ret.isNull() || + (ret.isObject() && !ret.asObject(rt).isFunction(rt) && !ret.asObject(rt).isArray(rt)); + + if (!isRetValid) { + std::string methodJsSignature = name_ + ".getConstants()"; + std::string errorPrefix = methodJsSignature + ": "; + throw jsi::JSError( + rt, + errorPrefix + "Expected return value to be null, undefined, or a plain object. But, got: " + + getType(rt, ret)); + } + + if (ret.isUndefined() || ret.isNull()) { + this->constantsCache_ = jsi::Object(rt); + } else { + this->constantsCache_ = jsi::Value(rt, ret); + } + + return ret; + }); + } + + return jsi::Function::createFromHostFunction( + runtime, + propName, + static_cast(methodDescriptors_[i].jsArgCount), + [this, i](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { + const std::string &methodName = this->methodDescriptors_[i].methodName; + NSString *moduleName = [[this->instance_ class] description]; + this->_logLegacyArchitectureWarning(moduleName, methodName); + + return this->invokeObjCMethod( + rt, + this->methodDescriptors_[i].jsReturnKind, + methodName, + this->methodDescriptors_[i].selector, + args, + count); + }); + } + } + + jsi::Object constants = getConstants(runtime).asObject(runtime); + jsi::Value constant = constants.getProperty(runtime, propName); + + if (!constant.isUndefined()) { + // TODO(T145105887): Output warning. Tried to access a constant as a + // property on the native module object. Please migrate to getConstants(). + } + + return constant; +} + +void ObjCInteropTurboModule::_logLegacyArchitectureWarning(NSString *moduleName, const std::string &methodName) +{ + if (!RCTAreLegacyLogsEnabled()) { + return; + } + + std::string separator = std::string("."); + + std::string moduleInvocation = [moduleName cStringUsingEncoding:NSUTF8StringEncoding] + separator + methodName; + if (warnedModuleInvocation_.find(moduleInvocation) == warnedModuleInvocation_.end()) { + RCTLogWarn( + @"The `%@` module is invoking the `%s` method using the TurboModule interop layer. This is part of the compatibility layer with the Legacy Architecture. If `%@` is a local module, please migrate it to be a Native Module as described at https://reactnative.dev/docs/next/turbo-native-modules-introduction. If `%@` is a third party dependency, please open an issue in the library repository.", + moduleName, + methodName.c_str(), + moduleName, + moduleName); + warnedModuleInvocation_.insert(moduleInvocation); + } +} + +void ObjCInteropTurboModule::setInvocationArg( + jsi::Runtime &runtime, + const char *methodNameCStr, + const std::string &objCArgType, + const jsi::Value &jsiArg, + size_t index, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation) +{ + NSString *methodName = @(methodNameCStr); + std::string methodJsSignature = name_ + "." + methodNameCStr + "()"; + + NSString *argumentType = getArgumentTypeName(runtime, methodName, static_cast(index)); + std::string errorPrefix = methodJsSignature + ": Error while converting JavaScript argument " + + std::to_string(index) + " to Objective C type " + [argumentType UTF8String] + ". "; + + SEL selector = selectorForType(argumentType); + + if ([RCTConvert respondsToSelector:selector]) { + id objCArg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); + + if (objCArgType == @encode(char)) { + char arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned char)) { + unsigned char arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(short)) { + short arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned short)) { + unsigned short arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(int)) { + int arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned int)) { + unsigned int arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(long)) { + long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned long)) { + unsigned long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(long long)) { + long long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(unsigned long long)) { + unsigned long long arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(float)) { + float arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(double)) { + double arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(BOOL)) { + BOOL arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(SEL)) { + SEL arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(const char *)) { + const char *arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(void *)) { + void *arg = RCTConvertTo(selector, objCArg); + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType == @encode(id)) { + id arg = RCTConvertTo(selector, objCArg); + + // Handle the special case where there is an argument and it must be nil + // Without this check, the JS side will receive an object. + // See: discussion at + // https://github.com/facebook/react-native/pull/49250#issuecomment-2668465893 + if (arg == [NSNull null]) { + return; + } + + if (arg != nullptr) { + [retainedObjectsForInvocation addObject:arg]; + } + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if (objCArgType[0] == _C_STRUCT_B) { + NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector]; + NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; + typeInvocation.selector = selector; + typeInvocation.target = [RCTConvert class]; + + void *returnValue = malloc(typeSignature.methodReturnLength); + if (returnValue == nullptr) { + // CWE - 391 : Unchecked error condition + // https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html + // https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c + abort(); + } + [typeInvocation setArgument:&objCArg atIndex:2]; + [typeInvocation invoke]; + + [typeInvocation getReturnValue:returnValue]; + [inv setArgument:returnValue atIndex:index + 2]; + free(returnValue); + return; + } + + const char *BLOCK_TYPE = @encode(__typeof__(^{ + })); + + if (objCArgType == BLOCK_TYPE) { + /** + * RCTModuleMethod doesn't actually call into RCTConvert in this case. + */ + id arg = [objCArg copy]; + if (arg != nullptr) { + [retainedObjectsForInvocation addObject:arg]; + } + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + throw jsi::JSError(runtime, errorPrefix + "Objective C type " + [argumentType UTF8String] + " is unsupported."); + } + + if ([argumentType isEqualToString:@"RCTResponseSenderBlock"]) { + if (!(jsiArg.isObject() && jsiArg.asObject(runtime).isFunction(runtime))) { + throw jsi::JSError( + runtime, errorPrefix + "JavaScript argument must be a function. Got " + getType(runtime, jsiArg)); + } + + RCTResponseSenderBlock arg = + (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); + if (arg != nullptr) { + [retainedObjectsForInvocation addObject:arg]; + } + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if ([argumentType isEqualToString:@"RCTResponseErrorBlock"]) { + if (!(jsiArg.isObject() && jsiArg.asObject(runtime).isFunction(runtime))) { + throw jsi::JSError( + runtime, errorPrefix + "JavaScript argument must be a function. Got " + getType(runtime, jsiArg)); + } + + RCTResponseSenderBlock senderBlock = + (RCTResponseSenderBlock)TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); + RCTResponseErrorBlock arg = ^(NSError *error) { + senderBlock(@[ RCTJSErrorFromNSError(error) ]); + }; + [retainedObjectsForInvocation addObject:arg]; + [inv setArgument:&arg atIndex:(index) + 2]; + return; + } + + if ([argumentType isEqualToString:@"RCTPromiseResolveBlock"] || + [argumentType isEqualToString:@"RCTPromiseRejectBlock"]) { + throw jsi::JSError( + runtime, + errorPrefix + "The TurboModule interop layer should not convert JavaScript arguments to " + + [argumentType UTF8String] + + " inside ObjCinteropTurboModule::setInvocationArg(). Please report this as an issue."); + } + + if ([argumentType hasPrefix:@"JS::"]) { + NSString *selectorNameForCxxType = + [[argumentType stringByReplacingOccurrencesOfString:@"::" withString:@"_"] stringByAppendingString:@":"]; + selector = NSSelectorFromString(selectorNameForCxxType); + + bool isPlainObject = jsiArg.isObject() && !jsiArg.asObject(runtime).isFunction(runtime) && + !jsiArg.asObject(runtime).isArray(runtime); + if (!isPlainObject) { + throw jsi::JSError( + runtime, errorPrefix + "JavaScript argument must be a plain object. Got " + getType(runtime, jsiArg)); + } + + id arg = TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsiArg, jsInvoker_, YES); + + RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; + RCTManagedPointer *box = convert([RCTCxxConvert class], selector, arg); + + void *pointer = box.voidPointer; + [inv setArgument:&pointer atIndex:index + 2]; + [retainedObjectsForInvocation addObject:box]; + return; + } +} + +jsi::Value ObjCInteropTurboModule::convertReturnIdToJSIValue( + jsi::Runtime &runtime, + const char *methodNameCStr, + TurboModuleMethodValueKind returnType, + id result) +{ + std::string methodJsSignature = name_ + "." + methodNameCStr + "()"; + std::string errorPrefix = + methodJsSignature + ": Error while converting return Objective C value to JavaScript type. "; + + if (returnType == VoidKind) { + return jsi::Value::undefined(); + } + + if (result == (id)kCFNull || result == nil) { + return jsi::Value::null(); + } + + jsi::Value returnValue = TurboModuleConvertUtils::convertObjCObjectToJSIValue(runtime, result); + if (!returnValue.isUndefined()) { + return returnValue; + } + + throw jsi::JSError(runtime, methodJsSignature + "Objective C type was unsupported."); +} + +NSString *ObjCInteropTurboModule::getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex) +{ + const char *methodNameCStr = [methodName UTF8String]; + std::string methodJsSignature = name_ + "." + methodNameCStr + "()"; + std::string errorPrefix = + methodJsSignature + ": Error while trying to get Objective C type of parameter " + std::to_string(argIndex) + "."; + + if (methodArgumentTypeNames_[methodName] == nil) { + throw jsi::JSError(runtime, errorPrefix + "No parameter types found for method."); + } + + if ([methodArgumentTypeNames_[methodName] count] <= argIndex) { + size_t paramCount = [methodArgumentTypeNames_[methodName] count]; + throw jsi::JSError(runtime, errorPrefix + "Method has only " + std::to_string(paramCount) + " parameter types."); + } + + return methodArgumentTypeNames_[methodName][argIndex]; +} + +bool ObjCInteropTurboModule::exportsConstants() +{ + for (size_t i = 0; i < methodDescriptors_.size(); i += 1) { + if (methodDescriptors_[i].methodName == "getConstants") { + return true; + } + } + + return false; +} + +const jsi::Value &ObjCInteropTurboModule::getConstants(jsi::Runtime &runtime) +{ + if (!constantsCache_.isUndefined()) { + return constantsCache_; + } + + if (!exportsConstants()) { + constantsCache_ = jsi::Object(runtime); + return constantsCache_; + } + + jsi::Value getConstantsProp = get(runtime, jsi::PropNameID::forAscii(runtime, "getConstants")); + + if (getConstantsProp.isObject()) { + jsi::Object getConstantsObj = getConstantsProp.asObject(runtime); + if (getConstantsObj.isFunction(runtime)) { + jsi::Function getConstantsFn = getConstantsObj.asFunction(runtime); + getConstantsFn.call(runtime); + return constantsCache_; + } + } + + // Unable to invoke the getConstants() method. + // Maybe the module didn't define a getConstants() method. + // Default constants to {}, so no constants are spread into the NativeModule + constantsCache_ = jsi::Object(runtime); + return constantsCache_; +} + +std::vector ObjCInteropTurboModule::getPropertyNames(facebook::jsi::Runtime &runtime) +{ + std::vector propNames = ObjCTurboModule::getPropertyNames(runtime); + + jsi::Object constants = getConstants(runtime).asObject(runtime); + jsi::Array constantNames = constants.getPropertyNames(runtime); + + for (size_t i = 0; i < constantNames.size(runtime); i += 1) { + jsi::Value constantName = constantNames.getValueAtIndex(runtime, i); + if (constantName.isString()) { + propNames.push_back(jsi::PropNameID::forString(runtime, constantName.asString(runtime))); + } + } + + return propNames; +} + +} // namespace facebook::react diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModule.h b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModule.h new file mode 100644 index 00000000..179ebf7d --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModule.h @@ -0,0 +1,212 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#define RCT_IS_TURBO_MODULE_CLASS(klass) \ + ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTTurboModule)])) +#define RCT_IS_TURBO_MODULE_INSTANCE(module) RCT_IS_TURBO_MODULE_CLASS([(module) class]) + +namespace facebook::react { + +class CallbackWrapper; +class Instance; +using EventEmitterCallback = std::function; + +namespace TurboModuleConvertUtils { +jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value); +id convertJSIValueToObjCObject( + jsi::Runtime &runtime, + const jsi::Value &value, + const std::shared_ptr &jsInvoker, + BOOL useNSNull = NO); +} // namespace TurboModuleConvertUtils + +template <> +struct Bridging { + static jsi::Value toJs(jsi::Runtime &rt, const id &value) + { + return TurboModuleConvertUtils::convertObjCObjectToJSIValue(rt, value); + } +}; + +/** + * ObjC++ specific TurboModule base class. + */ +class JSI_EXPORT ObjCTurboModule : public TurboModule { + public: + // TODO(T65603471): Should we unify this with a Fabric abstraction? + struct InitParams { + std::string moduleName; + id instance; + std::shared_ptr jsInvoker; + std::shared_ptr nativeMethodCallInvoker; + bool isSyncModule; + bool shouldVoidMethodsExecuteSync; + }; + + ObjCTurboModule(const InitParams ¶ms); + + jsi::Value invokeObjCMethod( + jsi::Runtime &runtime, + TurboModuleMethodValueKind returnType, + const std::string &methodName, + SEL selector, + const jsi::Value *args, + size_t count); + + id instance_; + std::shared_ptr nativeMethodCallInvoker_; + + protected: + void setMethodArgConversionSelector(NSString *methodName, size_t argIndex, NSString *fnName); + + void setEventEmitterCallback(EventEmitterCallback eventEmitterCallback); + + /** + * Why is this virtual? + * + * Purpose: Converts native module method returns from Objective C values to JavaScript values. + * + * ObjCTurboModule uses TurboModuleMethodValueKind to convert returns from Objective C values to JavaScript values. + * ObjCInteropTurboModule just blindly converts returns from Objective C values to JavaScript values by runtime type, + * because it cannot infer TurboModuleMethodValueKind from the RCT_EXPORT_METHOD annotations. + */ + virtual jsi::Value convertReturnIdToJSIValue( + jsi::Runtime &runtime, + const char *methodName, + TurboModuleMethodValueKind returnType, + id result); + + /** + * Why is this virtual? + * + * Purpose: Get a native module method's argument's type, given the method name, and argument index. + * + * ObjCInteropTurboModule computes the argument type names eagerly on module init. So, make this method virtual. That + * way, ObjCInteropTurboModule doesn't end up computing the argument types twice: once on module init, and second on + * method dispatch. + */ + virtual NSString *getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex); + + /** + * Why is this virtual? + * + * Purpose: Convert arguments from JavaScript values to Objective C values. Assign the Objective C argument to the + * method invocation. + * + * ObjCInteropTurboModule relies heavily on RCTConvert to convert arguments from JavaScript values to Objective C + * values. ObjCTurboModule tries to minimize reliance on RCTConvert: RCTConvert uses the RCT_EXPORT_METHOD macros, + * which we want to remove long term from React Native. + */ + virtual void setInvocationArg( + jsi::Runtime &runtime, + const char *methodName, + const std::string &objCArgType, + const jsi::Value &arg, + size_t i, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation); + + private: + // Does the NativeModule dispatch async methods to the JS thread? + const bool isSyncModule_; + + // Should void methods execute synchronously? + const bool shouldVoidMethodsExecuteSync_; + + /** + * TODO(ramanpreet): + * Investigate an optimization that'll let us get rid of this NSMutableDictionary. + * Perhaps, have the code-generated TurboModule subclass implement + * getMethodArgConversionSelector below. + */ + NSMutableDictionary *methodArgConversionSelectors_; + NSDictionary *> *methodArgumentTypeNames_; + + bool isMethodSync(TurboModuleMethodValueKind returnType); + BOOL hasMethodArgConversionSelector(NSString *methodName, size_t argIndex); + SEL getMethodArgConversionSelector(NSString *methodName, size_t argIndex); + NSInvocation *createMethodInvocation( + jsi::Runtime &runtime, + bool isSync, + const char *methodName, + SEL selector, + const jsi::Value *args, + size_t count, + NSMutableArray *retainedObjectsForInvocation); + id performMethodInvocation( + jsi::Runtime &runtime, + bool isSync, + const char *methodName, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation); + void performVoidMethodInvocation( + jsi::Runtime &runtime, + const char *methodName, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation); + + using PromiseInvocationBlock = void (^)(RCTPromiseResolveBlock resolveWrapper, RCTPromiseRejectBlock rejectWrapper); + jsi::Value createPromise(jsi::Runtime &runtime, const std::string &methodName, PromiseInvocationBlock invoke); +}; + +} // namespace facebook::react + +@interface EventEmitterCallbackWrapper : NSObject { + @public + facebook::react::EventEmitterCallback _eventEmitterCallback; +} +@end + +/** + * Factory object that can create a Turbomodule. It could be either a C++ TM or any TurboModule. + * This needs to be an Objective-C class so we can instantiate it at runtime. + */ +@protocol RCTModuleProvider + +/** + * Create an instance of a TurboModule with the JS Invoker. + */ +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params; +@end + +/** + * Protocol that objects can inherit to conform to be treated as turbomodules. + * It inherits from RCTTurboModuleProvider, meaning that a TurboModule can create itself + */ +@protocol RCTTurboModule + +@optional +- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper; +@end + +/** + * These methods are all implemented by RCTCxxBridge, which subclasses RCTBridge. Hence, they must only be used in + * contexts where the concrete class of an RCTBridge instance is RCTCxxBridge. This happens, for example, when + * [RCTCxxBridgeDelegate jsExecutorFactoryForBridge:(RCTBridge *)] is invoked by RCTCxxBridge. + * + * TODO: Consolidate this extension with the one in RCTSurfacePresenter. + */ +@interface RCTBridge (RCTTurboModule) +- (std::shared_ptr)jsCallInvoker; +- (std::shared_ptr)decorateNativeMethodCallInvoker: + (std::shared_ptr)nativeMethodCallInvoker; +@end diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModule.mm b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModule.mm new file mode 100644 index 00000000..66465702 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModule.mm @@ -0,0 +1,896 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTTurboModule.h" + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#include + +#import +#import +#import +#import +#import + +#import +#import + +using namespace facebook; +using namespace facebook::react; +using namespace facebook::react::TurboModuleConvertUtils; + +static int32_t getUniqueId() +{ + static int32_t counter = 0; + return counter++; +} + +namespace facebook::react { + +namespace TurboModuleConvertUtils { +/** + * All static helper functions are ObjC++ specific. + */ +static jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, NSNumber *value) +{ + return jsi::Value((bool)[value boolValue]); +} + +static jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *value) +{ + return jsi::Value([value doubleValue]); +} + +static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) +{ + return jsi::String::createFromUtf8(runtime, ([value UTF8String] != nullptr) ? [value UTF8String] : ""); +} + +static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value) +{ + jsi::Object result = jsi::Object(runtime); + for (NSString *k in value) { + result.setProperty(runtime, convertNSStringToJSIString(runtime, k), convertObjCObjectToJSIValue(runtime, value[k])); + } + return result; +} + +static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value) +{ + jsi::Array result = jsi::Array(runtime, value.count); + for (size_t i = 0; i < value.count; i++) { + result.setValueAtIndex(runtime, i, convertObjCObjectToJSIValue(runtime, value[i])); + } + return result; +} + +static std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, NSArray *value) +{ + std::vector result; + for (size_t i = 0; i < value.count; i++) { + result.emplace_back(convertObjCObjectToJSIValue(runtime, value[i])); + } + return result; +} + +jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) +{ + if ([value isKindOfClass:[NSString class]]) { + return convertNSStringToJSIString(runtime, (NSString *)value); + } else if ([value isKindOfClass:[NSNumber class]]) { + if ([value isKindOfClass:[@YES class]]) { + return convertNSNumberToJSIBoolean(runtime, (NSNumber *)value); + } + return convertNSNumberToJSINumber(runtime, (NSNumber *)value); + } else if ([value isKindOfClass:[NSDictionary class]]) { + return convertNSDictionaryToJSIObject(runtime, (NSDictionary *)value); + } else if ([value isKindOfClass:[NSArray class]]) { + return convertNSArrayToJSIArray(runtime, (NSArray *)value); + } else if (value == (id)kCFNull) { + return jsi::Value::null(); + } + return jsi::Value::undefined(); +} + +static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value) +{ + return [NSString stringWithUTF8String:value.utf8(runtime).c_str()]; +} + +static NSArray *convertJSIArrayToNSArray( + jsi::Runtime &runtime, + const jsi::Array &value, + const std::shared_ptr &jsInvoker, + BOOL useNSNull) +{ + size_t size = value.size(runtime); + NSMutableArray *result = [NSMutableArray new]; + for (size_t i = 0; i < size; i++) { + // Insert kCFNull when it's `undefined` value to preserve the indices. + id convertedObject = convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker, useNSNull); + [result addObject:(convertedObject != nullptr) ? convertedObject : (id)kCFNull]; + } + return result; +} + +static NSDictionary *convertJSIObjectToNSDictionary( + jsi::Runtime &runtime, + const jsi::Object &value, + const std::shared_ptr &jsInvoker, + BOOL useNSNull) +{ + jsi::Array propertyNames = value.getPropertyNames(runtime); + size_t size = propertyNames.size(runtime); + NSMutableDictionary *result = [NSMutableDictionary new]; + for (size_t i = 0; i < size; i++) { + jsi::String name = propertyNames.getValueAtIndex(runtime, i).getString(runtime); + NSString *k = convertJSIStringToNSString(runtime, name); + id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker, useNSNull); + if (v != nullptr) { + result[k] = v; + } + } + return result; +} + +static RCTResponseSenderBlock +convertJSIFunctionToCallback(jsi::Runtime &rt, jsi::Function &&function, const std::shared_ptr &jsInvoker) +{ + __block std::optional> callback({rt, std::move(function), jsInvoker}); + return ^(NSArray *args) { + if (!callback) { + LOG(FATAL) << "Callback arg cannot be called more than once"; + return; + } + + callback->call([args](jsi::Runtime &rt, jsi::Function &jsFunction) { + auto jsArgs = convertNSArrayToStdVector(rt, args); + jsFunction.call(rt, (const jsi::Value *)jsArgs.data(), jsArgs.size()); + }); + callback = std::nullopt; + }; +} + +id convertJSIValueToObjCObject( + jsi::Runtime &runtime, + const jsi::Value &value, + const std::shared_ptr &jsInvoker, + BOOL useNSNull) +{ + if (value.isUndefined() || (value.isNull() && !useNSNull)) { + return nil; + } + if (value.isNull() && useNSNull) { + return (id)kCFNull; + } + if (value.isBool()) { + return @(value.getBool()); + } + if (value.isNumber()) { + return @(value.getNumber()); + } + if (value.isString()) { + return convertJSIStringToNSString(runtime, value.getString(runtime)); + } + if (value.isObject()) { + jsi::Object o = value.getObject(runtime); + if (o.isArray(runtime)) { + return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker, useNSNull); + } + if (o.isFunction(runtime)) { + return convertJSIFunctionToCallback(runtime, o.getFunction(runtime), jsInvoker); + } + return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull); + } + + throw std::runtime_error("Unsupported jsi::Value kind"); +} + +static jsi::Value createJSRuntimeError(jsi::Runtime &runtime, const std::string &message) +{ + return runtime.global().getPropertyAsFunction(runtime, "Error").call(runtime, message); +} + +/** + * Creates JSError with current JS runtime and NSException stack trace. + */ +static jsi::JSError convertNSExceptionToJSError( + jsi::Runtime &runtime, + NSException *exception, + const std::string &moduleName, + const std::string &methodName) +{ + std::string reason = [exception.reason UTF8String]; + + jsi::Object cause(runtime); + cause.setProperty(runtime, "name", [exception.name UTF8String]); + cause.setProperty(runtime, "message", reason); + cause.setProperty(runtime, "stackSymbols", convertNSArrayToJSIArray(runtime, exception.callStackSymbols)); + cause.setProperty( + runtime, "stackReturnAddresses", convertNSArrayToJSIArray(runtime, exception.callStackReturnAddresses)); + + std::string message = moduleName + "." + methodName + " raised an exception: " + reason; + jsi::Value error = createJSRuntimeError(runtime, message); + error.asObject(runtime).setProperty(runtime, "cause", std::move(cause)); + return {runtime, std::move(error)}; +} + +/** + * Creates JS error value with current JS runtime and error details. + */ +static jsi::Value convertJSErrorDetailsToJSRuntimeError(jsi::Runtime &runtime, NSDictionary *jsErrorDetails) +{ + NSString *message = jsErrorDetails[@"message"]; + + auto jsError = createJSRuntimeError(runtime, [message UTF8String]); + for (NSString *key in jsErrorDetails) { + id value = jsErrorDetails[key]; + jsError.asObject(runtime).setProperty(runtime, [key UTF8String], convertObjCObjectToJSIValue(runtime, value)); + } + + return jsError; +} + +} // namespace TurboModuleConvertUtils + +jsi::Value +ObjCTurboModule::createPromise(jsi::Runtime &runtime, const std::string &methodName, PromiseInvocationBlock invoke) +{ + if (invoke == nullptr) { + return jsi::Value::undefined(); + } + + jsi::Function Promise = runtime.global().getPropertyAsFunction(runtime, "Promise"); + + // Note: the passed invoke() block is not retained by default, so let's retain it here to help keep it longer. + // Otherwise, there's a risk of it getting released before the promise function below executes. + PromiseInvocationBlock invokeCopy = [invoke copy]; + return Promise.callAsConstructor( + runtime, + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "fn"), + 2, + [invokeCopy, jsInvoker = jsInvoker_, moduleName = name_, methodName]( + jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { + // FIXME: do not allocate this upfront + std::string moduleMethod = moduleName + "." + methodName + "()"; + + if (count != 2) { + throw std::invalid_argument( + moduleMethod + ": Promise must pass constructor function two args. Passed " + std::to_string(count) + + " args."); + } + if (!invokeCopy) { + return jsi::Value::undefined(); + } + + __block BOOL resolveWasCalled = NO; + __block std::optional> resolve( + {rt, args[0].getObject(rt).getFunction(rt), std::move(jsInvoker)}); + __block std::optional> reject( + {rt, args[1].getObject(rt).getFunction(rt), std::move(jsInvoker)}); + __block std::shared_ptr mutex = std::make_shared(); + + RCTPromiseResolveBlock resolveBlock = ^(id result) { + std::optional> localResolve; + bool alreadyResolved = false; + bool alreadyRejected = false; + { + std::lock_guard lock(*mutex); + if (!resolve || !reject) { + alreadyResolved = resolveWasCalled; + alreadyRejected = !resolveWasCalled; + } else { + resolveWasCalled = YES; + localResolve = std::move(resolve); + resolve = std::nullopt; + reject = std::nullopt; + } + } + + if (alreadyResolved) { + RCTLogError(@"%s: Tried to resolve a promise more than once.", moduleMethod.c_str()); + return; + } + + if (alreadyRejected) { + RCTLogError(@"%s: Tried to resolve a promise after it's already been rejected.", moduleMethod.c_str()); + return; + } + + localResolve->call([result](jsi::Runtime &rt, jsi::Function &jsFunction) { + jsFunction.call(rt, convertObjCObjectToJSIValue(rt, result)); + }); + }; + + RCTPromiseRejectBlock rejectBlock = ^(NSString *code, NSString *message, NSError *error) { + std::optional> localReject; + bool alreadyResolved = false; + bool alreadyRejected = false; + { + std::lock_guard lock(*mutex); + if (!resolve || !reject) { + alreadyResolved = resolveWasCalled; + alreadyRejected = !resolveWasCalled; + } else { + resolveWasCalled = NO; + localReject = std::move(reject); + reject = std::nullopt; + resolve = std::nullopt; + } + } + + if (alreadyResolved) { + RCTLogError( + @"%s: Tried to reject a promise after it's already been resolved. Message: %s", + moduleMethod.c_str(), + message.UTF8String); + return; + } + + if (alreadyRejected) { + RCTLogError( + @"%s: Tried to reject a promise more than once. Message: %s", + moduleMethod.c_str(), + message.UTF8String); + return; + } + + NSDictionary *jsErrorDetails = RCTJSErrorFromCodeMessageAndNSError(code, message, error); + localReject->call([jsErrorDetails](jsi::Runtime &rt, jsi::Function &jsFunction) { + jsFunction.call(rt, convertJSErrorDetailsToJSRuntimeError(rt, jsErrorDetails)); + }); + }; + + invokeCopy(resolveBlock, rejectBlock); + return jsi::Value::undefined(); + })); +} + +/** + * Perform method invocation on a specific queue as configured by the module class. + * This serves as a backward-compatible support for RCTBridgeModule's methodQueue API. + * + * In the future: + * - This methodQueue support may be removed for simplicity and consistency with Android. + * - ObjC module methods will be always be called from JS thread. + * They may decide to dispatch to a different queue as needed. + */ +id ObjCTurboModule::performMethodInvocation( + jsi::Runtime &runtime, + bool isSync, + const char *methodName, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation) +{ + __block id result; + __weak id weakModule = instance_; + const char *moduleName = name_.c_str(); + std::string methodNameStr{methodName}; + __block int32_t asyncCallCounter = 0; + + void (^block)() = ^{ + id strongModule = weakModule; + if (strongModule == nullptr) { + return; + } + + if (isSync) { + TurboModulePerfLogger::syncMethodCallExecutionStart(moduleName, methodName); + } else { + TurboModulePerfLogger::asyncMethodCallExecutionStart(moduleName, methodName, asyncCallCounter); + } + + @try { + [inv invokeWithTarget:strongModule]; + } @catch (NSException *exception) { + if (isSync) { + // We can only convert NSException to JSError in sync method calls. + // See https://github.com/reactwg/react-native-new-architecture/discussions/276#discussioncomment-12567155 + throw convertNSExceptionToJSError(runtime, exception, std::string{moduleName}, methodNameStr); + } else { + @throw exception; + } + } @finally { + [retainedObjectsForInvocation removeAllObjects]; + } + + if (!isSync) { + TurboModulePerfLogger::asyncMethodCallExecutionEnd(moduleName, methodName, asyncCallCounter); + return; + } + + void *rawResult; + [inv getReturnValue:&rawResult]; + result = (__bridge id)rawResult; + TurboModulePerfLogger::syncMethodCallExecutionEnd(moduleName, methodName); + }; + + if (isSync) { + nativeMethodCallInvoker_->invokeSync(methodNameStr, [&]() -> void { block(); }); + return result; + } else { + asyncCallCounter = getUniqueId(); + TurboModulePerfLogger::asyncMethodCallDispatch(moduleName, methodName); + nativeMethodCallInvoker_->invokeAsync(methodNameStr, [block, moduleName, methodNameStr]() -> void { + TraceSection s( + "RCTTurboModuleAsyncMethodInvocation", + "module", + moduleName, + "method", + methodNameStr, + "returnType", + "promise"); + block(); + }); + return nil; + } +} + +void ObjCTurboModule::performVoidMethodInvocation( + jsi::Runtime &runtime, + const char *methodName, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation) +{ + __weak id weakModule = instance_; + const char *moduleName = name_.c_str(); + std::string methodNameStr{methodName}; + __block int32_t asyncCallCounter = 0; + + void (^block)() = ^{ + id strongModule = weakModule; + if (strongModule == nullptr) { + return; + } + + if (shouldVoidMethodsExecuteSync_) { + TurboModulePerfLogger::syncMethodCallExecutionStart(moduleName, methodName); + } else { + TurboModulePerfLogger::asyncMethodCallExecutionStart(moduleName, methodName, asyncCallCounter); + } + + @try { + [inv invokeWithTarget:strongModule]; + } @catch (NSException *exception) { + throw convertNSExceptionToJSError(runtime, exception, std::string{moduleName}, methodNameStr); + } @finally { + [retainedObjectsForInvocation removeAllObjects]; + } + + if (shouldVoidMethodsExecuteSync_) { + TurboModulePerfLogger::syncMethodCallExecutionEnd(moduleName, methodName); + } else { + TurboModulePerfLogger::asyncMethodCallExecutionEnd(moduleName, methodName, asyncCallCounter); + } + + return; + }; + + if (shouldVoidMethodsExecuteSync_) { + nativeMethodCallInvoker_->invokeSync(methodNameStr, [&]() -> void { block(); }); + } else { + asyncCallCounter = getUniqueId(); + TurboModulePerfLogger::asyncMethodCallDispatch(moduleName, methodName); + nativeMethodCallInvoker_->invokeAsync(methodNameStr, [moduleName, methodNameStr, block]() -> void { + TraceSection s( + "RCTTurboModuleAsyncMethodInvocation", "module", moduleName, "method", methodNameStr, "returnType", "void"); + block(); + }); + } +} + +jsi::Value ObjCTurboModule::convertReturnIdToJSIValue( + jsi::Runtime &runtime, + const char *methodName, + TurboModuleMethodValueKind returnType, + id result) +{ + if (returnType == VoidKind) { + return jsi::Value::undefined(); + } + + if (result == (id)kCFNull || result == nil) { + return jsi::Value::null(); + } + + jsi::Value returnValue = jsi::Value::undefined(); + + // TODO: Re-use value conversion logic from existing impl, if possible. + switch (returnType) { + case VoidKind: { + break; + } + case BooleanKind: { + returnValue = convertNSNumberToJSIBoolean(runtime, (NSNumber *)result); + break; + } + case NumberKind: { + returnValue = convertNSNumberToJSINumber(runtime, (NSNumber *)result); + break; + } + case StringKind: { + returnValue = convertNSStringToJSIString(runtime, (NSString *)result); + break; + } + case ObjectKind: { + returnValue = convertNSDictionaryToJSIObject(runtime, (NSDictionary *)result); + break; + } + case ArrayKind: { + returnValue = convertNSArrayToJSIArray(runtime, (NSArray *)result); + break; + } + case FunctionKind: + throw std::runtime_error("convertReturnIdToJSIValue: FunctionKind is not supported yet."); + case PromiseKind: + throw std::runtime_error("convertReturnIdToJSIValue: PromiseKind wasn't handled properly."); + } + + return returnValue; +} + +/** + * Given a method name, and an argument index, return type of that argument. + * Prerequisite: You must wrap the method declaration inside some variant of the + * RCT_EXPORT_METHOD macro. + * + * This method returns nil if the method for which you're querying the argument type + * is not wrapped in an RCT_EXPORT_METHOD. + * + * Note: This is only being introduced for backward compatibility. It will be removed + * in the future. + */ +NSString *ObjCTurboModule::getArgumentTypeName(jsi::Runtime &runtime, NSString *methodName, int argIndex) +{ + if (methodArgumentTypeNames_ == nullptr) { + NSMutableDictionary *> *methodArgumentTypeNames = [NSMutableDictionary new]; + + unsigned int numberOfMethods; + Class cls = [instance_ class]; + Method *methods = class_copyMethodList(object_getClass(cls), &numberOfMethods); + + if (methods != nullptr) { + for (unsigned int i = 0; i < numberOfMethods; i++) { + SEL s = method_getName(methods[i]); + NSString *mName = NSStringFromSelector(s); + if (![mName hasPrefix:@"__rct_export__"]) { + continue; + } + + // Message dispatch logic from old infra + RCTMethodInfo *(*getMethodInfo)(id, SEL) = (__typeof__(getMethodInfo))objc_msgSend; + RCTMethodInfo *methodInfo = getMethodInfo(cls, s); + + NSArray *arguments; + NSString *otherMethodName = RCTParseMethodSignature(methodInfo->objcName, &arguments); + + NSMutableArray *argumentTypes = [NSMutableArray arrayWithCapacity:[arguments count]]; + for (int j = 0; j < [arguments count]; j += 1) { + [argumentTypes addObject:arguments[j].type]; + } + + NSString *normalizedOtherMethodName = [otherMethodName componentsSeparatedByString:@":"][0]; + methodArgumentTypeNames[normalizedOtherMethodName] = argumentTypes; + } + + free(methods); + } + + methodArgumentTypeNames_ = methodArgumentTypeNames; + } + + if (methodArgumentTypeNames_[methodName] != nullptr) { + assert([methodArgumentTypeNames_[methodName] count] > argIndex); + return methodArgumentTypeNames_[methodName][argIndex]; + } + + return nil; +} + +void ObjCTurboModule::setInvocationArg( + jsi::Runtime &runtime, + const char *methodName, + const std::string &objCArgType, + const jsi::Value &arg, + size_t i, + NSInvocation *inv, + NSMutableArray *retainedObjectsForInvocation) +{ + if (arg.isBool()) { + bool v = arg.getBool(); + + /** + * JS type checking ensures the Objective C argument here is either a BOOL or NSNumber*. + */ + if (objCArgType == @encode(id)) { + id objCArg = [NSNumber numberWithBool:v]; + [inv setArgument:(void *)&objCArg atIndex:i + 2]; + [retainedObjectsForInvocation addObject:objCArg]; + } else { + [inv setArgument:(void *)&v atIndex:i + 2]; + } + + return; + } + + if (arg.isNumber()) { + double v = arg.getNumber(); + + /** + * JS type checking ensures the Objective C argument here is either a double or NSNumber* or NSInteger. + */ + if (objCArgType == @encode(id)) { + id objCArg = [NSNumber numberWithDouble:v]; + [inv setArgument:(void *)&objCArg atIndex:i + 2]; + [retainedObjectsForInvocation addObject:objCArg]; + } else if (objCArgType == @encode(NSInteger)) { + NSInteger integer = v; + [inv setArgument:&integer atIndex:i + 2]; + } else { + [inv setArgument:(void *)&v atIndex:i + 2]; + } + + return; + } + + /** + * Convert arg to ObjC objects. + */ + BOOL enableModuleArgumentNSNullConversionIOS = ReactNativeFeatureFlags::enableModuleArgumentNSNullConversionIOS(); + id objCArg = convertJSIValueToObjCObject(runtime, arg, jsInvoker_, enableModuleArgumentNSNullConversionIOS); + if (objCArg != nullptr) { + NSString *methodNameNSString = @(methodName); + + /** + * Convert objects using RCTConvert. + */ + if (objCArgType == @encode(id)) { + NSString *argumentType = getArgumentTypeName(runtime, methodNameNSString, static_cast(i)); + if (argumentType != nil) { + NSString *rctConvertMethodName = [NSString stringWithFormat:@"%@:", argumentType]; + SEL rctConvertSelector = NSSelectorFromString(rctConvertMethodName); + + if ([RCTConvert respondsToSelector:rctConvertSelector]) { + // Message dispatch logic from old infra + id (*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; + id convertedObjCArg = convert([RCTConvert class], rctConvertSelector, objCArg); + + if (enableModuleArgumentNSNullConversionIOS && convertedObjCArg == (id)kCFNull) { + return; + } + + [inv setArgument:(void *)&convertedObjCArg atIndex:i + 2]; + if (convertedObjCArg != nullptr) { + [retainedObjectsForInvocation addObject:convertedObjCArg]; + } + return; + } + } + } + + /** + * Convert objects using RCTCxxConvert to structs. + */ + if ([objCArg isKindOfClass:[NSDictionary class]] && hasMethodArgConversionSelector(methodNameNSString, i)) { + SEL methodArgConversionSelector = getMethodArgConversionSelector(methodNameNSString, i); + + // Message dispatch logic from old infra (link: + // https://github.com/facebook/react-native/commit/6783694158057662fd7b11fc123c339b2b21bfe6#diff-263fc157dfce55895cdc16495b55d190R350) + RCTManagedPointer *(*convert)(id, SEL, id) = (__typeof__(convert))objc_msgSend; + RCTManagedPointer *box = convert([RCTCxxConvert class], methodArgConversionSelector, objCArg); + + void *pointer = box.voidPointer; + [inv setArgument:&pointer atIndex:i + 2]; + [retainedObjectsForInvocation addObject:box]; + return; + } + } + + /** + * Insert converted args unmodified. + */ + [inv setArgument:(void *)&objCArg atIndex:i + 2]; + if (objCArg != nullptr) { + [retainedObjectsForInvocation addObject:objCArg]; + } +} + +NSInvocation *ObjCTurboModule::createMethodInvocation( + jsi::Runtime &runtime, + bool isSync, + const char *methodName, + SEL selector, + const jsi::Value *args, + size_t count, + NSMutableArray *retainedObjectsForInvocation) +{ + const char *moduleName = name_.c_str(); + const NSObject *module = instance_; + + if (isSync) { + TurboModulePerfLogger::syncMethodCallArgConversionStart(moduleName, methodName); + } else { + TurboModulePerfLogger::asyncMethodCallArgConversionStart(moduleName, methodName); + } + + NSMethodSignature *methodSignature = [module methodSignatureForSelector:selector]; + NSInvocation *inv = [NSInvocation invocationWithMethodSignature:methodSignature]; + [inv setSelector:selector]; + + for (size_t i = 0; i < count; i++) { + const jsi::Value &arg = args[i]; + const std::string objCArgType = [methodSignature getArgumentTypeAtIndex:i + 2]; + + setInvocationArg(runtime, methodName, objCArgType, arg, i, inv, retainedObjectsForInvocation); + } + + if (isSync) { + TurboModulePerfLogger::syncMethodCallArgConversionEnd(moduleName, methodName); + } else { + TurboModulePerfLogger::asyncMethodCallArgConversionEnd(moduleName, methodName); + } + + return inv; +} + +bool ObjCTurboModule::isMethodSync(TurboModuleMethodValueKind returnType) +{ + if (isSyncModule_) { + return true; + } + + if (returnType == VoidKind && shouldVoidMethodsExecuteSync_) { + return true; + } + + return !(returnType == VoidKind || returnType == PromiseKind); +} + +ObjCTurboModule::ObjCTurboModule(const InitParams ¶ms) + : TurboModule(params.moduleName, params.jsInvoker), + instance_(params.instance), + nativeMethodCallInvoker_(params.nativeMethodCallInvoker), + isSyncModule_(params.isSyncModule), + shouldVoidMethodsExecuteSync_(params.shouldVoidMethodsExecuteSync) +{ +} + +jsi::Value ObjCTurboModule::invokeObjCMethod( + jsi::Runtime &runtime, + TurboModuleMethodValueKind returnType, + const std::string &methodNameStr, + SEL selector, + const jsi::Value *args, + size_t count) +{ + const char *moduleName = name_.c_str(); + const char *methodName = methodNameStr.c_str(); + + bool isSyncInvocation = isMethodSync(returnType); + + if (isSyncInvocation) { + TurboModulePerfLogger::syncMethodCallStart(moduleName, methodName); + } else { + TurboModulePerfLogger::asyncMethodCallStart(moduleName, methodName); + } + + NSMutableArray *retainedObjectsForInvocation = [NSMutableArray arrayWithCapacity:count + 2]; + NSInvocation *inv = createMethodInvocation( + runtime, isSyncInvocation, methodName, selector, args, count, retainedObjectsForInvocation); + + jsi::Value returnValue = jsi::Value::undefined(); + + switch (returnType) { + case PromiseKind: { + returnValue = createPromise( + runtime, methodNameStr, ^(RCTPromiseResolveBlock resolveBlock, RCTPromiseRejectBlock rejectBlock) { + RCTPromiseResolveBlock resolveCopy = [resolveBlock copy]; + RCTPromiseRejectBlock rejectCopy = [rejectBlock copy]; + [inv setArgument:(void *)&resolveCopy atIndex:count + 2]; + [inv setArgument:(void *)&rejectCopy atIndex:count + 3]; + [retainedObjectsForInvocation addObject:resolveCopy]; + [retainedObjectsForInvocation addObject:rejectCopy]; + // The return type becomes void in the ObjC side. + performMethodInvocation(runtime, isSyncInvocation, methodName, inv, retainedObjectsForInvocation); + }); + break; + } + case VoidKind: { + performVoidMethodInvocation(runtime, methodName, inv, retainedObjectsForInvocation); + if (isSyncInvocation) { + TurboModulePerfLogger::syncMethodCallReturnConversionStart(moduleName, methodName); + } + returnValue = jsi::Value::undefined(); + if (isSyncInvocation) { + TurboModulePerfLogger::syncMethodCallReturnConversionEnd(moduleName, methodName); + } + break; + } + case BooleanKind: + case NumberKind: + case StringKind: + case ObjectKind: + case ArrayKind: + case FunctionKind: { + id result = performMethodInvocation(runtime, true, methodName, inv, retainedObjectsForInvocation); + TurboModulePerfLogger::syncMethodCallReturnConversionStart(moduleName, methodName); + returnValue = convertReturnIdToJSIValue(runtime, methodName, returnType, result); + TurboModulePerfLogger::syncMethodCallReturnConversionEnd(moduleName, methodName); + } break; + } + + if (isSyncInvocation) { + TurboModulePerfLogger::syncMethodCallEnd(moduleName, methodName); + } else { + TurboModulePerfLogger::asyncMethodCallEnd(moduleName, methodName); + } + + return returnValue; +} + +BOOL ObjCTurboModule::hasMethodArgConversionSelector(NSString *methodName, size_t argIndex) +{ + return (methodArgConversionSelectors_ != nullptr) && (methodArgConversionSelectors_[methodName] != nullptr) && + ![methodArgConversionSelectors_[methodName][argIndex] isEqual:(id)kCFNull]; +} + +SEL ObjCTurboModule::getMethodArgConversionSelector(NSString *methodName, size_t argIndex) +{ + assert(hasMethodArgConversionSelector(methodName, argIndex)); + return (SEL)((NSValue *)methodArgConversionSelectors_[methodName][argIndex]).pointerValue; +} + +void ObjCTurboModule::setMethodArgConversionSelector(NSString *methodName, size_t argIndex, NSString *fnName) +{ + if (methodArgConversionSelectors_ == nullptr) { + methodArgConversionSelectors_ = [NSMutableDictionary new]; + } + + if (methodArgConversionSelectors_[methodName] == nullptr) { + auto metaData = methodMap_.at([methodName UTF8String]); + auto argCount = metaData.argCount; + + methodArgConversionSelectors_[methodName] = [NSMutableArray arrayWithCapacity:argCount]; + for (int i = 0; i < argCount; i += 1) { + [methodArgConversionSelectors_[methodName] addObject:(id)kCFNull]; + } + } + + SEL selector = NSSelectorFromString(fnName); + NSValue *selectorValue = [NSValue valueWithPointer:selector]; + + methodArgConversionSelectors_[methodName][argIndex] = selectorValue; +} + +void ObjCTurboModule::setEventEmitterCallback(EventEmitterCallback eventEmitterCallback) +{ + if ([instance_ conformsToProtocol:@protocol(RCTTurboModule)] && + [instance_ respondsToSelector:@selector(setEventEmitterCallback:)]) { + EventEmitterCallbackWrapper *wrapper = [EventEmitterCallbackWrapper new]; + wrapper->_eventEmitterCallback = std::move(eventEmitterCallback); + [(id)instance_ setEventEmitterCallback:wrapper]; + } +} + +} // namespace facebook::react + +@implementation EventEmitterCallbackWrapper +@end diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleManager.h b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleManager.h new file mode 100644 index 00000000..254f78e6 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleManager.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#import + +#import +#import +#import +#import "RuntimeExecutor.h" +#import + +#import "RCTTurboModule.h" + +@class RCTBridgeProxy; +@class RCTTurboModuleManager; + +@protocol RCTTurboModuleManagerDelegate + +/** + * Given a module name, return its actual class. If nil is returned, basic ObjC class lookup is performed. + */ +- (Class)getModuleClassFromName:(const char *)name; + +/** + * Given a module class, provide an instance for it. If nil is returned, default initializer is used. + */ +- (id)getModuleInstanceFromClass:(Class)moduleClass; + +@optional + +/** + * This method is used to retrieve a factory object that can create a `facebook::react::TurboModule`, + * The class implementing `RCTTurboModuleProvider` must be an Objective-C class so that we can + * initialize it dynamically with Codegen. + */ +- (id)getModuleProvider:(const char *)name; + +/** + * Create an instance of a TurboModule without relying on any ObjC++ module instance. + */ +- (std::shared_ptr)getTurboModule:(const std::string &)name + jsInvoker: + (std::shared_ptr)jsInvoker; + +/** + * Return a pre-initialized list of leagcy native modules. + * These modules shouldn't be TurboModule-compatible (i.e: they should not conform to RCTTurboModule). + * + * This method is only used by the TurboModule interop layer. + * + * It must match the signature of RCTBridgeDelegate extraModulesForBridge: + * - (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge; + */ +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge + __attribute((deprecated("Please make all native modules returned from this method TurboModule-compatible."))); + +@end + +@interface RCTTurboModuleManager : NSObject + +- (instancetype)initWithBridge:(RCTBridge *)bridge + delegate:(id)delegate + jsInvoker:(std::shared_ptr)jsInvoker; + +- (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy + bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator + delegate:(id)delegate + jsInvoker:(std::shared_ptr)jsInvoker; + +- (void)installJSBindings:(facebook::jsi::Runtime &)runtime; + +- (void)invalidate; + +@end diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleManager.mm b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleManager.mm new file mode 100644 index 00000000..5168bbf4 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleManager.mm @@ -0,0 +1,1106 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTTurboModuleManager.h" +#import "RCTInteropTurboModule.h" + +#import +#import +#import +#import + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +using namespace facebook; +using namespace facebook::react; + +/** + * A global variable whose address we use to associate method queues to id objects. + */ +static char kAssociatedMethodQueueKey; + +namespace { +int32_t getUniqueId() +{ + static std::atomic counter{0}; + return counter++; +} + +class ModuleHolder { + private: + const int32_t moduleId_; + id module_; + bool isTryingToCreateModule_; + bool isDoneCreatingModule_; + std::mutex mutex_; + std::condition_variable cv_; + + public: + ModuleHolder() : moduleId_(getUniqueId()), module_(nil), isTryingToCreateModule_(false), isDoneCreatingModule_(false) + { + } + + int32_t getModuleId() const + { + return moduleId_; + } + + void setModule(id module) + { + module_ = module; + } + + id getModule() const + { + return module_; + } + + void startCreatingModule() + { + isTryingToCreateModule_ = true; + } + + void endCreatingModule() + { + isTryingToCreateModule_ = false; + isDoneCreatingModule_ = true; + } + + bool isDoneCreatingModule() const + { + return isDoneCreatingModule_; + } + + bool isCreatingModule() const + { + return isTryingToCreateModule_; + } + + std::mutex &mutex() + { + return mutex_; + } + + std::condition_variable &cv() + { + return cv_; + } +}; + +class ModuleNativeMethodCallInvoker : public NativeMethodCallInvoker { + private: + dispatch_queue_t methodQueue_; + + public: + ModuleNativeMethodCallInvoker(dispatch_queue_t methodQueue) : methodQueue_(methodQueue) {} + void invokeAsync(const std::string &methodName, std::function &&work) noexcept override + { + if (methodQueue_ == RCTJSThread) { + work(); + return; + } + + __block auto retainedWork = std::move(work); + dispatch_async(methodQueue_, ^{ + retainedWork(); + }); + } + + void invokeSync(const std::string &methodName, std::function &&work) override + { + work(); + } +}; + +class LegacyModuleNativeMethodCallInvoker : public ModuleNativeMethodCallInvoker { + bool requiresMainQueueSetup_; + + public: + LegacyModuleNativeMethodCallInvoker(dispatch_queue_t methodQueue, bool requiresMainQueueSetup) + : ModuleNativeMethodCallInvoker(methodQueue), requiresMainQueueSetup_(requiresMainQueueSetup) + { + } + + void invokeSync(const std::string &methodName, std::function &&work) override + { + if (requiresMainQueueSetup_ && methodName == "getConstants") { + __block auto retainedWork = std::move(work); + RCTUnsafeExecuteOnMainQueueSync(^{ + retainedWork(); + }); + return; + } + + ModuleNativeMethodCallInvoker::invokeSync(methodName, std::move(work)); + } +}; + +bool isTurboModuleClass(Class cls) +{ + return [cls conformsToProtocol:@protocol(RCTTurboModule)]; +} + +bool isTurboModuleInstance(id module) +{ + return isTurboModuleClass([module class]); +} + +} // namespace + +// Fallback lookup since RCT class prefix is sometimes stripped in the existing NativeModule system. +// This will be removed in the future. +static Class getFallbackClassFromName(const char *name) +{ + Class moduleClass = NSClassFromString([NSString stringWithUTF8String:name]); + if (!moduleClass) { + moduleClass = NSClassFromString([NSString stringWithFormat:@"RCT%s", name]); + } + return moduleClass; +} + +typedef struct { + id module; + dispatch_queue_t methodQueue; +} ModuleQueuePair; + +@implementation RCTTurboModuleManager { + std::shared_ptr _jsInvoker; + __weak id _delegate; + __weak RCTBridge *_bridge; + + /** + * TODO(T48018690): + * All modules are currently long-lived. + * We need to come up with a mechanism to allow modules to specify whether + * they want to be long-lived or short-lived. + * + * All instances of ModuleHolder are owned by the _moduleHolders map. + * We only reference ModuleHolders via pointers to entries in the _moduleHolders map. + */ + std::unordered_map _moduleHolders; + std::unordered_map> _turboModuleCache; + std::unordered_map> _legacyModuleCache; + + // Enforce synchronous access into _delegate + std::mutex _turboModuleManagerDelegateMutex; + + // Enforce synchronous access to _invalidating and _moduleHolders + std::mutex _moduleHoldersMutex; + std::atomic _invalidating; + + NSDictionary> *_legacyEagerlyInitializedModules; + NSDictionary *_legacyEagerlyRegisteredModuleClasses; + + RCTBridgeProxy *_bridgeProxy; + RCTBridgeModuleDecorator *_bridgeModuleDecorator; + + dispatch_queue_t _sharedModuleQueue; +} + +- (instancetype)initWithBridge:(RCTBridge *)bridge + bridgeProxy:(RCTBridgeProxy *)bridgeProxy + bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator + delegate:(id)delegate + jsInvoker:(std::shared_ptr)jsInvoker +{ + if (self = [super init]) { + _jsInvoker = std::move(jsInvoker); + _delegate = delegate; + _bridge = bridge; + _bridgeProxy = bridgeProxy; + _bridgeModuleDecorator = bridgeModuleDecorator; + _invalidating = false; + _sharedModuleQueue = dispatch_queue_create("com.meta.react.turbomodulemanager.queue", DISPATCH_QUEUE_SERIAL); + + if (RCTTurboModuleInteropEnabled()) { + // TODO(T174674274): Implement lazy loading of legacy modules in the new architecture. + NSMutableDictionary> *legacyInitializedModules = [NSMutableDictionary new]; + + if ([_delegate respondsToSelector:@selector(extraModulesForBridge:)]) { + for (id module in [_delegate extraModulesForBridge:nil]) { + if (!isTurboModuleInstance(module)) { + [legacyInitializedModules setObject:module forKey:RCTBridgeModuleNameForClass([module class])]; + } + } + } + _legacyEagerlyInitializedModules = legacyInitializedModules; + + NSMutableDictionary *legacyEagerlyRegisteredModuleClasses = [NSMutableDictionary new]; + for (Class moduleClass in RCTGetModuleClasses()) { + if (!isTurboModuleClass(moduleClass)) { + [legacyEagerlyRegisteredModuleClasses setObject:moduleClass forKey:RCTBridgeModuleNameForClass(moduleClass)]; + } + } + _legacyEagerlyRegisteredModuleClasses = legacyEagerlyRegisteredModuleClasses; + } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(bridgeWillInvalidateModules:) + name:RCTBridgeWillInvalidateModulesNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(bridgeDidInvalidateModules:) + name:RCTBridgeDidInvalidateModulesNotification + object:nil]; + } + return self; +} + +- (instancetype)initWithBridge:(RCTBridge *)bridge + delegate:(id)delegate + jsInvoker:(std::shared_ptr)jsInvoker +{ + return [self initWithBridge:bridge + bridgeProxy:nil + bridgeModuleDecorator:[bridge bridgeModuleDecorator] + delegate:delegate + jsInvoker:jsInvoker]; +} + +- (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy + bridgeModuleDecorator:(RCTBridgeModuleDecorator *)bridgeModuleDecorator + delegate:(id)delegate + jsInvoker:(std::shared_ptr)jsInvoker +{ + return [self initWithBridge:nil + bridgeProxy:bridgeProxy + bridgeModuleDecorator:bridgeModuleDecorator + delegate:delegate + jsInvoker:jsInvoker]; +} + +/** + * Given a name for a TurboModule, return a C++ object which is the instance + * of that TurboModule C++ class. This class wraps the TurboModule's ObjC instance. + * If no TurboModule ObjC class exist with the provided name, abort program. + * + * Note: All TurboModule instances are cached, which means they're all long-lived + * (for now). + */ + +- (std::shared_ptr)provideTurboModule:(const char *)moduleName runtime:(jsi::Runtime *)runtime +{ + auto turboModuleLookup = _turboModuleCache.find(moduleName); + if (turboModuleLookup != _turboModuleCache.end()) { + TurboModulePerfLogger::moduleJSRequireBeginningCacheHit(moduleName); + TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName); + return turboModuleLookup->second; + } + + TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName); + + /** + * Step 1: Look for pure C++ modules. + * Pure C++ modules get priority. + */ + if ([_delegate respondsToSelector:@selector(getTurboModule:jsInvoker:)]) { + int32_t moduleId = getUniqueId(); + TurboModulePerfLogger::moduleCreateStart(moduleName, moduleId); + auto turboModule = [_delegate getTurboModule:moduleName jsInvoker:_jsInvoker]; + if (turboModule != nullptr) { + _turboModuleCache.insert({moduleName, turboModule}); + TurboModulePerfLogger::moduleCreateEnd(moduleName, moduleId); + return turboModule; + } + + TurboModulePerfLogger::moduleCreateFail(moduleName, moduleId); + } + + auto &cxxTurboModuleMapProvider = globalExportedCxxTurboModuleMap(); + auto it = cxxTurboModuleMapProvider.find(moduleName); + if (it != cxxTurboModuleMapProvider.end()) { + auto turboModule = it->second(_jsInvoker); + _turboModuleCache.insert({moduleName, turboModule}); + return turboModule; + } + + /** + * Step 2: Look for platform-specific modules. + */ + id module = [self _moduleProviderForName:moduleName]; + + TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName); + + // If we request that a TurboModule be created, its respective ObjC class must exist + // If the class doesn't exist, then _provideObjCModule returns nil + if (!module) { + return nullptr; + } + + std::shared_ptr nativeMethodCallInvoker = nullptr; + dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(module, &kAssociatedMethodQueueKey); + if (methodQueue) { + /** + * Step 2c: Create and native CallInvoker from the TurboModule's method queue. + */ + nativeMethodCallInvoker = std::make_shared(methodQueue); + + /** + * Have RCTCxxBridge decorate native CallInvoker, so that it's aware of TurboModule async method calls. + * This helps the bridge fire onBatchComplete as readily as it should. + */ + if ([_bridge respondsToSelector:@selector(decorateNativeMethodCallInvoker:)]) { + nativeMethodCallInvoker = [_bridge decorateNativeMethodCallInvoker:nativeMethodCallInvoker]; + } + } + + /** + * Step 2d: If the moduleClass is a legacy CxxModule, return a TurboCxxModule instance that + * wraps CxxModule. + */ + Class moduleClass = [module class]; + if ([moduleClass isSubclassOfClass:RCTCxxModule.class]) { + // Use TurboCxxModule compat class to wrap the CxxModule instance. + // This is only for migration convenience, despite less performant. + auto turboModule = std::make_shared([((RCTCxxModule *)module) createModule], _jsInvoker); + _turboModuleCache.insert({moduleName, turboModule}); + return turboModule; + } + + /** + * Step 2e: Return an exact sub-class of ObjC TurboModule + * + * Use respondsToSelector: below to infer conformance to @protocol(RCTTurboModule). Using conformsToProtocol: is + * expensive. + */ + if ([module respondsToSelector:@selector(getTurboModule:)]) { + ObjCTurboModule::InitParams params = { + .moduleName = moduleName, + .instance = (id)module, + .jsInvoker = _jsInvoker, + .nativeMethodCallInvoker = nativeMethodCallInvoker, + .isSyncModule = methodQueue == RCTJSThread, + .shouldVoidMethodsExecuteSync = (bool)RCTTurboModuleSyncVoidMethodsEnabled(), + }; + + auto turboModule = [(id)module getTurboModule:params]; + if (turboModule == nullptr) { + RCTLogError(@"TurboModule \"%@\"'s getTurboModule: method returned nil.", moduleClass); + } + _turboModuleCache.insert({moduleName, turboModule}); + + if ([module respondsToSelector:@selector(installJSIBindingsWithRuntime:callInvoker:)]) { + [(id)module installJSIBindingsWithRuntime:*runtime callInvoker:_jsInvoker]; + } else if ([module respondsToSelector:@selector(installJSIBindingsWithRuntime:)]) { + // Old API without CallInvoker (deprecated) + [(id)module installJSIBindingsWithRuntime:*runtime]; + } + return turboModule; + } + + return nullptr; +} + +- (std::shared_ptr)provideLegacyModule:(const char *)moduleName +{ + auto legacyModuleLookup = _legacyModuleCache.find(moduleName); + if (legacyModuleLookup != _legacyModuleCache.end()) { + TurboModulePerfLogger::moduleJSRequireBeginningCacheHit(moduleName); + TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName); + return legacyModuleLookup->second; + } + + TurboModulePerfLogger::moduleJSRequireBeginningEnd(moduleName); + + // Create platform-specific native module object + id module = + [self _isLegacyModule:moduleName] ? [self _provideObjCModule:moduleName moduleProvider:nil] : nil; + + TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName); + + // If we request that a TurboModule be created, its respective ObjC class must exist + // If the class doesn't exist, then provideRCTBridgeModule returns nil + if (!module) { + return nullptr; + } + + Class moduleClass = [module class]; + + dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(module, &kAssociatedMethodQueueKey); + if (methodQueue == nil) { + RCTLogError(@"Legacy NativeModule \"%@\" was not associated with a method queue.", moduleClass); + } + + // Create a native call invoker from module's method queue + std::shared_ptr nativeMethodCallInvoker = + std::make_shared(methodQueue, [self _requiresMainQueueSetup:moduleClass]); + + // If module is a legacy cxx module, return TurboCxxModule + if ([moduleClass isSubclassOfClass:RCTCxxModule.class]) { + // Use TurboCxxModule compat class to wrap the CxxModule instance. + // This is only for migration convenience, despite less performant. + auto turboModule = std::make_shared([((RCTCxxModule *)module) createModule], _jsInvoker); + _legacyModuleCache.insert({moduleName, turboModule}); + return turboModule; + } + + // Create interop module + ObjCTurboModule::InitParams params = { + .moduleName = moduleName, + .instance = module, + .jsInvoker = _jsInvoker, + .nativeMethodCallInvoker = std::move(nativeMethodCallInvoker), + .isSyncModule = methodQueue == RCTJSThread, + .shouldVoidMethodsExecuteSync = (bool)RCTTurboModuleSyncVoidMethodsEnabled(), + }; + + auto turboModule = std::make_shared(params); + _legacyModuleCache.insert({moduleName, turboModule}); + return turboModule; +} + +#pragma mark - Private Methods + +- (BOOL)_isTurboModule:(const char *)moduleName +{ + Class moduleClass = [self _getModuleClassFromName:moduleName]; + return moduleClass != nil && (isTurboModuleClass(moduleClass) && ![moduleClass isSubclassOfClass:RCTCxxModule.class]); +} + +- (BOOL)_isLegacyModule:(const char *)moduleName +{ + Class moduleClass = [self _getModuleClassFromName:moduleName]; + return [self _isLegacyModuleClass:moduleClass]; +} + +- (BOOL)_isLegacyModuleClass:(Class)moduleClass +{ + return moduleClass != nil && (!isTurboModuleClass(moduleClass) || [moduleClass isSubclassOfClass:RCTCxxModule.class]); +} + +- (id)_moduleProviderForName:(const char *)moduleName +{ + id moduleProvider = nil; + if ([_delegate respondsToSelector:@selector(getModuleProvider:)]) { + moduleProvider = [_delegate getModuleProvider:moduleName]; + } + + if (RCTTurboModuleInteropEnabled() && ![self _isTurboModule:moduleName] && !moduleProvider) { + return nil; + } + + if (moduleProvider) { + if ([moduleProvider conformsToProtocol:@protocol(RCTTurboModule)]) { + // moduleProvider is also a TM, we need to initialize objectiveC properties, like the dispatch queue + return (id)[self _provideObjCModule:moduleName moduleProvider:moduleProvider]; + } + // module is Cxx module + return moduleProvider; + } + + // No module provider, the Module is registered without Codegen + return (id)[self _provideObjCModule:moduleName moduleProvider:nil]; +} + +- (ModuleHolder *)_getOrCreateModuleHolder:(const char *)moduleName +{ + std::lock_guard guard(_moduleHoldersMutex); + if (_invalidating) { + return nullptr; + } + + return &_moduleHolders[moduleName]; +} +/** + * Given a name for a NativeModule, return an ObjC object which is the instance + * of that NativeModule ObjC class. If no NativeModule exist with the provided name, + * return nil. + * + * Note: All NativeModule instances are cached, which means they're all long-lived + * (for now). + */ +- (id)_provideObjCModule:(const char *)moduleName moduleProvider:(id)moduleProvider +{ + if (strncmp("RCT", moduleName, 3) == 0) { + moduleName = [[[NSString stringWithUTF8String:moduleName] substringFromIndex:3] UTF8String]; + } + + ModuleHolder *moduleHolder = [self _getOrCreateModuleHolder:moduleName]; + + if (!moduleHolder) { + return nil; + } + + TurboModulePerfLogger::moduleCreateStart(moduleName, moduleHolder->getModuleId()); + id module = [self _provideObjCModule:moduleName + moduleHolder:moduleHolder + shouldPerfLog:YES + moduleProvider:moduleProvider]; + + if (module) { + TurboModulePerfLogger::moduleCreateEnd(moduleName, moduleHolder->getModuleId()); + } else { + TurboModulePerfLogger::moduleCreateFail(moduleName, moduleHolder->getModuleId()); + } + + return module; +} + +- (id)_provideObjCModule:(const char *)moduleName + moduleHolder:(ModuleHolder *)moduleHolder + shouldPerfLog:(BOOL)shouldPerfLog + moduleProvider:(id)moduleProvider +{ + bool shouldCreateModule = false; + + { + std::lock_guard guard(moduleHolder->mutex()); + + if (moduleHolder->isDoneCreatingModule()) { + if (shouldPerfLog) { + TurboModulePerfLogger::moduleCreateCacheHit(moduleName, moduleHolder->getModuleId()); + } + return moduleHolder->getModule(); + } + + if (!moduleHolder->isCreatingModule()) { + shouldCreateModule = true; + moduleHolder->startCreatingModule(); + } + } + + if (shouldCreateModule) { + /** + * Step 2a: Resolve platform-specific class. + */ + Class moduleClass = moduleProvider ? [moduleProvider class] : [self _getModuleClassFromName:moduleName]; + + __block id module = nil; + + if ([self _shouldCreateObjCModule:moduleClass]) { + __weak __typeof(self) weakSelf = self; + dispatch_block_t work = ^{ + auto strongSelf = weakSelf; + if (!strongSelf) { + return; + } + module = [strongSelf _createAndSetUpObjCModule:moduleClass moduleName:moduleName moduleId:moduleHolder + ->getModuleId()]; + }; + + if ([self _requiresMainQueueSetup:moduleClass]) { + RCTUnsafeExecuteOnMainQueueSync(work); + } else { + work(); + } + } + + { + std::lock_guard guard(moduleHolder->mutex()); + + moduleHolder->setModule(module); + moduleHolder->endCreatingModule(); + } + moduleHolder->cv().notify_all(); + + return module; + } + + std::unique_lock guard(moduleHolder->mutex()); + + while (moduleHolder->isCreatingModule()) { + /** + * TODO(T65905574): + * If the thread responsible for creating and initializing the NativeModule stalls, we'll wait here indefinitely. + * This is the behaviour in legacy NativeModules. Changing this now could lead to more crashes/problems in + * TurboModules than in NativeModules, which'll make it more difficult to test the TurboModules infra. Therefore, + * we should consider making it post TurboModule 100% rollout. + */ + moduleHolder->cv().wait(guard); + } + + return moduleHolder->getModule(); +} + +- (BOOL)_shouldCreateObjCModule:(Class)moduleClass +{ + if (RCTTurboModuleInteropEnabled()) { + return [moduleClass conformsToProtocol:@protocol(RCTBridgeModule)]; + } + + return [moduleClass conformsToProtocol:@protocol(RCTTurboModule)]; +} + +/** + * Given a NativeModule class, and its name, create and initialize it synchronously. + * + * This method can be called synchronously from two different contexts: + * - The thread that calls _provideObjCModule: + * - The main thread (if the NativeModule requires main queue init), blocking the thread that calls + * _provideObjCModule:. + */ +- (id)_createAndSetUpObjCModule:(Class)moduleClass + moduleName:(const char *)moduleName + moduleId:(int32_t)moduleId +{ + id module = nil; + + /** + * Step 2b: Ask hosting application/delegate to instantiate this class + */ + + TurboModulePerfLogger::moduleCreateConstructStart(moduleName, moduleId); + module = [self _getModuleInstanceFromClass:moduleClass]; + TurboModulePerfLogger::moduleCreateConstructEnd(moduleName, moduleId); + + TurboModulePerfLogger::moduleCreateSetUpStart(moduleName, moduleId); + + /** + * It is reasonable for NativeModules to not want/need the bridge. + * In such cases, they won't have `@synthesize bridge = _bridge` in their + * implementation, and a `- (RCTBridge *) bridge { ... }` method won't be + * generated by the ObjC runtime. The property will also not be backed + * by an ivar, which makes writing to it unsafe. Therefore, we check if + * this method exists to know if we can safely set the bridge to the + * NativeModule. + */ + if ([module respondsToSelector:@selector(bridge)] && (_bridge || _bridgeProxy)) { + /** + * Just because a NativeModule has the `bridge` method, it doesn't mean + * that it has synthesized the bridge in its implementation. Therefore, + * we need to surround the code that sets the bridge to the NativeModule + * inside a try/catch. This catches the cases where the NativeModule + * author specifies a `bridge` method manually. + */ + @try { + /** + * RCTBridgeModule declares the bridge property as readonly. + * Therefore, when authors of NativeModules synthesize the bridge + * via @synthesize bridge = bridge;, the ObjC runtime generates + * only a - (RCTBridge *) bridge: { ... } method. No setter is + * generated, so we have have to rely on the KVC API of ObjC to set + * the bridge property of these NativeModules. + */ + if (_bridge) { + [(id)module setValue:_bridge forKey:@"bridge"]; + } else if (_bridgeProxy) { + [(id)module setValue:_bridgeProxy forKey:@"bridge"]; + } + } @catch (NSException *exception) { + RCTLogError( + @"%@ has no setter or ivar for its bridge, which is not " + "permitted. You must either @synthesize the bridge property, " + "or provide your own setter method.", + RCTBridgeModuleNameForClass([module class])); + } + } + + // This is a more performant alternative for conformsToProtocol:@protocol(RCTCallInvokerModule) + if ([module respondsToSelector:@selector(setCallInvoker:)]) { + RCTCallInvoker *callInvoker = [[RCTCallInvoker alloc] initWithCallInvoker:_jsInvoker]; + [(id)module setCallInvoker:callInvoker]; + } + + /** + * Some modules need their own queues, but don't provide any, so we need to create it for them. + * These modules typically have the following: + * `@synthesize methodQueue = _methodQueue` + */ + + dispatch_queue_t methodQueue = nil; + BOOL moduleHasMethodQueueGetter = [module respondsToSelector:@selector(methodQueue)]; + + if (moduleHasMethodQueueGetter) { + methodQueue = [(id)module methodQueue]; + } + + /** + * Note: RCTJSThread, which is a valid method queue, is defined as (id)kCFNull. It should rightfully not enter the + * following if condition's block. + */ + if (!methodQueue) { + methodQueue = _sharedModuleQueue; + + if (moduleHasMethodQueueGetter) { + /** + * If the module has a method queue getter, two cases are possible: + * - We @synthesized the method queue. In this case, the getter will initially return nil. + * - We had a custom methodQueue function on the NativeModule. If we got this far, then that getter returned + * nil. + * + * Therefore, we do a try/catch and use ObjC's KVC API and try to assign the method queue to the NativeModule. + * In case 1, we'll succeed. In case 2, an exception will be thrown, which we'll ignore. + */ + + @try { + [(id)module setValue:methodQueue forKey:@"methodQueue"]; + } @catch (NSException *exception) { + RCTLogError( + @"%@ has no setter or ivar for its methodQueue, which is not " + "permitted. You must either @synthesize the methodQueue property, " + "or provide your own setter method.", + RCTBridgeModuleNameForClass([module class])); + } + } + } + + /** + * Decorate NativeModules with bridgeless-compatible APIs that call into the bridge. + */ + if (_bridgeModuleDecorator) { + [_bridgeModuleDecorator attachInteropAPIsToModule:module]; + } + + /** + * If the NativeModule conforms to RCTInitializing, invoke its initialize method. + */ + if ([module respondsToSelector:@selector(initialize)]) { + [(id)module initialize]; + } + + /** + * Attach method queue to id object. + * This is necessary because the id object can be eagerly created/initialized before the method + * queue is required. The method queue is required for an id for JS -> Native calls. So, we need it + * before we create the id's TurboModule jsi::HostObject in provideTurboModule:runtime:. + */ + objc_setAssociatedObject(module, &kAssociatedMethodQueueKey, methodQueue, OBJC_ASSOCIATION_RETAIN); + + /** + * NativeModules that implement the RCTFrameUpdateObserver protocol + * require registration with RCTDisplayLink. + * + * TODO(T55504345): Investigate whether we can improve this after TM + * rollout. + */ + if (_bridge) { + RCTModuleData *data = [[RCTModuleData alloc] initWithModuleInstance:(id)module + bridge:_bridge + moduleRegistry:_bridge.moduleRegistry + viewRegistry_DEPRECATED:nil + bundleManager:nil + callableJSModules:nil]; + [_bridge registerModuleForFrameUpdates:(id)module withModuleData:data]; + } + + /** + * Broadcast that this NativeModule was created. + * + * TODO(T41180176): Investigate whether we can delete this after TM + * rollout. + */ + [[NSNotificationCenter defaultCenter] + postNotificationName:RCTDidInitializeModuleNotification + object:_bridge + userInfo:@{@"module" : module, @"bridge" : RCTNullIfNil([_bridge parentBridge])}]; + + TurboModulePerfLogger::moduleCreateSetUpEnd(moduleName, moduleId); + + return module; +} + +- (Class)_getModuleClassFromName:(const char *)moduleName +{ + NSString *moduleNameStr = @(moduleName); + if (_legacyEagerlyInitializedModules && _legacyEagerlyInitializedModules[moduleNameStr]) { + return [_legacyEagerlyInitializedModules[moduleNameStr] class]; + } + + if (_legacyEagerlyRegisteredModuleClasses && _legacyEagerlyRegisteredModuleClasses[moduleNameStr]) { + return _legacyEagerlyRegisteredModuleClasses[moduleNameStr]; + } + + Class moduleClass = [_delegate getModuleClassFromName:moduleName]; + + if (moduleClass != nil) { + return moduleClass; + } + + moduleClass = getFallbackClassFromName(moduleName); + if (moduleClass != nil) { + return moduleClass; + } + + // fallback on modules registered throught RCT_EXPORT_MODULE with custom names + NSString *objcModuleName = [NSString stringWithUTF8String:moduleName]; + NSArray *modules = RCTGetModuleClasses(); + for (Class current in modules) { + NSString *currentModuleName = [current moduleName]; + if ([objcModuleName isEqualToString:currentModuleName]) { + return current; + } + } + + return moduleClass; +} + +- (id)_getModuleInstanceFromClass:(Class)moduleClass +{ + NSString *moduleNameStr = RCTBridgeModuleNameForClass(moduleClass); + if (_legacyEagerlyInitializedModules && _legacyEagerlyInitializedModules[moduleNameStr]) { + return _legacyEagerlyInitializedModules[moduleNameStr]; + } + + if (_legacyEagerlyRegisteredModuleClasses && _legacyEagerlyRegisteredModuleClasses[moduleNameStr]) { + return [_legacyEagerlyRegisteredModuleClasses[moduleNameStr] new]; + } + + id module = (id)[_delegate getModuleInstanceFromClass:moduleClass]; + + if (!module) { + module = [moduleClass new]; + } + + return module; +} + +/** + * Should this NativeModule be created and initialized on the main queue? + * + * For NativeModule ObjC classes that implement requiresMainQueueInit, return the result of this method. + * For NativeModule ObjC classes that don't. Return true if they have a custom init or constantsToExport method. + */ +- (BOOL)_requiresMainQueueSetup:(Class)moduleClass +{ + const BOOL implementsRequireMainQueueSetup = [moduleClass respondsToSelector:@selector(requiresMainQueueSetup)]; + if (implementsRequireMainQueueSetup) { + return [moduleClass requiresMainQueueSetup]; + } + + /** + * WARNING! + * This following logic exists for backwards compatibility with the legacy NativeModule system. + * + * TODO(T65864302) Remove the following logic after TM 100% rollout + */ + + /** + * If a module overrides `constantsToExport` and doesn't implement `requiresMainQueueSetup`, then we must assume + * that it must be called on the main thread, because it may need to access UIKit. + */ + BOOL hasConstantsToExport = [moduleClass instancesRespondToSelector:@selector(constantsToExport)]; + + static IMP objectInitMethod; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + objectInitMethod = [NSObject instanceMethodForSelector:@selector(init)]; + }); + + /** + * If a module overrides `init` then we must assume that it expects to be initialized on the main thread, because it + * may need to access UIKit. + */ + const BOOL hasCustomInit = [moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod; + + return hasConstantsToExport || hasCustomInit; +} + +- (void)installJSBindings:(facebook::jsi::Runtime &)runtime +{ + /** + * We keep TurboModuleManager alive until the JS VM is deleted. + * It is perfectly valid to only use/create TurboModules from JS. + * In such a case, we shouldn't dealloc TurboModuleManager if there + * aren't any strong references to it in ObjC. Hence, we give + * __turboModuleProxy a strong reference to TurboModuleManager. + */ + auto turboModuleProvider = [self, + runtime = &runtime](const std::string &name) -> std::shared_ptr { + auto moduleName = name.c_str(); + + TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName); + auto moduleWasNotInitialized = ![self moduleIsInitialized:moduleName]; + if (moduleWasNotInitialized) { + [self->_bridge.performanceLogger markStartForTag:RCTPLTurboModuleSetup]; + } + + /** + * By default, all TurboModules are long-lived. + * Additionally, if a TurboModule with the name `name` isn't found, then we + * trigger an assertion failure. + */ + auto turboModule = [self provideTurboModule:moduleName runtime:runtime]; + + if (moduleWasNotInitialized && [self moduleIsInitialized:moduleName]) { + [self->_bridge.performanceLogger markStopForTag:RCTPLTurboModuleSetup]; + } + + if (turboModule) { + TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName); + } else { + TurboModulePerfLogger::moduleJSRequireEndingFail(moduleName); + } + return turboModule; + }; + + if (RCTTurboModuleInteropEnabled()) { + auto legacyModuleProvider = [self](const std::string &name) -> std::shared_ptr { + auto moduleName = name.c_str(); + + TurboModulePerfLogger::moduleJSRequireBeginningStart(moduleName); + + /** + * By default, all TurboModules are long-lived. + * Additionally, if a TurboModule with the name `name` isn't found, then we + * trigger an assertion failure. + */ + auto turboModule = [self provideLegacyModule:moduleName]; + + if (turboModule) { + TurboModulePerfLogger::moduleJSRequireEndingEnd(moduleName); + } else { + TurboModulePerfLogger::moduleJSRequireEndingFail(moduleName); + } + return turboModule; + }; + + TurboModuleBinding::install(runtime, std::move(turboModuleProvider), std::move(legacyModuleProvider)); + } else { + TurboModuleBinding::install(runtime, std::move(turboModuleProvider)); + } +} + +#pragma mark RCTTurboModuleRegistry + +- (id)moduleForName:(const char *)moduleName +{ + return [self moduleForName:moduleName warnOnLookupFailure:YES]; +} + +- (id)moduleForName:(const char *)moduleName warnOnLookupFailure:(BOOL)warnOnLookupFailure +{ + // When the bridge is invalidating, TurboModules will be nil. + // Therefore, don't (1) do the lookup, and (2) warn on lookup. + if (_invalidating) { + return nil; + } + + id module = [self _provideObjCModule:moduleName moduleProvider:nil]; + + if (warnOnLookupFailure && !module) { + RCTLogError(@"Unable to find module for %@", [NSString stringWithUTF8String:moduleName]); + } + + return module; +} + +- (BOOL)moduleIsInitialized:(const char *)moduleName +{ + std::unique_lock guard(_moduleHoldersMutex); + return _moduleHolders.find(moduleName) != _moduleHolders.end(); +} + +#pragma mark Invalidation logic + +- (void)bridgeWillInvalidateModules:(NSNotification *)notification +{ + RCTBridge *bridge = notification.userInfo[@"bridge"]; + if (bridge != _bridge) { + return; + } + + [self _enterInvalidatingState]; +} + +- (void)bridgeDidInvalidateModules:(NSNotification *)notification +{ + RCTBridge *bridge = notification.userInfo[@"bridge"]; + if (bridge != _bridge) { + return; + } + + [self _invalidateModules]; +} + +- (void)invalidate +{ + [self _enterInvalidatingState]; + [self _invalidateModules]; +} + +- (void)_enterInvalidatingState +{ + // This should halt all insertions into _moduleHolders + std::lock_guard guard(_moduleHoldersMutex); + _invalidating = true; +} + +- (void)_invalidateModules +{ + // Backward-compatibility: RCTInvalidating handling. + dispatch_group_t moduleInvalidationGroup = dispatch_group_create(); + std::vector modulesToInvalidate; + for (auto &pair : _moduleHolders) { + std::string moduleName = pair.first; + ModuleHolder *moduleHolder = &pair.second; + + /** + * We could start tearing down ReactNative before a NativeModule is fully initialized. In this case, we should wait + * for NativeModule init to finish before calling invalidate on it. So, we call + * _provideObjCModule:moduleHolder, because it's guaranteed to return a fully initialized NativeModule. + */ + id module = [self _provideObjCModule:moduleName.c_str() + moduleHolder:moduleHolder + shouldPerfLog:NO + moduleProvider:nil]; + + if ([module respondsToSelector:@selector(invalidate)]) { + dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(module, &kAssociatedMethodQueueKey); + + if (methodQueue == nil) { + RCTLogError( + @"TurboModuleManager: Couldn't invalidate NativeModule \"%@\", because its method queue is nil.", + [module class]); + continue; + } + modulesToInvalidate.push_back({module, methodQueue}); + } + } + + for (auto unused : modulesToInvalidate) { + dispatch_group_enter(moduleInvalidationGroup); + } + + for (auto &moduleQueuePair : modulesToInvalidate) { + id module = moduleQueuePair.module; + dispatch_queue_t methodQueue = moduleQueuePair.methodQueue; + + dispatch_block_t invalidateModule = ^{ + [((id)module) invalidate]; + dispatch_group_leave(moduleInvalidationGroup); + }; + + if (_bridge) { + [_bridge dispatchBlock:invalidateModule queue:methodQueue]; + } else { + // Bridgeless mode + if (methodQueue == RCTJSThread) { + invalidateModule(); + } else { + dispatch_async(methodQueue, invalidateModule); + } + } + } + + if (dispatch_group_wait(moduleInvalidationGroup, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) { + RCTLogError(@"TurboModuleManager: Timed out waiting for modules to be invalidated"); + } + + _moduleHolders.clear(); + _turboModuleCache.clear(); + _legacyModuleCache.clear(); +} + +@end diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleWithJSIBindings.h b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleWithJSIBindings.h new file mode 100644 index 00000000..60e29bd1 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RCTTurboModuleWithJSIBindings.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#ifdef __cplusplus +#include +#include +#endif + +@protocol RCTTurboModuleWithJSIBindings + +#ifdef __cplusplus + +@optional +- (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)runtime + callInvoker:(const std::shared_ptr &)callinvoker; + +- (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)runtime + __attribute__((deprecated("Use 'installJSIBindingsWithRuntime:callInvoker:' instead"))); + +#endif + +@end diff --git a/packages/core/platforms/ios/lib_core/React/ReactCommon/RuntimeExecutor.h b/packages/core/platforms/ios/lib_core/React/ReactCommon/RuntimeExecutor.h new file mode 100644 index 00000000..a1322de8 --- /dev/null +++ b/packages/core/platforms/ios/lib_core/React/ReactCommon/RuntimeExecutor.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "jsi.h" + +namespace facebook::react { + +/* + * Takes a function and calls it with a reference to a Runtime. The function + * will be called when it is safe to do so (i.e. it ensures non-concurrent + * access) and may be invoked asynchronously, depending on the implementation. + * If you need to access a Runtime, it's encouraged to use a RuntimeExecutor + * instead of storing a pointer to the Runtime itself, which makes it more + * difficult to ensure that the Runtime is being accessed safely. + */ +using RuntimeExecutor = + std::function&& callback)>; + +} // namespace facebook::react