From 80c71e5caecd24cc2d0d02ba43caee7495a96688 Mon Sep 17 00:00:00 2001 From: Adam Ernst Date: Tue, 12 Jul 2016 05:13:30 -0700 Subject: [PATCH] Refine RCTJSCExecutor's APIs for using a thread/context that already exists Summary: The `initWithJSContextProvider:` API created a `RCTJSCExecutor` with a thread/context that already exists, but it did not solve the problem of loading an actual application script; the `executeApplicationScript:` API is also asynchronous. Create a new merged API that allows you to pass in a pre-created thread/context pair and immediately receive an `RCTJSCExector` that has already executed a specified application script. This also removes the `underlyingJSContext` API entirely, in favor of passing it back in a byref variable in the new API. This minimizes the surface area for API abuse. Reviewed By: bnham Differential Revision: D3545349 fbshipit-source-id: 1c564f44d2a5379b5e6f75640079a28fd7169f67 --- React/Executors/RCTJSCExecutor.h | 17 ++-- React/Executors/RCTJSCExecutor.mm | 126 +++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 44 deletions(-) diff --git a/React/Executors/RCTJSCExecutor.h b/React/Executors/RCTJSCExecutor.h index 474db76e2..11932c1d8 100644 --- a/React/Executors/RCTJSCExecutor.h +++ b/React/Executors/RCTJSCExecutor.h @@ -54,11 +54,6 @@ RCT_EXTERN NSString *const RCTJavaScriptContextCreatedNotification; */ - (instancetype)initWithUseCustomJSCLibrary:(BOOL)useCustomJSCLibrary; -/** - * Pass a RCTJSContextProvider object to use an NSThread/JSContext pair that have already been created. - */ -- (instancetype)initWithJSContextProvider:(RCTJSContextProvider *)JSContextProvider; - /** * Create a NSError from a JSError object. * @@ -68,8 +63,16 @@ RCT_EXTERN NSString *const RCTJavaScriptContextCreatedNotification; - (NSError *)convertJSErrorToNSError:(JSValueRef)jsError context:(JSContextRef)context; /** - * Returns the underlying JSContext. + * @experimental + * Pass a RCTJSContextProvider object to use an NSThread/JSContext pair that have already been created. + * The returned executor has already executed the supplied application script synchronously. + * The underlying JSContext will be returned in the JSContext pointer if it is non-NULL and there was no error. + * If an error occurs, this method will return nil and specify the error in the error pointer if it is non-NULL. */ -- (JSContext *)underlyingJSContext; ++ (instancetype)initializedExecutorWithContextProvider:(RCTJSContextProvider *)JSContextProvider + applicationScript:(NSData *)applicationScript + sourceURL:(NSURL *)sourceURL + JSContext:(JSContext **)JSContext + error:(NSError **)error; @end diff --git a/React/Executors/RCTJSCExecutor.mm b/React/Executors/RCTJSCExecutor.mm index 1e920a367..7359c0368 100644 --- a/React/Executors/RCTJSCExecutor.mm +++ b/React/Executors/RCTJSCExecutor.mm @@ -320,10 +320,26 @@ static NSThread *newJavaScriptThread(void) return self; } -- (instancetype)initWithJSContextProvider:(RCTJSContextProvider *)JSContextProvider ++ (instancetype)initializedExecutorWithContextProvider:(RCTJSContextProvider *)JSContextProvider + applicationScript:(NSData *)applicationScript + sourceURL:(NSURL *)sourceURL + JSContext:(JSContext **)JSContext + error:(NSError **)error +{ + const RCTJSContextData data = JSContextProvider.data; + if (JSContext) { + *JSContext = data.context; + } + RCTJSCExecutor *executor = [[RCTJSCExecutor alloc] initWithJSContextData:data]; + if (![executor _synchronouslyExecuteApplicationScript:applicationScript sourceURL:sourceURL JSContext:data.context error:error]) { + return nil; // error has been set by _synchronouslyExecuteApplicationScript: + } + return executor; +} + +- (instancetype)initWithJSContextData:(const RCTJSContextData &)data { if (self = [super init]) { - const RCTJSContextData data = JSContextProvider.data; _useCustomJSCLibrary = data.useCustomJSCLibrary; _valid = YES; _javaScriptThread = data.javaScriptThread; @@ -333,6 +349,30 @@ static NSThread *newJavaScriptThread(void) return self; } +- (BOOL)_synchronouslyExecuteApplicationScript:(NSData *)script + sourceURL:(NSURL *)sourceURL + JSContext:(JSContext *)context + error:(NSError **)error +{ + BOOL isRAMBundle = NO; + script = loadPossiblyBundledApplicationScript(script, sourceURL, _performanceLogger, isRAMBundle, _randomAccessBundle, error); + if (!script) { + return NO; + } + if (isRAMBundle) { + registerNativeRequire(context, self); + } + NSError *returnedError = executeApplicationScript(script, sourceURL, _jscWrapper, _performanceLogger, _context.context.JSGlobalContextRef); + if (returnedError) { + if (error) { + *error = returnedError; + } + return NO; + } else { + return YES; + } +} + - (RCTJavaScriptContext *)context { RCTAssertThread(_javaScriptThread, @"Must be called on JS thread."); @@ -343,11 +383,6 @@ static NSThread *newJavaScriptThread(void) return _context; } -- (JSContext *)underlyingJSContext -{ - return self.context.context; -} - - (void)setUp { #if RCT_PROFILE @@ -662,36 +697,16 @@ static void installBasicSynchronousHooksOnContext(JSContext *context) RCTAssertParam(script); RCTAssertParam(sourceURL); - // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. - uint32_t magicNumber = NSSwapLittleIntToHost(*((uint32_t *)script.bytes)); - BOOL isRAMBundle = magicNumber == RCTRAMBundleMagicNumber; - if (isRAMBundle) { - [_performanceLogger markStartForTag:RCTPLRAMBundleLoad]; + BOOL isRAMBundle = NO; + { NSError *error; - script = loadRAMBundle(sourceURL, &error, _randomAccessBundle); - [_performanceLogger markStopForTag:RCTPLRAMBundleLoad]; - [_performanceLogger setValue:script.length forTag:RCTPLRAMStartupCodeSize]; - - // Reset the counters that the native require implementation uses - [_performanceLogger setValue:0 forTag:RCTPLRAMNativeRequires]; - [_performanceLogger setValue:0 forTag:RCTPLRAMNativeRequiresCount]; - [_performanceLogger setValue:0 forTag:RCTPLRAMNativeRequiresSize]; - - if (error) { + script = loadPossiblyBundledApplicationScript(script, sourceURL, _performanceLogger, isRAMBundle, _randomAccessBundle, &error); + if (script == nil) { if (onComplete) { onComplete(error); } return; } - } else { - // JSStringCreateWithUTF8CString expects a null terminated C string. - // RAM Bundling already provides a null terminated one. - NSMutableData *nullTerminatedScript = [NSMutableData dataWithCapacity:script.length + 1]; - - [nullTerminatedScript appendData:script]; - [nullTerminatedScript appendBytes:"" length:1]; - - script = nullTerminatedScript; } [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ @@ -699,26 +714,63 @@ static void installBasicSynchronousHooksOnContext(JSContext *context) return; } if (isRAMBundle) { - __weak RCTJSCExecutor *weakSelf = self; - self.context.context[@"nativeRequire"] = ^(NSNumber *moduleID) { [weakSelf _nativeRequire:moduleID]; }; + registerNativeRequire(self.context.context, self); } - [self->_performanceLogger markStartForTag:RCTPLScriptExecution]; - NSError *error = executeApplicationScript(self->_jscWrapper, script, sourceURL, self->_context.context.JSGlobalContextRef); - [self->_performanceLogger markStopForTag:RCTPLScriptExecution]; + NSError *error = executeApplicationScript(script, sourceURL, self->_jscWrapper, self->_performanceLogger, + self->_context.context.JSGlobalContextRef); if (onComplete) { onComplete(error); } }), 0, @"js_call", (@{ @"url": sourceURL.absoluteString }))]; } -static NSError *executeApplicationScript(RCTJSCWrapper *jscWrapper, NSData *script, NSURL *sourceURL, JSGlobalContextRef ctx) +static NSData *loadPossiblyBundledApplicationScript(NSData *script, NSURL *sourceURL, + RCTPerformanceLogger *performanceLogger, + BOOL &isRAMBundle, RandomAccessBundleData &randomAccessBundle, + NSError **error) { + // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. + uint32_t magicNumber = NSSwapLittleIntToHost(*((uint32_t *)script.bytes)); + isRAMBundle = magicNumber == RCTRAMBundleMagicNumber; + if (isRAMBundle) { + [performanceLogger markStartForTag:RCTPLRAMBundleLoad]; + script = loadRAMBundle(sourceURL, error, randomAccessBundle); + [performanceLogger markStopForTag:RCTPLRAMBundleLoad]; + [performanceLogger setValue:script.length forTag:RCTPLRAMStartupCodeSize]; + + // Reset the counters that the native require implementation uses + [performanceLogger setValue:0 forTag:RCTPLRAMNativeRequires]; + [performanceLogger setValue:0 forTag:RCTPLRAMNativeRequiresCount]; + [performanceLogger setValue:0 forTag:RCTPLRAMNativeRequiresSize]; + + return script; + } else { + // JSStringCreateWithUTF8CString expects a null terminated C string. + // RAM Bundling already provides a null terminated one. + NSMutableData *nullTerminatedScript = [NSMutableData dataWithCapacity:script.length + 1]; + [nullTerminatedScript appendData:script]; + [nullTerminatedScript appendBytes:"" length:1]; + return nullTerminatedScript; + } +} + +static void registerNativeRequire(JSContext *context, RCTJSCExecutor *executor) +{ + __weak RCTJSCExecutor *weakExecutor = executor; + context[@"nativeRequire"] = ^(NSNumber *moduleID) { [weakExecutor _nativeRequire:moduleID]; }; +} + +static NSError *executeApplicationScript(NSData *script, NSURL *sourceURL, RCTJSCWrapper *jscWrapper, + RCTPerformanceLogger *performanceLogger, JSGlobalContextRef ctx) +{ + [performanceLogger markStartForTag:RCTPLScriptExecution]; JSValueRef jsError = NULL; JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString((const char *)script.bytes); JSStringRef bundleURL = jscWrapper->JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String); JSValueRef result = jscWrapper->JSEvaluateScript(ctx, execJSString, NULL, bundleURL, 0, &jsError); jscWrapper->JSStringRelease(bundleURL); jscWrapper->JSStringRelease(execJSString); + [performanceLogger markStopForTag:RCTPLScriptExecution]; return result ? nil : RCTNSErrorFromJSError(jscWrapper, ctx, jsError); }