diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h index aa14453ad..1d8a19ee5 100755 --- a/React/Base/RCTJavaScriptLoader.h +++ b/React/Base/RCTJavaScriptLoader.h @@ -11,6 +11,8 @@ #import "RCTBridgeDelegate.h" +extern uint32_t const RCTRAMBundleMagicNumber; + @class RCTBridge; /** diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index 763e36893..36d15d870 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -15,6 +15,10 @@ #import "RCTUtils.h" #import "RCTPerformanceLogger.h" +#include + +uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5; + @implementation RCTJavaScriptLoader RCT_NOT_IMPLEMENTED(- (instancetype)init) @@ -34,13 +38,48 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) // Load local script file if (scriptURL.fileURL) { - NSString *filePath = scriptURL.path; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error = nil; - NSData *source = [NSData dataWithContentsOfFile:filePath - options:NSDataReadingMappedIfSafe - error:&error]; - RCTPerformanceLoggerSet(RCTPLBundleSize, source.length); + NSData *source = nil; + + // Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle). + // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. + // The benefit of RAM bundle over a regular bundle is that we can lazily inject + // modules into JSC as they're required. + FILE *bundle = fopen(scriptURL.path.UTF8String, "r"); + if (!bundle) { + onComplete(RCTErrorWithMessage([NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]), source); + return; + } + + uint32_t magicNumber; + if (fread(&magicNumber, sizeof(magicNumber), 1, bundle) != 1) { + fclose(bundle); + onComplete(RCTErrorWithMessage(@"Error reading bundle"), source); + return; + } + + magicNumber = NSSwapLittleIntToHost(magicNumber); + + int32_t sourceLength = 0; + if (magicNumber == RCTRAMBundleMagicNumber) { + source = [NSData dataWithBytes:&magicNumber length:sizeof(magicNumber)]; + + struct stat statInfo; + if (stat(scriptURL.path.UTF8String, &statInfo) != 0) { + error = RCTErrorWithMessage(@"Error reading bundle"); + } else { + sourceLength = statInfo.st_size; + } + } else { + source = [NSData dataWithContentsOfFile:scriptURL.path + options:NSDataReadingMappedIfSafe + error:&error]; + sourceLength = source.length; + } + + RCTPerformanceLoggerSet(RCTPLBundleSize, sourceLength); + fclose(bundle); onComplete(error, source); }); return; diff --git a/React/Executors/RCTJSCExecutor.m b/React/Executors/RCTJSCExecutor.m index 5db8a5cbc..3ab9049a4 100644 --- a/React/Executors/RCTJSCExecutor.m +++ b/React/Executors/RCTJSCExecutor.m @@ -18,6 +18,7 @@ #import "RCTBridge+Private.h" #import "RCTDefines.h" #import "RCTDevMenu.h" +#import "RCTJavaScriptLoader.h" #import "RCTLog.h" #import "RCTProfile.h" #import "RCTPerformanceLogger.h" @@ -35,6 +36,7 @@ static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnable typedef struct ModuleData { uint32_t offset; + uint32_t length; uint32_t lineNo; } ModuleData; @@ -101,7 +103,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) RCTJavaScriptContext *_context; NSThread *_javaScriptThread; - NSData *_bundle; + FILE *_bundle; JSStringRef _bundleURL; CFMutableDictionaryRef _jsModules; } @@ -368,6 +370,7 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) if (_jsModules) { CFRelease(_jsModules); + fclose(_bundle); } #if RCT_DEV @@ -519,19 +522,33 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) sourceURL:(NSURL *)sourceURL onComplete:(RCTJavaScriptCompleteBlock)onComplete { - // Check if it's a `RAM bundle` ("Random Access Modules" bundle). - // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. - // The benefit of RAM bundle over a regular bundle is that we can lazily inject - // modules into JSC as they're required. - static const uint32_t ramBundleMagicNumber = 0xFB0BD1E5; - uint32_t magicNumber = *(uint32_t *)script.bytes; - if (magicNumber == ramBundleMagicNumber) { - script = [self loadRAMBundle:script]; - } - 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; @@ -544,14 +561,8 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) RCTPerformanceLoggerStart(RCTPLScriptExecution); - // JSStringCreateWithUTF8CString expects a null terminated C string - NSMutableData *nullTerminatedScript = [NSMutableData dataWithCapacity:script.length + 1]; - - [nullTerminatedScript appendData:script]; - [nullTerminatedScript appendBytes:"" length:1]; - JSValueRef jsError = NULL; - JSStringRef execJSString = JSStringCreateWithUTF8CString(nullTerminatedScript.bytes); + JSStringRef execJSString = JSStringCreateWithUTF8CString(script.bytes); JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError); JSStringRelease(jsURL); @@ -635,7 +646,7 @@ static int streq(const char *a, const char *b) return strcmp(a, b) == 0; } -static void freeModuleData(__unused CFAllocatorRef allocator, void *ptr) +static void freeModule(__unused CFAllocatorRef allocator, void *ptr) { free(ptr); } @@ -648,7 +659,19 @@ static uint32_t readUint32(const void **ptr) { return data; } -- (NSData *)loadRAMBundle:(NSData *)script +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 { __weak RCTJSCExecutor *weakSelf = self; _context.context[@"nativeRequire"] = ^(NSString *moduleName) { @@ -657,14 +680,18 @@ static uint32_t readUint32(const void **ptr) { return; } - ModuleData *moduleData = (ModuleData *)CFDictionaryGetValue(strongSelf->_jsModules, moduleName.UTF8String); - JSStringRef module = JSStringCreateWithUTF8CString((const char *)_bundle.bytes + moduleData->offset); - int lineNo = [moduleName isEqual:@""] ? 0 : moduleData->lineNo; + ModuleData *data = (ModuleData *)CFDictionaryGetValue(strongSelf->_jsModules, moduleName.UTF8String); + 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, module, NULL, strongSelf->_bundleURL, lineNo, NULL); + JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, code, NULL, strongSelf->_bundleURL, data->lineNo, NULL); CFDictionaryRemoveValue(strongSelf->_jsModules, moduleName.UTF8String); - JSStringRelease(module); + JSStringRelease(code); if (!result) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -673,45 +700,82 @@ static uint32_t readUint32(const void **ptr) { }); } }; +} + +- (NSData *)loadRAMBundle:(NSURL *)sourceURL error:(NSError **)error +{ + _bundle = fopen(sourceURL.path.UTF8String, "r"); + if (!_bundle) { + *error = RCTErrorWithMessage([NSString stringWithFormat:@"Bundle %@ cannot be opened: %d", sourceURL.path, errno]); + return nil; + } + + [self registerNativeRequire]; - _bundle = script; - CFDictionaryKeyCallBacks keyCallbacks = { 0, NULL, NULL, NULL, (CFDictionaryEqualCallBack)streq, (CFDictionaryHashCallBack)strlen }; // once a module has been loaded free its space from the heap, remove it from the index and release the module name - CFDictionaryValueCallBacks valueCallbacks = { 0, NULL, (CFDictionaryReleaseCallBack)freeModuleData, NULL, NULL }; + 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); - const uint8_t *bytes = script.bytes; - uint32_t currentOffset = 4; // skip magic number + uint32_t currentOffset = sizeof(uint32_t); // skip magic number uint32_t tableLength; - memcpy(&tableLength, bytes + currentOffset, sizeof(tableLength)); + if (readBundle(_bundle, currentOffset, sizeof(tableLength), &tableLength) != 0) { + *error = RCTErrorWithMessage(@"Error loading RAM Bundle"); + return nil; + } tableLength = NSSwapLittleIntToHost(tableLength); - // offset where the code starts on the bundle - const uint32_t baseOffset = currentOffset + tableLength; + currentOffset += sizeof(uint32_t); // skip table length - // pointer to first byte out of the index - const uint8_t *endOfTable = bytes + baseOffset; + // base offset to add to every module's offset to skip the header of the RAM bundle + uint32_t baseOffset = 4 + tableLength; - // pointer to current position on table - const uint8_t *tablePos = bytes + 2 * sizeof(uint32_t); // skip magic number and table length + char tableStart[tableLength]; + if (readBundle(_bundle, currentOffset, tableLength, tableStart) != 0) { + *error = RCTErrorWithMessage(@"Error loading RAM Bundle"); + return nil; + } - while (tablePos < endOfTable) { - const char *moduleName = (const char *)tablePos; + void *tableCursor = tableStart; + void *endOfTable = tableCursor + tableLength; + + while (tableCursor < endOfTable) { + uint32_t nameLength = strlen((const char *)tableCursor); + char *name = malloc(nameLength + 1); + + if (!name) { + *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 = malloc(sizeof(ModuleData)); - tablePos += strlen(moduleName) + 1; // null byte terminator + tableCursor += nameLength + 1; // null byte terminator - moduleData->offset = baseOffset + readUint32((const void **)&tablePos); - moduleData->lineNo = readUint32((const void **)&tablePos); + moduleData->offset = baseOffset + readUint32((const void **)&tableCursor); + moduleData->length = readUint32((const void **)&tableCursor); + moduleData->lineNo = readUint32((const void **)&tableCursor); - CFDictionarySetValue(_jsModules, moduleName, moduleData); + CFDictionarySetValue(_jsModules, name, moduleData); } - uint32_t offset = ((ModuleData *)CFDictionaryGetValue(_jsModules, ""))->offset; - return [NSData dataWithBytesNoCopy:((char *) script.bytes) + offset length:script.length - offset freeWhenDone:NO]; + ModuleData *startupData = ((ModuleData *)CFDictionaryGetValue(_jsModules, "")); + + void *startupCode; + if (!(startupCode = malloc(startupData->length))) { + *error = RCTErrorWithMessage(@"Error loading RAM Bundle"); + return nil; + } + + if (readBundle(_bundle, startupData->offset, startupData->length, startupCode) != 0) { + *error = RCTErrorWithMessage(@"Error loading RAM Bundle"); + return nil; + } + return [NSData dataWithBytesNoCopy:startupCode length:startupData->length freeWhenDone:YES]; } RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)name) diff --git a/local-cli/bundle/output/unbundle/as-indexed-file.js b/local-cli/bundle/output/unbundle/as-indexed-file.js index ea23cf38e..b8c2c725e 100644 --- a/local-cli/bundle/output/unbundle/as-indexed-file.js +++ b/local-cli/bundle/output/unbundle/as-indexed-file.js @@ -90,6 +90,7 @@ function buildModuleTable(buffers) { // entry: // - module_id: NUL terminated utf8 string // - module_offset: uint_32 offset into the module string + // - module_length: uint_32 length in bytes of the module // - module_line: uint_32 line on which module starts on the bundle const numBuffers = buffers.length; @@ -107,6 +108,7 @@ function buildModuleTable(buffers) { Buffer(i === 0 ? MAGIC_STARTUP_MODULE_ID : id, 'utf8'), nullByteBuffer, uInt32Buffer(currentOffset), + uInt32Buffer(length), uInt32Buffer(currentLine), ]);