From f42b5892a16b71f4c7b09892f142b2b89c5388c8 Mon Sep 17 00:00:00 2001 From: Kevin Gozali Date: Sun, 18 Mar 2018 23:49:51 -0700 Subject: [PATCH] iOS C++: Add ICxxExceptionManager to access RCTExceptionsManager native module Summary: It is sometimes useful to report soft/fatal errors from C++ (native) to the same RCTExceptionsManager that is already handling JS exceptions. `ICxxExceptionManager` is an approach to provide such access, which impl provided for ObjC++. Reviewed By: shergin Differential Revision: D7224944 fbshipit-source-id: 8c607226b67851d46f4c787f5b6e6c8cb6a1afea --- React/Base/RCTLog.h | 7 ++ React/Base/RCTLog.mm | 65 ++++++++++--------- React/CxxExceptions/RCTCxxExceptionManager.h | 27 ++++++++ React/CxxExceptions/RCTCxxExceptionManager.mm | 31 +++++++++ React/Modules/RCTExceptionsManager.h | 3 + ReactCommon/exceptions/BUCK | 40 ++++++++++++ ReactCommon/exceptions/ExceptionManager.h | 24 +++++++ 7 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 React/CxxExceptions/RCTCxxExceptionManager.h create mode 100644 React/CxxExceptions/RCTCxxExceptionManager.mm create mode 100644 ReactCommon/exceptions/BUCK create mode 100644 ReactCommon/exceptions/ExceptionManager.h diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 9f9317da41..6ef09d620c 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -120,6 +120,13 @@ RCT_EXTERN void RCTPerformBlockWithLogFunction(void (^block)(void), RCTLogFuncti */ RCT_EXTERN void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix); +/** + * This method computes the current call stack, useful for error reporting. + */ +RCT_EXTERN NSArray *RCTGetCallStack(const char *fileName, int lineNumber); + +#define RCT_CALLSTACK RCTGetCallStack(__FILE__, __LINE__) + /** * Private logging function - ignore this. */ diff --git a/React/Base/RCTLog.mm b/React/Base/RCTLog.mm index edb92fe014..4e65fa0cc8 100644 --- a/React/Base/RCTLog.mm +++ b/React/Base/RCTLog.mm @@ -197,6 +197,41 @@ static NSRegularExpression *nativeStackFrameRegex() return _regex; } +NSArray* RCTGetCallStack(const char *fileName, int lineNumber) +{ + NSArray *stackSymbols = [NSThread callStackSymbols]; + NSMutableArray *stack = + [NSMutableArray arrayWithCapacity:(stackSymbols.count - 1)]; + [stackSymbols enumerateObjectsUsingBlock:^(NSString *frameSymbols, NSUInteger idx, __unused BOOL *stop) { + if (idx == 0) { + // don't include the current frame + return; + } + + NSRange range = NSMakeRange(0, frameSymbols.length); + NSTextCheckingResult *match = [nativeStackFrameRegex() firstMatchInString:frameSymbols options:0 range:range]; + if (!match) { + return; + } + + NSString *methodName = [frameSymbols substringWithRange:[match rangeAtIndex:1]]; + char *demangledName = abi::__cxa_demangle([methodName UTF8String], NULL, NULL, NULL); + if (demangledName) { + methodName = @(demangledName); + free(demangledName); + } + + if (idx == 1 && fileName) { + NSString *file = [@(fileName) componentsSeparatedByString:@"/"].lastObject; + [stack addObject:@{@"methodName": methodName, @"file": file, @"lineNumber": @(lineNumber)}]; + } else { + [stack addObject:@{@"methodName": methodName}]; + } + }]; + + return [stack copy]; +} + void _RCTLogNativeInternal(RCTLogLevel level, const char *fileName, int lineNumber, NSString *format, ...) { RCTLogFunction logFunction = RCTGetLocalLogFunction(); @@ -217,35 +252,7 @@ void _RCTLogNativeInternal(RCTLogLevel level, const char *fileName, int lineNumb // Log to red box in debug mode. if (RCTSharedApplication() && level >= RCTLOG_REDBOX_LEVEL) { - NSArray *stackSymbols = [NSThread callStackSymbols]; - NSMutableArray *stack = - [NSMutableArray arrayWithCapacity:(stackSymbols.count - 1)]; - [stackSymbols enumerateObjectsUsingBlock:^(NSString *frameSymbols, NSUInteger idx, __unused BOOL *stop) { - if (idx == 0) { - // don't include the current frame - return; - } - - NSRange range = NSMakeRange(0, frameSymbols.length); - NSTextCheckingResult *match = [nativeStackFrameRegex() firstMatchInString:frameSymbols options:0 range:range]; - if (!match) { - return; - } - - NSString *methodName = [frameSymbols substringWithRange:[match rangeAtIndex:1]]; - char *demangledName = abi::__cxa_demangle([methodName UTF8String], NULL, NULL, NULL); - if (demangledName) { - methodName = @(demangledName); - free(demangledName); - } - - if (idx == 1 && fileName) { - NSString *file = [@(fileName) componentsSeparatedByString:@"/"].lastObject; - [stack addObject:@{@"methodName": methodName, @"file": file, @"lineNumber": @(lineNumber)}]; - } else { - [stack addObject:@{@"methodName": methodName}]; - } - }]; + NSArray *stack = RCT_CALLSTACK; dispatch_async(dispatch_get_main_queue(), ^{ // red box is thread safe, but by deferring to main queue we avoid a startup diff --git a/React/CxxExceptions/RCTCxxExceptionManager.h b/React/CxxExceptions/RCTCxxExceptionManager.h new file mode 100644 index 0000000000..c8e3de44c4 --- /dev/null +++ b/React/CxxExceptions/RCTCxxExceptionManager.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook { +namespace react { + +/** + * Connector class (from C++ to ObjC++) to allow FabricUIManager to invoke native UI operations/updates. + * UIKit-related impl doesn't live here, but this class gets passed to the FabricUIManager C++ impl directly. + */ +class RCTCxxExceptionManager : public ExceptionManager { +public: + + virtual void handleSoftException(const std::exception& e) const override; + virtual void handleFatalException(const std::exception& e) const override; +}; + +} // namespace react +} // namespace facebook diff --git a/React/CxxExceptions/RCTCxxExceptionManager.mm b/React/CxxExceptions/RCTCxxExceptionManager.mm new file mode 100644 index 0000000000..61e636378d --- /dev/null +++ b/React/CxxExceptions/RCTCxxExceptionManager.mm @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTCxxExceptionManager.h" + +#import + +#import +#import +#import +#import + +namespace facebook { +namespace react { + +void RCTCxxExceptionManager::handleSoftException(const std::exception& e) const { + RCTExceptionsManager *manager = [[RCTBridge currentBridge] moduleForClass:[RCTExceptionsManager class]]; + [manager reportSoftException:[NSString stringWithUTF8String:e.what()] stack:RCT_CALLSTACK exceptionId:@0]; +} + +void RCTCxxExceptionManager::handleFatalException(const std::exception& e) const { + RCTExceptionsManager *manager = [[RCTBridge currentBridge] moduleForClass:[RCTExceptionsManager class]]; + [manager reportFatalException:[NSString stringWithUTF8String:e.what()] stack:RCT_CALLSTACK exceptionId:@0]; +} + +} // namespace react +} // namespace facebook diff --git a/React/Modules/RCTExceptionsManager.h b/React/Modules/RCTExceptionsManager.h index 57e81fb9e4..950cdaeab2 100644 --- a/React/Modules/RCTExceptionsManager.h +++ b/React/Modules/RCTExceptionsManager.h @@ -23,6 +23,9 @@ - (instancetype)initWithDelegate:(id)delegate; +- (void)reportSoftException:(NSString *)message stack:(NSArray *)stack exceptionId:(nonnull NSNumber *)exceptionId; +- (void)reportFatalException:(NSString *)message stack:(NSArray *)stack exceptionId:(nonnull NSNumber *)exceptionId; + @property (nonatomic, weak) id delegate; @property (nonatomic, assign) NSUInteger maxReloadAttempts; diff --git a/ReactCommon/exceptions/BUCK b/ReactCommon/exceptions/BUCK new file mode 100644 index 0000000000..352bc036fd --- /dev/null +++ b/ReactCommon/exceptions/BUCK @@ -0,0 +1,40 @@ +load("//configurations/buck/apple:flag_defs.bzl", "get_debug_preprocessor_flags") +load("//ReactNative:DEFS.bzl", "IS_OSS_BUILD", "react_native_xplat_target", "rn_xplat_cxx_library", "APPLE_INSPECTOR_FLAGS") + +APPLE_COMPILER_FLAGS = [] + +if not IS_OSS_BUILD: + load("@xplat//configurations/buck/apple:flag_defs.bzl", "get_static_library_ios_flags", "flags") + APPLE_COMPILER_FLAGS = flags.get_flag_value(get_static_library_ios_flags(), 'compiler_flags') + +rn_xplat_cxx_library( + name = "exceptions", + srcs = glob(["*.cpp"]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ("", "*.h"), + ], + prefix = "cxxreact", + ), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, + fbobjc_preprocessor_flags = get_debug_preprocessor_flags() + APPLE_INSPECTOR_FLAGS, + force_static = True, + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + visibility = [ + "PUBLIC", + ], + deps = [ + "xplat//fbsystrace:fbsystrace", + "xplat//third-party/glog:glog", + ], +) diff --git a/ReactCommon/exceptions/ExceptionManager.h b/ReactCommon/exceptions/ExceptionManager.h new file mode 100644 index 0000000000..ba07912bfa --- /dev/null +++ b/ReactCommon/exceptions/ExceptionManager.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +namespace facebook { +namespace react { + +/** + * An abstract class for C++-based exception handling that differentiates + * soft exceptions from fatal exceptions. + */ +class ExceptionManager { +public: + virtual void handleSoftException(const std::exception &e) const = 0; + virtual void handleFatalException(const std::exception &e) const = 0; +}; + +} // namespace react +} // namespace facebook