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
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
Name the file as
logging
and check theCreate an associated header
option
Open the logging header file
logging.h
then we need to register ourc++
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
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);
}
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 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);
}
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:
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
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)
Trying to call those function will produce this colorfull log messages
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)