From 382f0898cf4112e64150c3b771e85df53ae86575 Mon Sep 17 00:00:00 2001 From: Christoph Nakazawa Date: Mon, 15 Jun 2020 06:47:38 -0700 Subject: [PATCH] Bytecode client for iOS Summary: Changelog: [Internal] Reviewed By: javache Differential Revision: D21206851 fbshipit-source-id: 67ab59688c19870ef419711fdfd489bf0442bb54 --- ...eURLProvider.m => RCTBundleURLProvider.mm} | 18 +++++++++++-- React/Base/RCTJavaScriptLoader.h | 5 ++++ React/Base/RCTJavaScriptLoader.mm | 25 ++++++++++++++++--- React/CxxBridge/RCTCxxBridge.mm | 10 +++++++- 4 files changed, 52 insertions(+), 6 deletions(-) rename React/Base/{RCTBundleURLProvider.m => RCTBundleURLProvider.mm} (95%) diff --git a/React/Base/RCTBundleURLProvider.m b/React/Base/RCTBundleURLProvider.mm similarity index 95% rename from React/Base/RCTBundleURLProvider.m rename to React/Base/RCTBundleURLProvider.mm index 0cdef61954e..b474d80beb6 100644 --- a/React/Base/RCTBundleURLProvider.m +++ b/React/Base/RCTBundleURLProvider.mm @@ -10,6 +10,11 @@ #import "RCTConvert.h" #import "RCTDefines.h" +#if __has_include("hermes.h") || __has_include() +#import +#define HAS_BYTECODE_VERSION +#endif + NSString *const RCTBundleURLProviderUpdatedNotification = @"RCTBundleURLProviderUpdatedNotification"; const NSUInteger kRCTBundleURLProviderDefaultPort = RCT_METRO_PORT; @@ -214,12 +219,21 @@ static NSURL *serverRootWithHostPort(NSString *hostPort) runModule:(BOOL)runModule { NSString *path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot]; +#ifdef HAS_BYTECODE_VERSION + NSString *runtimeBytecodeVersion = + [NSString stringWithFormat:@"&runtimeBytecodeVersion=%u", facebook::hermes::HermesRuntime::getBytecodeVersion()]; +#else + NSString *runtimeBytecodeVersion = @""; +#endif + // When we support only iOS 8 and above, use queryItems for a better API. - NSString *query = [NSString stringWithFormat:@"platform=ios&dev=%@&minify=%@&modulesOnly=%@&runModule=%@", + NSString *query = [NSString stringWithFormat:@"platform=ios&dev=%@&minify=%@&modulesOnly=%@&runModule=%@%@", enableDev ? @"true" : @"false", enableMinification ? @"true" : @"false", modulesOnly ? @"true" : @"false", - runModule ? @"true" : @"false"]; + runModule ? @"true" : @"false", + runtimeBytecodeVersion]; + NSString *bundleID = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]; if (bundleID) { query = [NSString stringWithFormat:@"%@&app=%@", query, bundleID]; diff --git a/React/Base/RCTJavaScriptLoader.h b/React/Base/RCTJavaScriptLoader.h index 9dbd7056e6c..e4123cb3b10 100755 --- a/React/Base/RCTJavaScriptLoader.h +++ b/React/Base/RCTJavaScriptLoader.h @@ -11,6 +11,11 @@ extern NSString *const RCTJavaScriptLoaderErrorDomain; +extern const UInt32 RCT_BYTECODE_ALIGNMENT; + +UInt32 RCTReadUInt32LE(NSData *script, UInt32 offset); +bool RCTIsBytecodeBundle(NSData *script); + NS_ENUM(NSInteger){ RCTJavaScriptLoaderErrorNoScriptURL = 1, RCTJavaScriptLoaderErrorFailedOpeningFile = 2, diff --git a/React/Base/RCTJavaScriptLoader.mm b/React/Base/RCTJavaScriptLoader.mm index a2ab5a22807..a95446b0093 100755 --- a/React/Base/RCTJavaScriptLoader.mm +++ b/React/Base/RCTJavaScriptLoader.mm @@ -21,6 +21,20 @@ NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomai static const int32_t JSNoBytecodeFileFormatVersion = -1; +const UInt32 RCT_BYTECODE_ALIGNMENT = 4; +UInt32 RCTReadUInt32LE(NSData *script, UInt32 offset) +{ + return [script length] < offset + 4 ? 0 : CFSwapInt32LittleToHost(*(((uint32_t *)[script bytes]) + offset / 4)); +} + +bool RCTIsBytecodeBundle(NSData *script) +{ + static const UInt32 BYTECODE_BUNDLE_MAGIC_NUMBER = 0xffe7c3c3; + return ( + [script length] > 8 && RCTReadUInt32LE(script, 0) == BYTECODE_BUNDLE_MAGIC_NUMBER && + RCTReadUInt32LE(script, 4) > 0); +} + @interface RCTSource () { @public NSURL *_url; @@ -37,7 +51,10 @@ static RCTSource *RCTSourceCreate(NSURL *url, NSData *data, int64_t length) NS_R { RCTSource *source = [RCTSource new]; source->_url = url; - source->_data = data; + // Multipart responses may give us an unaligned view into the buffer. This ensures memory is aligned. + source->_data = (RCTIsBytecodeBundle(data) && ((long)[data bytes] % RCT_BYTECODE_ALIGNMENT)) + ? [[NSData alloc] initWithData:data] + : data; source->_length = length; source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler; return source; @@ -300,7 +317,8 @@ static void attemptAsynchronousLoadOfBundleAtURL( // Validate that the packager actually returned javascript. NSString *contentType = headers[@"Content-Type"]; NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject]; - if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"]) { + if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"] && + ![mimeType isEqualToString:@"application/x-metro-bytecode-bundle"]) { NSString *description; if ([mimeType isEqualToString:@"application/json"]) { NSError *parseError; @@ -332,7 +350,8 @@ static void attemptAsynchronousLoadOfBundleAtURL( } progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) { // Only care about download progress events for the javascript bundle part. - if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) { + if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"] || + [headers[@"Content-Type"] isEqualToString:@"application/x-metro-bytecode-bundle"]) { onProgress(progressEventFromDownloadProgress(loaded, total)); } }]; diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index aa2a816d0a0..0d9f380da7c 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -1414,7 +1414,15 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithBundleURL // hold a local reference to reactInstance in case a parallel thread // resets it between null check and usage auto reactInstance = self->_reactInstance; - if (isRAMBundle(script)) { + if (reactInstance && RCTIsBytecodeBundle(script)) { + UInt32 offset = 8; + while (offset < script.length) { + UInt32 fileLength = RCTReadUInt32LE(script, offset); + NSData *unit = [script subdataWithRange:NSMakeRange(offset + 4, fileLength)]; + reactInstance->loadScriptFromString(std::make_unique(unit), sourceUrlStr.UTF8String, false); + offset += ((fileLength + RCT_BYTECODE_ALIGNMENT - 1) & ~(RCT_BYTECODE_ALIGNMENT - 1)) + 4; + } + } else if (isRAMBundle(script)) { [self->_performanceLogger markStartForTag:RCTPLRAMBundleLoad]; auto ramBundle = std::make_unique(sourceUrlStr.UTF8String); std::unique_ptr scriptStr = ramBundle->getStartupCode();