diff --git a/React/CxxBridge/RCTCxxBridge.h b/React/CxxBridge/RCTCxxBridge.h new file mode 100644 index 000000000..89ac4d01e --- /dev/null +++ b/React/CxxBridge/RCTCxxBridge.h @@ -0,0 +1,17 @@ +/** + * 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. + */ + +#import + +@interface RCTCxxBridge : RCTBridge + ++ (void)enable; ++ (void)disable; + +@end diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm new file mode 100644 index 000000000..e59dbb755 --- /dev/null +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -0,0 +1,1398 @@ +// 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 "RCTCxxBridge.h" + +#include +#include +#include + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#ifdef WITH_FBSYSTRACE +#import +#endif +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import "RCTMessageThread.h" +#import "RCTNativeModule.h" +#import "RCTObjcExecutor.h" + +#define RCTAssertJSThread() \ + RCTAssert(self.executorClass || self->_jsThread == [NSThread currentThread], \ + @"This method must be called on JS thread") + +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, +}; + +static JSValueRef nativeLoggingHook( + JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + RCTLogLevel level = RCTLogLevelInfo; + if (argumentCount > 1) { + level = MAX(level, (RCTLogLevel)Value(ctx, arguments[1]).asNumber()); + } + if (argumentCount > 0) { + String message = Value(ctx, arguments[0]).toString(); + _RCTLogJavaScriptInternal(level, @(message.str().c_str())); + } + return Value::makeUndefined(ctx); +} + +static JSValueRef nativePerformanceNow( + JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + return Value::makeNumber(ctx, CACurrentMediaTime() * 1000); +} + +static bool isRAMBundle(NSData *script) { + BundleHeader header{}; + [script getBytes:&header length:sizeof(header)]; + return parseTypeFromHeader(header) == ScriptTag::RAMBundle; +} + +static std::atomic_bool cxxBridgeEnabled(false); + +namespace { + +// RCTNativeModule arranges for native methods to be invoked on a queue which +// is not the JS thread. C++ modules don't use RCTNativeModule, so this little +// adapter does the work. + +class QueueNativeModule : public NativeModule { +public: + QueueNativeModule(RCTCxxBridge *bridge, + std::unique_ptr module) + : bridge_(bridge) + , module_(std::move(module)) + // Create new queue (store queueName, as it isn't retained by dispatch_queue) + , queueName_("com.facebook.react." + module_->getName() + "Queue") + , queue_(dispatch_queue_create(queueName_.c_str(), DISPATCH_QUEUE_SERIAL)) {} + + std::string getName() override { + return module_->getName(); + } + + std::vector getMethods() override { + return module_->getMethods(); + } + + folly::dynamic getConstants() override { + return module_->getConstants(); + } + + bool supportsWebWorkers() override { + return module_->supportsWebWorkers(); + } + + void invoke(ExecutorToken token, unsigned int reactMethodId, + folly::dynamic &¶ms) override { + __weak RCTCxxBridge *bridge = bridge_; + auto module = module_; + auto cparams = std::make_shared(std::move(params)); + dispatch_async(queue_, ^{ + if (!bridge || !bridge.valid) { + return; + } + + module->invoke(token, reactMethodId, std::move(*cparams)); + }); + } + + MethodCallResult callSerializableNativeHook( + ExecutorToken token, unsigned int reactMethodId, + folly::dynamic &&args) override { + return module_->callSerializableNativeHook(token, reactMethodId, std::move(args)); + } + +private: + RCTCxxBridge *bridge_; + std::shared_ptr module_; + std::string queueName_; + dispatch_queue_t queue_; +}; + + +// This is a temporary hack. The cxx bridge assumes a single native +// queue handles all native method calls, but that's false on ios. So +// this is a no-thread passthrough, and queues are handled in +// RCTNativeModule. A similar refactoring should be done on the Java +// bridge. +class InlineMessageQueueThread : public MessageQueueThread { +public: + void runOnQueue(std::function&& func) override { func(); } + void runOnQueueSync(std::function&& func) override { func(); } + void quitSynchronous() override {} +}; + +} + +@interface RCTCxxBridge () + +@property (nonatomic, weak, readonly) RCTBridge *parentBridge; +@property (nonatomic, assign, readonly) BOOL moduleSetupComplete; + +- (instancetype)initWithParentBridge:(RCTBridge *)bridge; +- (void)partialBatchDidFlush; +- (void)batchDidComplete; +- (void)handleError:(NSError *)error; + +@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]; + } + void incrementPendingJSCalls() override {} + void decrementPendingJSCalls() override {} + void onNativeException(const std::string &what) override { + [bridge_ handleError:RCTErrorWithMessage(@(what.c_str()))]; + } + ExecutorToken createExecutorToken() override { + return ExecutorToken(std::make_shared()); + } + void onExecutorStopped(ExecutorToken) override {} +}; + +@implementation RCTBridge (CxxBridge) + +- (void)CXX_createBatchedBridge +{ + if (cxxBridgeEnabled) { + self.batchedBridge = [[RCTCxxBridge alloc] initWithParentBridge:self]; + } else { + self.batchedBridge = [[RCTBatchedBridge alloc] initWithParentBridge:self]; + } +} + +@end + +@implementation RCTCxxBridge +{ + BOOL _wasBatchActive; + + NSMutableArray *_pendingCalls; + // This is accessed using OSAtomic... calls. + volatile int32_t _pendingCount; + + // Native modules + NSMutableDictionary *_moduleDataByName; + NSArray *_moduleDataByID; + NSArray *_moduleClassesByID; + NSUInteger _modulesInitializedOnMainQueue; + RCTDisplayLink *_displayLink; + + // JS thread management + NSThread *_jsThread; + std::promise> _jsMessageThreadPromise; + std::shared_ptr _jsMessageThread; + + // This is uniquely owned, but weak_ptr is used. + std::shared_ptr _reactInstance; +} + +@synthesize loading = _loading; +@synthesize valid = _valid; +@synthesize performanceLogger = _performanceLogger; + ++ (void)initialize +{ + if (self == [RCTCxxBridge class]) { + ReactMarker::logMarker = [](const std::string&) {}; + PerfLogging::installNativeHooks = [](JSGlobalContextRef){}; + JSNativeHooks::loggingHook = nativeLoggingHook; + JSNativeHooks::nowHook = nativePerformanceNow; + } +} + ++ (void)swizzleBridge +{ + // Swizzle RCTBridge to use this, instead of RCTBatchedBridge + static dispatch_once_t once; + dispatch_once(&once, ^{ + RCTSwapInstanceMethods([RCTBridge class], + NSSelectorFromString(@"createBatchedBridge"), + @selector(CXX_createBatchedBridge)); + }); +} + ++ (void)enable +{ + [RCTCxxBridge swizzleBridge]; +#ifdef WITH_FBSYSTRACE + [RCTFBSystrace registerCallbacks]; +#endif + cxxBridgeEnabled = true; +} + ++ (void)disable +{ + [RCTCxxBridge swizzleBridge]; +#ifdef WITH_FBSYSTRACE + [RCTFBSystrace unregisterCallbacks]; +#endif + cxxBridgeEnabled = false; +} + +- (JSContext *)jsContext +{ + return contextForGlobalContextRef((JSGlobalContextRef) self->_reactInstance->getJavaScriptContext()); +} + +- (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]; + + RCTLogInfo(@"Initializing %@ (parent: %@, executor: %@)", self, bridge, [self executorClass]); + + /** + * Set Initial State + */ + _valid = YES; + _loading = YES; + _pendingCalls = [NSMutableArray new]; + _displayLink = [RCTDisplayLink new]; + + [RCTBridge setCurrentBridge:self]; + } + return self; +} + +- (void)runJSRunLoop +{ + @autoreleasepool { + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge runJSRunLoop] setup", nil); + + // copy thread name to pthread name + pthread_setname_np([NSThread currentThread].name.UTF8String); + + __typeof(self) weakSelf = self; + _jsMessageThreadPromise.set_value(std::make_shared( + [NSRunLoop currentRunLoop], + ^(NSError *error) { + if (error) { + [weakSelf handleError:error]; + } + })); + + // 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. + } + } +} + +static NSError *tryAndReturnError(dispatch_block_t block) { + // TODO #10487027: This is mostly duplicated in RCTMessageThread. + try { + @try { + block(); + return nil; + } + @catch (NSException *exception) { + NSString *message = + [NSString stringWithFormat:@"Exception '%@' was thrown from JS thread", exception]; + return RCTErrorWithMessage(message); + } + @catch (id exception) { + // This will catch any other ObjC exception, but no C++ exceptions + return RCTErrorWithMessage(@"non-std ObjC Exception"); + } + } catch (const JSException &ex) { + // This is a special case. We want to extract the stack + // information and pass it to the redbox. This will lose the C++ + // stack, but it's of limited value. + NSDictionary *errorInfo = @{ + RCTJSRawStackTraceKey: @(ex.getStack().c_str()), + NSLocalizedDescriptionKey: [@"Unhandled JS Exception: " stringByAppendingString:@(ex.what())] + }; + return [NSError errorWithDomain:RCTErrorDomain code:1 userInfo:errorInfo]; + } catch (const std::exception &ex) { + return RCTErrorWithMessage(@(ex.what())); + } catch (...) { + // On a 64-bit platform, this would catch ObjC exceptions, too, but not on + // 32-bit platforms, so we catch those with id exceptions above. + return RCTErrorWithMessage(@"non-std C++ exception"); + } +} + +- (void)_tryAndHandleError:(dispatch_block_t)block +{ + NSError *error = tryAndReturnError(block); + if (error) { + [self handleError:error]; + } +} + +- (void)executeBlockOnJavaScriptThread:(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. + + if ([NSThread currentThread] == _jsThread) { + [self _tryAndHandleError:block]; + } else { + [self performSelector:@selector(_tryAndHandleError:) + onThread:_jsThread + withObject:block + waitUntilDone:NO]; + } +} + +- (void)start +{ + [[NSNotificationCenter defaultCenter] + postNotificationName:RCTJavaScriptWillStartLoadingNotification + object:_parentBridge userInfo:@{@"bridge": self}]; + + // Set up the JS thread early + _jsThread = [[NSThread alloc] initWithTarget:self + selector:@selector(runJSRunLoop) + object:nil]; + _jsThread.name = RCTJSThreadName; + _jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive; + [_jsThread start]; + + dispatch_group_t initModulesAndLoadSource = dispatch_group_create(); + dispatch_queue_t bridgeQueue = dispatch_queue_create("com.facebook.react.RCTBridgeQueue", + DISPATCH_QUEUE_SERIAL); + + // Initialize all native modules that cannot be loaded lazily + [self _initModulesWithDispatchGroup:initModulesAndLoadSource]; + + // This doesn't really do anything. The real work happens in initializeBridge. + _reactInstance.reset(new Instance); + + // Prepare executor factory (shared_ptr for copy into block) + __weak RCTCxxBridge *weakSelf = self; + std::shared_ptr executorFactory; + if (!self.executorClass) { + 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 + ("UseCustomJSC", (bool)useCustomJSC) +#if RCT_PROFILE + ("StartSamplingProfilerOnInit", (bool)self.devMenu.startSamplingProfilerOnLaunch) +#endif + )); + } else { + id 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_async(initModulesAndLoadSource, bridgeQueue, ^{ + [weakSelf _initializeBridge:executorFactory]; + }); + + // 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(initModulesAndLoadSource); + __block NSData *sourceCode; + [self loadSource:^(NSError *error, NSData *source, int64_t sourceLength) { + if (error) { + [weakSelf handleError:error]; + } + + sourceCode = source; + dispatch_group_leave(initModulesAndLoadSource); + } onProgress:^(RCTLoadingProgress *progressData) { +#ifdef RCT_DEV + 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(initModulesAndLoadSource, bridgeQueue, ^{ + RCTCxxBridge *strongSelf = weakSelf; + if (sourceCode && strongSelf.loading) { + [strongSelf executeSourceCode:sourceCode sync:NO]; + } + }); + } +} + +- (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad onProgress:(RCTSourceLoadProgressBlock)onProgress +{ + [_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, NSData *source, int64_t sourceLength) { + RCTProfileEndAsyncEvent(0, @"native", cookie, @"JavaScript download", @"JS async"); + [performanceLogger markStopForTag:RCTPLScriptDownload]; + [performanceLogger setValue:sourceLength forTag:RCTPLBundleSize]; + _onSourceLoad(error, source, sourceLength); + }; + + 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, 0); + } else { + [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:^(NSError *error, NSData *source, int64_t sourceLength) { + 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); + self.bundleURL = fallbackURL; + [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onProgress:onProgress onComplete:onSourceLoad]; + return; + } + } + onSourceLoad(error, source, sourceLength); + }]; + } +} + +- (NSArray *)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)_createModuleRegistry +{ + if (!self.valid) { + return {}; + } + + [_performanceLogger markStartForTag:RCTPLNativeModulePrepareConfig]; + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge createModuleRegistry]", nil); + + std::vector> modules; + for (RCTModuleData *moduleData in _moduleDataByID) { + // TODO mhorowitz #10487027: unwrap C++ modules and register them directly. + if ([moduleData.moduleClass isSubclassOfClass:[RCTCxxModule class]]) { + RCTCxxModule *cxxInstance = moduleData.instance; + // If a module does not support automatic instantiation, and + // wasn't provided as an extra module, it may not have an + // instance. If so, skip it. + if (!cxxInstance) { + continue; + } + modules.emplace_back( + new QueueNativeModule(self, std::make_unique( + _reactInstance, [cxxInstance move]))); + } else { + modules.emplace_back(new RCTNativeModule(self, moduleData)); + } + } + + auto registry = std::make_shared(std::move(modules)); + + [_performanceLogger markStopForTag:RCTPLNativeModulePrepareConfig]; + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + + return registry; +} + +- (void)_initializeBridge:(std::shared_ptr)executorFactory +{ + if (!self.valid) { + return; + } + + if (!_jsMessageThread) { + _jsMessageThread = _jsMessageThreadPromise.get_future().get(); + } + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTCxxBridge initializeBridge:]", nil); + // This can only be false if the bridge was invalidated before startup completed + if (_reactInstance) { + // This is async, but any calls into JS are blocked by the m_syncReady CV in Instance + _reactInstance->initializeBridge( + std::unique_ptr(new RCTInstanceCallback(self)), + executorFactory, + _jsMessageThread, + std::unique_ptr(new InlineMessageQueueThread), + std::move([self _createModuleRegistry])); + +#if RCT_PROFILE + if (RCTProfileIsProfiling()) { + _reactInstance->setGlobalVariable( + "__RCTProfileIsProfiling", + std::make_unique("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; +} + +- (void)_initModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup +{ + [_performanceLogger markStartForTag:RCTPLNativeModuleInit]; + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"-[RCTCxxBridge initModulesWithDispatchGroup:] extraModules", nil); + NSArray> *extraModules = nil; + if (self.delegate) { + 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 + + NSMutableArray *moduleClassesByID = [NSMutableArray new]; + NSMutableArray *moduleDataByID = [NSMutableArray new]; + NSMutableDictionary *moduleDataByName = [NSMutableDictionary new]; + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"-[RCTCxxBridge initModulesWithDispatchGroup:] preinitialized moduleData", nil); + // Set up moduleData for pre-initialized module instances + for (id 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, @""); + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"-[RCTCxxBridge initModulesWithDispatchGroup:] autoexported moduleData", nil); + // Set up moduleData for automatically-exported modules + for (Class moduleClass in RCTGetModuleClasses()) { + 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 isEqual:@"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]; + } + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + + // Store modules + _moduleDataByID = [moduleDataByID copy]; + _moduleDataByName = [moduleDataByName copy]; + _moduleClassesByID = [moduleClassesByID copy]; + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"-[RCTCxxBridge initModulesWithDispatchGroup:] moduleData.hasInstance", nil); + // Dispatch module init onto main thead for those modules that require it + 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 the loop below. + (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]; + + [_performanceLogger markStopForTag:RCTPLNativeModuleInit]; + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + +#if RCT_PROFILE + if (RCTProfileIsProfiling()) { + // Depends on moduleDataByID being loaded + RCTProfileHookModules(self); + } +#endif +} + +- (void)_prepareModulesWithDispatchGroup:(dispatch_group_t)dispatchGroup +{ + RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBatchedBridge prepareModulesWithDispatch]", nil); + + NSArray *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 || moduleData.hasConstantsToExport) { + // 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)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 executeBlockOnJavaScriptThread:^{ + // 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 ([RCTGetURLQueryParam(self.bundleURL, @"hot") boolValue]) { + 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. + [self redBox]; + + _loading = NO; + _valid = NO; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (self->_jsMessageThread) { + auto thread = self->_jsMessageThread; + self->_jsMessageThread->runOnQueue([thread] { + thread->quitSynchronous(); + }); + self->_jsMessageThread.reset(); + } + + [[NSNotificationCenter defaultCenter] + postNotificationName:RCTJavaScriptDidFailToLoadNotification + object:self->_parentBridge userInfo:@{@"bridge": self, @"error": error}]; + + if ([error userInfo][RCTJSRawStackTraceKey]) { + [self.redBox showErrorMessage:[error localizedDescription] + withRawStack:[error userInfo][RCTJSRawStackTraceKey]]; + } + + RCTFatal(error); + }); +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL + moduleProvider:(__unused RCTBridgeModuleProviderBlock)block + launchOptions:(__unused NSDictionary *)launchOptions) + +/** + * Prevent super from calling setUp (that'd create another batchedBridge) + */ +- (void)setUp {} +- (void)bindKeys {} + +- (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)delegate +{ + return _parentBridge.delegate; +} + +- (void)dispatchBlock:(dispatch_block_t)block + queue:(dispatch_queue_t)queue +{ + if (queue == RCTJSThread) { + [self executeBlockOnJavaScriptThread:block]; + } else if (queue) { + dispatch_async(queue, block); + } +} + +#pragma mark - RCTInvalidating + +- (void)invalidate +{ + if (!_valid) { + return; + } + + RCTAssertMainQueue(); + RCTAssert(_reactInstance != nil, @"Can't complete invalidation without a react instance"); + + _loading = NO; + _valid = NO; + if ([RCTBridge currentBridge] == self) { + [RCTBridge setCurrentBridge:nil]; + } + + // Invalidate modules + dispatch_group_t group = dispatch_group_create(); + for (RCTModuleData *moduleData in _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(group); + [self dispatchBlock:^{ + [(id)moduleData.instance invalidate]; + dispatch_group_leave(group); + } queue:moduleData.methodQueue]; + } + [moduleData invalidate]; + } + + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + [self executeBlockOnJavaScriptThread:^{ + [self->_displayLink invalidate]; + self->_displayLink = nil; + + self->_reactInstance.reset(); + if (self->_jsMessageThread) { + self->_jsMessageThread->quitSynchronous(); + self->_jsMessageThread.reset(); + } + + if (RCTProfileIsProfiling()) { + RCTProfileUnhookModules(self); + } + + self->_moduleDataByName = nil; + self->_moduleDataByID = nil; + self->_moduleClassesByID = nil; + self->_pendingCalls = nil; + }]; + }); +} + +- (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. + OSAtomicIncrement32Barrier(&_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(); + OSAtomicDecrement32Barrier(&self->_pendingCount); + } + }; + [self executeBlockOnJavaScriptThread:jsQueueBlock]; + } else { + // Phase 2/Phase D: blocks are executed directly, adding work to the JS queue. + block(); + } +} + +- (void)_flushPendingCalls +{ + [_performanceLogger markStopForTag:RCTPLBridgeStartup]; + + RCT_PROFILE_BEGIN_EVENT(0, @"Processing pendingCalls", @{ @"count": @(_pendingCalls.count) }); + // 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(); + OSAtomicDecrement32Barrier(&_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(self->_reactInstance->getMainExecutorToken(), + [module UTF8String], [method UTF8String], + [RCTConvert folly_dynamic:args ?: @[]]); + + if (completion) { + [self executeBlockOnJavaScriptThread:completion]; + } + } + }]; + + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); +} + +/** + * Called by RCTModuleMethod from any thread. + */ +- (void)enqueueCallback:(NSNumber *)cbID args:(NSArray *)args +{ + /** + * AnyThread + */ + + RCTProfileBeginFlowEvent(); + + [self _runAfterLoad:^{ + RCTProfileEndFlowEvent(); + + if (self->_reactInstance) + self->_reactInstance->callJSCallback( + self->_reactInstance->getMainExecutorToken(), [cbID unsignedLongLongValue], + [RCTConvert folly_dynamic:args ?: @[]]); + }]; +} + +/** + * Private hack to support `setTimeout(fn, 0)` + */ +- (void)_immediatelyCallTimer:(NSNumber *)timer +{ + RCTAssertJSThread(); + + if (_reactInstance) + _reactInstance->callJSFunction(_reactInstance->getMainExecutorToken(), + "JSTimersExecution", "callTimers", + folly::dynamic::array(folly::dynamic::array([timer doubleValue]))); +} + +- (void)enqueueApplicationScript:(NSData *)script + url:(NSURL *)url + onComplete:(dispatch_block_t)onComplete +{ + RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"-[RCTCxxBridge enqueueApplicationScript]", nil); + + [self _tryAndHandleError:^{ + if (isRAMBundle(script)) { + auto ramBundle = std::make_unique(url.path.UTF8String); + std::unique_ptr scriptStr = ramBundle->getStartupCode(); + if (self->_reactInstance) + self->_reactInstance->loadUnbundle(std::move(ramBundle), std::move(scriptStr), + [[url absoluteString] UTF8String]); + } else if (self->_reactInstance) { + auto bigbuf = std::make_unique([script length]); + memcpy(bigbuf->data(), [script bytes], bigbuf->size()); + + self->_reactInstance->loadScriptFromString(std::move(bigbuf), + [[url absoluteString] UTF8String]); + } + }]; + + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + + // Assumes that onComplete can be called when the next block on the JS thread is scheduled + [self executeBlockOnJavaScriptThread:^{ + onComplete(); + }]; +} + +- (void)executeApplicationScriptSync:(NSData *)script url:(NSURL *)url +{ + [self _tryAndHandleError:^{ + if (isRAMBundle(script)) { + auto ramBundle = std::make_unique(url.path.UTF8String); + std::unique_ptr scriptStr = ramBundle->getStartupCode(); + if (self->_reactInstance) { + self->_reactInstance->loadUnbundleSync(std::move(ramBundle), std::move(scriptStr), + [[url absoluteString] UTF8String]); + } + } else if (self->_reactInstance) { + auto bigbuf = std::make_unique([script length]); + memcpy(bigbuf->data(), [script bytes], bigbuf->size()); + + self->_reactInstance->loadScriptFromStringSync(std::move(bigbuf), [[url absoluteString] UTF8String]); + } else { + throw std::logic_error( + "Attempt to call loadApplicationScriptSync: on uninitialized bridge"); + } + }]; +} + +static JSContext *contextForGlobalContextRef(JSGlobalContextRef contextRef) +{ + static std::mutex s_mutex; + static NSMapTable *s_contextCache; + + if (!contextRef) { + return nil; + } + + // Adding our own lock here, since JSC internal ones are insufficient + std::lock_guard lock(s_mutex); + if (!s_contextCache) { + NSPointerFunctionsOptions keyOptions = NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality; + NSPointerFunctionsOptions valueOptions = NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPersonality; + s_contextCache = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:valueOptions capacity:0]; + } + + JSContext *ctx = [s_contextCache objectForKey:(__bridge id)contextRef]; + if (!ctx) { + ctx = [JSC_JSContext(contextRef) contextWithJSGlobalContextRef:contextRef]; + [s_contextCache setObject:ctx forKey:(__bridge id)contextRef]; + } + return ctx; +} + +/* + * The ValueEncoder::toValue is used by callFunctionSync below. + * Note: Because the NSArray * is really a NSArray * __strong the toValue is + * accepting NSArray *const __strong instead of NSArray *&&. + */ +template <> +struct ValueEncoder { + static Value toValue(JSGlobalContextRef ctx, NSArray *const __strong array) + { + JSValue *value = [JSValue valueWithObject:array inContext:contextForGlobalContextRef(ctx)]; + return {ctx, [value JSValueRef]}; + } +}; + +- (JSValue *)callFunctionOnModule:(NSString *)module + method:(NSString *)method + arguments:(NSArray *)arguments + error:(NSError **)error +{ + if (!_reactInstance) { + if (error) { + *error = RCTErrorWithMessage( + @"Attempt to call sync callFunctionOnModule: on uninitialized bridge"); + } + return nil; + } else if (self.executorClass) { + if (error) { + *error = RCTErrorWithMessage( + @"sync callFunctionOnModule: can only be used with JSC executor"); + } + return nil; + } else if (!self.valid) { + if (error) { + *error = RCTErrorWithMessage( + @"sync callFunctionOnModule: bridge is no longer valid"); + } + return nil; + } else if (self.loading) { + if (error) { + *error = RCTErrorWithMessage( + @"sync callFunctionOnModule: bridge is still loading"); + } + return nil; + } + + __block JSValue *ret = nil; + + RCT_PROFILE_BEGIN_EVENT(0, @"callFunctionOnModule", (@{ @"module": module, @"method": method })); + NSError *errorObj = tryAndReturnError(^{ + Value result = self->_reactInstance->callFunctionSync( + [module UTF8String], [method UTF8String], 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 executeBlockOnJavaScriptThread:^{ + RCTProfileInit(self); + }]; +} + +- (void)stopProfiling:(void (^)(NSData *))callback +{ + RCTAssertMainQueue(); + + [self executeBlockOnJavaScriptThread:^{ + RCTProfileEnd(self, ^(NSString *log) { + NSData *logData = [log dataUsingEncoding:NSUTF8StringEncoding]; + callback(logData); + }); + }]; +} + +- (BOOL)isBatchActive +{ + return _wasBatchActive; +} + +@end diff --git a/React/CxxBridge/RCTMessageQueue.h b/React/CxxBridge/RCTMessageQueue.h new file mode 100644 index 000000000..cdb50390a --- /dev/null +++ b/React/CxxBridge/RCTMessageQueue.h @@ -0,0 +1,31 @@ +/** + * 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 + +#include +#include + +namespace facebook { +namespace react { + +class RCTMessageQueue : public MessageQueueThread { + public: + explicit RCTMessageQueue(const std::string &name); + void runOnQueue(std::function&&) override; + void runOnQueueSync(std::function&&) override; + void quitSynchronous() override; + + private: + dispatch_queue_t m_queue; + std::atomic_bool m_shutdown; +}; + +} +} diff --git a/React/CxxBridge/RCTMessageQueue.mm b/React/CxxBridge/RCTMessageQueue.mm new file mode 100644 index 000000000..70d4f6274 --- /dev/null +++ b/React/CxxBridge/RCTMessageQueue.mm @@ -0,0 +1,47 @@ +/** + * 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 "RCTMessageQueue.h" + +namespace facebook { +namespace react { + +RCTMessageQueue::RCTMessageQueue(const std::string &name) { + m_queue = dispatch_queue_create(name.c_str(), NULL); +} + +void RCTMessageQueue::runOnQueue(std::function&& func) { + if (m_shutdown) { + return; + } + dispatch_async(m_queue, ^{ + if (!m_shutdown) { + func(); + } + }); +} + +void RCTMessageQueue::runOnQueueSync(std::function&& func) { + if (m_shutdown) { + return; + } + dispatch_sync(m_queue, ^{ + if (!m_shutdown) { + func(); + } + }); +} + +void RCTMessageQueue::quitSynchronous() { + m_shutdown = true; + dispatch_sync(m_queue, ^{}); +} + +} +} diff --git a/React/CxxBridge/RCTMessageThread.h b/React/CxxBridge/RCTMessageThread.h new file mode 100644 index 000000000..c8a26737e --- /dev/null +++ b/React/CxxBridge/RCTMessageThread.h @@ -0,0 +1,39 @@ +/** + * 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. + */ + +#import + +#import + +#import +#import + +namespace facebook { +namespace react { + +class RCTMessageThread : public MessageQueueThread { + public: + RCTMessageThread(NSRunLoop *runLoop, RCTJavaScriptCompleteBlock errorBlock); + ~RCTMessageThread() override; + void runOnQueue(std::function&&) override; + void runOnQueueSync(std::function&&) override; + void quitSynchronous() override; + + private: + void tryFunc(const std::function& func); + void runAsync(std::function func); + void runSync(std::function func); + + CFRunLoopRef m_cfRunLoop; + RCTJavaScriptCompleteBlock m_errorBlock; + std::atomic_bool m_shutdown; +}; + +} +} diff --git a/React/CxxBridge/RCTMessageThread.mm b/React/CxxBridge/RCTMessageThread.mm new file mode 100644 index 000000000..205ece517 --- /dev/null +++ b/React/CxxBridge/RCTMessageThread.mm @@ -0,0 +1,122 @@ +/** + * 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 "RCTMessageThread.h" + +#include +#include + +#include +#include + +// A note about the implementation: This class is not used +// generically. It's a thin wrapper around a run loop which +// implements a C++ interface, for use by the C++ xplat bridge code. +// This means it can make certain non-generic assumptions. In +// particular, the sync functions are only used for bridge setup and +// teardown, and quitSynchronous is guaranteed to be called. + +namespace facebook { +namespace react { + +RCTMessageThread::RCTMessageThread(NSRunLoop *runLoop, RCTJavaScriptCompleteBlock errorBlock) + : m_cfRunLoop([runLoop getCFRunLoop]) + , m_errorBlock(errorBlock) + , m_shutdown(false) { + CFRetain(m_cfRunLoop); +} + +RCTMessageThread::~RCTMessageThread() { + CFRelease(m_cfRunLoop); +} + +// This is analogous to dispatch_async +void RCTMessageThread::runAsync(std::function func) { + CFRunLoopPerformBlock(m_cfRunLoop, kCFRunLoopCommonModes, ^{ func(); }); + CFRunLoopWakeUp(m_cfRunLoop); +} + +// This is analogous to dispatch_sync +void RCTMessageThread::runSync(std::function func) { + if (m_cfRunLoop == CFRunLoopGetCurrent()) { + func(); + return; + } + + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + runAsync([func=std::make_shared>(std::move(func)), &sema] { + (*func)(); + dispatch_semaphore_signal(sema); + }); + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); +} + +void RCTMessageThread::tryFunc(const std::function& func) { + try { + @try { + func(); + } + @catch (NSException *exception) { + NSString *message = + [NSString stringWithFormat:@"Exception '%@' was thrown from JS thread", exception]; + m_errorBlock(RCTErrorWithMessage(message)); + } + @catch (id exception) { + // This will catch any other ObjC exception, but no C++ exceptions + m_errorBlock(RCTErrorWithMessage(@"non-std ObjC Exception")); + } + } catch (const facebook::react::JSException &ex) { + // This is a special case. We want to extract the stack + // information and pass it to the redbox. This will lose the C++ + // stack, but it's of limited value. + NSDictionary *errorInfo = @{ + RCTJSRawStackTraceKey: @(ex.getStack().c_str()), + NSLocalizedDescriptionKey: [@"Unhandled JS Exception: " stringByAppendingString:@(ex.what())] + }; + m_errorBlock([NSError errorWithDomain:RCTErrorDomain code:1 userInfo:errorInfo]); + } catch (const std::exception &ex) { + m_errorBlock(RCTErrorWithMessage(@(ex.what()))); + } catch (...) { + // On a 64-bit platform, this would catch ObjC exceptions, too, but not on + // 32-bit platforms, so we catch those with id exceptions above. + m_errorBlock(RCTErrorWithMessage(@"non-std C++ exception")); + } +} + +void RCTMessageThread::runOnQueue(std::function&& func) { + if (m_shutdown) { + return; + } + + runAsync([this, func=std::make_shared>(std::move(func))] { + if (!m_shutdown) { + tryFunc(*func); + } + }); +} + +void RCTMessageThread::runOnQueueSync(std::function&& func) { + if (m_shutdown) { + return; + } + runSync([this, func=std::move(func)] { + if (!m_shutdown) { + tryFunc(func); + } + }); +} + +void RCTMessageThread::quitSynchronous() { + m_shutdown = true; + runSync([]{}); + CFRunLoopStop(m_cfRunLoop); +} + +} +} diff --git a/React/CxxBridge/RCTNativeModule.h b/React/CxxBridge/RCTNativeModule.h new file mode 100644 index 000000000..1832d98b6 --- /dev/null +++ b/React/CxxBridge/RCTNativeModule.h @@ -0,0 +1,34 @@ +/** + * 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. + */ + +#import +#import + +namespace facebook { +namespace react { + +class RCTNativeModule : public NativeModule { + public: + RCTNativeModule(RCTBridge *bridge, RCTModuleData *moduleData); + + std::string getName() override; + std::vector getMethods() override; + folly::dynamic getConstants() override; + bool supportsWebWorkers() override; + void invoke(ExecutorToken token, unsigned int methodId, folly::dynamic &¶ms) override; + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, + folly::dynamic &¶ms) override; + + private: + __weak RCTBridge *m_bridge; + RCTModuleData *m_moduleData; +}; + +} +} diff --git a/React/CxxBridge/RCTNativeModule.mm b/React/CxxBridge/RCTNativeModule.mm new file mode 100644 index 000000000..cd67d6b37 --- /dev/null +++ b/React/CxxBridge/RCTNativeModule.mm @@ -0,0 +1,127 @@ +/** + * 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. + */ + +#import "RCTNativeModule.h" + +#import +#import +#import +#import +#import +#import + +namespace facebook { +namespace react { + +RCTNativeModule::RCTNativeModule(RCTBridge *bridge, RCTModuleData *moduleData) + : m_bridge(bridge) + , m_moduleData(moduleData) {} + +std::string RCTNativeModule::getName() { + return [m_moduleData.name UTF8String]; +} + +std::vector RCTNativeModule::getMethods() { + std::vector descs; + + for (id method in m_moduleData.methods) { + descs.emplace_back( + method.JSMethodName.UTF8String, + method.functionType == RCTFunctionTypePromise ? "promise" : "async" + ); + } + + return descs; +} + +folly::dynamic RCTNativeModule::getConstants() { + // TODO mhorowitz #10487027: This does unnecessary work since it + // only needs constants. Think about refactoring RCTModuleData or + // NativeModule to make this more natural. + + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"[RCTNativeModule getConstants] moduleData.config", nil); + NSArray *config = m_moduleData.config; + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + if (!config || config == (id)kCFNull) { + return nullptr; + } + id constants = config[1]; + if (![constants isKindOfClass:[NSDictionary class]]) { + return nullptr; + } + RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, + @"[RCTNativeModule getConstants] convert", nil); + folly::dynamic ret = [RCTConvert folly_dynamic:constants]; + RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); + return ret; +} + +bool RCTNativeModule::supportsWebWorkers() { + return false; +} + +void RCTNativeModule::invoke(ExecutorToken token, unsigned int methodId, folly::dynamic &¶ms) { + // The BatchedBridge version of this buckets all the callbacks by thread, and + // queues one block on each. This is much simpler; we'll see how it goes and + // iterate. + + // There is no flow event handling here until I can understand it. + + auto sparams = std::make_shared(std::move(params)); + + __weak RCTBridge *bridge = m_bridge; + + dispatch_block_t block = ^{ + if (!bridge || !bridge.valid) { + return; + } + + id method = m_moduleData.methods[methodId]; + if (RCT_DEBUG && !method) { + RCTLogError(@"Unknown methodID: %ud for module: %@", + methodId, m_moduleData.name); + } + + NSArray *objcParams = RCTConvertFollyDynamic(*sparams); + + @try { + [method invokeWithBridge:bridge module:m_moduleData.instance arguments:objcParams]; + } + @catch (NSException *exception) { + // Pass on JS exceptions + if ([exception.name hasPrefix:RCTFatalExceptionName]) { + @throw exception; + } + + NSString *message = [NSString stringWithFormat: + @"Exception '%@' was thrown while invoking %@ on target %@ with params %@", + exception, method.JSMethodName, m_moduleData.name, objcParams]; + RCTFatal(RCTErrorWithMessage(message)); + } + }; + + dispatch_queue_t queue = m_moduleData.methodQueue; + + if (queue == RCTJSThread) { + block(); + } else if (queue) { + dispatch_async(queue, block); + } +} + +MethodCallResult RCTNativeModule::callSerializableNativeHook( + ExecutorToken token, unsigned int reactMethodId, folly::dynamic &¶ms) { + RCTFatal(RCTErrorWithMessage(@"callSerializableNativeHook is not yet supported on iOS")); + return {nullptr, true}; +} + + +} +} diff --git a/React/CxxBridge/RCTObjcExecutor.h b/React/CxxBridge/RCTObjcExecutor.h new file mode 100644 index 000000000..a4283c01c --- /dev/null +++ b/React/CxxBridge/RCTObjcExecutor.h @@ -0,0 +1,34 @@ +/** + * 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 +#include + +#import +#import +#include + +namespace facebook { +namespace react { + +class RCTObjcExecutorFactory : public JSExecutorFactory { +public: + RCTObjcExecutorFactory(id jse, RCTJavaScriptCompleteBlock errorBlock); + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override; + +private: + NSURL *m_url; + id m_jse; + RCTJavaScriptCompleteBlock m_errorBlock; +}; + +} +} diff --git a/React/CxxBridge/RCTObjcExecutor.mm b/React/CxxBridge/RCTObjcExecutor.mm new file mode 100644 index 000000000..0f5132cf1 --- /dev/null +++ b/React/CxxBridge/RCTObjcExecutor.mm @@ -0,0 +1,141 @@ +/** + * 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. + */ + +#import "RCTObjcExecutor.h" + +#import +#import +#import +#import +#import +#import +#import +#import + +namespace facebook { +namespace react { + +namespace { + +class JSEException : public std::runtime_error { +public: + JSEException(NSError *error) + : runtime_error([[error description] UTF8String]) {} +}; + +class RCTObjcExecutor : public JSExecutor { +public: + RCTObjcExecutor(id jse, RCTJavaScriptCompleteBlock errorBlock, + std::shared_ptr delegate) + : m_jse(jse) + , m_errorBlock(errorBlock) + , m_delegate(delegate) + { + m_jsCallback = ^(id json, NSError *error) { + if (error) { + m_errorBlock(error); + return; + } + + m_delegate->callNativeModules(*this, [RCTConvert folly_dynamic:json], true); + }; + + // Synchronously initialize the executor + [jse setUp]; + + folly::dynamic nativeModuleConfig = folly::dynamic::array; + auto moduleRegistry = delegate->getModuleRegistry(); + for (const auto &name : moduleRegistry->moduleNames()) { + auto config = moduleRegistry->getConfig(name); + nativeModuleConfig.push_back(config ? config->config : nullptr); + } + + folly::dynamic config = + folly::dynamic::object("remoteModuleConfig", std::move(nativeModuleConfig)); + + setGlobalVariable( + "__fbBatchedBridgeConfig", + std::make_unique(folly::toJson(config))); + } + + void loadApplicationScript( + std::unique_ptr script, + std::string sourceURL) override { + RCTProfileBeginFlowEvent(); + [m_jse executeApplicationScript:[NSData dataWithBytes:script->c_str() length:script->size()] + sourceURL:[[NSURL alloc] + initWithString:@(sourceURL.c_str())] + onComplete:^(NSError *error) { + RCTProfileEndFlowEvent(); + + if (error) { + m_errorBlock(error); + return; + } + + [m_jse flushedQueue:m_jsCallback]; + }]; + } + + void setJSModulesUnbundle(std::unique_ptr) override { + RCTLogWarn(@"Unbundle is not supported in RCTObjcExecutor"); + } + + void callFunction(const std::string &module, const std::string &method, + const folly::dynamic &arguments) override { + [m_jse callFunctionOnModule:@(module.c_str()) + method:@(method.c_str()) + arguments:RCTConvertFollyDynamic(arguments) + callback:m_jsCallback]; + } + + void invokeCallback(double callbackId, const folly::dynamic &arguments) override { + [m_jse invokeCallbackID:@(callbackId) + arguments:RCTConvertFollyDynamic(arguments) + callback:m_jsCallback]; + } + + virtual void setGlobalVariable( + std::string propName, + std::unique_ptr jsonValue) override { + [m_jse injectJSONText:@(jsonValue->c_str()) + asGlobalObjectNamed:@(propName.c_str()) + callback:m_errorBlock]; + } + + virtual bool supportsProfiling() override { + return false; + }; + virtual void startProfiler(const std::string &titleString) override {}; + virtual void stopProfiler(const std::string &titleString, + const std::string &filename) override {}; + +private: + id m_jse; + RCTJavaScriptCompleteBlock m_errorBlock; + std::shared_ptr m_delegate; + RCTJavaScriptCallback m_jsCallback; +}; + +} + +RCTObjcExecutorFactory::RCTObjcExecutorFactory( + id jse, RCTJavaScriptCompleteBlock errorBlock) + : m_jse(jse) + , m_errorBlock(errorBlock) {} + +std::unique_ptr RCTObjcExecutorFactory::createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) { + return std::unique_ptr( + new RCTObjcExecutor(m_jse, m_errorBlock, delegate)); +} + +} +} diff --git a/React/CxxModule/RCTCxxMethod.h b/React/CxxModule/RCTCxxMethod.h new file mode 100644 index 000000000..85fc2e050 --- /dev/null +++ b/React/CxxModule/RCTCxxMethod.h @@ -0,0 +1,19 @@ +/** + * 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. + */ + +#import + +#import +#import + +@interface RCTCxxMethod : NSObject + +- (instancetype)initWithCxxMethod:(const facebook::xplat::module::CxxModule::Method &)cxxMethod; + +@end diff --git a/React/CxxModule/RCTCxxMethod.mm b/React/CxxModule/RCTCxxMethod.mm new file mode 100644 index 000000000..cd5b7f86c --- /dev/null +++ b/React/CxxModule/RCTCxxMethod.mm @@ -0,0 +1,123 @@ +/** + * 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. + */ + +#import "RCTCxxMethod.h" + +#import +#import +#import +#import +#import + +#import "RCTCxxUtils.h" + +using facebook::xplat::module::CxxModule; + +@implementation RCTCxxMethod +{ + std::unique_ptr _method; +} + +@synthesize JSMethodName = _JSMethodName; + +- (instancetype)initWithCxxMethod:(const CxxModule::Method &)method +{ + if ((self = [super init])) { + _JSMethodName = @(method.name.c_str()); + _method = folly::make_unique(method); + } + return self; +} + +- (id)invokeWithBridge:(RCTBridge *)bridge + module:(id)module + arguments:(NSArray *)arguments +{ + // module is unused except for printing errors. The C++ object it represents + // is also baked into _method. + + // the last N arguments are callbacks, according to the Method data. The + // preceding arguments are values whic have already been parsed from JS: they + // may be NSNumber (bool, int, double), NSString, NSArray, or NSObject. + + CxxModule::Callback first; + CxxModule::Callback second; + + if (arguments.count < _method->callbacks) { + RCTLogError(@"Method %@.%s expects at least %lu arguments, but got %tu", + RCTBridgeModuleNameForClass([module class]), _method->name.c_str(), + _method->callbacks, arguments.count); + return nil; + } + + if (_method->callbacks >= 1) { + if (![arguments[arguments.count - 1] isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%s should be a function", + arguments.count - 1, arguments[arguments.count - 1], + RCTBridgeModuleNameForClass([module class]), _method->name.c_str()); + return nil; + } + + NSNumber *id1; + if (_method->callbacks == 2) { + if (![arguments[arguments.count - 2] isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%s should be a function", + arguments.count - 2, arguments[arguments.count - 2], + RCTBridgeModuleNameForClass([module class]), _method->name.c_str()); + return nil; + } + + id1 = arguments[arguments.count - 2]; + NSNumber *id2 = arguments[arguments.count - 1]; + + second = ^(std::vector args) { + [bridge enqueueCallback:id2 args:RCTConvertFollyDynamic(folly::dynamic(args.begin(), args.end()))]; + }; + } else { + id1 = arguments[arguments.count - 1]; + } + + first = ^(std::vector args) { + [bridge enqueueCallback:id1 args:RCTConvertFollyDynamic(folly::dynamic(args.begin(), args.end()))]; + }; + } + + folly::dynamic args = [RCTConvert folly_dynamic:arguments]; + args.resize(args.size() - _method->callbacks); + + try { + if (_method->func) { + _method->func(std::move(args), first, second); + return nil; + } else { + auto result = _method->syncFunc(std::move(args)); + // TODO: we should convert this to JSValue directly + return RCTConvertFollyDynamic(result); + } + } catch (const facebook::xplat::JsArgumentException &ex) { + RCTLogError(@"Method %@.%s argument error: %s", + RCTBridgeModuleNameForClass([module class]), _method->name.c_str(), + ex.what()); + return nil; + } +} + +- (RCTFunctionType)functionType +{ + // TODO: support promise-style APIs + return _method->syncFunc ? RCTFunctionTypeSync : RCTFunctionTypeNormal; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p; name = %@>", + [self class], self, self.JSMethodName]; +} + +@end diff --git a/React/CxxModule/RCTCxxModule.h b/React/CxxModule/RCTCxxModule.h new file mode 100644 index 000000000..b95889df7 --- /dev/null +++ b/React/CxxModule/RCTCxxModule.h @@ -0,0 +1,27 @@ +/** + * 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. + */ + +#import + +#import + +#import +#import + +@interface RCTCxxModule : NSObject + +- (instancetype)initWithCxxModule:(std::unique_ptr)module; + +- (NSArray *)methodsToExport; +- (NSDictionary *)constantsToExport; + +// Extracts the module from its objc wrapper +- (std::unique_ptr)move; + +@end diff --git a/React/CxxModule/RCTCxxModule.mm b/React/CxxModule/RCTCxxModule.mm new file mode 100644 index 000000000..61df218be --- /dev/null +++ b/React/CxxModule/RCTCxxModule.mm @@ -0,0 +1,73 @@ +/** + * 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. + */ + +#import "RCTCxxModule.h" + +#import +#import + +#import "RCTCxxMethod.h" +#import "RCTCxxUtils.h" + +@implementation RCTCxxModule +{ + std::unique_ptr _module; +} + +- (instancetype)init +{ + return nil; +} + +- (instancetype)initWithCxxModule:(std::unique_ptr)module +{ + RCTAssert([RCTBridgeModuleNameForClass([self class]) isEqualToString:@(module->getName().c_str())], + @"CxxModule class name %@ does not match runtime name %s", + RCTBridgeModuleNameForClass([self class]), module->getName().c_str()); + + if ((self = [super init])) { + _module = std::move(module); + } + + return self; +} + +- (std::unique_ptr)move +{ + return std::move(_module); +} + ++ (NSString *)moduleName +{ + return @""; +} + +- (NSArray *)methodsToExport +{ + CHECK(_module) << "Can't call methodsToExport on moved module"; + + NSMutableArray *moduleMethods = [NSMutableArray new]; + for (const auto &method : _module->getMethods()) { + [moduleMethods addObject:[[RCTCxxMethod alloc] initWithCxxMethod:method]]; + } + return moduleMethods; +} + +- (NSDictionary *)constantsToExport +{ + CHECK(_module) << "Can't call constantsToExport on moved module"; + + NSMutableDictionary *moduleConstants = [NSMutableDictionary new]; + for (const auto &c : _module->getConstants()) { + moduleConstants[@(c.first.c_str())] = RCTConvertFollyDynamic(c.second); + } + return moduleConstants; +} + +@end diff --git a/React/CxxModule/RCTCxxUtils.h b/React/CxxModule/RCTCxxUtils.h new file mode 100644 index 000000000..f63c53676 --- /dev/null +++ b/React/CxxModule/RCTCxxUtils.h @@ -0,0 +1,19 @@ +/** + * 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. + */ + +#import +#include + +id RCTConvertFollyDynamic(const folly::dynamic &dyn); + +@interface RCTConvert (folly) + ++ (folly::dynamic)folly_dynamic:(id)json; + +@end diff --git a/React/CxxModule/RCTCxxUtils.mm b/React/CxxModule/RCTCxxUtils.mm new file mode 100644 index 000000000..bba4cfca2 --- /dev/null +++ b/React/CxxModule/RCTCxxUtils.mm @@ -0,0 +1,35 @@ +/** + * 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. + */ + +#import "RCTCxxUtils.h" + +#import + +using namespace react::CxxUtils; + +id RCTConvertFollyDynamic(const folly::dynamic &dyn) { + return convertFollyDynamicToId(dyn); +} + +@implementation RCTConvert (folly) + ++ (folly::dynamic)folly_dynamic:(id)json; +{ + if (json == nil || json == (id)kCFNull) { + return nullptr; + } else { + folly::dynamic dyn = convertIdToFollyDynamic(json); + if (dyn == nil) { + RCTAssert(false, @"RCTConvert input json is of an impossible type"); + } + return dyn; + } +} + +@end diff --git a/React/CxxUtils/RCTFollyConvert.h b/React/CxxUtils/RCTFollyConvert.h new file mode 100644 index 000000000..c73620bb5 --- /dev/null +++ b/React/CxxUtils/RCTFollyConvert.h @@ -0,0 +1,19 @@ +// 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 + +namespace react { namespace CxxUtils { + +folly::dynamic convertIdToFollyDynamic(id json); +id convertFollyDynamicToId(const folly::dynamic &dyn); + +}} diff --git a/React/CxxUtils/RCTFollyConvert.mm b/React/CxxUtils/RCTFollyConvert.mm new file mode 100644 index 000000000..a60470166 --- /dev/null +++ b/React/CxxUtils/RCTFollyConvert.mm @@ -0,0 +1,116 @@ +// 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. + */ + +#import "RCTFollyConvert.h" + +#import + +#import + +namespace react { namespace CxxUtils { + +id convertFollyDynamicToId(const folly::dynamic &dyn) { + // I could imagine an implementation which avoids copies by wrapping the + // dynamic in a derived class of NSDictionary. We can do that if profiling + // implies it will help. + + switch (dyn.type()) { + case folly::dynamic::NULLT: + return (id)kCFNull; + case folly::dynamic::BOOL: + return dyn.getBool() ? @YES : @NO; + case folly::dynamic::INT64: + return @(dyn.getInt()); + case folly::dynamic::DOUBLE: + return @(dyn.getDouble()); + case folly::dynamic::STRING: + return [[NSString alloc] initWithData:[NSData dataWithBytes:dyn.data() length:dyn.size()] + encoding:NSUTF8StringEncoding]; + case folly::dynamic::ARRAY: { + NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:dyn.size()]; + for (auto &elem : dyn) { + [array addObject:convertFollyDynamicToId(elem)]; + } + return array; + } + case folly::dynamic::OBJECT: { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:dyn.size()]; + for (auto &elem : dyn.items()) { + dict[convertFollyDynamicToId(elem.first)] = convertFollyDynamicToId(elem.second); + } + return dict; + } + } +} + +folly::dynamic convertIdToFollyDynamic(id json) +{ + if (json == nil || json == (id)kCFNull) { + return nullptr; + } else if ([json isKindOfClass:[NSNumber class]]) { + const char *objCType = [json objCType]; + switch (objCType[0]) { + // This is a c++ bool or C99 _Bool. On some platforms, BOOL is a bool. + case _C_BOOL: + return [json boolValue]; + case _C_CHR: + // On some platforms, objc BOOL is a signed char, but it + // might also be a small number. Use the same hack JSC uses + // to distinguish them: + // https://phabricator.intern.facebook.com/diffusion/FBS/browse/master/fbobjc/xplat/third-party/jsc/safari-600-1-4-17/JavaScriptCore/API/JSValue.mm;b8ee03916489f8b12143cd5c0bca546da5014fc9$901 + if ([json isKindOfClass:[@YES class]]) { + return [json boolValue]; + } else { + return [json longLongValue]; + } + case _C_UCHR: + case _C_SHT: + case _C_USHT: + case _C_INT: + case _C_UINT: + case _C_LNG: + case _C_ULNG: + case _C_LNG_LNG: + case _C_ULNG_LNG: + return [json longLongValue]; + + case _C_FLT: + case _C_DBL: + return [json doubleValue]; + + // default: + // fall through + } + } else if ([json isKindOfClass:[NSString class]]) { + NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; + return std::string(reinterpret_cast(data.bytes), + data.length); + } else if ([json isKindOfClass:[NSArray class]]) { + folly::dynamic array = folly::dynamic::array; + for (id element in json) { + array.push_back(convertIdToFollyDynamic(element)); + } + return array; + } else if ([json isKindOfClass:[NSDictionary class]]) { + __block folly::dynamic object = folly::dynamic::object(); + + [json enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, __unused BOOL *stop) { + object.insert(convertIdToFollyDynamic(key), + convertIdToFollyDynamic(value)); + }]; + + return object; + } + + return nil; +} + +}}