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. 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 Select Android Native Library then finish 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. 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 Name the file as logging and 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 void log_print(JNIEnv* env, jint priority, jstring tag, jstring message); #endif //NDK_PLAYGROUND_LOGGING_H 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 #include 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); } 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 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 ) 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

Mar 15, 2025 - 07:54
 0
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 

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

#endif //NDK_PLAYGROUND_LOGGING_H

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 
#include 

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);
}

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
)

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 
#include 
#include 
#include 

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);
}

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)
}

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")
    }
 }

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

Bonus