
Hello everyone. In this course you are going to learn about Android NDK development by using C/C++ code.
This is a practical class about Android NDK programming. After learning this class, you will be able to write high performance program with C/C++ code for your Android Apps. And you can integrate existing C/C++ libraries into your Android Apps.
This course has two prerequisites. Firstly, basic Java programming skills is required as most Android apps are developed in Java code. Secondly, basic C/C++ programming skills is recommended. It will be helpull if you hava some basic Android development experience.
These references & resources are very userfull in NDK programming.
All the source code will be discussed and developed in this course is presented in the two github url. Before we dive into NDK programming, let's learn some Basic Concepts about NDK and JNI.
So what is NDK?
The Android NDK is a set of tools allowing you to embed C or C++ (“native code”) into your Android apps. The ability to use native code in Android apps can be particularly useful to developers who wish to do one or more of the following:
Port their apps between platforms.
Reuse existing libraries, or provide their own libraries for reuse.
Increase performance in certain cases, particularly computationally intensive ones like games.
Whait is JNI
JNI is the Java Native Interface. 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.
In next lecture we will setup Android NDK devlop environment and create our fist NDK program. Thanks for watching.
In this lecture we are going to setup Android NDK develop environment and create our first NDK program.
First you need to install Android Studio. You can download Android Studio in this website. After Android Studio is installed, you need to setup SDK and NDK. Let's open Android Studio, click SDK Manager, edit Android SDK Location, click next to install SDK and then click finish. Click SDK Tools, select Android SDK Build-Tools, select NDK and CMake, then click OK.
Let's create a new project with C/C++ support.
To create a new project with support for native code, the process is similar to creating any other Android Studio project, but with an additional step:
In the Choose your project section of the wizard, select the Native C++ project type.
Click Next.
Complete all other fields in the next section of the wizard.
Click Next.
In the Customize C++ Support section of the wizard, you can customize your project with the C++ Standard field.
Use the drop-down list to select which standardization of C++ you want to use. Selecting Toolchain Default uses the default CMake setting.
Click Finish.
After Android Studio finishes creating your new project, open the Project pane from the left side of the IDE and select the Android view from the menuAndroid Studio adds the cpp group:
The cpp group is where you can find all the native source files, headers, build scripts for CMake or ndk-build, and prebuilt libraries that are a part of your project. For new projects, Android Studio creates a sample C++ source file, native-lib.cpp, and places it in the src/main/cpp/ directory of your app module. This sample code provides a simple C++ function, stringFromJNI(), that returns the string "Hello from C++".
Similar to how build.gradle files instruct Gradle how to build your app, CMake and ndk-build require a build script to know how to build your native library. For new projects, Android Studio creates a CMake build script,CMakeLists.txt, and places it in your module’s root directory.
Let's build and run the sample app.
When you click Run, Android Studio builds and launches an app that displays the text "Hello from C++" on your Android device or emulator.
Now we have setup Android NDK develop environment. In next lecture, we will learn NDK program with android log.
In this lecture we are going to learn Android NDK programming with log.
__android_log_print is a very usefull function. It writes a formatted string to the log, with priority prio and tag tag. The useage of Logging is well documented in this URL. Let's try this function,
We are going to create a native header file ndk_utils.h
#ifndef NDK_DEMOS_NDK_UTILS_H #define NDK_DEMOS_NDK_UTILS_H #include <android/log.h> #define TAG "ndk_log" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__); #endif
Let's add a Button in the layout of activity_main, set id to call_jni
<Button android:id="@+id/call_jni" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="160dp" android:text="Call JNI" app:layout_constraintBottom_toBottomOf="parent" tools:layout_editor_absoluteX="-16dp" />
In MainActivity, set click listener for call_jni button.
TextView tv = binding.sampleText; binding.callJni.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String result = stringFromJNI(); tv.setText(result); } });
Let's run the project, then click button. Sounds good.
Let's use android log in native code.
Let's open native-lib.cpp, android include "ndk_utils.h", call LOG_D
#include <jni.h> #include <string> #include "ndk_utils.h" extern "C" JNIEXPORT jstring JNICALL Java_ndk_demo_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; LOG_D("Hello from native"); return env->NewStringUTF(hello.c_str()); }
Let's run the app, open the logcat. click the button, we can see log string in logcat, and the string from native is displayed on the Andoid device.
In next lecture we will write Java native method that use c codes. Thanks for watching.
In this lecture we will learn interface with C in Android NDK develop. There are 3 steps to use C code in JNI programming.
Step 1: Write a Java Class that uses C Codes
Step 2: Compile the Java Program & Generate the C/C++ Header File
Step 3: Implementing the C Program
Let's write Java class HelloJNI.java.
public class HelloJNI { // Save as HelloJNI.java static { System.loadLibrary("demo"); // Load native library libdemo.so // at runtime // This library contains a native method called sayHello() } // Declare an instance native method sayHello() which receives no parameter and returns void public native void sayHello(); }
We are going to create static initializer block. The static initializer invokes System.loadLibrary() to load the native library "demo" (which contains a native method called sayHello()) during the class loading. It will be mapped to "libdemo.so". The program will throw a UnsatisfiedLinkError if the library cannot be found in runtime.
Next, we declare the method sayHello() as a native instance method, via keyword native which denotes that this method is implemented in another language. A native method does not contain a body. The sayHello() shall be found in the native library loaded.
Let's compile the Java Program HelloJNI.java & Generate the C/C++ Header File HelloJNI.h
Starting from JDK 8, you should use "javac -h" to compile the Java program AND generate C/C++ header file called HelloJNI.h as follows:
javac -h ../cpp ndk/demo/HelloJNI.java
The "-h dir" option generates C/C++ header and places it in the directory specified (in the above example, '../cpp' for the cpp directory in parent directory).
Inspect the header file HelloJNI.h:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class ndk_demo_HelloJNI */ #ifndef _Included_ndk_demo_HelloJNI #define _Included_ndk_demo_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: ndk_demo_HelloJNI * Method: sayHello * Signature: ()V */ JNIEXPORT void JNICALL Java_ndk_demo_HelloJNI_sayHello (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
The header declares a C function
Java_ndk_demo_HelloJNI_sayHello
The naming convention for the C function is Java_{package_and_classname}_{function_name}(JNI_arguments). The dot in package name is replaced by underscore.
The arguments are:
JNIEnv*: reference to JNI environment, which lets you access all the JNI functions.
jobject: reference to "this" Java object.
We are not using these arguments in this hello-world example, but will be using them later. Ignore the macros JNIEXPORT and JNICALL for the time being.
The extern "C" is recognized by C++ compiler only. It notifies the C++ compiler that these functions are to be compiled using C's function naming protocol instead of C++ naming protocol. C and C++ have different function naming protocols as C++ support function overloading and uses a name mangling scheme to differentiate the overloaded functions.
Implementing the C Program HelloJNI.c
#include "ndk_demo_HelloJNI.h" #include "ndk_utils.h" JNIEXPORT void JNICALL Java_ndk_demo_HelloJNI_sayHello (JNIEnv *env, jobject obj) { LOG_D("Hello NDK C!"); }
open CMakeLists.txt, add HelloJNI.c in add_library.
Open MainActivity.java, create function helloJNI(), then create a HelloJNI object that calls sayHello().
Then register this function into test case
private String helloJNI() { HelloJNI hello = new HelloJNI(); hello.sayHello(); return "Hello Java use C"; }
Let's run the program. We can see Hello NDK C in the catlog.
In next lecture we are going to learn JNI with C++.
In this lecture we will learn interface with C++ in Android develop. There are 3 steps to use C++ code in JNI programming.
Step 1: Write a Java Class that uses C++ Codes
Step 2: Compile the Java Program & Generate the C/C++ Header File
Step 3: Implementing the C++ Program
Let's create a Java class HelloCPP.java
package ndk.demo; public class HelloCPP { static { System.loadLibrary("demo"); // Load native library libdemo.so // at runtime // This library contains a native method called sayHello() } // Declare an instance native method sayHello() which receives no parameter and returns void public native void sayHelloCPP(); }
Let's compile the Java Program HelloCPP.java & Generate the C/C++ Header File HelloCPP.h.
We are going to use command javac -h ../cpp ndk/demo/HelloCPP.java. Header file is generated.
Let's create CPP file HelloCPP.cpp
#include "ndk_demo_HelloCPP.h" #include "ndk_utils.h" JNIEXPORT void JNICALL Java_ndk_demo_HelloCPP_sayHelloCPP (JNIEnv *env, jobject thisObj) { LOG_D("Hello NDK C++!"); }
open CMakeLists.txt, add HelloCPP.cpp in add_library.
Open MainActivity.java, create new HelloCPP() call sayHelloCPP().
private String helloCPP() { HelloCPP hello = new HelloCPP(); hello.sayHelloCPP(); return "Hello Java use C++"; }
Let's run the program. We can see Hello NDK C++ in the catlog.
In next lecture we are going to learn JNI with C/C++ Mixture.
In this lecture we will learn interface with C/C++ in Android develop. There are 3 steps to use C/C++ code in JNI programming.
Step 1: Write a Java Class that uses C Codes
Step 2: Compile the Java Program & Generate the C/C++ Header File
Step 3: Implementing the C/C++ Program
Let's create a Java class HelloMixture.java
package ndk.demo; public class HelloMixture { static { System.loadLibrary("demo"); // hello.dll (Windows) or libhello.so (Unixes) } // Native method declaration public native void sayHelloMixture(); }
Let's compile the Java Program HelloMixture.java & Generate the C/C++ Header File.
We are going to use command javac -h ../cpp ndk/demo/HelloMixture.java. Header file is generated.
Let's create HelloJNICppImpl.h
#ifndef NDK_DEMO_HELLOJNICPPIMPL_H #define NDK_DEMO_HELLOJNICPPIMPL_H #ifdef __cplusplus extern "C" { #endif void sayHello (); #ifdef __cplusplus } #endif #endif //NDK_DEMO_HELLOJNICPPIMPL_H
Then create HelloJNICppImpl.cpp
#include "HelloJNICppImpl.h" #include "ndk_utils.h" using namespace std; void sayHello () { LOG_D("Hello World from C++!"); return; }
Let's create HelloMixture.c
#include "ndk_demo_HelloMixture.h" #include "HelloJNICppImpl.h" #include "ndk_utils.h" JNIEXPORT void JNICALL Java_ndk_demo_HelloMixture_sayHelloMixture (JNIEnv *env, jobject obj) { LOG_D("Hello Mixture!"); sayHello(); }
open CMakeLists.txt, add HelloMixture.c, HelloJNICppImpl.cpp in add_library.
Open MainActivity.java, create new HelloMixture() call sayHelloMixture().
private String helloMixture() { HelloMixture hello = new HelloMixture(); hello.sayHelloMixture(); return "Hello Java use Mixture"; }
Let's run the program, check catlog.
In next lecture we are going to learn Passing Arguments and Result between Java & Native Programs
This lecture we will learn passing primitive types between Java & Native programs.
Let's discuss how the JNI maps Java types to native C types.
This slide describes Java primitive types and their machine-dependent native equivalents.
Passing Java primitives is straight forward. A jxxx type is defined in the native system, i.e,. jint, jbyte, jshort, jlong, jfloat, jdouble, jchar and jboolean for each of the Java's primitives int, byte, short, long, float, double, char and boolean, respectively.
Let's open AndroidStudio.
Let's create a Java JNI Program: TestJNIPrimitive.java
package ndk.demo; public class TestJNIPrimitive { static { System.loadLibrary("demo"); } // Declare a native method average() that receives two ints and return a double containing the average public native double average(int n1, int n2); }
This JNI program loads a shared library libdemo.so (Unixes). It declares a native method average() that receives two int's and returns a double containing the average value of the two int's.
Compile the Java program into "TestJNIPrimitive.class" and generate the C/C++ header file "TestJNIPrimitive.h":
javac -h ../cpp ndk/demo/TestJNIPrimitive.java
The header file TestJNIPrimitive.h contains a function declaration Java_ndk_demo_TestJNIPrimitive_average() which takes a JNIEnv* (for accessing JNI environment interface), a jobject (for referencing this object), two jint's (Java native method's two arguments) and returns a jdouble (Java native method's return-type).
The JNI types jint and jdouble correspond to Java's type int and double, respectively.
The "jni.h" contains these typedef statements for the eight JNI primitives and an additional jsize.
Now we have the header file. Let's create the C implementation TestJNIPrimitive.c
include "ndk_demo_TestJNIPrimitive.h" #include "ndk_utils.h" JNIEXPORT jdouble JNICALL Java_ndk_demo_TestJNIPrimitive_average (JNIEnv *env, jobject thisObj, jint n1, jint n2) { jdouble result; LOG_D("In C, the numbers are %d and %d\n", n1, n2); result = ((jdouble)n1 + n2) / 2.0; // jint is mapped to int, jdouble is mapped to double return result; }
open CMakeLists.txt, add TestJNIPrimitive.c in add_library.
Open MainActivity.java, create test case and call test function.
private String testJNIPrimitive() { double average = new TestJNIPrimitive().average(2, 3); return "Average: " + average; }
Let's run the program, the result is displayed on the screen and catlog shows the message.
In next lecture we are going to learn Passing Strings between Java & Native Programs
This lecture we will learn Passing Strings between Java & Native Programs
Passing strings is more complicated than passing primitives, as Java's String is an object (reference type), while C-string is a NULL-terminated char array. You need to convert between Java String (represented as JNI jstring) and C-string (char*).
The JNI Environment (accessed via the argument JNIEnv*) provides functions for the conversion:
To get a C-string (char*) from JNI string (jstring), invoke method const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*).
To get a JNI string (jstring) from a C-string (char*), invoke method jstring NewStringUTF(JNIEnv*, char*).
Let's create a Java JNI Program: TestJNIString.java
package ndk.demo; public class TestJNIString { static { System.loadLibrary("demo"); } // Native method that receives a Java String and return a Java String public native String sayHello(String msg); }
This JNI program declares a native method sayHello() that receives a Java String and returns a Java String.
Compile the Java program into "TestJNIString.class" and generate the C/C++ header file "TestJNIString.h":
javac -h ../cpp ndk/demo/TestJNIString.java
Now we have the header file. Let's create the C implementation file TestJNIString.c
#include "ndk_utils.h" #include "ndk_demo_TestJNIString.h" JNIEXPORT jstring JNICALL Java_ndk_demo_TestJNIString_sayHello (JNIEnv *env, jobject thisObj, jstring inJNIStr) { // Step 1: Convert the JNI String (jstring) into C-String (char*) const char *inCStr = (*env)->GetStringUTFChars(env, inJNIStr, NULL); if (NULL == inCStr) return NULL; // Step 2: Perform its intended operations LOG_D("In C, the received string is: %s\n", inCStr); (*env)->ReleaseStringUTFChars(env, inJNIStr, inCStr); // release resources // Create a C-string char outCStr[] = "Hello from native!"; // Step 3: Convert the C-string (char*) into JNI String (jstring) and return return (*env)->NewStringUTF(env, outCStr); }
The C implementation TestJNIString.c is as follows.
It receives the JNI string (jstring), convert into a C-string (char*), via GetStringUTFChars().
It then performs its intended operations - displays the string received and create another string to be returned.
It converts the returned C-string (char*) to JNI string (jstring), via NewStringUTF(), and return the jstring.
open CMakeLists.txt, add TestJNIString.c in add_library.
Open MainActivity.java, create test case and call test function.
private String testJNIString() { return new TestJNIString().sayHello("Hello from Java"); }
Let's run the program, the result is displayed on the screen and catlog shows the message.
In next lecture we are going to learn Passing Array of Primitives between Java & Native Programs
This lecture we will learn Passing Array of Primitives between Java & Native Programs
In Java, array is a reference type, similar to a class. There are 9 types of Java arrays, one each of the eight primitives and an array of java.lang.Object. JNI defines a type for each of the eight Java primitive arrays, i.e, jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray for Java's primitive array of int, byte, short, long, float, double, char and boolean, respectively. It also define a jobjectArray for Java's array of Object (to be discussed later).
Again, you need to convert between JNI array and native array, e.g., between jintArray and C's jint[], or jdoubleArray and C's jdouble[]. The JNI Environment interface provides a set of functions for the conversion:
To get a C native jint[] from a JNI jintArray, invoke jint* GetIntArrayElements(JNIEnv *env, jintArray a, jboolean *iscopy).
To get a JNI jintArray from C native jint[], first, invoke jintArray NewIntArray(JNIEnv *env, jsize len) to allocate, then use void SetIntArrayRegion(JNIEnv *env, jintArray a, jsize start, jsize len, const jint *buf) to copy from the jint[] to jintArray.
There are 8 sets of the above functions, one for each of the eight Java primitives.
The native program is required to:
Receive the incoming JNI array (e.g., jintArray), convert to C's native array (e.g., jint[]).
Perform its intended operations.
Convert the return C's native array (e.g., jdouble[]) to JNI array (e.g., jdoubleArray), and return the JNI array.
Let's open AndroidStudio and write code.
Let's create a Java JNI Program: TestJNIPrimitiveArray.java
package ndk.demo; public class TestJNIPrimitiveArray { static { System.loadLibrary("demo"); } // Declare a native method sumAndAverage() that receives an int[] and // return a double[2] array with [0] as sum and [1] as average public native double[] sumAndAverage(int[] numbers); }
This JNI program declare a native method sumAndAverage() that receives an int[] and return a double[2] array with [0] as sum and [1] as average
Compile the Java program into ".class" and generate the C/C++ header file :
javac -h ../cpp ndk/demo/TestJNIPrimitiveArray.java
Now we have the header file. Let's create the C implementation file TestJNIPrimitiveArray.c
#include "ndk_utils.h" #include "ndk_demo_TestJNIPrimitiveArray.h" JNIEXPORT jdoubleArray JNICALL Java_ndk_demo_TestJNIPrimitiveArray_sumAndAverage (JNIEnv *env, jobject thisObj, jintArray inJNIArray) { // Step 1: Convert the incoming JNI jintarray to C's jint[] jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL); if (NULL == inCArray) return NULL; jsize length = (*env)->GetArrayLength(env, inJNIArray); // Step 2: Perform its intended operations jint sum = 0; int i; for (i = 0; i < length; i++) { sum += inCArray[i]; } jdouble average = (jdouble)sum / length; (*env)->ReleaseIntArrayElements(env, inJNIArray, inCArray, 0); // release resources jdouble outCArray[] = {sum, average}; // Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return jdoubleArray outJNIArray = (*env)->NewDoubleArray(env, 2); // allocate if (NULL == outJNIArray) return NULL; (*env)->SetDoubleArrayRegion(env, outJNIArray, 0 , 2, outCArray); // copy return outJNIArray; }
open CMakeLists.txt, add TestJNIPrimitiveArray.c in add_library.
Open MainActivity.java, create test case and call test function.
private String testJNIPrimitiveArray() { int[] numbers = {22, 33, 33}; double[] results = new TestJNIPrimitiveArray().sumAndAverage(numbers); return String.format("In Java, the sum is %s, the average is %s", results[0], results[1]); }
Let's run the program, the result is displayed on the device screen.
In next lecture we are going to learn access objects variables
In this lecture we are going to learn accessing object's instance variables.
To access the instance variable of an object:
Get a reference to this object's class via GetObjectClass().
Get the Field ID of the instance variable to be accessed via GetFieldID() from the class reference. You need to provide the variable name and its field descriptor (or signature). For a Java class, the field descriptor is in the form of "L;", with dot replaced by forward slash (/), e.g.,, the class descriptor for String is "Ljava/lang/String;". For primitives, use "I" for int, "B" for byte, "S" for short, "J" for long, "F" for float, "D" for double, "C" for char, and "Z" for boolean. For arrays, include a prefix "[", e.g., "[Ljava/lang/Object;" for an array of Object; "[I" for an array of int.
Based on the Field ID, retrieve the instance variable via GetObjectField() or GetField() function.
To update the instance variable, use the SetObjectField() or SetField() function, providing the Field ID.
Let's open AndroidStudio and write code.
Let's create a Java JNI Program: TestJNIInstanceVariable.java
package ndk.demo; import androidx.annotation.NonNull; public class TestJNIInstanceVariable { static { System.loadLibrary("demo"); // myjni.dll (Windows) or libmyjni.so (Unixes) } // Instance variables private int number = 88; private String message = "Hello from Java"; // Declare a native method that modifies the instance variables public native void modifyInstanceVariable(); @NonNull @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("In Java, int is "); result.append(number); result.append("\nIn Java, message is "); result.append(message); return result.toString(); } }
This JNI program declare a native method modifyInstanceVariable() that modifies the instance variables.
Compile the Java program into ".class" and generate the C/C++ header file :
javac -h ../cpp ndk/demo/TestJNIInstanceVariable.java
Now we have the header file. Let's create the C implementation file TestJNIInstanceVariable.c
#include "ndk_utils.h" #include "ndk_demo_TestJNIInstanceVariable.h" JNIEXPORT void JNICALL Java_ndk_demo_TestJNIInstanceVariable_modifyInstanceVariable (JNIEnv *env, jobject thisObj) { // Get a reference to this object's class jclass thisClass = (*env)->GetObjectClass(env, thisObj); // int // Get the Field ID of the instance variables "number" jfieldID fidNumber = (*env)->GetFieldID(env, thisClass, "number", "I"); if (NULL == fidNumber) return; // Get the int given the Field ID jint number = (*env)->GetIntField(env, thisObj, fidNumber); LOG_D("In C, the int is %d\n", number); // Change the variable number = 99; (*env)->SetIntField(env, thisObj, fidNumber, number); // Get the Field ID of the instance variables "message" jfieldID fidMessage = (*env)->GetFieldID(env, thisClass, "message", "Ljava/lang/String;"); if (NULL == fidMessage) return; // String // Get the object given the Field ID jstring message = (*env)->GetObjectField(env, thisObj, fidMessage); // Create a C-string with the JNI String const char *cStr = (*env)->GetStringUTFChars(env, message, NULL); if (NULL == cStr) return; LOG_D("In C, the string is %s\n", cStr); (*env)->ReleaseStringUTFChars(env, message, cStr); // Create a new C-string and assign to the JNI string message = (*env)->NewStringUTF(env, "Hello from C"); if (NULL == message) return; // modify the instance variables (*env)->SetObjectField(env, thisObj, fidMessage, message); }
open CMakeLists.txt, add TestJNIInstanceVariable.c in add_library.
Open MainActivity.java, create test case and call test function.
private String testJNIInstanceVariable() { TestJNIInstanceVariable obj = new TestJNIInstanceVariable(); obj.modifyInstanceVariable(); return obj.toString(); }
Let's run the program, the result is displayed on the device screen and logcat shows some debug messages.
In next lecture we are going to learn access objects static variables
In this lecture we are going to learn accessing the static variables of a Class:
Accessing static variables is similar to accessing instance variable, except that you use functions such as GetStaticFieldID(), Get|SetStaticObjectField(), Get|SetStaticField().
Let's open AndroidStudio and write code.
Let's create a Java JNI Program: TestJNIInstanceVariable.java
package ndk.demo; import androidx.annotation.NonNull; public class TestJNIStaticVariable { static { System.loadLibrary("demo"); // nyjni.dll (Windows) or libmyjni.so (Unixes) } // Static variables private static double number = 55.66; // Declare a native method that modifies the static variable public native void modifyStaticVariable(); @NonNull @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("In Java, int is "); result.append(number); return result.toString(); } }
This JNI program declare a native method modifyStaticVariable() that modifies the static variable
Compile the Java program into ".class" and generate the C/C++ header file :
javac -h ../cpp ndk/demo/TestJNIStaticVariable.java
Now we have the header file. Let's create the C implementation file TestJNIStaticVariable.c
#include "ndk_utils.h" #include "ndk_demo_TestJNIStaticVariable.h" JNIEXPORT void JNICALL Java_ndk_demo_TestJNIStaticVariable_modifyStaticVariable (JNIEnv *env, jobject thisObj) { // Get a reference to this object's class jclass cls = (*env)->GetObjectClass(env, thisObj); // Read the int static variable and modify its value jfieldID fidNumber = (*env)->GetStaticFieldID(env, cls, "number", "D"); if (NULL == fidNumber) return; jdouble number = (*env)->GetStaticDoubleField(env, cls, fidNumber); LOG_D("In C, the double is %f\n", number); number = 77.88; (*env)->SetStaticDoubleField(env, cls, fidNumber, number); }
open CMakeLists.txt, add TestJNIStaticVariable.c in add_library.
Open MainActivity.java, create test case and call test function.
priprivate String testJNIStaticVariable() { TestJNIStaticVariable obj = new TestJNIStaticVariable(); obj.modifyStaticVariable(); return obj.toString(); }
Let's run the program, the result is displayed on the device screen and logcat shows some debug messages.
In next lecture we are going to learn Callback Instance Methods and Static Methods
In this lecture we are going to learn Callbacking Instance Methods and Static Methods
To call back an instance method from the native code:
1.Get a reference to this object's class via GetObjectClass().
2.From the class reference, get the Method ID via GetMethodID(). You need to provide the method name and the signature. The signature is in the form "(parameters)return-type". You can list the method signature for a Java program via javap utility (Class File Disassembler) with -s (print signature) and -p (show private members):
3.Based on the Method ID, you could invoke CallMethod() or CallVoidMethod() or CallObjectMethod(), where the return-type is , void and Object, respectively. Append the argument, if any, before the argument list. For non-void return-type, the method returns a value.
To callback a static method, use GetStaticMethodID(), CallStaticMethod(), CallStaticVoidMethod() or CallStaticObjectMethod().
The JNI functions for calling back instance method and static method are list on this slide.
Let's open AndroidStuido and write a demo.
Let's create a Java JNI program TestJNICallBackMethod.
public class TestJNICallBackMethod { static { System.loadLibrary("demo"); // myjni.dll (Windows) or libmyjni.so (Unixes) } // Instance method to be called back by the native code private void callback() { System.out.println("In Java"); } private void callback(String message) { System.out.println("In Java with " + message); } private double callbackAverage(int n1, int n2) { return ((double)n1 + n2) / 2.0; } // Static method to be called back by the native code private static String callbackStatic() { return "From static Java method"; } // Declare a native method that calls back // various instance and static methods defined in this class. public native void nativeMethod(); }
We need click make module.
Find classes directory, open it in terminal.
Use javah to generate the header file.
Move it into cpp directory.
Let's create the C implementation file TestJNICallBackMethod.c
Open CMakeLists.txt, add the C file we jave just created in add_library.
// // Created by james on 9/6/2024. // #include "ndk_utils.h" #include "ndk_demo_TestJNICallBackMethod.h" JNIEXPORT void JNICALL Java_ndk_demo_TestJNICallBackMethod_nativeMethod (JNIEnv *env, jobject thisObj) { // Get a class reference for this object jclass thisClass = (*env)->GetObjectClass(env, thisObj); // Get the Method ID for method "callback", which takes no arg and return void jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V"); // check if null pointer if (NULL == midCallBack) return; LOG_D("In C, call back Java's callback()\n"); // Call back the method (which returns void), based on the Method ID (*env)->CallVoidMethod(env, thisObj, midCallBack); // Get the Method ID for method "callback", which takes string and return void jmethodID midCallBackStr = (*env)->GetMethodID(env, thisClass, "callback", "(Ljava/lang/String;)V"); if (NULL == midCallBackStr) return; LOG_D("In C, call back Java's called(String)\n"); // Create a jni string jstring message = (*env)->NewStringUTF(env, "Hello from C"); // Call back the method (which receives string and returns void), based on the Method ID (*env)->CallVoidMethod(env, thisObj, midCallBackStr, message); // Get the Method ID for method "callbackAverage", which takes two int and return double jmethodID midCallBackAverage = (*env)->GetMethodID(env, thisClass, "callbackAverage", "(II)D"); if (NULL == midCallBackAverage) return; jdouble average = (*env)->CallDoubleMethod(env, thisObj, midCallBackAverage, 2, 3); LOG_D("In C, the average is %f\n", average); // Get the Method ID for method "callbackStatic", which takes no arg int and return String jmethodID midCallBackStatic = (*env)->GetStaticMethodID(env, thisClass, "callbackStatic", "()Ljava/lang/String;"); if (NULL == midCallBackStatic) return; jstring resultJNIStr = (*env)->CallStaticObjectMethod(env, thisClass, midCallBackStatic); // Convert jstring to C string const char *resultCStr = (*env)->GetStringUTFChars(env, resultJNIStr, NULL); if (NULL == resultCStr) return; LOG_D("In C, the returned string is %s\n", resultCStr); (*env)->ReleaseStringUTFChars(env, resultJNIStr, resultCStr); }
Open MainActivity.java, create test case and call test function.
private String testJNICallback() { TestJNICallBackMethod obj = new TestJNICallBackMethod(); obj.nativeMethod(); return "callback test"; }
Let's run the program, the result is displayed on the device screen and logcat shows some debug messages.
In next lecture we are going to learn Callback Overridden Superclass' Instance Method。
In this lecture we are going to learn Callback Overridden Superclass' Instance Method
JNI provides a set of CallNonvirtualMethod() functions to invoke superclass' instance methods which has been overridden in this class (similar to a super.methodName() call inside a Java subclass):
Get the Method ID, via GetMethodID().
Based on the Method ID, invoke one of the CallNonvirtualMethod(), with the object, superclass, and arguments.
The JNI function for calling the overridden superclass' instance method are:
Let's open AndroidStudio and write code.
Let's create a Java Class People.java
package ndk.demo; import android.util.Log; public class People { private static final String TAG = People.class.getSimpleName(); public void say() { Log.d(TAG, "people say"); sayAge(); } public void sayAge() { Log.d(TAG, "people say age"); } }
Let's create a Java JNI program Baby.
package ndk.demo; import android.util.Log; public class Baby extends People { static { System.loadLibrary("demo"); // myjni.dll (Windows) or libmyjni.so (Unixes) } private static final String TAG = Baby.class.getSimpleName(); private int age; private String name; public Baby(int age, String name) { this.age = age; this.name = name; } @Override public void say() { Log.d(TAG, "baby say:my name is " + name); } @Override public void sayAge() { Log.d(TAG, "baby say:my age is " + age); } // Declare a native method that calls back the Baby say() // and calls back the Superclass method say of Baby public native void superMethod(); }
This JNI program declare a native superMethod that calls back the say method and it's parents's say method
Compile the Java program into ".class" and generate the C/C++ header file :
We need click make module, then open build directory, goto classes directory, open it in terminal.
javah ndk.demo.Baby
Now we have the header file. Copy it into cpp directory.
Let's create the C implementation file Baby.c
#include "ndk_utils.h" #include "ndk_demo_Baby.h" JNIEXPORT void JNICALL Java_ndk_demo_Baby_superMethod (JNIEnv *env, jobject thisObj) { // Get a class reference for this object jclass thisClass = (*env)->GetObjectClass(env, thisObj); // Get the Method ID for method "callback", which takes no arg and return void jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "say", "()V"); if (NULL == midCallBack) return; LOG_D("In C, call back Baby's say()\n"); // Call back the method (which returns void), baed on the Method ID (*env)->CallVoidMethod(env, thisObj, midCallBack); // Get parent class: People jclass classPeople = (*env)->FindClass(env, "ndk/demo/People"); jmethodID peopleSay = (*env)->GetMethodID(env, classPeople, "say", "()V"); LOG_D("In C, call back object's superclass say()\n"); (*env)->CallNonvirtualVoidMethod(env, thisObj, classPeople, peopleSay); }
open CMakeLists.txt, add Baby.c in add_library.
Open MainActivity.java, create test case and call test function.
private String testSuperMethod() { Baby obj = new Baby(7, "james"); obj.say(); obj.superMethod(); return "Super method test"; }
Let's run the program, the result is displayed on the device screen and logcat shows some debug messages.
In next lecture we are going to learn create Java Objects in Native code.
In this lecture we are going to learn Creating Java Objects in Native Code
The JNI functions for creating object (jobject) are:
Let's open AndroidStudio and write code.
Let's create a Java JNI program Baby.
package ndk.demo; public class TestJNIConstructor { static { System.loadLibrary("demo"); // myjni.dll (Windows) or libmyjni.so (Unixes) } // Native method that calls back the constructor and return the constructed object. // Return an Integer object with the given int. public native Integer getIntegerObject(int number); }
This class declares a native method getIntegerObject(). The native code shall create and return an Integer object, based on the argument given.
Compile the Java program into ".class" and generate the C/C++ header file :
We need click make module, then open build directory, goto classes directory, open it in terminal.
javah ndk.demo.TestJNIConstructor
Now we have the header file. Copy it into cpp directory.
Let's create the C implementation file Baby.c
#include "ndk_utils.h" #include "ndk_demo_TestJNIConstructor.h" JNIEXPORT jobject JNICALL Java_ndk_demo_TestJNIConstructor_getIntegerObject (JNIEnv *env, jobject thisObj, jint number) { // Get a class reference for java.lang.Integer jclass cls = (*env)->FindClass(env, "java/lang/Integer"); // Get the Method ID of the constructor which takes an int jmethodID midInit = (*env)->GetMethodID(env, cls, "<init>", "(I)V"); if (NULL == midInit) return NULL; // Call back constructor to allocate a new instance, with an int argument jobject newObj = (*env)->NewObject(env, cls, midInit, number); // Try running the toString() on this newly create object jmethodID midToString = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;"); if (NULL == midToString) return NULL; jstring resultStr = (*env)->CallObjectMethod(env, newObj, midToString); const char *resultCStr = (*env)->GetStringUTFChars(env, resultStr, NULL); LOG_D("In C: the number is %s\n", resultCStr); //May need to call releaseStringUTFChars() before return return newObj; }
open CMakeLists.txt, add Baby.c in add_library.
Open MainActivity.java, create test case and call test function.
private String testConstructor() { TestJNIConstructor obj = new TestJNIConstructor(); return "In Java, the number is :" + obj.getIntegerObject(9999); }
Let's run the program, the result is displayed on the device screen and logcat shows some debug messages.
In next lecture we are going to learn create Java Array of Objects in Native code.
In this lecture we are going to learn Creating Java Array of Objects in Native Code
Unlike primitive array which can be processed in bulk, for object array, you need to use the Get|SetObjectArrayElement() to process each of the elements.
The JNI functions for creating and manipulating object array (jobjectArray) are:
Let's open AndroidStudio and write code.
Let's create a Java JNI program Baby.
public class TestJNIObjectArray { static { System.loadLibrary("demo"); // myjni.dll (Windows) or libmyjni.so (Unixes) } // Native method that receives an Integer[] and // returns a Double[2] with [0] as sum and [1] as average public native Double[] sumAndAverage(Integer[] numbers); }
For illustration, this class declares a native method that takes an array of Integer, compute their sum and average, and returns as an array of Double. Take note the arrays of objects are pass into and out of the native method.
Compile the Java program into ".class" and generate the C/C++ header file :
We need click make module, then open build directory, goto classes directory, open it in terminal.
javah ndk.demo.TestJNIObjectArray
Now we have the header file. Move it into cpp directory.
Let's create the C implementation file Baby.c
#include "ndk_utils.h" #include "ndk_demo_TestJNIObjectArray.h" JNIEXPORT jobjectArray JNICALL Java_ndk_demo_TestJNIObjectArray_sumAndAverage (JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) { // Get a class reference for java.lang.Integer jclass classInteger = (*env)->FindClass(env, "java/lang/Integer"); // Use Integer.intValue() to retrieve the int jmethodID midIntValue = (*env)->GetMethodID(env, classInteger, "intValue", "()I"); if (NULL == midIntValue) return NULL; // Get the value of each Integer object in the array jsize length = (*env)->GetArrayLength(env, inJNIArray); jint sum = 0; int i; for (i = 0; i < length; i++) { jobject objInteger = (*env)->GetObjectArrayElement(env, inJNIArray, i); if (NULL == objInteger) return NULL; jint value = (*env)->CallIntMethod(env, objInteger, midIntValue); sum += value; } double average = (double)sum / length; LOG_D("In C, the sum is %d\n", sum); LOG_D("In C, the average is %f\n", average); // Get a class reference for java.lang.Double jclass classDouble = (*env)->FindClass(env, "java/lang/Double"); // Allocate a jobjectArray of 2 java.lang.Double jobjectArray outJNIArray = (*env)->NewObjectArray(env, 2, classDouble, NULL); // Construct 2 Double objects by calling the constructor jmethodID midDoubleInit = (*env)->GetMethodID(env, classDouble, "<init>", "(D)V"); if (NULL == midDoubleInit) return NULL; jobject objSum = (*env)->NewObject(env, classDouble, midDoubleInit, (double)sum); jobject objAve = (*env)->NewObject(env, classDouble, midDoubleInit, average); // Set to the jobjectArray (*env)->SetObjectArrayElement(env, outJNIArray, 0, objSum); (*env)->SetObjectArrayElement(env, outJNIArray, 1, objAve); return outJNIArray; }
open CMakeLists.txt, add Baby.c in add_library.
Open MainActivity.java, create test case and call test function.
pprivate String testJNIObjectArray() { Integer[] numbers = {11, 22, 33}; TestJNIObjectArray objectArray = new TestJNIObjectArray(); Double[] results = objectArray.sumAndAverage(numbers); return String.format("In Java, the sum is %s, the average is %s", results[0], results[1]); }
Let's run the program, the result is displayed on the device screen and logcat shows some debug messages.
In next lecture we are going to learn integrate existing C/C++ libs(FFmpeg) into our Android app.
In next lecture we are going to learn local and global references in Native code.
In this lecture we are going to learn Compile/Build Latest ffmpeg for Android and Use it in Android Studio
FFMPEG is one of the most popular or powerful media processing library with multiple platform support and it is capable of doing Most of Media processing task.
Let's Compile the Latest FFMPEG for Android with Latest Android NDK.
Prepare Your Host Computer. I use wsl2 with ubuntu 22 on windows, other Linux distribution will works. lsb_release -a
And download Android NDK Later from Here. https://developer.android.com/ndk/downloads
Download Latest FFMPEG Source From Here. https://ffmpeg.org/download.html
Copy the download files into ubuntu, use unzip command to unzip ndk and ffmpeg.
Now Create a Directory and put ffmpeg source and android ndk files inside this directory.
Now Open Terminal inside this Directory and Run following command.
sudo apt install build-essential
And after that create a new file named android.sh in this directory.
You can download this file in this github repository. Copy the contents in your file.
(android_ffmpeg_examples/static at main · xLox-x/android_ffmpeg_examples · GitHub)
You can Customize FFMPEG Modules to Enable or Disable just use appropriate Flags.
Now open Terminal where you created android.sh file and add excutable permission.
chmod +x android.sh
Then run the script ./android.sh
And Now Your ffmpeg Compilation is Begins and make sure you mentioned your ffmpeg source directory or android ndk directory in the above script.
After compilation, we can see the so libs and header filers in ffmpeg_android directory.
In next lecture, we are going to use the ffmpeg products in Android Studio Projects
In this lecture we are going to use FFmpeg in Android Studio Projects
Prepare Compiled FFMPEG files
You can download precompiled FFMPEG.so files in this website
GitHub - umirtech/ffmpegAndroid: Precompiled ffmpeg android .SO Shared Library files
You can use Your Own Compiled FFMPEG .so files if you compiled using previous compilation Tutorial.
Let's Open AndroidStuido, Create libs folder inside your projects app folder.
Copy and paste ffmpeg files we build last lecture inside libs folder.
And after that modify code in Your cmakeList.txt file. You can copy this file into your project
Also modify code in your Module build.gradlel file. You need to config externalNativeBuild, set cmake and ndk
And now just rebuild our project and start using ffmpeg c++ libraries in your Android Studio Project.
Let's write some ffmpeg program in native-lib.cpp file, and write demo code in MainActivity, then build our project. It works, we can see some FFmpeg message displayed on the sreen.
In next lecture, we are going to learn play video on Android use ffmpeg.
Prepare a SurfaceView, setFormat to PixelFormat.RGBA_8888
Create a native render method that receive SurfaceView's surface, and render video on this SurfaceView
Create a thread to call native render method.
Let's prepare a demo mp4 video file, use adb to push this file into the Download directorey of your Android device.
Let's click play button, it works.
Thanks for watching, see you in next course.
This is a practical class about Android NDK programming. After learning this class, you will be able to write high performance program with C/C++ code for your Android Apps. And you can integrate existing C/C++ libraries into your Android Apps.
This course has two prerequisites. Firstly, basic Java programming skills is required as most Android apps are developed in Java code. Secondly, basic C/C++ programming skills is recommended. It will be helpull if you hava some basic Android development experience.
The Android NDK is a set of tools allowing you to embed C or C++ (“native code”) into your Android apps. The ability to use native code in Android apps can be particularly useful to developers who wish to do one or more of the following:
Port their apps between platforms.
Reuse existing libraries, or provide their own libraries for reuse.
Increase performance in certain cases, particularly computationally intensive ones like games.
JNI is the Java Native Interface. 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.
This lecture will teach you setup Android NDK develop environment and create your first NDK program.
FFMPEG is one of the most popular or powerful media processing library with multiple platform support and it is capable of doing Most of Media processing task. You will learn Compile/Build Latest ffmpeg for Android and Use it in Android Studio.