Tutorial: Crash Reporters and java exceptions
16 Feb 2018 | tutorialUPDATE: Crashlytics reports also processed NPE, this happens as it uses mach exception handlers. Check following post for workaround.
Third party native SDK might report crash from following sources:
- by sending crash report stored on device;
- signals;
- native unhandled exceptions.
Last two cases require special consideration to be taken to let SDK receive required events and keep RoboVM operations as expected.
Sending crash reports stored on device
Fabric/Crashlytic did/does this way. Once app crashed – nothing will be sent. On next launch it will pick up crash log and deliver it.
Signals - initializing SDKs right
Crash reporting SDK most of cases registers for signal to receive callback once crash situation happens. There could be several signals intercepted and SIGSEGV
is one that always intercepted. Its segmentation fault or access violation fault conditions. Often this mean that EXC_BAD_ACCESS
happened. Example of such report is bellow:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 0
Signals also special case for RoboVM. It also intercepts SIGSEGV
for special cases:
null pointer
exceptionstack overflow
exception.
RoboVM installs signal handler during initialization. Any SDK initialized from RoboVM code will cause SDK’s handler to be set instead of RoboVMs one. This means that RoboVM will not be able to handle null pointer exception and application will be terminated.
Example of code that will cause crash
public static void testNPE() {
String s = null;
try {
s.toString();
} catch (NullPointerException e) {
e.printStackTrace();
}
}
This valid java code will cause application to be terminated. As NPE will not be handled by RoboVM but SDK. And for SDK it pretty valid failure case and subject for abort();
Proper way to initialize SDK that uses Signals
If SDK uses signal handler (most of crash reporting ones do) it has to be initialized in special code block:
Signals.installSignals(() -> {
MySignalSDK.init("SDK-KEY");
});
Signals.installSignals
calls InstallSignalsCallback.install()
. User has to initialize all SDKs inside this method. And as final Signals.installSignals
will restore RoboVM signals.
Native unhandled exceptions
SDKs also register themselves to receive unhandled exceptions with NSSetUncaughtExceptionHandler
. It handles all exception that are being generated by native code and NSException in particular (all that goes through objc_exception_throw
). SDKs are able to pick up useful information from exceptions such as reason and deliver it to their backend.
Unhandled RoboVM Java exceptions
Exceptions in RoboVM java code are not ObjC exceptions and will not go to objc_exception_throw
by default. Default behavior in java is to terminate thread due unhandled exception. So if this happens in:
- main thread – application will exit with negative exit code;
- non-main thread – thread will die;
It is up to developer how to deal with this. There is Thread.UncaughtExceptionHandler as well as Thread.setDefaultUncaughtExceptionHandler
. Best case scenario is to capture these and deliver with crash reporter API. Fast way scenario for mobile apps – it is to crash and allow dev to fix bug by finding it in crash report.
Fast way: turning Java exception into NSException and crashing App
Following API will do all needed: NSException.registerDefaultJavaUncaughtExceptionHandler()
. Lets check what it does:
/**
* Registers a default java uncaught exception handler that forwards exceptions to RoboVM's signal handlers.
* Use this if you want Java exceptions to be logged by crash reporters.
*/
public static void registerDefaultJavaUncaughtExceptionHandler() {
Thread.setDefaultUncaughtExceptionHandler(new java.lang.Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException (Thread thread, Throwable ex) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
pw.flush();
Foundation.log(sw.toString());
NSException exception = new NSException(ex.getClass().getName(), sw.toString(), new NSDictionary<>());
if (NSThread.getCurrentThread().isMainThread()) {
exception.raise();
} else {
long handler = getUncaughtExceptionHandler();
callUncaughtExceptionHandler(handler, exception);
// We should never get to this line!
}
}
});
}
Long story short:
- it calls
Exception.printStackTrace()
to receive exception stack trace as string; - it creates NSException with exception information and stacktrace put into
reason
parameters; - raises NSException and allows SDK to receive this exception as native one and report it.
Whats wrong with reported Java exception
It will contain wrong call stack. In general there will be call stack to from VM start to default exception handler. Crash call stack will be available only in reason
field of NSException.
0 CoreFoundation 0x182987164 __exceptionPreprocess + 124
1 libobjc.A.dylib 0x181bd0528 objc_exception_throw + 55
2 CoreFoundation 0x182986e2c -[NSException raise] + 11
3 SampleApp 0x1014c2f10 [J]org.robovm.objc.$M.void_objc_msgSendSuper+ 18083600 (Lorg/robovm/objc/ObjCSuper;Lorg/robovm/objc/Selector;)V + 243
4 SampleApp 0x1014c6cc4 [J]org.robovm.objc.$M.void_objc_msgSend_instance+ 18099396 (Lorg/robovm/apple/foundation/NSObject;Lorg/robovm/objc/Selector;)V + 167
5 SampleApp 0x1014c0bb0 [j]org.robovm.objc.$M.void_objc_msgSend_instance+ 18074544 (Lorg/robovm/apple/foundation/NSObject;Lorg/robovm/objc/Selector;)V[clinit] + 63
6 SampleApp 0x100e631d0 [j]org.robovm.objc.$M.void_objc_msgSend_instance(Lorg/robovm/apple/foundation/NSObject;Lorg/robovm/objc/Selector;)V[Invokestatic+ 11399632 (org/robovm/apple/foundation/NSException)] + 11
7 SampleApp 0x100e64074 [J]org.robovm.apple.foundation.NSException.raise+ 11403380 ()V + 83
8 SampleApp 0x100e65590 [j]org.robovm.apple.foundation.NSException.raise()V[Invokevirtual+ 11408784 (org/robovm/apple/foundation/NSException$1,org/robovm/apple/foundation/NSException$NSExceptionWrap)] + 11
9 SampleApp 0x100e65890 [J]org.robovm.apple.foundation.NSException$1.uncaughtException+ 11409552 (Ljava/lang/Thread;Ljava/lang/Throwable;)V + 547
10 SampleApp 0x1006fe670 [j]java.lang.Thread$UncaughtExceptionHandler.uncaughtException(Ljava/lang/Thread;Ljava/lang/Throwable;)V[Invokeinterface+ 3647088 (java/lang/ThreadGroup)] + 11
11 SampleApp 0x100705df4 [J]java.lang.ThreadGroup.uncaughtException+ 3677684 (Ljava/lang/Thread;Ljava/lang/Throwable;)V + 351
12 SampleApp 0x101576cf4 _call0 + 83
13 SampleApp 0x101560a0c callVoidMethod + 18729484 (method.c:623)
14 SampleApp 0x1015604c4 rvmCallVoidInstanceMethodA + 18728132 (method.c:722)
15 SampleApp 0x101560a8c rvmCallVoidInstanceMethodV + 18729612 (method.c:728)
16 SampleApp 0x101560e70 rvmCallVoidInstanceMethod + 18730608 (method.c:734)
17 SampleApp 0x1015760ac threadExitUncaughtException + 18817196 (thread.c:375)
18 SampleApp 0x101574ebc detachThread + 18812604 (thread.c:404)
19 SampleApp 0x101574de4 rvmDetachCurrentThread + 18812388 (thread.c:505)
20 SampleApp 0x101553cb8 rvmDestroyVM + 18676920 (init.c:513)
21 SampleApp 0x101553bec rvmRun + 18676716 (init.c:503)
22 SampleApp 0x101540284 bcmain + 18596484 (bc.c:97)
23 SampleApp 0x1015401b0 main + 18596272 (bc.c:103)
As result:
- if SDK doesn’t report ‘reason’ from NSException then crash call stack will be not available;
- Exception reason is not stored in crash report file on device. So these will be not useful;
What can be fixed with Java exception report
Not much. NSException.registerDefaultJavaUncaughtExceptionHandler()
can be modified to throw subclass of NSException which will return proper PC addresses in ‘NSException.getCallStackReturnAddresses()’. Similar approach was described in RoboVM/RoboVM #540. This will help with SDKs that picks crash information from NSException as it will be filled with proper addresses of crash (and not related to UncaughtExceptionHandler
).
But crash files on devise still will contain stack traces to UncaughtExceptionHandler
. As objc_exception_throw
doesn’t use stack addresses from NSException objects but obtains ones with backtrace
call (proper way).
Artifacts from ‘NSException.getCallStackReturnAddresses()’ experiment
I did quick and dirty test of providing addresses in NSException, it is just simple POC as it includes Java SDK classes modification and is not final:
===================================================================
--- compiler/rt/libcore/luni/src/main/java/java/lang/StackTraceElement.java (date 1518615691000)
+++ compiler/rt/libcore/luni/src/main/java/java/lang/StackTraceElement.java (date 1518791455000)
@@ -40,6 +40,8 @@
int lineNumber;
+ long nativePC;
+
/**
* Constructs a new {@code StackTraceElement} for a specified execution
* point.
@@ -73,10 +75,11 @@
* Private constructor used by RoboVM only.
*/
@SuppressWarnings("unused")
- private StackTraceElement(Class<?> cls, String method, String file, int line) {
+ private StackTraceElement(Class<?> cls, String method, String file, int line, long nativePC) {
this(cls.getName(), method, file, line);
+ this.nativePC = nativePC;
}
/**
* Compares this instance with the specified object and indicates if they
* are equal. In order to be equal, the following conditions must be
@@ -176,6 +179,17 @@
return (methodName == null) ? "<unknown method>" : methodName;
}
+ /**
+ * RoboVM: returns the memory address of native PC this {@code
+ * StackTraceElement} belongs to.
+ * this functionality is used to convert Java stack frames to native exception (e.g. NSException)
+ * @return memory address of native frame
+ */
+ public long getNativePC() {
+ return nativePC;
+ }
+
+
@Override
public int hashCode() {
/*
Index: compiler/vm/core/src/method.c
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- compiler/vm/core/src/method.c (date 1518615691000)
+++ compiler/vm/core/src/method.c (date 1518780431000)
@@ -117,7 +117,7 @@
return FALSE;
}
java_lang_StackTraceElement_constructor = rvmGetInstanceMethod(env, java_lang_StackTraceElement, "<init>",
- "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;I)V");
+ "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;IJ)V");
if (!java_lang_StackTraceElement_constructor) {
return FALSE;
}
@@ -395,7 +395,7 @@
if (!array) return NULL;
if (length > 0) {
- jvalue args[4];
+ jvalue args[5];
index = first;
jint i;
for (i = 0; i < length; i++) {
@@ -409,6 +409,7 @@
return NULL;
}
args[3].i = frame->lineNumber;
+ args[4].l = frame->pc;
array->values[i] = rvmNewObjectA(env, java_lang_StackTraceElement,
java_lang_StackTraceElement_constructor, args);
if (!array->values[i]) return NULL;
Index: compiler/cocoatouch/src/main/java/org/robovm/apple/foundation/NSException.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- compiler/cocoatouch/src/main/java/org/robovm/apple/foundation/NSException.java (date 1518615691000)
+++ compiler/cocoatouch/src/main/java/org/robovm/apple/foundation/NSException.java (date 1518777686000)
@@ -105,7 +105,7 @@
ex.printStackTrace(pw);
pw.flush();
Foundation.log(sw.toString());
- NSException exception = new NSException(ex.getClass().getName(), sw.toString(), new NSDictionary<>());
+ NSException exception = new NSExceptionWrap(ex, sw.toString());
if (NSThread.getCurrentThread().isMainThread()) {
exception.raise();
} else {
@@ -135,4 +135,30 @@
@Method(selector = "initWithCoder:")
protected native @Pointer long init(NSCoder decoder);
/*</methods>*/
+
+ public static class NSExceptionWrap extends NSException {
+ private NSMutableArray<NSNumber> nativeCallStack;
+ private NSMutableArray<NSString> nativeCallStackSymbolds;
+ public NSExceptionWrap(Throwable ex, String aReason) {
+ super(ex.getClass().getName(), aReason, new NSDictionary<>());
+ nativeCallStack = new NSMutableArray<>();
+ nativeCallStackSymbolds = new NSMutableArray<>();
+ for (StackTraceElement ste : ex.getStackTrace()) {
+ nativeCallStack.add(NSNumber.valueOf(ste.getNativePC()));
+ nativeCallStackSymbolds.add(ste.toString());
+ }
+ }
+
+ @Override
+ public NSArray<NSNumber> getCallStackReturnAddresses() {
+ return nativeCallStack;
+ }
+
+ @Override
+ public NSArray<NSString> getCallStackSymbols() {
+ return nativeCallStackSymbolds;
+ }
+ }
}
Comments