react-native/React/CxxBridge/RCTCxxBridge.mm

1326 lines
45 KiB
Plaintext

// Copyright 2004-present Facebook. All Rights Reserved.
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#include <atomic>
#include <future>
#import <React/RCTAssert.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTBridgeMethod.h>
#import <React/RCTConvert.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTCxxModule.h>
#import <React/RCTCxxUtils.h>
#import <React/RCTDevSettings.h>
#import <React/RCTDisplayLink.h>
#import <React/RCTJavaScriptLoader.h>
#import <React/RCTLog.h>
#import <React/RCTModuleData.h>
#import <React/RCTPerformanceLogger.h>
#import <React/RCTProfile.h>
#import <React/RCTRedBox.h>
#import <React/RCTUtils.h>
#import <React/RCTFollyConvert.h>
#import <cxxreact/CxxNativeModule.h>
#import <cxxreact/Instance.h>
#import <cxxreact/JSBundleType.h>
#import <cxxreact/JSCExecutor.h>
#import <cxxreact/JSIndexedRAMBundle.h>
#import <cxxreact/Platform.h>
#import <jschelpers/Value.h>
#import "NSDataBigString.h"
#import "RCTJSCHelpers.h"
#import "RCTMessageThread.h"
#import "RCTObjcExecutor.h"
#ifdef WITH_FBSYSTRACE
#import <React/RCTFBSystrace.h>
#endif
#if RCT_DEV && __has_include("RCTDevLoadingView.h")
#import "RCTDevLoadingView.h"
#endif
@interface RCTCxxBridge : RCTBridge
@end
#define RCTAssertJSThread() \
RCTAssert(self.executorClass || self->_jsThread == [NSThread currentThread], \
@"This method must be called on JS thread")
static NSString *const RCTJSThreadName = @"com.facebook.react.JavaScript";
using namespace facebook::react;
/**
* Must be kept in sync with `MessageQueue.js`.
*/
typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
RCTBridgeFieldRequestModuleIDs = 0,
RCTBridgeFieldMethodIDs,
RCTBridgeFieldParams,
RCTBridgeFieldCallID,
};
namespace {
class GetDescAdapter : public JSExecutorFactory {
public:
GetDescAdapter(RCTCxxBridge *bridge, std::shared_ptr<JSExecutorFactory> factory)
: bridge_(bridge)
, factory_(factory) {}
std::unique_ptr<JSExecutor> createJSExecutor(
std::shared_ptr<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> jsQueue) override {
auto ret = factory_->createJSExecutor(delegate, jsQueue);
bridge_.bridgeDescription =
[NSString stringWithFormat:@"RCTCxxBridge %s",
ret->getDescription().c_str()];
return std::move(ret);
}
private:
RCTCxxBridge *bridge_;
std::shared_ptr<JSExecutorFactory> factory_;
};
}
static bool isRAMBundle(NSData *script) {
BundleHeader header;
[script getBytes:&header length:sizeof(header)];
return parseTypeFromHeader(header) == ScriptTag::RAMBundle;
}
static void registerPerformanceLoggerHooks(RCTPerformanceLogger *performanceLogger) {
__weak RCTPerformanceLogger *weakPerformanceLogger = performanceLogger;
ReactMarker::logTaggedMarker = [weakPerformanceLogger](const ReactMarker::ReactMarkerId markerId, const char *tag) {
switch (markerId) {
case ReactMarker::RUN_JS_BUNDLE_START:
[weakPerformanceLogger markStartForTag:RCTPLScriptExecution];
break;
case ReactMarker::RUN_JS_BUNDLE_STOP:
[weakPerformanceLogger markStopForTag:RCTPLScriptExecution];
break;
case ReactMarker::NATIVE_REQUIRE_START:
[weakPerformanceLogger appendStartForTag:RCTPLRAMNativeRequires];
break;
case ReactMarker::NATIVE_REQUIRE_STOP:
[weakPerformanceLogger appendStopForTag:RCTPLRAMNativeRequires];
[weakPerformanceLogger addValue:1 forTag:RCTPLRAMNativeRequiresCount];
break;
case ReactMarker::CREATE_REACT_CONTEXT_STOP:
case ReactMarker::JS_BUNDLE_STRING_CONVERT_START:
case ReactMarker::JS_BUNDLE_STRING_CONVERT_STOP:
case ReactMarker::NATIVE_MODULE_SETUP_START:
case ReactMarker::NATIVE_MODULE_SETUP_STOP:
// These are not used on iOS.
break;
}
};
}
@interface RCTCxxBridge ()
@property (nonatomic, weak, readonly) RCTBridge *parentBridge;
@property (nonatomic, assign, readonly) BOOL moduleSetupComplete;
- (instancetype)initWithParentBridge:(RCTBridge *)bridge;
- (void)partialBatchDidFlush;
- (void)batchDidComplete;
@end
struct RCTInstanceCallback : public InstanceCallback {
__weak RCTCxxBridge *bridge_;
RCTInstanceCallback(RCTCxxBridge *bridge): bridge_(bridge) {};
void onBatchComplete() override {
// There's no interface to call this per partial batch
[bridge_ partialBatchDidFlush];
[bridge_ batchDidComplete];
}
};
@implementation RCTCxxBridge
{
BOOL _wasBatchActive;
BOOL _didInvalidate;
NSMutableArray<dispatch_block_t> *_pendingCalls;
std::atomic<NSInteger> _pendingCount;
// Native modules
NSMutableDictionary<NSString *, RCTModuleData *> *_moduleDataByName;
NSMutableArray<RCTModuleData *> *_moduleDataByID;
NSMutableArray<Class> *_moduleClassesByID;
NSUInteger _modulesInitializedOnMainQueue;
RCTDisplayLink *_displayLink;
// JS thread management
NSThread *_jsThread;
std::shared_ptr<RCTMessageThread> _jsMessageThread;
// This is uniquely owned, but weak_ptr is used.
std::shared_ptr<Instance> _reactInstance;
}
@synthesize loading = _loading;
@synthesize valid = _valid;
@synthesize performanceLogger = _performanceLogger;
@synthesize bridgeDescription = _bridgeDescription;
+ (void)initialize
{
if (self == [RCTCxxBridge class]) {
RCTPrepareJSCExecutor();
}
}
- (JSContext *)jsContext
{
return contextForGlobalContextRef([self jsContextRef]);
}
- (JSGlobalContextRef)jsContextRef
{
return (JSGlobalContextRef)(self->_reactInstance ? self->_reactInstance->getJavaScriptContext() : nullptr);
}
- (instancetype)initWithParentBridge:(RCTBridge *)bridge
{
RCTAssertParam(bridge);
if ((self = [super initWithDelegate:bridge.delegate
bundleURL:bridge.bundleURL
moduleProvider:bridge.moduleProvider
launchOptions:bridge.launchOptions])) {
_parentBridge = bridge;
_performanceLogger = [bridge performanceLogger];
registerPerformanceLoggerHooks(_performanceLogger);
RCTLogInfo(@"Initializing %@ (parent: %@, executor: %@)", self, bridge, [self executorClass]);
/**
* Set Initial State
*/
_valid = YES;
_loading = YES;
_pendingCalls = [NSMutableArray new];
_displayLink = [RCTDisplayLink new];
_moduleDataByName = [NSMutableDictionary new];
_moduleClassesByID = [NSMutableArray new];
_moduleDataByID = [NSMutableArray new];
[RCTBridge setCurrentBridge:self];
}
return self;
}
+ (void)runRunLoop
{
@autoreleasepool {
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge runJSRunLoop] setup", nil);
// copy thread name to pthread name
pthread_setname_np([NSThread currentThread].name.UTF8String);
// Set up a dummy runloop source to avoid spinning
CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
CFRelease(noSpinSource);
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
// run the run loop
while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) {
RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad.
}
}
}
- (void)_tryAndHandleError:(dispatch_block_t)block
{
NSError *error = tryAndReturnError(block);
if (error) {
[self handleError:error];
}
}
/**
* Ensure block is run on the JS thread. If we're already on the JS thread, the block will execute synchronously.
* If we're not on the JS thread, the block is dispatched to that thread. Any errors encountered while executing
* the block will go through handleError:
*/
- (void)ensureOnJavaScriptThread:(dispatch_block_t)block
{
RCTAssert(_jsThread, @"This method must not be called before the JS thread is created");
// This does not use _jsMessageThread because it may be called early before the runloop reference is captured
// and _jsMessageThread is valid. _jsMessageThread also doesn't allow us to shortcut the dispatch if we're
// already on the correct thread.
if ([NSThread currentThread] == _jsThread) {
[self _tryAndHandleError:block];
} else {
[self performSelector:@selector(_tryAndHandleError:)
onThread:_jsThread
withObject:block
waitUntilDone:NO];
}
}
- (void)start
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge start]", nil);
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptWillStartLoadingNotification
object:_parentBridge userInfo:@{@"bridge": self}];
// Set up the JS thread early
_jsThread = [[NSThread alloc] initWithTarget:[self class]
selector:@selector(runRunLoop)
object:nil];
_jsThread.name = RCTJSThreadName;
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
#if RCT_DEBUG
_jsThread.stackSize *= 2;
#endif
[_jsThread start];
dispatch_group_t prepareBridge = dispatch_group_create();
[_performanceLogger markStartForTag:RCTPLNativeModuleInit];
[self registerExtraModules];
// Initialize all native modules that cannot be loaded lazily
[self _initModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];
[_performanceLogger markStopForTag:RCTPLNativeModuleInit];
// This doesn't really do anything. The real work happens in initializeBridge.
_reactInstance.reset(new Instance);
__weak RCTCxxBridge *weakSelf = self;
// Prepare executor factory (shared_ptr for copy into block)
std::shared_ptr<JSExecutorFactory> executorFactory;
if (!self.executorClass) {
if ([self.delegate conformsToProtocol:@protocol(RCTCxxBridgeDelegate)]) {
id<RCTCxxBridgeDelegate> cxxDelegate = (id<RCTCxxBridgeDelegate>) self.delegate;
executorFactory = [cxxDelegate jsExecutorFactoryForBridge:self];
}
if (!executorFactory) {
BOOL useCustomJSC =
[self.delegate respondsToSelector:@selector(shouldBridgeUseCustomJSC:)] &&
[self.delegate shouldBridgeUseCustomJSC:self];
// The arg is a cache dir. It's not used with standard JSC.
executorFactory.reset(new JSCExecutorFactory(folly::dynamic::object
("OwnerIdentity", "ReactNative")
("UseCustomJSC", (bool)useCustomJSC)
#if RCT_PROFILE
("StartSamplingProfilerOnInit", (bool)self.devSettings.startSamplingProfilerOnLaunch)
#endif
));
}
} else {
id<RCTJavaScriptExecutor> objcExecutor = [self moduleForClass:self.executorClass];
executorFactory.reset(new RCTObjcExecutorFactory(objcExecutor, ^(NSError *error) {
if (error) {
[weakSelf handleError:error];
}
}));
}
// Dispatch the instance initialization as soon as the initial module metadata has
// been collected (see initModules)
dispatch_group_enter(prepareBridge);
[self ensureOnJavaScriptThread:^{
[weakSelf _initializeBridge:executorFactory];
dispatch_group_leave(prepareBridge);
}];
// Optional load and execute JS source synchronously
// TODO #10487027: should this be async on reload?
if (!self.executorClass &&
[self.delegate respondsToSelector:@selector(shouldBridgeLoadJavaScriptSynchronously:)] &&
[self.delegate shouldBridgeLoadJavaScriptSynchronously:_parentBridge]) {
NSError *error;
const int32_t bcVersion = systemJSCWrapper()->JSBytecodeFileFormatVersion;
NSData *sourceCode = [RCTJavaScriptLoader attemptSynchronousLoadOfBundleAtURL:self.bundleURL
runtimeBCVersion:bcVersion
sourceLength:NULL
error:&error];
if (error) {
[self handleError:error];
} else {
[self executeSourceCode:sourceCode sync:YES];
}
} else {
// Load the source asynchronously, then store it for later execution.
dispatch_group_enter(prepareBridge);
__block NSData *sourceCode;
[self loadSource:^(NSError *error, RCTSource *source) {
if (error) {
[weakSelf handleError:error];
}
sourceCode = source.data;
dispatch_group_leave(prepareBridge);
} onProgress:^(RCTLoadingProgress *progressData) {
#if RCT_DEV && __has_include("RCTDevLoadingView.h")
RCTDevLoadingView *loadingView = [weakSelf moduleForClass:[RCTDevLoadingView class]];
[loadingView updateProgress:progressData];
#endif
}];
// Wait for both the modules and source code to have finished loading
dispatch_group_notify(prepareBridge, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
RCTCxxBridge *strongSelf = weakSelf;
if (sourceCode && strongSelf.loading) {
[strongSelf executeSourceCode:sourceCode sync:NO];
}
});
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress
{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName:RCTBridgeWillDownloadScriptNotification object:_parentBridge];
[_performanceLogger markStartForTag:RCTPLScriptDownload];
NSUInteger cookie = RCTProfileBeginAsyncEvent(0, @"JavaScript download", nil);
// Suppress a warning if RCTProfileBeginAsyncEvent gets compiled out
(void)cookie;
RCTPerformanceLogger *performanceLogger = _performanceLogger;
RCTSourceLoadBlock onSourceLoad = ^(NSError *error, RCTSource *source) {
RCTProfileEndAsyncEvent(0, @"native", cookie, @"JavaScript download", @"JS async");
[performanceLogger markStopForTag:RCTPLScriptDownload];
[performanceLogger setValue:source.length forTag:RCTPLBundleSize];
NSDictionary *userInfo = source ? @{ RCTBridgeDidDownloadScriptNotificationSourceKey: source } : nil;
[center postNotificationName:RCTBridgeDidDownloadScriptNotification object:self->_parentBridge userInfo:userInfo];
_onSourceLoad(error, source);
};
if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:onProgress:onComplete:)]) {
[self.delegate loadSourceForBridge:_parentBridge onProgress:onProgress onComplete:onSourceLoad];
} else if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) {
[self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad];
} else if (!self.bundleURL) {
NSError *error = RCTErrorWithMessage(@"No bundle URL present.\n\nMake sure you're running a packager " \
"server or have included a .jsbundle file in your application bundle.");
onSourceLoad(error, nil);
} else {
[RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:^(NSError *error, RCTSource *source) {
if (error && [self.delegate respondsToSelector:@selector(fallbackSourceURLForBridge:)]) {
NSURL *fallbackURL = [self.delegate fallbackSourceURLForBridge:self->_parentBridge];
if (fallbackURL && ![fallbackURL isEqual:self.bundleURL]) {
RCTLogError(@"Failed to load bundle(%@) with error:(%@ %@)", self.bundleURL, error.localizedDescription, error.localizedFailureReason);
self.bundleURL = fallbackURL;
[RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:onSourceLoad];
return;
}
}
onSourceLoad(error, source);
}];
}
}
- (NSArray<Class> *)moduleClasses
{
if (RCT_DEBUG && _valid && _moduleClassesByID == nil) {
RCTLogError(@"Bridge modules have not yet been initialized. You may be "
"trying to access a module too early in the startup procedure.");
}
return _moduleClassesByID;
}
/**
* Used by RCTUIManager
*/
- (RCTModuleData *)moduleDataForName:(NSString *)moduleName
{
return _moduleDataByName[moduleName];
}
- (id)moduleForName:(NSString *)moduleName
{
return _moduleDataByName[moduleName].instance;
}
- (BOOL)moduleIsInitialized:(Class)moduleClass
{
return _moduleDataByName[RCTBridgeModuleNameForClass(moduleClass)].hasInstance;
}
- (std::shared_ptr<ModuleRegistry>)_buildModuleRegistry
{
if (!self.valid) {
return {};
}
[_performanceLogger markStartForTag:RCTPLNativeModulePrepareConfig];
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge buildModuleRegistry]", nil);
__weak __typeof(self) weakSelf = self;
ModuleRegistry::ModuleNotFoundCallback moduleNotFoundCallback = ^bool(const std::string &name) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
return [strongSelf.delegate respondsToSelector:@selector(bridge:didNotFindModule:)] &&
[strongSelf.delegate bridge:strongSelf didNotFindModule:@(name.c_str())];
};
auto registry = std::make_shared<ModuleRegistry>(
createNativeModules(_moduleDataByID, self, _reactInstance),
moduleNotFoundCallback);
[_performanceLogger markStopForTag:RCTPLNativeModulePrepareConfig];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
return registry;
}
- (void)_initializeBridge:(std::shared_ptr<JSExecutorFactory>)executorFactory
{
if (!self.valid) {
return;
}
RCTAssertJSThread();
__weak RCTCxxBridge *weakSelf = self;
_jsMessageThread = std::make_shared<RCTMessageThread>([NSRunLoop currentRunLoop], ^(NSError *error) {
if (error) {
[weakSelf handleError:error];
}
});
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge initializeBridge:]", nil);
// This can only be false if the bridge was invalidated before startup completed
if (_reactInstance) {
#if RCT_DEV
executorFactory = std::make_shared<GetDescAdapter>(self, executorFactory);
#endif
// This is async, but any calls into JS are blocked by the m_syncReady CV in Instance
_reactInstance->initializeBridge(
std::make_unique<RCTInstanceCallback>(self),
executorFactory,
_jsMessageThread,
[self _buildModuleRegistry]);
#if RCT_PROFILE
if (RCTProfileIsProfiling()) {
_reactInstance->setGlobalVariable(
"__RCTProfileIsProfiling",
std::make_unique<JSBigStdString>("true"));
}
#endif
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
- (NSArray *)configForModuleName:(NSString *)moduleName
{
RCTModuleData *moduleData = _moduleDataByName[moduleName];
if (moduleData) {
#if RCT_DEV
if ([self.delegate respondsToSelector:@selector(whitelistedModulesForBridge:)]) {
NSArray *whitelisted = [self.delegate whitelistedModulesForBridge:self];
RCTAssert(!whitelisted || [whitelisted containsObject:[moduleData moduleClass]],
@"Required config for %@, which was not whitelisted", moduleName);
}
#endif
}
return moduleData.config;
}
- (NSArray<RCTModuleData *> *)registerModulesForClasses:(NSArray<Class> *)moduleClasses
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] autoexported moduleData", nil);
NSMutableArray<RCTModuleData *> *moduleDataByID = [NSMutableArray arrayWithCapacity:moduleClasses.count];
for (Class moduleClass in moduleClasses) {
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
// Don't initialize the old executor in the new bridge.
// TODO mhorowitz #10487027: after D3175632 lands, we won't need
// this, because it won't be eagerly initialized.
if ([moduleName isEqualToString:@"RCTJSCExecutor"]) {
continue;
}
// Check for module name collisions
RCTModuleData *moduleData = _moduleDataByName[moduleName];
if (moduleData) {
if (moduleData.hasInstance) {
// Existing module was preregistered, so it takes precedence
continue;
} else if ([moduleClass new] == nil) {
// The new module returned nil from init, so use the old module
continue;
} else if ([moduleData.moduleClass new] != nil) {
// Both modules were non-nil, so it's unclear which should take precedence
RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the "
"name '%@', but name was already registered by class %@",
moduleClass, moduleName, moduleData.moduleClass);
}
}
// Instantiate moduleData
// TODO #13258411: can we defer this until config generation?
moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass bridge:self];
_moduleDataByName[moduleName] = moduleData;
[_moduleClassesByID addObject:moduleClass];
[moduleDataByID addObject:moduleData];
}
[_moduleDataByID addObjectsFromArray:moduleDataByID];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
return moduleDataByID;
}
- (void)registerExtraModules
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] extraModules", nil);
NSArray<id<RCTBridgeModule>> *extraModules = nil;
if ([self.delegate respondsToSelector:@selector(extraModulesForBridge:)]) {
extraModules = [self.delegate extraModulesForBridge:_parentBridge];
} else if (self.moduleProvider) {
extraModules = self.moduleProvider();
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
#if RCT_DEBUG
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTVerifyAllModulesExported(extraModules);
});
#endif
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] preinitialized moduleData", nil);
// Set up moduleData for pre-initialized module instances
for (id<RCTBridgeModule> module in extraModules) {
Class moduleClass = [module class];
NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
if (RCT_DEBUG) {
// Check for name collisions between preregistered modules
RCTModuleData *moduleData = _moduleDataByName[moduleName];
if (moduleData) {
RCTLogError(@"Attempted to register RCTBridgeModule class %@ for the "
"name '%@', but name was already registered by class %@",
moduleClass, moduleName, moduleData.moduleClass);
continue;
}
}
// Instantiate moduleData container
RCTModuleData *moduleData = [[RCTModuleData alloc] initWithModuleInstance:module bridge:self];
_moduleDataByName[moduleName] = moduleData;
[_moduleClassesByID addObject:moduleClass];
[_moduleDataByID addObject:moduleData];
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
- (void)_initModules:(NSArray<id<RCTBridgeModule>> *)modules
withDispatchGroup:(dispatch_group_t)dispatchGroup
lazilyDiscovered:(BOOL)lazilyDiscovered
{
RCTAssert(!(RCTIsMainQueue() && lazilyDiscovered), @"Lazy discovery can only happen off the Main Queue");
// Set up moduleData for automatically-exported modules
NSArray<RCTModuleData *> *moduleDataById = [self registerModulesForClasses:modules];
#ifdef RCT_DEBUG
if (lazilyDiscovered) {
// Lazily discovered modules do not require instantiation here,
// as they are not allowed to have pre-instantiated instance
// and must not require the main queue.
for (RCTModuleData *moduleData in moduleDataById) {
RCTAssert(!(moduleData.requiresMainQueueSetup || moduleData.hasInstance),
@"Module \'%@\' requires initialization on the Main Queue or has pre-instantiated, which is not supported for the lazily discovered modules.", moduleData.name);
}
}
else
#endif
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways,
@"-[RCTCxxBridge initModulesWithDispatchGroup:] moduleData.hasInstance", nil);
// Dispatch module init onto main thread for those modules that require it
// For non-lazily discovered modules we run through the entire set of modules
// that we have, otherwise some modules coming from the delegate
// or module provider block, will not be properly instantiated.
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.hasInstance && (!moduleData.requiresMainQueueSetup || RCTIsMainQueue())) {
// Modules that were pre-initialized should ideally be set up before
// bridge init has finished, otherwise the caller may try to access the
// module directly rather than via `[bridge moduleForClass:]`, which won't
// trigger the lazy initialization process. If the module cannot safely be
// set up on the current thread, it will instead be async dispatched
// to the main thread to be set up in _prepareModulesWithDispatchGroup:.
(void)[moduleData instance];
}
}
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
// From this point on, RCTDidInitializeModuleNotification notifications will
// be sent the first time a module is accessed.
_moduleSetupComplete = YES;
[self _prepareModulesWithDispatchGroup:dispatchGroup];
}
#if RCT_PROFILE
if (RCTProfileIsProfiling()) {
// Depends on moduleDataByID being loaded
RCTProfileHookModules(self);
}
#endif
}
- (void)registerAdditionalModuleClasses:(NSArray<Class> *)modules
{
[self _initModules:modules withDispatchGroup:NULL lazilyDiscovered:YES];
}
- (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup
{
RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge prepareModulesWithDispatch]", nil);
NSArray<Class> *whitelistedModules = nil;
if ([self.delegate respondsToSelector:@selector(whitelistedModulesForBridge:)]) {
whitelistedModules = [self.delegate whitelistedModulesForBridge:self];
}
BOOL initializeImmediately = NO;
if (dispatchGroup == NULL) {
// If no dispatchGroup is passed in, we must prepare everything immediately.
// We better be on the right thread too.
RCTAssertMainQueue();
initializeImmediately = YES;
} else if ([self.delegate respondsToSelector:@selector(shouldBridgeInitializeNativeModulesSynchronously:)]) {
initializeImmediately = [self.delegate shouldBridgeInitializeNativeModulesSynchronously:self];
}
// Set up modules that require main thread init or constants export
[_performanceLogger setValue:0 forTag:RCTPLNativeModuleMainThread];
for (RCTModuleData *moduleData in _moduleDataByID) {
if (whitelistedModules && ![whitelistedModules containsObject:[moduleData moduleClass]]) {
continue;
}
if (moduleData.requiresMainQueueSetup) {
// Modules that need to be set up on the main thread cannot be initialized
// lazily when required without doing a dispatch_sync to the main thread,
// which can result in deadlock. To avoid this, we initialize all of these
// modules on the main thread in parallel with loading the JS code, so
// they will already be available before they are ever required.
dispatch_block_t block = ^{
if (self.valid && ![moduleData.moduleClass isSubclassOfClass:[RCTCxxModule class]]) {
[self->_performanceLogger appendStartForTag:RCTPLNativeModuleMainThread];
(void)[moduleData instance];
[moduleData gatherConstants];
[self->_performanceLogger appendStopForTag:RCTPLNativeModuleMainThread];
}
};
if (initializeImmediately && RCTIsMainQueue()) {
block();
} else {
// We've already checked that dispatchGroup is non-null, but this satisifies the
// Xcode analyzer
if (dispatchGroup) {
dispatch_group_async(dispatchGroup, dispatch_get_main_queue(), block);
}
}
_modulesInitializedOnMainQueue++;
}
}
[_performanceLogger setValue:_modulesInitializedOnMainQueue forTag:RCTPLNativeModuleMainThreadUsesCount];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
- (void)whitelistedModulesDidChange
{
RCTAssertMainQueue();
[self _prepareModulesWithDispatchGroup:NULL];
}
- (void)registerModuleForFrameUpdates:(id<RCTBridgeModule>)module
withModuleData:(RCTModuleData *)moduleData
{
[_displayLink registerModuleForFrameUpdates:module withModuleData:moduleData];
}
- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync
{
// This will get called from whatever thread was actually executing JS.
dispatch_block_t completion = ^{
// Flush pending calls immediately so we preserve ordering
[self _flushPendingCalls];
// Perform the state update and notification on the main thread, so we can't run into
// timing issues with RCTRootView
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptDidLoadNotification
object:self->_parentBridge userInfo:@{@"bridge": self}];
// Starting the display link is not critical to startup, so do it last
[self ensureOnJavaScriptThread:^{
// Register the display link to start sending js calls after everything is setup
[self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop]];
}];
});
};
if (sync) {
[self executeApplicationScriptSync:sourceCode url:self.bundleURL];
completion();
} else {
[self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion];
}
#if RCT_DEV
if (self.devSettings.isHotLoadingEnabled) {
NSString *path = [self.bundleURL.path substringFromIndex:1]; // strip initial slash
NSString *host = self.bundleURL.host;
NSNumber *port = self.bundleURL.port;
[self enqueueJSCall:@"HMRClient"
method:@"enable"
args:@[@"ios", path, host, RCTNullIfNil(port)]
completion:NULL]; }
#endif
}
- (void)handleError:(NSError *)error
{
// This is generally called when the infrastructure throws an
// exception while calling JS. Most product exceptions will not go
// through this method, but through RCTExceptionManager.
// There are three possible states:
// 1. initializing == _valid && _loading
// 2. initializing/loading finished (success or failure) == _valid && !_loading
// 3. invalidated == !_valid && !_loading
// !_valid && _loading can't happen.
// In state 1: on main queue, move to state 2, reset the bridge, and RCTFatal.
// In state 2: go directly to RCTFatal. Do not enqueue, do not collect $200.
// In state 3: do nothing.
if (self->_valid && !self->_loading) {
if ([error userInfo][RCTJSRawStackTraceKey]) {
[self.redBox showErrorMessage:[error localizedDescription]
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
}
RCTFatal(error);
// RN will stop, but let the rest of the app keep going.
return;
}
if (!_valid || !_loading) {
return;
}
// Hack: once the bridge is invalidated below, it won't initialize any new native
// modules. Initialize the redbox module now so we can still report this error.
RCTRedBox *redBox = [self redBox];
_loading = NO;
_valid = NO;
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_jsMessageThread) {
// Make sure initializeBridge completed
self->_jsMessageThread->runOnQueueSync([] {});
}
self->_reactInstance.reset();
self->_jsMessageThread.reset();
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTJavaScriptDidFailToLoadNotification
object:self->_parentBridge userInfo:@{@"bridge": self, @"error": error}];
if ([error userInfo][RCTJSRawStackTraceKey]) {
[redBox showErrorMessage:[error localizedDescription]
withRawStack:[error userInfo][RCTJSRawStackTraceKey]];
}
RCTFatal(error);
});
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithDelegate:(__unused id<RCTBridgeDelegate>)delegate
bundleURL:(__unused NSURL *)bundleURL
moduleProvider:(__unused RCTBridgeModuleListProvider)block
launchOptions:(__unused NSDictionary *)launchOptions)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL
moduleProvider:(__unused RCTBridgeModuleListProvider)block
launchOptions:(__unused NSDictionary *)launchOptions)
/**
* Prevent super from calling setUp (that'd create another batchedBridge)
*/
- (void)setUp {}
- (void)reload
{
[_parentBridge reload];
}
- (Class)executorClass
{
return _parentBridge.executorClass;
}
- (void)setExecutorClass:(Class)executorClass
{
RCTAssertMainQueue();
_parentBridge.executorClass = executorClass;
}
- (NSURL *)bundleURL
{
return _parentBridge.bundleURL;
}
- (void)setBundleURL:(NSURL *)bundleURL
{
_parentBridge.bundleURL = bundleURL;
}
- (id<RCTBridgeDelegate>)delegate
{
return _parentBridge.delegate;
}
- (void)dispatchBlock:(dispatch_block_t)block
queue:(dispatch_queue_t)queue
{
if (queue == RCTJSThread) {
[self ensureOnJavaScriptThread:block];
} else if (queue) {
dispatch_async(queue, block);
}
}
#pragma mark - RCTInvalidating
- (void)invalidate
{
if (_didInvalidate) {
return;
}
RCTAssertMainQueue();
RCTLogInfo(@"Invalidating %@ (parent: %@, executor: %@)", self, _parentBridge, [self executorClass]);
_loading = NO;
_valid = NO;
_didInvalidate = YES;
if ([RCTBridge currentBridge] == self) {
[RCTBridge setCurrentBridge:nil];
}
// Stop JS instance and message thread
[self ensureOnJavaScriptThread:^{
[self->_displayLink invalidate];
self->_displayLink = nil;
if (RCTProfileIsProfiling()) {
RCTProfileUnhookModules(self);
}
// Invalidate modules
// We're on the JS thread (which we'll be suspending soon), so no new calls will be made to native modules after
// this completes. We must ensure all previous calls were dispatched before deallocating the instance (and module
// wrappers) or we may have invalid pointers still in flight.
dispatch_group_t moduleInvalidation = dispatch_group_create();
for (RCTModuleData *moduleData in self->_moduleDataByID) {
// Be careful when grabbing an instance here, we don't want to instantiate
// any modules just to invalidate them.
if (![moduleData hasInstance]) {
continue;
}
if ([moduleData.instance respondsToSelector:@selector(invalidate)]) {
dispatch_group_enter(moduleInvalidation);
[self dispatchBlock:^{
[(id<RCTInvalidating>)moduleData.instance invalidate];
dispatch_group_leave(moduleInvalidation);
} queue:moduleData.methodQueue];
}
[moduleData invalidate];
}
if (dispatch_group_wait(moduleInvalidation, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
RCTLogError(@"Timed out waiting for modules to be invalidated");
}
self->_reactInstance.reset();
self->_jsMessageThread.reset();
self->_moduleDataByName = nil;
self->_moduleDataByID = nil;
self->_moduleClassesByID = nil;
self->_pendingCalls = nil;
[self->_jsThread cancel];
self->_jsThread = nil;
CFRunLoopStop(CFRunLoopGetCurrent());
}];
}
- (void)logMessage:(NSString *)message level:(NSString *)level
{
if (RCT_DEBUG && _valid) {
[self enqueueJSCall:@"RCTLog"
method:@"logIfNoNativeHook"
args:@[level, message]
completion:NULL];
}
}
#pragma mark - RCTBridge methods
- (void)_runAfterLoad:(dispatch_block_t)block
{
// Ordering here is tricky. Ideally, the C++ bridge would provide
// functionality to defer calls until after the app is loaded. Until that
// happens, we do this. _pendingCount keeps a count of blocks which have
// been deferred. It is incremented using an atomic barrier call before each
// block is added to the js queue, and decremented using an atomic barrier
// call after the block is executed. If _pendingCount is zero, there is no
// work either in the js queue, or in _pendingCalls, so it is safe to add new
// work to the JS queue directly.
if (self.loading || _pendingCount > 0) {
// From the callers' perspecive:
// Phase 1: jsQueueBlocks are added to the queue; _pendingCount is
// incremented for each. If the first block is created after self.loading is
// true, phase 1 will be nothing.
_pendingCount++;
dispatch_block_t jsQueueBlock = ^{
// From the perspective of the JS queue:
if (self.loading) {
// Phase A: jsQueueBlocks are executed. self.loading is true, so they
// are added to _pendingCalls.
[self->_pendingCalls addObject:block];
} else {
// Phase C: More jsQueueBlocks are executed. self.loading is false, so
// each block is executed, adding work to the queue, and _pendingCount is
// decremented.
block();
self->_pendingCount--;
}
};
[self ensureOnJavaScriptThread:jsQueueBlock];
} else {
// Phase 2/Phase D: blocks are executed directly, adding work to the JS queue.
block();
}
}
- (void)_flushPendingCalls
{
// Log metrics about native requires during the bridge startup.
uint64_t nativeRequiresCount = [self->_performanceLogger valueForTag:RCTPLRAMNativeRequiresCount];
[_performanceLogger setValue:nativeRequiresCount forTag:RCTPLRAMStartupNativeRequiresCount];
uint64_t nativeRequires = [self->_performanceLogger valueForTag:RCTPLRAMNativeRequires];
[_performanceLogger setValue:nativeRequires forTag:RCTPLRAMStartupNativeRequires];
[_performanceLogger markStopForTag:RCTPLBridgeStartup];
RCT_PROFILE_BEGIN_EVENT(0, @"Processing pendingCalls", @{ @"count": [@(_pendingCalls.count) stringValue] });
// Phase B: _flushPendingCalls happens. Each block in _pendingCalls is
// executed, adding work to the queue, and _pendingCount is decremented.
// loading is set to NO.
NSArray *pendingCalls = _pendingCalls;
_pendingCalls = nil;
for (dispatch_block_t call in pendingCalls) {
call();
_pendingCount--;
}
_loading = NO;
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
/**
* Public. Can be invoked from any thread.
*/
- (void)enqueueJSCall:(NSString *)module method:(NSString *)method args:(NSArray *)args completion:(dispatch_block_t)completion
{
if (!self.valid) {
return;
}
/**
* AnyThread
*/
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge enqueueJSCall:]", nil);
RCTProfileBeginFlowEvent();
[self _runAfterLoad:^{
RCTProfileEndFlowEvent();
if (self->_reactInstance) {
self->_reactInstance->callJSFunction([module UTF8String], [method UTF8String],
convertIdToFollyDynamic(args ?: @[]));
// ensureOnJavaScriptThread may execute immediately, so use jsMessageThread, to make sure
// the block is invoked after callJSFunction
if (completion) {
if (self->_jsMessageThread) {
self->_jsMessageThread->runOnQueue(completion);
} else {
RCTLogWarn(@"Can't invoke completion without messageThread");
}
}
}
}];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
}
/**
* Called by RCTModuleMethod from any thread.
*/
- (void)enqueueCallback:(NSNumber *)cbID args:(NSArray *)args
{
if (!self.valid) {
return;
}
/**
* AnyThread
*/
RCTProfileBeginFlowEvent();
[self _runAfterLoad:^{
RCTProfileEndFlowEvent();
if (self->_reactInstance) {
self->_reactInstance->callJSCallback([cbID unsignedLongLongValue], convertIdToFollyDynamic(args ?: @[]));
}
}];
}
/**
* Private hack to support `setTimeout(fn, 0)`
*/
- (void)_immediatelyCallTimer:(NSNumber *)timer
{
RCTAssertJSThread();
if (_reactInstance) {
_reactInstance->callJSFunction("JSTimers", "callTimers",
folly::dynamic::array(folly::dynamic::array([timer doubleValue])));
}
}
- (void)enqueueApplicationScript:(NSData *)script
url:(NSURL *)url
onComplete:(dispatch_block_t)onComplete
{
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge enqueueApplicationScript]", nil);
[self _tryAndHandleError:^{
NSString *sourceUrlStr = deriveSourceURL(url);
if (isRAMBundle(script)) {
[self->_performanceLogger markStartForTag:RCTPLRAMBundleLoad];
auto ramBundle = std::make_unique<JSIndexedRAMBundle>(sourceUrlStr.UTF8String);
std::unique_ptr<const JSBigString> scriptStr = ramBundle->getStartupCode();
[self->_performanceLogger markStopForTag:RCTPLRAMBundleLoad];
[self->_performanceLogger setValue:scriptStr->size() forTag:RCTPLRAMStartupCodeSize];
if (self->_reactInstance) {
self->_reactInstance->loadUnbundle(std::move(ramBundle), std::move(scriptStr),
sourceUrlStr.UTF8String, false);
}
} else if (self->_reactInstance) {
self->_reactInstance->loadScriptFromString(std::make_unique<NSDataBigString>(script),
sourceUrlStr.UTF8String, false);
} else {
throw std::logic_error("Attempt to call loadApplicationScript: on uninitialized bridge");
}
}];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
// Assumes that onComplete can be called when the next block on the JS thread is scheduled
if (onComplete) {
RCTAssert(_jsMessageThread != nullptr, @"Cannot invoke completion without jsMessageThread");
_jsMessageThread->runOnQueue(onComplete);
}
}
- (void)executeApplicationScriptSync:(NSData *)script url:(NSURL *)url
{
[self _tryAndHandleError:^{
NSString *sourceUrlStr = deriveSourceURL(url);
if (isRAMBundle(script)) {
[self->_performanceLogger markStartForTag:RCTPLRAMBundleLoad];
auto ramBundle = std::make_unique<JSIndexedRAMBundle>(sourceUrlStr.UTF8String);
std::unique_ptr<const JSBigString> scriptStr = ramBundle->getStartupCode();
[self->_performanceLogger markStopForTag:RCTPLRAMBundleLoad];
[self->_performanceLogger setValue:scriptStr->size() forTag:RCTPLRAMStartupCodeSize];
if (self->_reactInstance) {
self->_reactInstance->loadUnbundle(std::move(ramBundle), std::move(scriptStr),
sourceUrlStr.UTF8String, true);
}
} else if (self->_reactInstance) {
self->_reactInstance->loadScriptFromString(std::make_unique<NSDataBigString>(script),
sourceUrlStr.UTF8String, true);
} else {
throw std::logic_error("Attempt to call loadApplicationScriptSync: on uninitialized bridge");
}
}];
}
- (JSValue *)callFunctionOnModule:(NSString *)module
method:(NSString *)method
arguments:(NSArray *)arguments
error:(NSError **)error
{
if (!_reactInstance) {
if (error) {
*error = RCTErrorWithMessage(
@"callFunctionOnModule was called on uninitialized bridge");
}
return nil;
} else if (self.executorClass) {
if (error) {
*error = RCTErrorWithMessage(
@"callFunctionOnModule can only be used with JSC executor");
}
return nil;
} else if (!self.valid) {
if (error) {
*error = RCTErrorWithMessage(
@"Bridge is no longer valid");
}
return nil;
} else if (self.loading) {
if (error) {
*error = RCTErrorWithMessage(
@"Bridge is still loading");
}
return nil;
}
RCT_PROFILE_BEGIN_EVENT(0, @"callFunctionOnModule", (@{ @"module": module, @"method": method }));
__block JSValue *ret = nil;
NSError *errorObj = tryAndReturnError(^{
Value result = self->_reactInstance->callFunctionSync([module UTF8String], [method UTF8String], (id)arguments);
JSContext *context = contextForGlobalContextRef(JSC_JSContextGetGlobalContext(result.context()));
ret = [JSC_JSValue(result.context()) valueWithJSValueRef:result inContext:context];
});
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call");
if (error) {
*error = errorObj;
}
return ret;
}
#pragma mark - Payload Processing
- (void)partialBatchDidFlush
{
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.implementsPartialBatchDidFlush) {
[self dispatchBlock:^{
[moduleData.instance partialBatchDidFlush];
} queue:moduleData.methodQueue];
}
}
}
- (void)batchDidComplete
{
// TODO #12592471: batchDidComplete is only used by RCTUIManager,
// can we eliminate this special case?
for (RCTModuleData *moduleData in _moduleDataByID) {
if (moduleData.implementsBatchDidComplete) {
[self dispatchBlock:^{
[moduleData.instance batchDidComplete];
} queue:moduleData.methodQueue];
}
}
}
- (void)startProfiling
{
RCTAssertMainQueue();
[self ensureOnJavaScriptThread:^{
#if WITH_FBSYSTRACE
[RCTFBSystrace registerCallbacks];
#endif
RCTProfileInit(self);
[self enqueueJSCall:@"Systrace" method:@"setEnabled" args:@[@YES] completion:NULL];
}];
}
- (void)stopProfiling:(void (^)(NSData *))callback
{
RCTAssertMainQueue();
[self ensureOnJavaScriptThread:^{
[self enqueueJSCall:@"Systrace" method:@"setEnabled" args:@[@NO] completion:NULL];
RCTProfileEnd(self, ^(NSString *log) {
NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding];
callback(logData);
#if WITH_FBSYSTRACE
[RCTFBSystrace unregisterCallbacks];
#endif
});
}];
}
- (BOOL)isBatchActive
{
return _wasBatchActive;
}
@end