Is it possible to write mobile applications for Android on classic C languageif official documentation recommended Kotlin or Java? The answer is yes, and this opens up unique opportunities for creating high-performance native applications. Unlike a virtual machine Dalvik/ART, C code is compiled directly into machine instructions, which is critical for game engines, multimedia processing or computer vision algorithms.

However, development in C for Android requires a special approach: this is not possible without Native Development Kit (NDK) โ€”a toolkit from Googlethat connects native code with the Java/Kotlin part of the application via JNI (Java Native Interface). In this article we will analyze the entire process: from setting up the environment to optimizing and publishing the application in Google Play. You will learn when to choose C instead Kotlin, how to avoid common mistakes when working with JNI, and why some popular applications (for example, WhatsApp or games on Unity) actively use native code.

Why C for Android: pros and cons cons

The main advantage of C is performance. Native code runs 2โ€“10 times faster than bytecode Java/Kotlin, which is critical for:

  • ๐ŸŽฎ Game engines (Unreal Engine, Cocos2d-x)
  • ๐Ÿ“น Processing video/audio real-time (filters, codecs)
  • ๐Ÿค– Machine learning (inference models TensorFlow Lite)
  • ๐Ÿ”’ Cryptographic operations (encryption, hashing)

But there are also pitfalls:

  • โš ๏ธ Difficulty of debugging: Errors in native code often lead to the crash of the entire application (SIGSEGV).
  • ๐Ÿ”„ Double support: You will have to write both the native part in C and the โ€œwrapperโ€ in Java/Kotlin.
  • ๐Ÿ“ฆ Increasing the size of the APK: Libraries .so for different architectures (armeabi-v7a, arm64-v8a) take up a lot of space.

โš ๏ธ Attention: Google does not recommend using NDK for regular applications. If your task is a simple UI or working with an API, it is better to stay with Kotlin. for resource-intensive tasks.

๐Ÿ“Š What do you want to use C for in Android development?
Game engine
Multimedia processing
Machine learning
Cryptography
Other

Required tools: what to install

To start development, prepare the following:

Tool Version (2026) Purpose
Android Studio Giraffe or later Main IDE with NDK support
Android NDK r26+ Compiler clang, header files, utilities
CMake 3.22.1+ Build system for native libraries
LLDB Built into NDK Debugger for native code

Install them via Android Studio:

  1. Open Settings โ†’ Appearance & Behavior โ†’ System Settings โ†’ Android SDK.
  2. Go to the tab SDK Tools and mark NDK and CMake.
  3. Click Apply and wait for the download (~1.5 GB).

โš ๏ธ Attention: If you use Windows, make sure that the paths to NDK and the project do not contain spaces or Cyrillic characters. This may break the build.

โ˜‘๏ธ Preparing the environment

Done: 0 / 5

Creating the first project with C support

Let's start with a basic project that outputs a message from native code to Androidapplication.

  1. In Android Studio create a new project with a template Native C++ (File โ†’ New โ†’ New Project โ†’ Native C++).
  2. In file MainActivity.kt (or .java) find the method stringFromJNI() - this is a bridge between Java and C.
  3. Open native-lib.cpp in the folder cpp. There is already a code example here:
#include <jni.h>

#include <string>

extern "C" JNIEXPORT jstring JNICALL

Java_com_example_myapp_MainActivity_stringFromJNI(

JNIEnv* env,

jobject / this /) {

std::string hello = "Hello from C++";

return env->NewStringUTF(hello.c_str());

}

The assembled application will display the text "Hello from C++" on the screen. This example demonstrates:

  • ๐Ÿ”— Java โ†” C binding via JNI.
  • ๐Ÿ“œ Function name format: Java___.
  • ๐Ÿ”„ Conversion types: std::string โ†’ jstring.

โš ๏ธ Attention: Function names in JNI are case sensitive and must exactly match the method signature in Java/Kotlin. An error in one character will lead to java.lang.UnsatisfiedLinkError.

๐Ÿ’ก

Use the utility javah (or -Xjni v Kotlin) to automatically generate header files .h with the correct JNI function signatures.

Working with JNI: data transfer between Java and C

JNI is a bridge between managed code (Java/Kotlin) and native (C/C++). Let's consider the main data exchange scenarios:

1. Passing primitive types

Primitives (int, float) are passed directly:

// Java

public native int sum(int a, int b);

// C

JNIEXPORT jint JNICALL

Java_com_example_myapp_MainActivity_sum(JNIEnv* env, jobject obj, jint a, jint b) {

return a + b;

}

2. Working with strings

Strings (jstring) require conversion:

// Java

public native String concat(String s1, String s2);

// C

JNIEXPORT jstring JNICALL

Java_com_example_myapp_MainActivity_concat(JNIEnv* env, jobject obj, jstring s1, jstring s2) {

const char *str1 = env->GetStringUTFChars(s1, NULL);

const char *str2 = env->GetStringUTFChars(s2, NULL);

std::string result = std::string(str1) + std::string(str2);

env->ReleaseStringUTFChars(s1, str1);

env->ReleaseStringUTFChars(s2, str2);

return env->NewStringUTF(result.c_str());

}

3. Arrays and buffers

For arrays, use Get<Type>ArrayElements:

// Java

public native int[] sortArray(int[] arr);

// C

JNIEXPORT jintArray JNICALL

Java_com_example_myapp_MainActivity_sortArray(JNIEnv* env, jobject obj, jintArray arr) {

jint *body = env->GetIntArrayElements(arr, NULL);

jsize length = env->GetArrayLength(arr);

// Sorting (for example, std::sort)

env->ReleaseIntArrayElements(arr, body, 0);

return arr;

}

โš ๏ธ Attention: Always free resources after working with JNIobjects (call Release... methods). Memory leaks in native code lead to unstable operation of the application.

What is JNIEnv?

JNIEnv is a pointer to a structure with methods for interacting with the JVM. Through it you call functions to work with Java objects (creating strings, arrays, etc.). Each thread has its own JNIEnv, and it cannot be transferred between threads!

Optimization and debugging of native code

Native code requires a special approach to optimization and error diagnosis. Here are the key tools:

1. Profiling with Android Profiler

In Android Studio open View โ†’ Tool Windows โ†’ Profiler and select:

  • ๐Ÿ“Š CPU Profiler โ€” to analyze the execution time of native functions.
  • ๐Ÿ—‘๏ธ Memory Profiler โ€” to search for leaks in native memory.

2. Debugging with LLDB

To set a breakpoint in native code:

  1. Build the project in mode Debug.
  2. Launch the application on the device/emulator.
  3. Open Android Studio open Run โ†’ Attach Debugger to Android Process and select your application.

3. Logging

Use __android_log_print to display logs:

#include <android/log.h>

#define LOG_TAG "MyNativeApp"

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

void myFunction() {

LOGD("This message will appear in Logcat!");

}

View logs through adb logcat with a filter by tag:

adb logcat MyNativeApp:D *:S

Critical: Native crashes (SIGSEGV, SIGABRT) do not fall into Google Play Console like normal exceptions. To catch them, integrate with NDK support or use Crashlytics with NDK support or use signal() to intercept signals. Debugging native code is more difficult than Java: always test on real devices (the emulator can hide problems with the architecture). APK to Google Play

๐Ÿ’ก

Debugging native code is more difficult than Java: always test on real devices (the emulator may hide architectural problems).

Build and publish: from APK to Google Play

When building a project with native code Android Studio automatically:

  1. Compiles C code into .solibraries for each architecture (armeabi-v7a, arm64-v8a, x86).
  2. Packages libraries in lib/ inside APK.
  3. Connects libraries to the application via System.loadLibrary().

To reduce the size APK, configure build.gradle:

android {

defaultConfig {

ndk {

// Build only for 64-bit architectures

abiFilters 'arm64-v8a', 'x86_64'

}

}

}

Before publishing in Google Play:

  • ๐Ÿ” Check compatibility with 64-bit architectures (mandatory from 2019).
  • ๐Ÿ“ฆ Make sure that APK does not exceed 150 MB (download limit).
  • ๐Ÿ›ก๏ธ Add to AndroidManifest.xml resolution android:extractNativeLibs="true" (if you need to extract libraries when installation).

โš ๏ธ Attention: Google Play can block the application if it is built only for x86 (this is suspicious, since most devices are on ARM). Always turn on at least arm64-v8a.

Examples of real applications in C

Many popular applications use native code for critical tasks:

Application Task in C/C++ Reason
WhatsApp Message encryption (Signal Protocol) High speed of processing cryptographic operations
VLC for Android Video/audio decoding Using libraries FFmpeg and libVLC
PUBG Mobile Game engine (Unreal Engine 4) Maximum rendering performance
Google Chrome Engine Blink and JavaScript (V8) Cross-platform and optimization

If you are developing such an application, study the sources of open projects:

  • ๐ŸŒ Signal - open source messenger on Java + C.
  • ๐ŸŽต VLC - media player with native codecs.
adb logcat | ndk-stack -sym ./obj/local/arm64-v8a
-->

FAQ: Frequently asked questions about development in C for Android

Is it possible to write the entire application in C, without Java/Kotlin?

Technically yes, but it is extremely inconvenient. Android required Java/Kotlin for:

  • Creation Activity/Service (main components of the application).
  • Works with UI (XML-markup, Jetpack Compose).
  • Interactions with Android API (camera, sensors, notifications).

Native code is used only for heavy calculations, and the application logic is written in Java/Kotlin.

Which compiler is used in NDK: GCC or Clang?

Since 2022 Google has completely switched to Clang as the main compiler in NDK. GCC is no longer supported. Clang provides:

  • Stronger type control.
  • Better compatibility with C++17/20.
  • Optimizations for ARM64.
How to reduce the size of .so libraries?

Several ways:

  1. Use -Os or -Oz instead of -O2 in CMakeLists.txt to optimize for size.
  2. Remove unnecessary symbols using strip --strip-unneeded libnative-lib.so.
  3. Build only for the architectures you need (for example, only arm64-v8a).
  4. Use -ffunction-sections -fdata-sections -Wl,--gc-sections to remove unused code.
Can I use C++ instead of C?

Yes, and this is even preferable! NDK fully supports C++including:

  • Exceptions (try/catch).
  • STL (std::string, std::vector).
  • RTTI (dynamic_cast, typeid).

Example CMakeLists.txt for C++:

cmake_minimum_required(VERSION 3.22.1)

project("my-app")

add_library(native-lib SHARED native-lib.cpp)

find_package(log-log LOG REQUIRED)

target_link_libraries(native-lib log)

How to update NDK to the latest version?

Via Android Studio:

  1. Open Settings โ†’ Appearance & Behavior โ†’ System Settings โ†’ Android SDK.
  2. Go to the tab SDK Tools.
  3. Uncheck the current version NDK, check the new one and click Apply.

Or via the command line:

sdkmanager "ndk;26.1.10909125"

โš ๏ธ After the update, clear the build cache (Build โ†’ Clean Project) and rebuild the project.