diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m index 165bed01aa8..866a14f3af9 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTLoggingTests.m @@ -38,11 +38,10 @@ if (getenv("CI_USE_PACKAGER")) { NSString *app = @"IntegrationTests/IntegrationTestsApp"; scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]]; - RCTAssert(scriptURL != nil, @"No scriptURL set"); } else { scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; - RCTAssert(scriptURL != nil, @"Could not locate main.jsBundle"); } + RCTAssert(scriptURL != nil, @"No scriptURL set"); _bridge = [[RCTBridge alloc] initWithBundleURL:scriptURL moduleProvider:NULL launchOptions:nil]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:60]; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m index 82542de0656..05d73709713 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitNotificationRaceTests.m @@ -21,6 +21,7 @@ #import "RCTUtils.h" #import "RCTUIManager.h" #import "RCTViewManager.h" +#import "RCTJavaScriptExecutor.h" #define RUN_RUNLOOP_WHILE(CONDITION) \ { \ diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m index 215a5a20257..3e5a350e349 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleInitTests.m @@ -19,6 +19,7 @@ #import "RCTBridge+Private.h" #import "RCTBridgeModule.h" #import "RCTUtils.h" +#import "RCTJavaScriptExecutor.h" #define RUN_RUNLOOP_WHILE(CONDITION) \ { \ diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 446a233fc17..11428a56840 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -9,10 +9,12 @@ #import "RCTImageLoader.h" -#import -#import #import +#import + +#import + #import "RCTConvert.h" #import "RCTDefines.h" #import "RCTImageUtils.h" diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index d6d8f59e4d7..c9d84a18360 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -47,11 +47,10 @@ static const NSTimeInterval kTestTeardownTimeoutSeconds = 30; if (getenv("CI_USE_PACKAGER")) { _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?platform=ios&dev=true", app]]; - RCTAssert(_scriptURL != nil, @"No scriptURL set"); } else { _scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; - RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle"); } + RCTAssert(_scriptURL != nil, @"No scriptURL set"); } return self; } diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 7f8c7067f55..6555631a4e7 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -68,6 +68,7 @@ class MessageQueue { [ 'invokeCallbackAndReturnFlushedQueue', 'callFunctionReturnFlushedQueue', + 'callFunction', 'flushedQueue', ].forEach((fn) => (this[fn] = this[fn].bind(this))); @@ -98,6 +99,16 @@ class MessageQueue { return this.flushedQueue(); } + callFunction(module, method, args) { + let result; + guard(() => { + result = this.__callFunction(module, method, args); + this.__callImmediates(); + }); + + return result; + } + invokeCallbackAndReturnFlushedQueue(cbID, args) { guard(() => { this.__invokeCallback(cbID, args); @@ -190,8 +201,9 @@ class MessageQueue { 'Module %s is not a registered callable module.', module ); - moduleMethods[method].apply(moduleMethods, args); + const result = moduleMethods[method].apply(moduleMethods, args); Systrace.endEvent(); + return result; } __invokeCallback(cbID, args) { diff --git a/Libraries/WebSocket/RCTWebSocketModule.m b/Libraries/WebSocket/RCTWebSocketModule.m index 6a914a6f482..cc5dc4dc638 100644 --- a/Libraries/WebSocket/RCTWebSocketModule.m +++ b/Libraries/WebSocket/RCTWebSocketModule.m @@ -9,6 +9,8 @@ #import "RCTWebSocketModule.h" +#import + #import "RCTConvert.h" #import "RCTUtils.h" diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index bd8ff7b41b2..612d05d17ec 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -589,7 +589,24 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR queue:(dispatch_queue_t)queue { if (queue == RCTJSThread) { - [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; + __weak __typeof(self) weakSelf = self; + RCTProfileBeginFlowEvent(); + RCTAssert(_javaScriptExecutor != nil, @"Need JS executor to schedule JS work"); + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + RCTProfileEndFlowEvent(); + + RCTBatchedBridge *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + if (strongSelf.loading) { + [strongSelf->_pendingCalls addObject:block]; + } else { + block(); + } + }]; } else if (queue) { dispatch_async(queue, block); } @@ -680,32 +697,19 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR */ RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[RCTBatchedBridge enqueueJSCall:]", nil); + if (!_valid) { + return; + } NSArray *ids = [moduleDotMethod componentsSeparatedByString:@"."]; NSString *module = ids[0]; NSString *method = ids[1]; - RCTProfileBeginFlowEvent(); - - __weak RCTBatchedBridge *weakSelf = self; - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - RCTProfileEndFlowEvent(); - - RCTBatchedBridge *strongSelf = weakSelf; - if (!strongSelf || !strongSelf.valid) { - return; - } - - if (strongSelf.loading) { - dispatch_block_t pendingCall = ^{ - [weakSelf _actuallyInvokeAndProcessModule:module method:method arguments:args ?: @[]]; - }; - [strongSelf->_pendingCalls addObject:pendingCall]; - } else { - [strongSelf _actuallyInvokeAndProcessModule:module method:method arguments:args ?: @[]]; - } - }]; + __weak __typeof(self) weakSelf = self; + [self dispatchBlock:^{ + [weakSelf _actuallyInvokeAndProcessModule:module method:method arguments:args ?: @[]]; + } queue:RCTJSThread]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"", nil); } @@ -718,27 +722,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR /** * AnyThread */ + if (!_valid) { + return; + } - RCTProfileBeginFlowEvent(); - - __weak RCTBatchedBridge *weakSelf = self; - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - RCTProfileEndFlowEvent(); - - RCTBatchedBridge *strongSelf = weakSelf; - if (!strongSelf || !strongSelf.valid) { - return; - } - - if (strongSelf.loading) { - dispatch_block_t pendingCall = ^{ - [weakSelf _actuallyInvokeCallback:cbID arguments:args ?: @[]]; - }; - [strongSelf->_pendingCalls addObject:pendingCall]; - } else { - [strongSelf _actuallyInvokeCallback:cbID arguments:args]; - } - }]; + __weak __typeof(self) weakSelf = self; + [self dispatchBlock:^{ + [weakSelf _actuallyInvokeCallback:cbID arguments:args]; + } queue:RCTJSThread]; } /** @@ -922,11 +913,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR }); }; - if (queue == RCTJSThread) { - [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } else if (queue) { - dispatch_async(queue, block); - } + [self dispatchBlock:block queue:queue]; } _flowID = callID; diff --git a/React/Base/RCTBridge+Private.h b/React/Base/RCTBridge+Private.h index 14c3d6ab411..e7693685b7a 100644 --- a/React/Base/RCTBridge+Private.h +++ b/React/Base/RCTBridge+Private.h @@ -11,6 +11,7 @@ @class RCTModuleData; @class RCTPerformanceLogger; +@protocol RCTJavaScriptExecutor; @interface RCTBridge () { @@ -67,7 +68,9 @@ @interface RCTBridge (RCTBatchedBridge) /** - * Used for unit testing, to detect when executor has been invalidated. + * Access the underlying JavaScript executor. You can use this in unit tests to detect + * when the executor has been invalidated, or when you want to schedule calls on the + * JS VM outside of React Native. Use with care! */ @property (nonatomic, weak, readonly) id javaScriptExecutor; diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index c6db0eca53d..ee67b8cc598 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -14,7 +14,6 @@ #import "RCTDefines.h" #import "RCTFrameUpdate.h" #import "RCTInvalidating.h" -#import "RCTJavaScriptExecutor.h" @class RCTBridge; @class RCTEventDispatcher; diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h index aa47d3cae19..55dc458ee8e 100644 --- a/React/Base/RCTJavaScriptExecutor.h +++ b/React/Base/RCTJavaScriptExecutor.h @@ -15,7 +15,7 @@ #import "RCTInvalidating.h" typedef void (^RCTJavaScriptCompleteBlock)(NSError *error); -typedef void (^RCTJavaScriptCallback)(id json, NSError *error); +typedef void (^RCTJavaScriptCallback)(id result, NSError *error); /** * Abstracts away a JavaScript execution context - we may be running code in a diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m index 96aefd2358b..30e5141a153 100644 --- a/React/Base/RCTModuleData.m +++ b/React/Base/RCTModuleData.m @@ -9,6 +9,8 @@ #import "RCTModuleData.h" +#import + #import "RCTBridge.h" #import "RCTBridge+Private.h" #import "RCTModuleMethod.h" diff --git a/React/Executors/RCTJSCExecutor.h b/React/Executors/RCTJSCExecutor.h index 447f39d03bd..66d61a16bd4 100644 --- a/React/Executors/RCTJSCExecutor.h +++ b/React/Executors/RCTJSCExecutor.h @@ -11,6 +11,8 @@ #import "RCTJavaScriptExecutor.h" +typedef void (^RCTJavaScriptValueCallback)(JSValue *result, NSError *error); + /** * Default name for the JS thread */ @@ -75,4 +77,15 @@ RCT_EXTERN NSString *const RCTJavaScriptContextCreatedNotification; JSContext:(JSContext **)JSContext error:(NSError **)error; +/** + * Invokes the given module/method directly. The completion block will be called with the + * JSValue returned by the JS context. + * + * Currently this does not flush the JS-to-native message queue. + */ +- (void)callFunctionOnModule:(NSString *)module + method:(NSString *)method + arguments:(NSArray *)args + jsValueCallback:(RCTJavaScriptValueCallback)onComplete; + @end diff --git a/React/Executors/RCTJSCExecutor.mm b/React/Executors/RCTJSCExecutor.mm index cd32ae19f19..4a78e706fdd 100644 --- a/React/Executors/RCTJSCExecutor.mm +++ b/React/Executors/RCTJSCExecutor.mm @@ -595,16 +595,29 @@ static void installBasicSynchronousHooksOnContext(JSContext *context) - (void)flushedQueue:(RCTJavaScriptCallback)onComplete { // TODO: Make this function handle first class instead of dynamically dispatching it. #9317773 - [self _executeJSCall:@"flushedQueue" arguments:@[] callback:onComplete]; + [self _executeJSCall:@"flushedQueue" arguments:@[] unwrapResult:YES callback:onComplete]; } -- (void)callFunctionOnModule:(NSString *)module - method:(NSString *)method - arguments:(NSArray *)args - callback:(RCTJavaScriptCallback)onComplete +- (void)_callFunctionOnModule:(NSString *)module + method:(NSString *)method + arguments:(NSArray *)args + flushQueue:(BOOL)flushQueue + unwrapResult:(BOOL)unwrapResult + callback:(RCTJavaScriptCallback)onComplete { // TODO: Make this function handle first class instead of dynamically dispatching it. #9317773 - [self _executeJSCall:@"callFunctionReturnFlushedQueue" arguments:@[module, method, args] callback:onComplete]; + NSString *bridgeMethod = flushQueue ? @"callFunctionReturnFlushedQueue" : @"callFunction"; + [self _executeJSCall:bridgeMethod arguments:@[module, method, args] unwrapResult:unwrapResult callback:onComplete]; +} + +- (void)callFunctionOnModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args callback:(RCTJavaScriptCallback)onComplete +{ + [self _callFunctionOnModule:module method:method arguments:args flushQueue:YES unwrapResult:YES callback:onComplete]; +} + +- (void)callFunctionOnModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args jsValueCallback:(RCTJavaScriptValueCallback)onComplete +{ + [self _callFunctionOnModule:module method:method arguments:args flushQueue:NO unwrapResult:NO callback:onComplete]; } - (void)invokeCallbackID:(NSNumber *)cbID @@ -612,11 +625,12 @@ static void installBasicSynchronousHooksOnContext(JSContext *context) callback:(RCTJavaScriptCallback)onComplete { // TODO: Make this function handle first class instead of dynamically dispatching it. #9317773 - [self _executeJSCall:@"invokeCallbackAndReturnFlushedQueue" arguments:@[cbID, args] callback:onComplete]; + [self _executeJSCall:@"invokeCallbackAndReturnFlushedQueue" arguments:@[cbID, args] unwrapResult:YES callback:onComplete]; } - (void)_executeJSCall:(NSString *)method arguments:(NSArray *)arguments + unwrapResult:(BOOL)unwrapResult callback:(RCTJavaScriptCallback)onComplete { RCTAssert(onComplete != nil, @"onComplete block should not be nil"); @@ -673,20 +687,16 @@ static void installBasicSynchronousHooksOnContext(JSContext *context) error = RCTNSErrorFromJSError(jscWrapper, contextJSRef, errorJSRef); } onComplete(nil, error); - return; + } else { + id objcValue = nil; + // We often return `null` from JS when there is nothing for native side. [JSValue toValue] + // returns [NSNull null] in this case, which we don't want. + if (!jscWrapper->JSValueIsNull(contextJSRef, resultJSRef)) { + JSValue *result = [jscWrapper->JSValue valueWithJSValueRef:resultJSRef inContext:context]; + objcValue = unwrapResult ? [result toObject] : result; + } + onComplete(objcValue, nil); } - - // Looks like making lots of JSC API calls is slower than communicating by using a JSON - // string. Also it ensures that data stuctures don't have cycles and non-serializable fields. - // see [RCTJSCExecutorTests testDeserializationPerf] - id objcValue; - // We often return `null` from JS when there is nothing for native side. JSONKit takes an extra hundred microseconds - // to handle this simple case, so we are adding a shortcut to make executeJSCall method even faster - if (!jscWrapper->JSValueIsNull(contextJSRef, resultJSRef)) { - objcValue = [[jscWrapper->JSValue valueWithJSValueRef:resultJSRef inContext:context] toObject]; - } - - onComplete(objcValue, nil); }), 0, @"js_call", (@{@"method": method, @"args": arguments}))]; }