Getting Started with Android NDK: Unlocking Native Power
A Comprehensive Kotlin Tutorial to Integrate C/C++ for High-Performance Android Development.
Getting Started with Android NDK: Unlocking Native Power

Android NDK
Not a Medium Member? “Read For Free”
Android development primarily thrives on Java or Kotlin, offering a robust and high-level environment. However, there are scenarios where tapping into the raw power and performance of C/C++ can be a game-changer. This is where the Android Native Development Kit (NDK) comes into play. The NDK allows you to implement parts of your application using native-code languages like C and C++.
This tutorial will guide you through the process of setting up your environment, writing native code, and integrating it into your Android application, all while keeping things engaging and easy to understand with Kotlin examples!
Why Consider the Android NDK?
Before we dive into the “how,” let’s explore the “why.” While the NDK isn’t for every project, it offers significant advantages in specific situations:
- Performance-Critical Operations: For tasks demanding high computational power, such as game engines, signal processing, or complex algorithms, native code can provide a substantial performance boost.
- Reusing Existing Native Libraries: If you have existing C/C++ libraries that you want to integrate into your Android app, the NDK provides a seamless bridge. A critical security consideration when working with native code is key management. If your C/C++ logic handles sensitive encryption, the keys must be stored securely. Always couple your native security routines with the gold standard for on-device key management, which is detailed in my article on the Android KeyStore: Secure On-Device Data Encryption.
- Low-Level Hardware Access: In some specialized cases, you might need direct access to hardware features that are not exposed through the standard Android SDK.
- Security (to an extent): While not a foolproof solution, obfuscating critical logic in native code can make reverse engineering slightly more challenging.
Setting Up Your Development Environment
To begin our journey into native development, you’ll need a few tools. Luckily, Android Studio makes this process quite straightforward.
- Install the NDK and CMake: Open Android Studio, navigate to
Tools > SDK Manager. In theSDK Toolstab, checkNDK (Side by side)andCMake. ClickApplyto install them. CMake is a tool that manages the build process of native code.

Install the NDK and CMake
2. Create a New Android Project (or use an existing one): When creating a new project, you can select the Native C++ template, which will pre-configure everything for you. However, we'll assume you're adding NDK to an existing project or a basic empty activity for this tutorial to cover the manual setup.
Your First Native Function: Hello NDK!
Let’s create a simple native function that returns a string. This will demonstrate the fundamental steps of calling C++ code from Kotlin.
Step 1: Create a Native Source File
Inside your app module, create a new directory named cpp (if it doesn't already exist). Inside cpp, create a new C++ file, for example, native-lib.cpp.
Your project structure might look like this:
app
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── myndkapp
│ │ │ └── MainActivity.kt
│ │ ├── cpp
│ │ │ └── native-lib.cpp
│ │ └── AndroidManifest.xml
│ └── build.gradle
└── CMakeLists.txt
Now, add the following C++ code to native-lib.cpp:
#include <jni.h> // Essential for Java Native Interface
#include <string> // For string manipulation
// This function is called from Kotlin/Java and returns a C++ string
extern "C" JNIEXPORT jstring JNICALL // 'extern "C"' ensures C-style linkage
Java_com_example_myndkapp_MainActivity_getNativeGreeting( // Function signature derived from package, class, and method name
JNIEnv* env, // Pointer to the JNI environment
jobject /* this */) { // Reference to the calling Java object (MainActivity instance)
std::string message = "Hello from NDK! This is a native message.";
return env->NewStringUTF(message.c_str()); // Convert C++ string to JNI string
}
// Let's add another simple function to demonstrate arithmetic
extern "C" JNIEXPORT jint JNICALL
Java_com_example_myndkapp_MainActivity_calculateSumNative(
JNIEnv* env,
jobject /* this */,
jint a, // First integer parameter from Kotlin
jint b) { // Second integer parameter from Kotlin
return a + b; // Return the sum
}
Explanation of the C++ code:
**#include <jni.h>**: This header provides the interface between your C/C++ code and the Java Virtual Machine (JVM).**extern "C" JNIEXPORT jstring JNICALL**: This is crucial.extern "C": Tells the compiler to use C naming conventions, which is necessary for JNI to find the function.JNIEXPORT: A macro that ensures the function is exported from the shared library.jstring/jint: These are JNI types that correspond to Java'sStringandint, respectively.JNICALL: A macro that ensures the correct calling convention.**Java_com_example_myndkapp_MainActivity_getNativeGreeting*: This is the naming convention* for JNI functions. It follows the pattern:Java_PackageName_ClassName_MethodName.PackageName: Your Android application's package name (e.g.,com_example_myndkapp). Replace underscores with dots in your package name.ClassName: The name of the Kotlin/Java class that will declare and call this native method (e.g.,MainActivity).MethodName: The name you will give to the native method in your Kotlin/Java class (e.g.,getNativeGreeting).**JNIEnv* env**: A pointer to the JNI environment, which provides functions for interacting with the JVM (e.g., creating new strings, accessing Java objects).**jobject /* this */*: A reference to the calling Java object. We typically don't use it for static native methods, hence the `/ this */` comment.**env->NewStringUTF(message.c_str())**: Converts a C++std::stringto a JNIjstring(UTF-8 encoded) that can be returned to Java/Kotlin.
Step 2: Configure CMake
Now, we need to tell Android Studio how to build our native code. Create a file named CMakeLists.txt in the app module's root directory (next to build.gradle).
# Sets the minimum version of CMake required to build your native library.
# This ensures consistency across different build environments.
cmake_minimum_required(VERSION 3.4.1)
# Declares and names your native library.
# 'SHARED' means it will be built as a dynamic shared library (.so file).
# '${CMAKE_SOURCE_DIR}/src/main/cpp/native-lib.cpp' specifies the source file.
add_library( # Sets the name of the library.
native-lib
# Sets the type of library.
SHARED
# Specifies the source files for your library.
src/main/cpp/native-lib.cpp ) # Path to our C++ source
# Searches for a prebuilt static library called 'log'
# that is provided by the Android NDK. This library
# provides logging functions (e.g., __android_log_print)
# that you can use to output messages to logcat.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies linked libraries.
# Adds the NDK 'log' library to the build target 'native-lib'.
# This allows 'native-lib' to use the logging functions from 'log-lib'.
target_link_libraries( # Specifies the target library to link against.
native-lib
# Links the target library to the log library.
${log-lib} )
Explanation of CMakeLists.txt:
**cmake_minimum_required(VERSION 3.4.1)**: Specifies the minimum CMake version.**add_library(native-lib SHARED src/main/cpp/native-lib.cpp)**: This is the core command.native-lib: The name of our shared library (this will be the.sofile name).SHARED: Indicates that we are building a dynamic shared library.src/main/cpp/native-lib.cpp: The path to our C++ source file.**find_library(log-lib log)**: Finds the NDK's logging library.**target_link_libraries(native-lib ${log-lib})**: Links ournative-libwith the logging library so we can use__android_log_printfor debugging.
Step 3: Link CMake to your build.gradle (Module Level)
Now, we need to tell your app/build.gradle file about our native setup.
Open your module-level build.gradle (usually app/build.gradle) and add the externalNativeBuild block within the android block:
android {
namespace 'com.example.myndkapp'
compileSdk 34
defaultConfig {
applicationId "com.example.myndkapp"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Add this block for NDK configuration
externalNativeBuild {
cmake {
cppFlags "" // You can add C++ specific flags here, e.g., "-std=c++17"
arguments "-DANDROID_STL=c++_static" // Example: use a static C++ standard library
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
// This block links your project to the CMakeLists.txt file
externalNativeBuild {
cmake {
path file('CMakeLists.txt') // Specify the path to your CMakeLists.txt
}
}
}
Important: After modifying build.gradle, Android Studio will prompt you to "Sync Now." Make sure you sync your project!
Step 4: Declare and Call Native Methods in Kotlin
Finally, let’s call our native functions from our MainActivity.kt.
package com.example.myndkapp
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import com.example.myndkapp.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Example 1: Get greeting from native code
binding.sampleText.text = getNativeGreeting() // Calls the native method
// Example 2: Calculate sum using native code
val num1 = 10
val num2 = 25
val sum = calculateSumNative(num1, num2) // Calls the native sum method
binding.sumResultText.text = "Sum of $num1 and $num2 from NDK: $sum"
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
external fun getNativeGreeting(): String // Declare the native function (no body in Kotlin)
/**
* Another native method to calculate the sum of two integers.
*/
external fun calculateSumNative(a: Int, b: Int): Int
companion object {
// Used to load the 'native-lib' library on application startup.
// The name "native-lib" must match the name used in CMakeLists.txt (add_library).
init {
System.loadLibrary("native-lib")
}
}
}
Explanation of the Kotlin code:
**external fun getNativeGreeting(): String*: Theexternalkeyword tells Kotlin that this function is implemented in native code. Its signature (name, parameters, return type) must* match the JNI function signature in C++.**companion object { init { System.loadLibrary("native-lib") } }: This static block loads our native shared library (native-lib.so) when theMainActivityclass is initialized. The namenative-libhere must** match the name you gave inCMakeLists.txtusingadd_library.- The
TextView(with idsample_text) will display the greeting from our native code. You'll need to add this TextView to youractivity_main.xmllayout if you don't have it already.
Step 5: Update activity_main.xml
Ensure your layout file (activity_main.xml) has TextView elements to display the results:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toTopOf="@+id/sum_result_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"/>
<TextView
android:id="@+id/sum_result_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Sum will appear here."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sample_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
Run Your Application!
Now, run your Android application on an emulator or a physical device. You should see “Hello from NDK! This is a native message.” and “Sum of 10 and 25 from NDK: 35” displayed on your screen.

Run Your Application!
This confirms that your Kotlin code is successfully calling and receiving results from your native C++ code!
Advanced Concepts & Best Practices
Passing Data Types
You’ve seen jstring and jint. JNI provides mappings for almost all primitive and complex Java/Kotlin types:
boolean->jbooleanbyte->jbytechar->jcharshort->jshortlong->jlongfloat->jfloatdouble->jdoubleObject->jobjectString->jstring- Arrays (e.g.,
int[]) ->jintArray - Custom Objects ->
jobject(requires more complex JNI calls to access fields/methods)
Error Handling in NDK
Native code doesn’t have the luxury of Java/Kotlin exceptions. You typically handle errors by:
- Return Codes: Return specific integer codes to indicate success or different error types.
- JNI Exceptions: You can explicitly throw Java exceptions from native code using
JNIEnvfunctions likeThrowNew.
Debugging Native Code
Android Studio offers excellent debugging capabilities for native code.
- Set breakpoints in your
native-lib.cppfile. - Select
Debugwhen running your application. - The debugger will pause at your C++ breakpoints, allowing you to inspect variables and step through native code.
Threading with NDK
If your native code performs long-running operations, you should run it on a separate thread to avoid blocking the UI. You can create threads in Kotlin/Java and call native functions from them, or even create native threads directly within your C++ code. Be mindful of JNI thread attachment if creating native threads.
NDK Beyond the Basics: Practical Use Cases
Let’s consider a slightly more involved example: image processing. Imagine you have a complex image filter algorithm written in C++ that you want to apply to an Bitmap in Android.
Scenario: Applying a simple grayscale filter to a bitmap efficiently.
1. C++ Grayscale Function (image-utils.cpp)
#include <jni.h>
#include <android/bitmap.h> // For Android Bitmap functions
#include <android/log.h> // For logging to logcat
#define LOG_TAG "ImageUtilsNative" // Tag for log messages
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_myndkapp_MainActivity_applyGrayscaleNative(
JNIEnv* env,
jobject /* this */,
jobject bitmap) { // jObject for the Bitmap
AndroidBitmapInfo info;
void* pixels;
int ret;
// Get bitmap info
if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
LOGE("AndroidBitmap_getInfo() failed! error=%d", ret);
return;
}
// Check if the bitmap is in a format we can work with (e.g., RGBA_8888)
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
LOGE("Bitmap format is not RGBA_8888! format=%d", info.format);
return;
}
// Lock the bitmap to get access to its pixels
if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixels)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed! error=%d", ret);
return;
}
// Process pixels: apply grayscale
uint32_t* p = (uint32_t*)pixels;
int width = info.width;
int height = info.height;
LOGI("Applying grayscale filter to bitmap: %dx%d", width, height);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
uint32_t pixel = p[y * width + x];
// Extract ARGB components (assuming little-endian)
uint8_t alpha = (pixel >> 24) & 0xFF;
uint8_t red = (pixel >> 16) & 0xFF;
uint8_t green = (pixel >> 8) & 0xFF;
uint8_t blue = (pixel >> 0) & 0xFF;
// Calculate grayscale value (luminosity method)
uint8_t gray = (uint8_t)(0.299 * red + 0.587 * green + 0.114 * blue);
// Recompose pixel with grayscale value
p[y * width + x] = (alpha << 24) | (gray << 16) | (gray << 8) | gray;
}
}
// Unlock the bitmap
AndroidBitmap_unlockPixels(env, bitmap);
LOGI("Grayscale filter applied successfully.");
}
2. Update CMakeLists.txt
You’d need to add image-utils.cpp to your add_library command and link against android library for AndroidBitmap functions:
cmake_minimum_required(VERSION 3.4.1)
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp
src/main/cpp/image-utils.cpp ) # Add the new source file here
find_library( log-lib log )
# Add the 'android' library for bitmap functions
find_library( android-lib android )
target_link_libraries( native-lib
${log-lib}
${android-lib} ) # Link against android-lib
3. Kotlin MainActivity
package com.example.myndkapp
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import com.example.myndkapp.databinding.ActivityMainBinding
import java.io.IOException
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// ... (previous native calls)
// Example 3: Image Processing with NDK
binding.applyFilterButton.setOnClickListener {
try {
// Load an image from assets
assets.open("sample_image.jpg").use { inputStream ->
val originalBitmap = BitmapFactory.decodeStream(inputStream)
// Ensure the bitmap is mutable for in-place processing
val mutableBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true)
// Display original (optional)
binding.imageViewOriginal.setImageBitmap(originalBitmap)
// Apply grayscale filter using native code
applyGrayscaleNative(mutableBitmap)
// Display processed image
binding.imageViewProcessed.setImageBitmap(mutableBitmap)
}
} catch (e: IOException) {
e.printStackTrace()
binding.sumResultText.text = "Error loading image: ${e.message}"
}
}
}
// Declare the new native function
external fun applyGrayscaleNative(bitmap: Bitmap)
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
You’d also need to add an ImageView for original and processed images, and a Button to trigger the filter in activity_main.xml. Don't forget to put a sample_image.jpg in your app/src/main/assets folder.
This example showcases how you can pass complex objects like Bitmap to native code and modify their underlying pixel data for high-performance operations.

Result
Frequently Asked Questions (FAQs)
When should I not use the NDK?
If your task can be efficiently accomplished with Kotlin/Java, stick to it. NDK development adds complexity, increases build times, and can make debugging more challenging. It’s best reserved for performance-critical sections or leveraging existing native libraries.
Can I mix Java/Kotlin and C++ code freely?
Yes, that’s the whole point of JNI! You can call native functions from your Java/Kotlin code, and native code can even call back into Java/Kotlin methods if needed (though that’s more advanced).
What are the performance implications of JNI calls?
There’s a small overhead associated with each JNI call due to the context switch between the JVM and native code. For a few calls, it’s negligible. For very frequent calls (e.g., inside a tight loop), it can become a bottleneck. In such cases, it’s often better to pass larger chunks of data or perform more work within a single native call.
How do I handle string encoding between Kotlin and C++?
JNI jstrings are typically UTF-8 encoded when converted from C++ to Java using NewStringUTF. When receiving a jstring in C++, you can convert it to a C-style string using GetStringUTFChars. Remember to release the memory using ReleaseStringUTFChars when done!
Is using NDK more secure for sensitive data or algorithms?
While native code is harder to decompile and reverse-engineer than Java bytecode, it’s not truly “secure.” A determined attacker can still analyze native binaries. For critical security, consider hardware-backed security features or strong encryption rather than solely relying on NDK obfuscation.
What are your thoughts?
- Have you used NDK in any of your projects? What was your experience?
- What kind of performance gains have you observed by moving tasks to native code?
- Are there any other advanced NDK topics you’d like to explore in a future tutorial?
📘 Master Your Next Technical Interview
Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.
- E-book (Best Value! 🚀): **$1.99 on Google Play**
- Kindle Edition: **$3.49 on Amazon**
- Also available in Paperback & Hardcover.
메타데이터
- post_id
- 71ca0ed1fabc
- slug
- getting-started-with-android-ndk-unlocking-native-power-71ca0ed1fabc
- url
- https://medium.com/@sivavishnu0705/getting-started-with-android-ndk-unlocking-native-power-71ca0ed1fabc
- canonical_url
- https://medium.com/@sivavishnu0705/getting-started-with-android-ndk-unlocking-native-power-71ca0ed1fabc
- author_url
- https://medium.com/@sivavishnu0705
- status
- ok
- fetched_at
- 2026-06-14 11:28:49