Error handling on Android part 7: capturing signals and exceptions in Android NDK apps

Jamie Lynch

Error handling on Android

Accent divider.

How many times have you been in the middle of using a new shiny app, only to have it crash on you?

Android app displaying a crash dialog

This is the final part in a series of posts that will investigate how the exception handling mechanism works in Java and Android, and how crash reporting SDKs can capture diagnostic information, so that you're not flying blind in production.

Capturing signals and exceptions in Android NDK apps

We previously learnt how to add useful metadata to our crash reports, so that we can prioritise the most important issues and debug faster. Our goal now is to capture a different class of error entirely: signals and exceptions from Android's NDK layer.

Error handling in the NDK

Android's Native Development Kit allows developers to write Android apps using the C/C++ programming languages. This can be beneficial for applications which rely on functionality delivered by native code, such as image recognition, or where high performance code is required, such as in mobile gaming.

Capturing errors from an NDK app presents a host of different challenges to capturing errors from a JVM app. C++ can throw exceptions whereas C raises signals, meaning there are various different sources of errors which all require different handling. Additionally, stack tracing implementations can vary substantially across architecture and API level, meaning something that would be simple on the JVM such as obtaining a useful stacktrace can be much more involved in NDK crash reporters.

C++ Exception Handler

C++ can throw exceptions, and provides an error handling API which accepts a function. When the application is about to terminate, that function will be invoked, allowing us to generate a crash report using a similar high-level approach to the JVM by: capturing diagnostic information, writing it to disk, then making an HTTP request to deliver it on the next application launch.

In the example below, a user calls #std::terminate() which will end the application as it is not handled within the `foo` method:

void foo() {
   std::terminate(); // ends the app
}
void init() {
   prev_handler = std::set_terminate(handle_cpp_terminate);
}
void handle_cpp_terminate() {
   // obtain a stacktrace and create a report here
 
   if (prev_handler != NULL) {
       prev_handler();
   }
}

Before the application terminates, the program will invoke #handle_cpp_terminate(), where we can attempt to obtain a stacktrace by using a library such as lib corkscrew, then serialize it to JSON and save to disk in a similar way as we did in the JVM crash reporter.

C Signal Handlers

C on the other hand, can raise a variety of signals which can interrupt the normal flow of a program so that it can attempt to handle an error. It's possible to define a signal handler that intercepts this signal, and within the handling code generate a diagnostic report that can be written to disk. Consider the following code, which raises a SIGABRT signal in the #foo() method:

void foo() {
   abort(); // ends the app with SIGABRT
}
void handle_signal(int signum,
                  siginfo_t *info,
                  void *user_context) {
   // obtain a stacktrace and create a report here
   invoke_previous_handler(signum, info, user_context);
}

We'll assume that the signal handler has already been installed and will be called whenever a corresponding signal is raised — bugsnag-android-ndk’s source code provides a full example of how to do this if you’re interested in the finer details. The #signum parameter corresponds to a particular signal type, which may affect how we capture diagnostic data — for example, #SIGABRT and #SIGSEGV will have unique integer values. The remaining parameters give additional context surrounding the signal which we can also use to capture useful information.

Communicating using the Java Native Interface

Google recommends that most Android apps are written in Kotlin, but sometimes it can make sense to write a critical bottleneck in an application in C/C++ to achieve superior performance. For this use-case, the Java Native Interface allows us to call native code directly from the JVM. Consider the updateDeviceOrientation() method from the bugsnag-android repository which is called whenever the device rotates:

public static native void updateDeviceOrientation(String newOrientation);

JNIEXPORT void JNICALL
Java_com_bugsnag_android_NativeBridge_updateDeviceOrientation(
   JNIEnv *env, jobject _this, jstring new_value) {
   bugsnag_report->device_orientation = new_value
}

You may have noticed the #native modifier, which indicates this Java method calls into a C method, which is defined below, with the same name prefixed with the package and several JNI symbols. The #newOrientation parameter from Java will be converted into a #jstring that can be assigned to a field on a C struct, so that the value is up to date if a C error crashes the app.

Would you like to know more?

Hopefully this has helped you learn a bit more about Error Handling on Android. If you have any questions or feedback, please feel free to get in touch.

———

Try Bugsnag's Android crash reporting.

Share