react-native/React/Executors/RCTJSCExecutor.mm

874 lines
29 KiB
Plaintext

/**
* 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 "RCTJSCExecutor.h"
#import <pthread.h>
#ifdef WITH_FB_JSC_TUNING
#include <string>
#include <fbjsc/jsc_config_ios.h>
#endif
#import <JavaScriptCore/JavaScriptCore.h>
#import <UIKit/UIDevice.h>
#import "RCTAssert.h"
#import "RCTBridge+Private.h"
#import "RCTDefines.h"
#import "RCTDevMenu.h"
#import "RCTJavaScriptLoader.h"
#import "RCTLog.h"
#import "RCTProfile.h"
#import "RCTPerformanceLogger.h"
#import "RCTUtils.h"
#import "RCTJSCProfiler.h"
#import "RCTRedBox.h"
#import "RCTSourceCode.h"
NSString *const RCTJSCThreadName = @"com.facebook.react.JavaScript";
NSString *const RCTJavaScriptContextCreatedNotification = @"RCTJavaScriptContextCreatedNotification";
static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled";
typedef struct ModuleData {
uint32_t offset;
uint32_t length;
uint32_t lineNo;
} ModuleData;
@interface RCTJavaScriptContext : NSObject <RCTInvalidating>
@property (nonatomic, strong, readonly) JSContext *context;
@property (nonatomic, assign, readonly) JSGlobalContextRef ctx;
- (instancetype)initWithJSContext:(JSContext *)context
onThread:(NSThread *)javaScriptThread NS_DESIGNATED_INITIALIZER;
@end
@implementation RCTJavaScriptContext
{
RCTJavaScriptContext *_selfReference;
NSThread *_javaScriptThread;
}
- (instancetype)initWithJSContext:(JSContext *)context
onThread:(NSThread *)javaScriptThread
{
if ((self = [super init])) {
_context = context;
_javaScriptThread = javaScriptThread;
/**
* Explicitly introduce a retain cycle here - The RCTJSCExecutor might
* be deallocated while there's still work enqueued in the JS thread, so
* we wouldn't be able kill the JSContext. Instead we create this retain
* cycle, and enqueue the -invalidate message in this object, it then
* releases the JSContext, breaks the cycle and stops the runloop.
*/
_selfReference = self;
}
return self;
}
RCT_NOT_IMPLEMENTED(-(instancetype)init)
- (JSGlobalContextRef)ctx
{
return _context.JSGlobalContextRef;
}
- (BOOL)isValid
{
return _context != nil;
}
- (void)invalidate
{
if (self.isValid) {
RCTAssertThread(_javaScriptThread, @"Must be invalidated on JS thread.");
_context = nil;
_selfReference = nil;
_javaScriptThread = nil;
CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]);
}
}
@end
@implementation RCTJSCExecutor
{
RCTJavaScriptContext *_context;
NSThread *_javaScriptThread;
CFMutableDictionaryRef _cookieMap;
FILE *_bundle;
JSStringRef _bundleURL;
CFMutableDictionaryRef _jsModules;
}
@synthesize valid = _valid;
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
static NSString *RCTJSValueToNSString(JSContextRef context, JSValueRef value, JSValueRef *exception)
{
JSStringRef JSString = JSValueToStringCopy(context, value, exception);
if (!JSString) {
return nil;
}
CFStringRef string = JSStringCopyCFString(kCFAllocatorDefault, JSString);
JSStringRelease(JSString);
return (__bridge_transfer NSString *)string;
}
static NSString *RCTJSValueToJSONString(JSContextRef context, JSValueRef value, JSValueRef *exception, unsigned indent)
{
JSStringRef JSString = JSValueCreateJSONString(context, value, indent, exception);
CFStringRef string = JSStringCopyCFString(kCFAllocatorDefault, JSString);
JSStringRelease(JSString);
return (__bridge_transfer NSString *)string;
}
NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
{
NSMutableDictionary *errorInfo = [NSMutableDictionary new];
NSString *description = jsError ? RCTJSValueToNSString(context, jsError, NULL) : @"Unknown JS error";
errorInfo[NSLocalizedDescriptionKey] = [@"Unhandled JS Exception: " stringByAppendingString:description];
NSString *details = jsError ? RCTJSValueToJSONString(context, jsError, NULL, 0) : nil;
if (details) {
errorInfo[NSLocalizedFailureReasonErrorKey] = details;
// Format stack as used in RCTFormatError
id json = RCTJSONParse(details, NULL);
if ([json isKindOfClass:[NSDictionary class]]) {
if (json[@"stack"]) {
NSError *regexError;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^([^@]+)@(.*):(\\d+):(\\d+)$" options:0 error:&regexError];
if (regexError) {
RCTLogError(@"Failed to build regex: %@", [regexError localizedDescription]);
}
NSMutableArray *stackTrace = [NSMutableArray array];
for (NSString *stackLine in [json[@"stack"] componentsSeparatedByString:@"\n"]) {
NSTextCheckingResult *result = [regex firstMatchInString:stackLine options:0 range:NSMakeRange(0, stackLine.length)];
if (result) {
[stackTrace addObject:@{
@"methodName": [stackLine substringWithRange:[result rangeAtIndex:1]],
@"file": [stackLine substringWithRange:[result rangeAtIndex:2]],
@"lineNumber": [stackLine substringWithRange:[result rangeAtIndex:3]],
@"column": [stackLine substringWithRange:[result rangeAtIndex:4]]
}];
}
}
if ([stackTrace count]) {
errorInfo[RCTJSStackTraceKey] = stackTrace;
}
}
// Fall back to just logging the line number
if (!errorInfo[RCTJSStackTraceKey] && json[@"line"]) {
errorInfo[RCTJSStackTraceKey] = @[@{
@"methodName": @"",
@"file": RCTNullIfNil(json[@"sourceURL"]),
@"lineNumber": RCTNullIfNil(json[@"line"]),
@"column": @0,
}];
}
}
}
return [NSError errorWithDomain:RCTErrorDomain code:1 userInfo:errorInfo];
}
#if RCT_DEV
static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
{
if (RCTJSCProfilerIsSupported()) {
[bridge.devMenu addItem:[RCTDevMenuItem toggleItemWithKey:RCTJSCProfilerEnabledDefaultsKey title:@"Start Profiling" selectedTitle:@"Stop Profiling" handler:^(BOOL shouldStart) {
if (shouldStart != RCTJSCProfilerIsProfiling(context)) {
if (shouldStart) {
RCTJSCProfilerStart(context);
} else {
NSString *outputFile = RCTJSCProfilerStop(context);
NSData *profileData = [NSData dataWithContentsOfFile:outputFile options:NSDataReadingMappedIfSafe error:NULL];
RCTProfileSendResult(bridge, @"cpu-profile", profileData);
}
}
}]];
}
}
#endif
+ (void)runRunLoopThread
{
@autoreleasepool {
// copy thread name to pthread name
pthread_setname_np([NSThread currentThread].name.UTF8String);
// Set up a dummy runloop source to avoid spinning
CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef noSpinSource = CFRunLoopSourceCreate(NULL, 0, &noSpinCtx);
CFRunLoopAddSource(CFRunLoopGetCurrent(), noSpinSource, kCFRunLoopDefaultMode);
CFRelease(noSpinSource);
// run the run loop
while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) {
RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad.
}
}
}
- (instancetype)init
{
if (self = [super init]) {
_valid = YES;
_javaScriptThread = [[NSThread alloc] initWithTarget:[self class]
selector:@selector(runRunLoopThread)
object:nil];
_javaScriptThread.name = RCTJSCThreadName;
if ([_javaScriptThread respondsToSelector:@selector(setQualityOfService:)]) {
[_javaScriptThread setQualityOfService:NSOperationQualityOfServiceUserInteractive];
} else {
_javaScriptThread.threadPriority = [NSThread mainThread].threadPriority;
}
[_javaScriptThread start];
}
return self;
}
- (RCTJavaScriptContext *)context
{
RCTAssertThread(_javaScriptThread, @"Must be called on JS thread.");
if (!self.isValid) {
return nil;
}
if (!_context) {
JSContext *context = [JSContext new];
_context = [[RCTJavaScriptContext alloc] initWithJSContext:context onThread:_javaScriptThread];
[[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptContextCreatedNotification
object:context];
}
return _context;
}
- (void)addSynchronousHookWithName:(NSString *)name usingBlock:(id)block
{
__weak RCTJSCExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:^{
weakSelf.context.context[name] = block;
}];
}
- (void)setUp
{
__weak RCTJSCExecutor *weakSelf = self;
#ifdef WITH_FB_JSC_TUNING
[self executeBlockOnJavaScriptQueue:^{
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return;
}
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
RCTAssert(cachesPath != nil, @"cachesPath should not be nil");
if (cachesPath) {
std::string path = std::string([cachesPath UTF8String]);
configureJSContextForIOS(strongSelf.context.ctx, path);
}
}];
#endif
[self addSynchronousHookWithName:@"noop" usingBlock:^{}];
[self addSynchronousHookWithName:@"nativeLoggingHook" usingBlock:^(NSString *message, NSNumber *logLevel) {
RCTLogLevel level = RCTLogLevelInfo;
if (logLevel) {
level = MAX(level, (RCTLogLevel)logLevel.integerValue);
}
_RCTLogJavaScriptInternal(level, message);
}];
[self addSynchronousHookWithName:@"nativeRequireModuleConfig" usingBlock:^NSString *(NSString *moduleName) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return nil;
}
RCT_PROFILE_BEGIN_EVENT(0, @"nativeRequireModuleConfig", nil);
NSArray *config = [strongSelf->_bridge configForModuleName:moduleName];
NSString *result = config ? RCTJSONStringify(config, NULL) : nil;
RCT_PROFILE_END_EVENT(0, @"js_call,config", @{ @"moduleName": moduleName });
return result;
}];
[self addSynchronousHookWithName:@"nativeFlushQueueImmediate" usingBlock:^(NSArray<NSArray *> *calls){
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid || !calls) {
return;
}
RCT_PROFILE_BEGIN_EVENT(0, @"nativeFlushQueueImmediate", nil);
[strongSelf->_bridge handleBuffer:calls batchEnded:NO];
RCT_PROFILE_END_EVENT(0, @"js_call", nil);
}];
[self addSynchronousHookWithName:@"nativePerformanceNow" usingBlock:^{
return @(CACurrentMediaTime() * 1000);
}];
#if RCT_DEV
if (RCTProfileIsProfiling()) {
// Cheating, since it's not a "hook", but meh
[self addSynchronousHookWithName:@"__RCTProfileIsProfiling" usingBlock:@YES];
}
_cookieMap = CFDictionaryCreateMutable(NULL, 0, NULL, NULL);
[self addSynchronousHookWithName:@"nativeTraceBeginAsyncSection" usingBlock:^(uint64_t tag, NSString *name, NSUInteger cookie) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
NSUInteger newCookie = RCTProfileBeginAsyncEvent(tag, name, nil);
CFDictionarySetValue(strongSelf->_cookieMap, (const void *)cookie, (const void *)newCookie);
}];
[self addSynchronousHookWithName:@"nativeTraceEndAsyncSection" usingBlock:^(uint64_t tag, NSString *name, NSUInteger cookie) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
NSUInteger newCookie = (NSUInteger)CFDictionaryGetValue(strongSelf->_cookieMap, (const void *)cookie);
RCTProfileEndAsyncEvent(tag, @"js,async", newCookie, name, @"JS async", nil);
CFDictionaryRemoveValue(strongSelf->_cookieMap, (const void *)cookie);
}];
[self addSynchronousHookWithName:@"nativeTraceBeginSection" usingBlock:^(NSNumber *tag, NSString *profileName){
static int profileCounter = 1;
if (!profileName) {
profileName = [NSString stringWithFormat:@"Profile %d", profileCounter++];
}
RCT_PROFILE_BEGIN_EVENT(tag.longLongValue, profileName, nil);
}];
[self addSynchronousHookWithName:@"nativeTraceEndSection" usingBlock:^(NSNumber *tag) {
RCT_PROFILE_END_EVENT(tag.longLongValue, @"console", nil);
}];
__weak RCTBridge *weakBridge = _bridge;
#ifndef __clang_analyzer__
_bridge.flowIDMap = CFDictionaryCreateMutable(NULL, 0, NULL, NULL);
#endif
_bridge.flowIDMapLock = [NSLock new];
[self addSynchronousHookWithName:@"nativeTraceBeginAsyncFlow" usingBlock:^(__unused uint64_t tag, __unused NSString *name, int64_t cookie) {
if (RCTProfileIsProfiling()) {
[weakBridge.flowIDMapLock lock];
int64_t newCookie = [_RCTProfileBeginFlowEvent() longLongValue];
CFDictionarySetValue(weakBridge.flowIDMap, (const void *)cookie, (const void *)newCookie);
[weakBridge.flowIDMapLock unlock];
}
}];
[self addSynchronousHookWithName:@"nativeTraceEndAsyncFlow" usingBlock:^(__unused uint64_t tag, __unused NSString *name, int64_t cookie) {
if (RCTProfileIsProfiling()) {
[weakBridge.flowIDMapLock lock];
int64_t newCookie = (int64_t)CFDictionaryGetValue(weakBridge.flowIDMap, (const void *)cookie);
_RCTProfileEndFlowEvent(@(newCookie));
CFDictionaryRemoveValue(weakBridge.flowIDMap, (const void *)cookie);
[weakBridge.flowIDMapLock unlock];
}
}];
[self executeBlockOnJavaScriptQueue:^{
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return;
}
JSContext *context = strongSelf.context.context;
RCTInstallJSCProfiler(_bridge, context.JSGlobalContextRef);
}];
for (NSString *event in @[RCTProfileDidStartProfiling, RCTProfileDidEndProfiling]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(toggleProfilingFlag:)
name:event
object:nil];
}
// Inject handler used by HMR
[self addSynchronousHookWithName:@"nativeInjectHMRUpdate" usingBlock:^(NSString *sourceCode, NSString *sourceCodeURL) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf.valid) {
return;
}
JSStringRef execJSString = JSStringCreateWithUTF8CString(sourceCode.UTF8String);
JSStringRef jsURL = JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String);
JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, NULL);
JSStringRelease(jsURL);
JSStringRelease(execJSString);
}];
#endif
}
- (void)toggleProfilingFlag:(NSNotification *)notification
{
[self executeBlockOnJavaScriptQueue:^{
BOOL enabled = [notification.name isEqualToString:RCTProfileDidStartProfiling];
[_bridge enqueueJSCall:@"Systrace.setEnabled" args:@[enabled ? @YES : @NO]];
}];
}
- (void)invalidate
{
if (!self.isValid) {
return;
}
_valid = NO;
#if RCT_DEV
[[NSNotificationCenter defaultCenter] removeObserver:self];
#endif
}
- (void)dealloc
{
[self invalidate];
[_context performSelector:@selector(invalidate)
onThread:_javaScriptThread
withObject:nil
waitUntilDone:NO];
_context = nil;
if (_jsModules) {
CFRelease(_jsModules);
fclose(_bundle);
}
if (_cookieMap) {
CFRelease(_cookieMap);
}
}
- (void)flushedQueue:(RCTJavaScriptCallback)onComplete
{
// TODO: Make this function handle first class instead of dynamically dispatching it. #9317773
[self _executeJSCall:@"flushedQueue" arguments:@[] callback:onComplete];
}
- (void)callFunctionOnModule:(NSString *)module
method:(NSString *)method
arguments:(NSArray *)args
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];
}
- (void)invokeCallbackID:(NSNumber *)cbID
arguments:(NSArray *)args
callback:(RCTJavaScriptCallback)onComplete
{
// TODO: Make this function handle first class instead of dynamically dispatching it. #9317773
[self _executeJSCall:@"invokeCallbackAndReturnFlushedQueue" arguments:@[cbID, args] callback:onComplete];
}
- (void)_executeJSCall:(NSString *)method
arguments:(NSArray *)arguments
callback:(RCTJavaScriptCallback)onComplete
{
RCTAssert(onComplete != nil, @"onComplete block should not be nil");
__weak RCTJSCExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf || !strongSelf.isValid) {
return;
}
NSError *error;
JSValueRef errorJSRef = NULL;
JSValueRef resultJSRef = NULL;
JSGlobalContextRef contextJSRef = JSContextGetGlobalContext(strongSelf->_context.ctx);
JSContext *context = strongSelf->_context.context;
JSObjectRef globalObjectJSRef = JSContextGetGlobalObject(strongSelf->_context.ctx);
// get the BatchedBridge object
JSStringRef moduleNameJSStringRef = JSStringCreateWithUTF8CString("__fbBatchedBridge");
JSValueRef moduleJSRef = JSObjectGetProperty(contextJSRef, globalObjectJSRef, moduleNameJSStringRef, &errorJSRef);
JSStringRelease(moduleNameJSStringRef);
if (moduleJSRef != NULL && errorJSRef == NULL && !JSValueIsUndefined(contextJSRef, moduleJSRef)) {
// get method
JSStringRef methodNameJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)method);
JSValueRef methodJSRef = JSObjectGetProperty(contextJSRef, (JSObjectRef)moduleJSRef, methodNameJSStringRef, &errorJSRef);
JSStringRelease(methodNameJSStringRef);
if (methodJSRef != NULL && errorJSRef == NULL && !JSValueIsUndefined(contextJSRef, methodJSRef)) {
JSValueRef jsArgs[arguments.count];
for (NSUInteger i = 0; i < arguments.count; i++) {
jsArgs[i] = [JSValue valueWithObject:arguments[i] inContext:context].JSValueRef;
}
resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, arguments.count, jsArgs, &errorJSRef);
} else {
if (!errorJSRef && JSValueIsUndefined(contextJSRef, methodJSRef)) {
error = RCTErrorWithMessage([NSString stringWithFormat:@"Unable to execute JS call: method %@ is undefined", method]);
}
}
} else {
if (!errorJSRef && JSValueIsUndefined(contextJSRef, moduleJSRef)) {
error = RCTErrorWithMessage(@"Unable to execute JS call: __fbBatchedBridge is undefined");
}
}
if (errorJSRef || error) {
if (!error) {
error = RCTNSErrorFromJSError(contextJSRef, errorJSRef);
}
onComplete(nil, error);
return;
}
// 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 (!JSValueIsNull(contextJSRef, resultJSRef)) {
objcValue = [[JSValue valueWithJSValueRef:resultJSRef inContext:context] toObject];
}
onComplete(objcValue, nil);
}), 0, @"js_call", (@{@"method": method, @"args": arguments}))];
}
- (void)executeApplicationScript:(NSData *)script
sourceURL:(NSURL *)sourceURL
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
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) {
NSError *error;
script = [self loadRAMBundle:sourceURL error:&error];
if (error) {
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;
}
_bundleURL = JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String);
__weak RCTJSCExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf || !strongSelf.isValid) {
return;
}
RCTPerformanceLoggerStart(RCTPLScriptExecution);
JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithUTF8CString((const char *)script.bytes);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, _bundleURL, 0, &jsError);
JSStringRelease(execJSString);
RCTPerformanceLoggerEnd(RCTPLScriptExecution);
if (onComplete) {
NSError *error;
if (!result) {
error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError);
}
onComplete(error);
}
}), 0, @"js_call", (@{ @"url": sourceURL.absoluteString }))];
}
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
if ([NSThread currentThread] != _javaScriptThread) {
[self performSelector:@selector(executeBlockOnJavaScriptQueue:)
onThread:_javaScriptThread withObject:block waitUntilDone:NO];
} else {
block();
}
}
- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block
{
[self performSelector:@selector(executeBlockOnJavaScriptQueue:)
onThread:_javaScriptThread
withObject:block
waitUntilDone:NO];
}
- (void)injectJSONText:(NSString *)script
asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete
{
if (RCT_DEBUG) {
RCTAssert(RCTJSONParse(script, NULL) != nil, @"%@ wasn't valid JSON!", script);
}
__weak RCTJSCExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf || !strongSelf.isValid) {
return;
}
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
JSValueRef valueToInject = JSValueMakeFromJSONString(strongSelf->_context.ctx, execJSString);
JSStringRelease(execJSString);
if (!valueToInject) {
NSString *errorDesc = [NSString stringWithFormat:@"Can't make JSON value from script '%@'", script];
RCTLogError(@"%@", errorDesc);
if (onComplete) {
NSError *error = [NSError errorWithDomain:RCTErrorDomain code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}];
onComplete(error);
}
return;
}
JSObjectRef globalObject = JSContextGetGlobalObject(strongSelf->_context.ctx);
JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)objectName);
JSObjectSetProperty(strongSelf->_context.ctx, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL);
JSStringRelease(JSName);
if (onComplete) {
onComplete(nil);
}
}), 0, @"js_call,json_call", (@{@"objectName": objectName}))];
}
static int streq(const char *a, const char *b)
{
return strcmp(a, b) == 0;
}
static void freeModule(__unused CFAllocatorRef allocator, void *ptr)
{
free(ptr);
}
static uint32_t readUint32(const char **ptr)
{
uint32_t data;
memcpy(&data, *ptr, sizeof(uint32_t));
data = NSSwapLittleIntToHost(data);
*ptr += sizeof(uint32_t);
return data;
}
static int readBundle(FILE *fd, size_t offset, size_t length, void *ptr)
{
if (fseek(fd, offset, SEEK_SET) != 0) {
return 1;
}
if (fread(ptr, sizeof(uint8_t), length, fd) != length) {
return 1;
}
return 0;
}
- (void)registerNativeRequire
{
RCTPerformanceLoggerSet(RCTPLRAMNativeRequires, 0);
RCTPerformanceLoggerSet(RCTPLRAMNativeRequiresCount, 0);
RCTPerformanceLoggerSet(RCTPLRAMNativeRequiresSize, 0);
__weak RCTJSCExecutor *weakSelf = self;
[self addSynchronousHookWithName:@"nativeRequire" usingBlock:^(NSString *moduleName) {
RCTJSCExecutor *strongSelf = weakSelf;
if (!strongSelf || !moduleName) {
return;
}
RCTPerformanceLoggerAdd(RCTPLRAMNativeRequiresCount, 1);
RCTPerformanceLoggerAppendStart(RCTPLRAMNativeRequires);
RCT_PROFILE_BEGIN_EVENT(0, [@"nativeRequire_" stringByAppendingString:moduleName], nil);
ModuleData *data = (ModuleData *)CFDictionaryGetValue(strongSelf->_jsModules, moduleName.UTF8String);
RCTPerformanceLoggerAdd(RCTPLRAMNativeRequiresSize, data->length);
char bytes[data->length];
if (readBundle(strongSelf->_bundle, data->offset, data->length, bytes) != 0) {
RCTFatal(RCTErrorWithMessage(@"Error loading RAM module"));
return;
}
JSStringRef code = JSStringCreateWithUTF8CString(bytes);
JSValueRef jsError = NULL;
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, code, NULL, strongSelf->_bundleURL, data->lineNo, NULL);
CFDictionaryRemoveValue(strongSelf->_jsModules, moduleName.UTF8String);
JSStringRelease(code);
RCT_PROFILE_END_EVENT(0, @"js_call", nil);
RCTPerformanceLoggerAppendEnd(RCTPLRAMNativeRequires);
if (!result) {
dispatch_async(dispatch_get_main_queue(), ^{
RCTFatal(RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError));
[strongSelf invalidate];
});
}
}];
}
- (NSData *)loadRAMBundle:(NSURL *)sourceURL error:(NSError **)error
{
RCTPerformanceLoggerStart(RCTPLRAMBundleLoad);
_bundle = fopen(sourceURL.path.UTF8String, "r");
if (!_bundle) {
if (error) {
*error = RCTErrorWithMessage([NSString stringWithFormat:@"Bundle %@ cannot be opened: %d", sourceURL.path, errno]);
}
return nil;
}
[self registerNativeRequire];
// once a module has been loaded free its space from the heap, remove it from the index and release the module name
CFDictionaryKeyCallBacks keyCallbacks = { 0, NULL, (CFDictionaryReleaseCallBack)freeModule, NULL, (CFDictionaryEqualCallBack)streq, (CFDictionaryHashCallBack)strlen };
CFDictionaryValueCallBacks valueCallbacks = { 0, NULL, (CFDictionaryReleaseCallBack)freeModule, NULL, NULL };
_jsModules = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks);
uint32_t currentOffset = sizeof(uint32_t); // skip magic number
uint32_t tableLength;
if (readBundle(_bundle, currentOffset, sizeof(tableLength), &tableLength) != 0) {
if (error) {
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
}
return nil;
}
tableLength = NSSwapLittleIntToHost(tableLength);
currentOffset += sizeof(uint32_t); // skip table length
// base offset to add to every module's offset to skip the header of the RAM bundle
uint32_t baseOffset = 4 + tableLength;
char tableStart[tableLength];
if (readBundle(_bundle, currentOffset, tableLength, tableStart) != 0) {
if (error) {
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
}
return nil;
}
char *tableCursor = tableStart;
char *endOfTable = tableCursor + tableLength;
while (tableCursor < endOfTable) {
uint32_t nameLength = strlen((const char *)tableCursor);
char *name = (char *)malloc(nameLength + 1);
if (!name) {
if (error) {
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
}
return nil;
}
strcpy(name, tableCursor);
// the space allocated for each module's metada gets freed when the module is injected into JSC on `nativeRequire`
ModuleData *moduleData = (ModuleData *)malloc(sizeof(ModuleData));
tableCursor += nameLength + 1; // null byte terminator
moduleData->offset = baseOffset + readUint32((const char **)&tableCursor);
moduleData->length = readUint32((const char **)&tableCursor);
moduleData->lineNo = readUint32((const char **)&tableCursor);
CFDictionarySetValue(_jsModules, name, moduleData);
}
ModuleData *startupData = ((ModuleData *)CFDictionaryGetValue(_jsModules, ""));
void *startupCode;
if (!(startupCode = malloc(startupData->length))) {
if (error) {
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
}
return nil;
}
if (readBundle(_bundle, startupData->offset, startupData->length, startupCode) != 0) {
if (error) {
*error = RCTErrorWithMessage(@"Error loading RAM Bundle");
}
free(startupCode);
return nil;
}
RCTPerformanceLoggerEnd(RCTPLRAMBundleLoad);
RCTPerformanceLoggerSet(RCTPLRAMStartupCodeSize, startupData->length);
return [NSData dataWithBytesNoCopy:startupCode length:startupData->length freeWhenDone:YES];
}
RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)name)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-pointer-compare"
if (JSGlobalContextSetName != NULL) {
#pragma clang diagnostic pop
JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)name);
JSGlobalContextSetName(_context.ctx, JSName);
JSStringRelease(JSName);
}
}
@end