DEV Community

Cover image for Adding NDK to an existing Android app and using C++ to log
Tristan Elliott
Tristan Elliott

Posted on

Adding NDK to an existing Android app and using C++ to log

Table of contents

  1. The goal of this blog post?
  2. Install NDK and CMake
  3. Link Gradle to your native code
  4. Create a CMake build script
  5. The JNI ( Java Native Interface.)
  6. The C++ code
  7. The JNI ( Java Native Interface.)
  8. Running the code

Resources

My app on the Google play store

My app's GitHub code

The Goal of this blog post

  • The goal of this blog post is simple. use C++ code to log a statement to the Android Studio Logcat

Install NDK and CMake

  • Documentation
  • The first thing that we need to do is to install the The Android Native Development Kit (NDK) and CMake (an external build tool that works alongside Gradle).
  • You can read a more detailed version inside of the documentation but essentially inside of android studio we go,
  • Tools > SDK Manager > SDK Tools tab > Select the NDK (Side by side) and CMake checkboxes > select ok

Link Gradle to your native code

  • Documentation
  • Now in order for us to use our C++ code with Gradle, we need to tell Gradle where our CMake build file is. Gradle can then use the instructions we provide inside of this file and build our C++ code
  • We can inform Gradle inside of the app's build.gradle file:
android {
  ...
  defaultConfig {...}
  buildTypes {...}

  // Encapsulates your external native build configurations.
  externalNativeBuild {

    // Encapsulates your CMake build configurations.
    cmake {

      // Provides a relative path to your CMake build script.
      path 'src/main/cpp/CMakeLists.txt'
    }
  }
}

Enter fullscreen mode Exit fullscreen mode
  • The externalNativeBuild section is the important part. At this point you should be able to run the Gradle build and receive an error stating that the file: src/main/cpp/CMakeLists.txt can not be found. This is a good error, we are on the right path

Create a CMake build script

  • Documentation

  • Now we can create the src/main/cpp/CMakeLists.txt file and fill it with this code:

# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.

cmake_minimum_required(VERSION 3.4.1)



# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add_library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.


#Add a library to the project using the specified source files.
add_library(gl_code SHARED
        gl_code.cpp)

# add lib dependencies
target_link_libraries(gl_code
        android
        log
        EGL
        GLESv2)

Enter fullscreen mode Exit fullscreen mode
  • The important section of this code is the add_library(gl_code SHARED gl_code.cpp). So lets brake it down:

  • add_library(): as the documentation states: Adding a library to the project using the specified source files. Which basically means, we are creating a place for our C++ code

  • gl_code: has to be a globally unique identifier and is how we will refer to our C++ code throughout the application. If you change this name, you must clean your project and then rebuild it for it to work again

  • SHARED: Just means it is a dynamic library that may be linked by other targets and loaded at runtime.

  • gl_code.cpp: This is the source file that will be compiled into the library(the file that holds our C++ code).

The JNI ( Java Native Interface.)

  • Documentation
  • As the documentation states: It defines a way for the bytecode that Android compiles from managed code (written in the Java or Kotlin programming languages) to interact with native code (written in C/C++). JNI is vendor-neutral, has support for loading code from dynamic shared libraries, and while cumbersome at times is reasonably efficient.
  • Which is really just nerd talk for: We can create a Kotlin/Java class and use said class to run some C++ code
  • We can do that like this:
class NativeLoading {

    init {
        System.loadLibrary("gl_code");
    }

    external fun init()
}

Enter fullscreen mode Exit fullscreen mode
  • The System.loadLibrary("gl_code") is what is going to allow us to access the gl_code.cpp file we mentioned earlier and defined its identifier as: gl_code

The actual C++ code

  • So now we can actually create some C++ code that will log a statement for us:
#include <jni.h>

#include <android/log.h>

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "native-lib", __VA_ARGS__))


extern "C"
JNIEXPORT void JNICALL
Java_com_example_clicker_nativeLibraryClasses_NativeLoading_init(JNIEnv *env, jobject thiz) {

    LOGI("int %s,",  "--------------THIS IS A LOG FROM THE NATIVE SIDE ----------------------");

}

Enter fullscreen mode Exit fullscreen mode

I apologise

  • I have ran out of time to write this blog post(I set a 1hr time cap) so from here on out, Chat GPT will explain the code for you:

1. Header Files

#include <jni.h>
#include <android/log.h>
#include <jni.h>: 
Enter fullscreen mode Exit fullscreen mode
  • This includes the JNI header file, which provides the necessary functions and types to interact between Java/Kotlin and C++. JNI allows native code (C/C++) to communicate with Java objects and methods in a virtual machine. #include : This includes Android's logging utility, used for printing messages to the log. The android/log.h header provides functions to log messages in native code (similar to Log.d() in Java/Kotlin).

2. Logging Macro Definition

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "native-lib", __VA_ARGS__))

Enter fullscreen mode Exit fullscreen mode

LOGI: This defines a macro for logging information messages. Macros in C/C++ are like shorthand codes that get expanded when the program is compiled.
android_log_print(): This function logs a message. It takes a priority level (ANDROID_LOG_INFO for informational messages), a tag ("native-lib" in this case, which will help identify the source of the log), and the message (VA_ARGS__ allows for variable arguments).
The macro makes it easy to log messages without typing the full function each time, using LOGI(...) instead.

3. C++ extern "C" Declaration

extern "C"

Enter fullscreen mode Exit fullscreen mode

The extern "C" tells the compiler to use C linkage for the following code. Since JNI functions must be compatible with Java’s calling conventions (which follow C linkage), this ensures the function name is not mangled by the C++ compiler.

4. JNIEXPORT and JNICALL

JNIEXPORT void JNICALL

Enter fullscreen mode Exit fullscreen mode

JNIEXPORT: A macro indicating that this function will be used as part of the JNI interface, making it accessible from Java/Kotlin.

JNICALL: A macro that ensures the function uses the correct calling convention for JNI (used to handle the interaction between Java and C++).

5. JNI Function Declaration

Java_com_example_clicker_nativeLibraryClasses_NativeLoading_init(JNIEnv *env, jobject thiz)

Enter fullscreen mode Exit fullscreen mode

This is the native implementation of the init() method that you declared in Kotlin as external fun init().
The function name follows a strict JNI naming convention: Java_packageName_className_methodName. In this case:
Java_com_example_clicker_nativeLibraryClasses_NativeLoading_init corresponds to the init() function in the NativeLoading class located in the com.example.clicker.nativeLibraryClasses package.
The function has two parameters:
JNIEnv *env: A pointer to the JNI environment, which gives access to JNI functions, such as creating Java objects, calling methods, etc.
jobject thiz: A reference to the instance of the calling Java/Kotlin object (this).

6. Logging a Message

LOGI("int %s,",  "--------------THIS IS A LOG FROM THE NATIVE SIDE ----------------------");

Enter fullscreen mode Exit fullscreen mode

This line logs an informational message from the native code. It uses the LOGI macro defined earlier to print a message:
"int %s,": This is a formatted string. %s is a placeholder for a string value, which is replaced by the second argument.
"--------------THIS IS A LOG FROM THE NATIVE SIDE ----------------------": The string to be inserted into the placeholder.
Summary
This C++ function logs a message when the init() function is called from Kotlin.
The function adheres to JNI standards, allowing it to interact with Kotlin/Java.
The LOGI() macro simplifies logging in native code, outputting messages to the Android log with the tag "native-lib".
This setup is useful for debugging and logging from native code in Android. When you call NativeLoading.init() from Kotlin, this C++ code is executed, and a log message will appear in the Android log (logcat) with the message: "--------------THIS IS A LOG FROM THE NATIVE SIDE ----------------------".

Running the code:

  • To run the code we simple implement it like anyother type of kotlin class:
  val nativeCode= NativeLoading()
        nativeCode.init()
Enter fullscreen mode Exit fullscreen mode
  • doing this will produce a log like any other

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.

Top comments (0)