djinni icon indicating copy to clipboard operation
djinni copied to clipboard

Show original C++ exception stack on ObjC

Open q790838939 opened this issue 1 year ago • 6 comments

I'm trying to disable C++ exception translation in ObjC because I want to see the original C++ stack. However, when the stack is thrown to the ObjC layer, it will be captured by __cxa_throw, and then uniformly converted into the same stack information as bellow:

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libsystem_kernel.dylib          0x9750ea6a 0x974fa000 + 84586 (__pthread_kill + 10)
1   libsystem_sim_c.dylib           0x04d56578 0x4d0f000 + 292216 (abort + 137)
2   libc++abi.dylib                 0x04ed6f78 0x4ed4000 + 12152 (abort_message + 102)
3   libc++abi.dylib                 0x04ed4a20 0x4ed4000 + 2592 (_ZL17default_terminatev + 29)
4   libobjc.A.dylib                 0x013110d0 0x130b000 + 24784 (_ZL15_objc_terminatev + 109)
5   libc++abi.dylib                 0x04ed4a60 0x4ed4000 + 2656 (_ZL19safe_handler_callerPFvvE + 8)
6   libc++abi.dylib                 0x04ed4ac8 0x4ed4000 + 2760 (_ZSt9terminatev + 18)
7   libc++abi.dylib                 0x04ed5c48 0x4ed4000 + 7240 (__cxa_rethrow + 77)
8   libobjc.A.dylib                 0x01310fb8 0x130b000 + 24504 (objc_exception_rethrow + 42)
9   CoreFoundation                  0x01f2af98 0x1ef9000 + 204696 (CFRunLoopRunSpecific + 360)

Is there any way to save the original stack of C++ and display it in the ObjC layer? Thank you very much.

q790838939 avatar Aug 31 '22 10:08 q790838939

There is a command line flag "objcpp-disable-exception-translation" to remove the translation. After that you should be able to capture the exception in xcode.

finalpatch avatar Aug 31 '22 11:08 finalpatch

There is a command line flag "objcpp-disable-exception-translation" to remove the translation. After that you should be able to capture the exception in xcode.

Thank you for your answer, but I hope that C++ stack information can be displayed in crash stack, not in xcode. So set "objcpp-disable-exception-translation = true" can not achieve my purpose.

q790838939 avatar Sep 01 '22 09:09 q790838939

C++ exceptions do not automatically capture their crash stack (unlike ObjC and Java exceptions, which do). So the only way to see where the exception happens is to run the program in a debugger and has the debugger stop on exceptions. This is not a limitation of Djinni, but a C++ thing.

LiFengSC avatar Sep 02 '22 00:09 LiFengSC

C++ exceptions do not automatically capture their crash stack (unlike ObjC and Java exceptions, which do).

HACK: This is correct, but you can get C++ exceptions showing up in production stack traces if you crash within the external throw interface in the c++ support lib.

in header (e.g: this_is_a_crash_handler.h)

heres an example

#ifdef __APPLE___
#include <cxxabi.h>
extern "C" void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*));
#endif

in impl: this_is_a_crash_handler.cc

#ifdef __APPLE__
// thrown_exception: Address of thrown exception object                                              
// tinfo: static type of thrown object                                                               
// dest: destructor of the exception.                                                                
void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*)) {               
  if (tinfo) {                                                                                       
    int status;                                                                                      
    char* real_name = abi::__cxa_demangle(tinfo->name(), 0, 0, &status);                             
    if (status != 0) {                                                                               
      // -1: malloc failed                                                                           
      // -2: mangled_name is not a valid name under the C++ ABI mangling rules.                      
      // -3: one of args is invalid                                                                  
      PrintWarning("Demangling of object type failed - err: '{}'", status);                    
    } else {                                                                                         
      PrintWarning("Static type name: '{}'", real_name);                                       
      std::string real_name_s{real_name};                                                            
      // !!All exceptions generated by the standard library inherit from std::exception!!            
      // so we can safely cast from void* -> std::exception*                                         
      const bool can_safely_cast =                                                                   
          real_name_s.find("std::") != std::string::npos ||             
          // Or one of your own c++ exceptions!                             
          real_name_s.find("SnapChatException::MyCustomSnapChatException") != std::string::npos;                        
      if (can_safely_cast) {                                                                         
        std::exception* e_ptr = static_cast<std::exception*>(thrown_exception);                      
        PrintWarning("std::exception thrown - e.what(): '{}'", e_ptr->what());                 
      }                                                                                              
      // Since you can throw any type in C++ (i.e. throw int/char/RandomClass), dont cast            
      // to anything else and just free.                                                             
      free(real_name);                                                                               
    }                                                                                                
  }                                                                                                  
  PrintWarning(                                                                                
      "[Custom Exception] Crashing on custom exception -- see stacktrace + logs in your crash reporting tools  "    
      "for info.");              
      // This is the C++ equivalent of `fatalError()`                                                                    
  __builtin_unreachable();                                                                           
}                                                                                                    
}                                                                                                    
#endif                                                                                               

This means anytime you throw <exception>..., the app will crash and the stack trace will be visible.

Lowkey some bullshit c++ black magic but hey, atleast you have a stack trace

tom-skydio avatar Sep 02 '22 00:09 tom-skydio

Thanks @tom-skydio , I have try your advice like bellow, here I use a global variable last_frames to save the stack information of C++ exception and put it in the message of NSException.

#define BACKTRACE_MAX_FRAME_NUMBER 8
void * last_frames[BACKTRACE_MAX_FRAME_NUMBER];
size_t last_size;

typedef void (*cxa_throw_type)(void*, std::type_info*, void (*)(void*));
cxa_throw_type original_cxa_throw = nullptr;
extern "C" void __cxa_throw(void *exception, std::type_info *typeinfo, void (*dest)(void *)) {
    if (original_cxa_throw == nullptr) {
        original_cxa_throw = (cxa_throw_type)(dlsym(RTLD_NEXT, "__cxa_throw"));
    }
    last_size = backtrace(last_frames, BACKTRACE_MAX_FRAME_NUMBER);
    original_cxa_throw(exception, typeinfo, dest);
}
[[noreturn]] __attribute__((weak)) void throwNSExceptionFromCurrent(const char * /*ctx*/) {
    try {
        throw;
    } catch (const std::exception & e) {
        char **symbols = backtrace_symbols(last_frames, static_cast<int>(last_size));
        NSMutableArray<NSString *> * stack = [NSMutableArray new];
        for (std::size_t i = 0; i < last_size; i++) {
            NSString *symbol = [[NSString alloc] initWithUTF8String:symbols[i]];
            [stack addObject:symbol];
        }
        free(symbols);
        NSString *what = [[NSString alloc] initWithUTF8String:e.what()];
        NSString *stacks = [stack componentsJoinedByString:@"\n"];
        NSString *message = [NSString stringWithFormat:@"%@\n%@", what, stacks];
        [NSException raise:message format:@"%@", message];
        __builtin_unreachable();
    }
}

However, rewrite __cxa_throw() will hook the global throw event, so it is possible that multiple threads throw at the same time will overwrite the global variable last_frames, and finally the stack information contained in it may not be what we expected. Do you have any advice?

q790838939 avatar Sep 05 '22 06:09 q790838939

you can use a std::lock_guard<std::mutex> to lock r/w on the array --

also what's the reasoning behind catching, then rethrowing the exception? that results in the original stack trace being missing, no?

[[noreturn]] __attribute__((weak)) void throwNSExceptionFromCurrent(const char * /*ctx*/) {
    try {
        throw;
    }
    ```

tom-skydio avatar Sep 27 '22 20:09 tom-skydio