DEV Community

Cover image for Android NDK: Implementing Logging with JNI & C++
Rhony
Rhony

Posted on

Android NDK: Implementing Logging with JNI & C++

Introduction

As android app developers, we know that Android NDK has been around for a long time, with its first release, NDK r1 in June 2009. The Android Native Development Kit (NDK) allows developers to write native code using C and C++, improving performance in critical areas like multimedia processing and computational tasks. One essential feature when working with the NDK is logging, which helps in debugging and monitoring native code execution.

In this article, we’ll explore how to integrate and use the NDK logging library (android/log.h) to log messages from native code into Logcat, the standard Android logging system. And we will be calling the function from our kotlin code.

Prerequisites

Before we dive in, ensure you have the following set up:

  • Android Studio installed
  • NDK and CMake installed
  • Basic knowledge of JNI (Java Native Interface). Don't worry we didn't need that much knowledge for now.

NDK and CMake

Setting Up Your Project for NDK

To enable NDK support in your Android project:

  • Open your Android Studio project.
  • Create a new project, in this case we will be using a default jetpack compose project
  • Now, add a new Native module, simply right click to open the menu
    add a new Native module

  • Select Android Native Library then finish
    Select Android Native Library

Project Structure

Inspect the project, now we are having a CMakeLists.txt file inside our nativelib library. There is a log library declared on the target_link_libraries block which is the NDK android log.
Project Structure

Notice that we also has NativeLib class which will bridge between our kotlin code with the JNI. And the JNI will directly call our native C++ function.

Implementing Native Logging Function

We already finish with native library setup. As we know on Android there is common logging function Log.v for Verbose logging, Log.d for Debug and etc. Complete Android Log. In NDK there is also similar logging name but this time it is defined with enum instead of functions:

  • ANDROID_LOG_VERBOSE
  • ANDROID_LOG_DEBUG
  • ANDROID_LOG_INFO
  • ANDROID_LOG_WARN
  • ANDROID_LOG_ERROR

This enum will supplied into a single function __android_log_print from the NDK. So how do we make it ?

The C++ Class

Move to our nativelib library folder then:

  • Create a new folder: logging
  • Select C/C++ Source File
    Select  raw `C/C++ Source File` endraw

  • Name the file as logging and check the Create an associated header option
    Check the Create an associated header option

  • Open the logging header file logging.h then we need to register our c++ function, which will be shown like this:

// logging.h 
#ifndef NDK_PLAYGROUND_LOGGING_H
#define NDK_PLAYGROUND_LOGGING_H

#include <jni.h>

void log_print(JNIEnv* env, jint priority, jstring tag, jstring message);

#endif //NDK_PLAYGROUND_LOGGING_H
Enter fullscreen mode Exit fullscreen mode

We can threat this class similarly like interface on highlevel programming language

  • Open the logging.cpp class then we declare our real function implementation, since the difference of the log is only the enum value (aka: priority) it is enough to just declare a single function.
#include "logging.h"
#include <android/log.h>
#include <jni.h>

void log_print(JNIEnv* env, jint priority, jstring tag, jstring message) {
    const char* tagStr = env->GetStringUTFChars(tag, nullptr);
    const char* msgStr = env->GetStringUTFChars(message, nullptr);

    if (tagStr && msgStr) {
        __android_log_print(priority, tagStr, "%s", msgStr);
    }

    env->ReleaseStringUTFChars(tag, tagStr);
    env->ReleaseStringUTFChars(message, msgStr);
}
Enter fullscreen mode Exit fullscreen mode

Great! Until this steps you see a warning logging.cpp and logging.h file which tells that this file is not part of the project bla bla bla

File warning

Moving back to our CMakeList.txt file we should register both of the classes so later on our nativelib.cpp classs will be able to import our logging classes.

  • First, we need to set our source dir set(NATIVE_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})
  • Set the target_sources
  • Then target_include_directories

Put those below our target_link_libraries block which remains like this:

add_library(${CMAKE_PROJECT_NAME} SHARED
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        nativelib.cpp
)

# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log)

set(NATIVE_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})

target_sources(
        ${CMAKE_PROJECT_NAME}
        PRIVATE
        ${NATIVE_SRC_DIR}/nativelib.cpp
        ${NATIVE_SRC_DIR}/logging/logging.cpp
)

target_include_directories(
        ${CMAKE_PROJECT_NAME}
        PRIVATE
        ${NATIVE_SRC_DIR}/logging
)
Enter fullscreen mode Exit fullscreen mode

Then sync the project

The JNI, nativelib.cpp class

We are done with writing our logging c++ class, now it's time to work on JNI part. The class extension remains the same .cpp but this class we use it to register our native cpp function to JNI so then we can expose it to our kotlin code. Don't worry, let's code !

Since we want to log in verbose, debug, info, warn and error we can declare it to each functions

#include <jni.h>
#include <string>
#include <logging.h>
#include <android/log.h>

extern "C" JNIEXPORT void JNICALL
Java_com_ndk_nativelib_NativeLib_logV(
        JNIEnv *env,
        jobject /* this */,
        jstring tag,
        jstring message) {
    log_print(env, ANDROID_LOG_VERBOSE, tag, message);
}

extern "C" JNIEXPORT void JNICALL
Java_com_ndk_nativelib_NativeLib_logD(
        JNIEnv *env,
        jobject /* this */,
        jstring tag,
        jstring message) {
    log_print(env, ANDROID_LOG_DEBUG, tag, message);
}

extern "C" JNIEXPORT void JNICALL
Java_com_ndk_nativelib_NativeLib_logI(
        JNIEnv *env,
        jobject /* this */,
        jstring tag,
        jstring message) {
    log_print(env, ANDROID_LOG_INFO, tag, message);
}

extern "C" JNIEXPORT void JNICALL
Java_com_ndk_nativelib_NativeLib_logW(
        JNIEnv *env,
        jobject /* this */,
        jstring tag,
        jstring message) {
    log_print(env, ANDROID_LOG_WARN, tag, message);
}

extern "C" JNIEXPORT void JNICALL
Java_com_ndk_nativelib_NativeLib_logE(
        JNIEnv *env,
        jobject /* this */,
        jstring tag,
        jstring message) {
    log_print(env, ANDROID_LOG_ERROR, tag, message);
}

Enter fullscreen mode Exit fullscreen mode

Awesome! But we still not finished yet, let's open up NativeLib we have to bind the nativelib into our kotlin code which is our upper layer:

internal object NativeLib {

    init {
        System.loadLibrary("nativelib")
    }

    external fun logV(tag: String, message: String)
    external fun logD(tag: String, message: String)
    external fun logI(tag: String, message: String)
    external fun logW(tag: String, message: String)
    external fun logE(tag: String, message: String)
}
Enter fullscreen mode Exit fullscreen mode

Notice that we load the library on the init block System.loadLibrary("nativelib")

Implementation

Open up our MainAcvitiy class then we can directly call the native function anywhere. For example:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        //some initialization
        NativeLib.logV("MainActivity", "Hello from MainActivity")
    }
 }
Enter fullscreen mode Exit fullscreen mode

If we run the project we will see that the log is printed:
Native log

Bonus 🎁

Event better, we can provide a public function so than we don't have to call the NativeLib class. Create a new file NativeLog.kt on the same folder inside our nativelib package

Native log

Then we declare our public function, we can provide a function with tag and also using the class name itself as the log tag:

package com.ndk.nativelib

@Suppress("FunctionName")
fun LOG_V(tag: String, message: String) = NativeLib.logV(tag, message)

@Suppress("FunctionName")
fun<T : Any> T.LOG_V(message: String) = NativeLib.logV(this::class.java.simpleName, message)

@Suppress("FunctionName")
fun<T : Any> T.LOG_D(message: String) = NativeLib.logD(this::class.java.simpleName, message)

@Suppress("FunctionName")
fun<T : Any> T.LOG_I(message: String) = NativeLib.logI(this::class.java.simpleName, message)

@Suppress("FunctionName")
fun<T : Any> T.LOG_W(message: String) = NativeLib.logW(this::class.java.simpleName, message)

@Suppress("FunctionName")
fun<T : Any> T.LOG_E(message: String) = NativeLib.logE(this::class.java.simpleName, message)
Enter fullscreen mode Exit fullscreen mode

Trying to call those function will produce this colorfull log messages

Log result

Conclusion

I hope by integrating the NDK logging library this gives us (yeah, us since I'm always be a newbie 😀) clear understanding how we can use NDK. While NDK is the SDK for native development the JNI itself play a role as the bridge of our native cpp class. The Native API is available on the official android documentation.
Be Aware, that touching native codebases will require deep understanding on C++ language meanwhile we know that C++ is a complex language and isn't simple as high level language like java and kotlin.

Source code: ndk-playground

Top comments (0)