Android JNI development Using NDK

in utopian-io •  7 years ago  (edited)

What Will I Learn?

  • You will learn Android JNI development Using NDK

Requirements

  1. Android-NDK;
  2. Windows;
  3. JDK;
  4. Android SDK;
  5. Eclipse;
  6. Cygwin;
  7. Eclipse CDT (C/C++ Development Tooling, optional, at http://www.eclipse.org/cdt/
  8. Visual Studio (optional)

Difficulty

  • Intermediate

Tutorial Contents

  • Introducing JNI
  • First JNI Sample -Hello World
  • Step 1: create the Java project and create the .java file
  • Step 2: build the Java project to get the .class file
  • Step 3: generate the header file
  • Step 4: write the native method and generate the dll file
  • Step 5: rebuild the Java project
  • Mapping Types Support
  • Access Java Applications from Native Methods
  • Access Java string
  • Access Java array
  • Access Java methods
  • Exception handling in native methods
  • Multi-threading programming in native methods

Android JNI development Using NDK

Before using NDK, you should first familiarize yourself with Java JNI programming because JNI is the prerequisites of programming NDK. In this first tutorial of this small series, we will try to delve into the Java JNI inner workings and give elementary samples. In the subsequent tutorial, we will explore Android NDK programming. 

 The test environments we'll utilize in this series include:

1. Android-NDK;

2. Windows;

3. JDK;

4. Android SDK;

5. Eclipse;

6. Cygwin;

7. Eclipse CDT (C/C++ Development Tooling, optional, at http://www.eclipse.org/cdt/

8. Visual Studio (optional) 

Introducing JNI

Similar to P/Invoke on the .NET Framework targeting enabling managed code to call native code, JNI is built serving as a bridge between Java and native code. With JNI, Java applications that use the JNI can incorporate native code written in languages such as C, C++, and Assemble, as well as code written in the Java programming language. In this way, JNI allows programmers to take advantage of the power of the Java platform, without having to abandon their investments in legacy code. The first part of Figure 1 illustrates the various cases of a Java application that uses JNI. In contrast, the second part shows the C based native code can connect with Java libraries, methods and classes through JNI.

Figure 1: JNI bridges Java app and a variety of native goodies

 As is seen, JNI plays the role of setting up a bridge between Java apps and native code. Figure 2 indicates the relations among C based native code, JNI, and Java apps.

Figure 2: JNI serves as a gateway between native code and Java

Although JNI enables us to use tons of ready-made native libraries, it easily results in potential security risks because what the native code executes is the machine code it bears the right to utilize any resource of the host system. In another word, the native code is not limited by the execution environment.

Besides above, there are still some pitfalls that cannot be ignored. For example, subtle errors in the use of JNI may destabilize the entire JVM, an application that relies on JNI in some degree loses the platform portability Java offers. And also, the JNI framework does not provide any automatic garbage collection for non-JVM memory resources on the native side, etc.

Starting from the next paragraph, we are going to look into some typical cases of JNI programming and related notes.

First JNI Sample -Hello World

To make things clearer, I will detail into the steps to finish the first sample project.

Step 1: create the Java project and create the .java file

Start up Eclipse to create a simple Java project named JavaJNITest using the execution environment JavaSE. Next, create a new class named HelloWorld under the folder src. The final result looks like the following. 

public class HelloWorld {

public native void displayHelloWorld(String s);

static{

System.loadLibrary("hello");

}

public static void main(String[] args) {

new HelloWorld().displayHelloWorld("Hello world!");

}

}

There are several points worth noticing above. First, we use the keyword native to declare the method displayHelloWorld as a native method. And further, this native method will be implemented in a native function library which will be loaded in the Java running time environment. 

Next, we invoke the method loadLibrary of the static class System to load a native function library named hello. Whether the extension is .so or .dll depends upon the operation system type (in Linux being .so and in Windows .dll). And also, the keyword static identifies this library can only be loaded once. If for some reason the load fails, this method will throw a related exception.

Step 2: build the Java project to get the .class file

Now, click the menu item "Project"|"Build Project" to build the above project. In fact, you can use also the command below to achieve the same task:

javac  HelloWorld.java

As a result, you've got a byte code file named HelloWorld.class under the folder bin.

Step 3: generate the header file

In this step, we will generate the header file with the name same as the above class. To do this, we run the following command at the command line:

javah  -jni HelloWorld

Note by default the header file is generated at the same folder as the file HelloWorld.class. Of course, we can also use the switch -o to change the target directory. 

Now, let's look at the content of the header file HelloWorld.h. 

#include <jni.h>

/* Header for class HelloWorld */

#ifndef _Included_HelloWorld

#define _Included_HelloWorld

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class:     HelloWorld

* Method:    displayHelloWorld

* Signature: (Ljava/lang/String;)V

*/

JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld

 (JNIEnv *, jobject, jstring);

#ifdef __cplusplus

}

#endif

#endif

As you may have noticed, Java_HelloWorld_displayHelloWorld is the C function name that corresponds to the native method displayHelloWorld of the above Java class. And also, notice that the native method prototype contains two parameters, JNIEnv* and jobject, which are the MUST HAVE in JNI programming. The first parameter is a JNIEnv interface pointer, through which the native method can access the parameters and objects that a Java application passes in, while the second parameter of type jobject is the object itself. In some sense, it is similar to this pointer in a Java application.  

Step 4: write the native method and generate the dll file

In this step, we will write the native method used above. To do this, you can use any C/C++ editor. In this case, I used Visual Studio. The related details are as follows: Launch Visual Studio to create a new Visual C++ CLR "Class Library" named hello.

Create a new Visual C++ CLR “Class Library”Next, modify the header file Stdafx.h as follows: 

#pragma once

#include <jni.h>

#include "HelloWorld.h"

Here we should let C++ compiler able to locate the header files path. To do this, in Visual Studio 2010, we can click the menu item "View"|"Other Windows"|"Property Manager" to open the Property Manager pane. And then, double click the file Microsoft.Cpp.Win32.user to open another dialog, as shown in Figure 4.

Figure 4: The new way in Visual Studio to set up the VC++ Directoires

Then from the "Include Directoies" position select "Edit" to open another dialog "Include Directoies" and add the following three paths required in our VC++ project (you may change the first parts of the paths in your case):

D:\prj\JavaJNITest\bin

C:\Program Files\Java\jdk\include\win32

C:\Program Files\Java\jdk\include

Visual Studio will follow up the above paths to search for the required header files.

Now, let's open the main VC++ file hello.cpp to complete its final form, like the following:  
 

#include "hello.h"

#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld

 (JNIEnv *env, jobject obj, jstring jMsg)

{

const char *strMsgPtr = env->GetStringUTFChars( jMsg , 0);

printf(strMsgPtr);

env->ReleaseStringChars(jMsg, (jchar *)strMsgPtr);

}

Note in the native method we have to use the JNI function to convert the Java string into the native string. In this case, we use the method GetStringUTFChars to convert the Unicode string into a UTF-8 string. After the conversion, the result string can be used in most C/C++ functions, such as printf. In addition, after the UTF-8 string related operation is over we have to invoke another corresponding method ReleaseStringUTFChars to notify the virtual machine to release the UTF-8 occupied memory; or else, this many result in memory leak to run out of memory resources.

OK, now we can click the menu item "Build"|"Build hello" to build the project. If everything goes well, we can get the VC++ dynamic link library file named hello.dll.

Step 5: rebuild the Java project

Now let's return to Eclipse to reopen the previous Java project JavaJNITest to set up the Java native library location. To do this, click the menu "Project"- "Properties" to open the dialog as shown in Figure 5. From the left pane click "Java Build Path" and then click the "Source" tab on the right pane to open up the sub item "Native library location". Then click the button "Edit" to specify the path "D:/2011-dotnetslackers-prj/hello/Debug" (this path depends on your C++ library generated above) we built in the VC++ IDE.

Figure 5: Specify Native library location for the Java project

Till now, Eclipse can locate the native link library related position and run the Java sample application. Now, let's press Ctrl+F11 to run the current Java sample application JavaJNITest. Without any compile error, you will see the running-time result as shown in the Console window in Figure 6.

Figure 6: The running-time result of the first Java application

 As is seen, the string "Hello world!" is output in the bottom Console window inside Eclipse.

Mapping Types Support

In this section we are going to delve into how to use the data types from Java in the native programming. This is commonly-required under the following cases:

  1. Use the passed-in arguments from Java in the native program;
  2. Invoke Java objects from native application;
  3. Native methods may return variables to its caller - Java applications. 

Figure 7: Native data types set

 

Access Java Applications from Native Methods

JNI provides a set of standard interface functions, through which we can create Java objects, access and handle them, release them, as well as invoke Java methods. This section will detail into how to use these functions.

Access Java string

Java application passes a string to native methods in the form of jstring. jstring is different from the string type defined in C/C++, so if you try to directly invoke the function printf to print the string, the Java virtual machine may crash. The following code illustrates the improper use of such a case. 

/*DO NOT USE jstring THIS WAY!!*/

JNIEXPORT jstring JNICALL Java_Prompt_getLine (

JNIEnv *env,

jobject obj,

jstring prompt)

{

printf("%s",prompt);

//...

}

As is used in the preceding example, the native methods have to use JNI functions to convert Java string to native string, and then they can use it. For example, we rest upon the two methods GetStringUTFChars and ReleaseStringChars of the interface pointer JNIEnv* to achieve the target of accessing Java string.  

Access Java array

As with the jstring, you cannot access jarray directly in you native methods. Instead, you have to rely on the JNI provided interface functions. For instance, the following code is illegal: 

/*DO NOT USE jarray THIS WAY!!*/

JNIEXPORT jint JNICALL Java_IntArray_sumArray (

JNIEnv *env,

jobject obj,

jintArray arr)

{

int i, sum=0;

for (i=0; i<10; i++){

sum+=arr[i];

}

//...

}

Instead, you should use JNI specific functions to achieve the above target. In the following example the Java application will pass an integer array to the native method which adds up all the integers and returns the result to the invoker.

1. IntArray.java
 

public class IntArray{

private native int sumArray(int arr[]);

public static void main(String args[]){

IntArray p=new IntArray();

int arr[]=new int[10];

for(int i=0; i<10; i++){

arr[i]=i;

int sum=p.sumArray(arr);

System.out.println("sum="+sum);

}

static{

System.loadLibrary("MyImpOfIntArray");

}

}

The related C code is as follows:

2. IntArray.c 

#include <jni.h>

#include "IntArray.h"

JNIEXPORT jint JNICALL Java_IntArray_sumArray (

JNIEnv *env,

jobject obj,

jintArray arr)

{

jsize len=(*env)->GetArrayLength(env,arr);

int i, sum=0;

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

for (i=0; i<len; i++){

sum+=body[i];

}

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

return sum;

}

In the above code, we first use the JNI method GetArrayLength to get the length of the incoming array. And then, we invoke another JNI method GetIntArrayElements to obtain the pointer pointed to this array. Finally, we use the array (add up each element in the array) according to C syntax.  

Access Java methods

JNI provides support for call back operations. In another word, JNI permits native code to invoke the methods (of a class or object) defined in a Java application.

On the whole, there are three steps needed to invoke a method of an object in Java:

1. Invoke the JNI function GetObjectClass to get the type of a Java object;

2. Invoke the JNI function GetMethodID to locate the method defined in a Java class according to the method identifier and name. If the method does not exist then return 0 and throw an exception NoSuchMethodError in Java app.

3. Invoke the JNI function CallVoidMethod to call the method with no returned values defined in Java. Note this function requires at least three arguments: object, method ID, and related parameters.

In the following example, the native code invokes the method defined in Java, and the method defined in Java in turn invokes the native method. When the recursion depth becomes 5 the Java method will no more call the native method and return.  

public class Callbacks {

private native void nativeMethod(int depth);

private void callback(int depth)

{

if(depth<5){

System.out.println("In Java, depth="+depth+",about to enter C");

nativeMethod(depth+1);

System.out.println("In Java, depth="+depth+",back from C");

}else

System.out.println("In Java, depth="+depth+",limit exceeded");

}

public static void main(String[] args) {

Callbacks c=new Callbacks();

c.nativeMethod(0);

}

static{

System.loadLibrary("MyImpOfCallbacks");

}

}

Next, let's start up Visual Studio to create a VC++ based CLR library named MyImpOfCallbacks. The following gives the related C++ method to invoke the method defined in the above Java code. 

#include "stdafx.h"

#include "MyImpOfCallbacks.h"

JNIEXPORT void JNICALL

Java_Callbacks_nativeMethod(JNIEnv *env, jobject obj, jint depth){

jclass cls=env->GetObjectClass(obj);

jmethodID mid=env->GetMethodID(cls, "callback", "(I)V");

//jthrowable exc;

if(mid==0){

return;

}

printf("In C++, depth=%d, about to enter Java\n", depth);

env->CallVoidMethod(obj, mid, depth);

printf("In C++, depth=%d, back from Java\n", depth);

}

As is shown above, the C++ native method callbacks the Java method callback of the Callbacks class, the whole process of which is consistent with the preceding steps we describe.

Another important point to be noticed is that JNI searches for symbols according to the method identifier and name. This can make sure that when there are new methods added into the Java class native code can still be used without any modification.

In addition, JNI, according to the method identifier, identifies the argument and return value type of the method. For instance, the above example uses (I)V to indicate that it wants to find a Java method having a integer-typed argument and no return value.

On the whole, the common form of the method identifier can be described as follows:"(argument-type)return-type"

Now, let's look at the running-time result of the above sample.

Figure 8: The running-time snapshot related to the sample project Callbacks

 

Exception handling in native methods

In Java, when an exception is thrown out Java virtual machine will automatically invoke the corresponding exception handling code. Although some languages, such as C and C++, also provide the similar exception handling mechanism there are no consistent and standard means to handle such cases. For this, JNI produces its own functions to help native code throw Java exceptions. And further, it requires native methods to check the possible exceptions after the invocation of Java methods. Note that the exceptions thrown in native code not only can be handled in other part of the native method but also be dealt with in the Java application that calls the native method.Let's consider an example. First, look at the Java code: 

public class CatchThrow {

private native void catchThrow() throws IllegalArgumentException;

private void callback() throws NullPointerException{

throw new NullPointerException("thrown in CatchThrow.callback");

}

public static void main(String[] args) {

CatchThrow c=new CatchThrow();

try{

c.catchThrow();

} catch(Exception e){

System.out.println("In Java:\n"+e);

}

}

static {

System.loadLibrary("MyImpOfCatchThrow");

}

}

As usual, you should use javah.exe to generate the corresponding head file CatchThrow.h. Now, let's continue to look at the C++ code (contained in the VC++ sample class library named MyImpOfCatchThrow).  
 

#include "stdafx.h"

#include "MyImpOfCatchThrow.h"

JNIEXPORT void JNICALL

Java_CatchThrow_catchThrow(JNIEnv *env, jobject obj){

jclass cls=env->GetObjectClass(obj);

jmethodID mid=env->GetMethodID(cls, "callback", "()V");

jthrowable exc;

if(mid==0){

return;

}

env->CallVoidMethod(obj, mid);

exc=env->ExceptionOccurred();

if(exc)

{

/*

We do not do much with the exception, except that we print a

debug message using ExceptionDescribe, clear it, and throw

a new exception.

*/

jclass newExcCls;

env->ExceptionDescribe();

env->ExceptionClear();

newExcCls=env->FindClass( "java/lang/IllegalArgumentException");

if(newExcCls==0)

{

//unable to find the new exception class, give up.

return;

}

env->ThrowNew(newExcCls,"throw from C code");

}

}

Obviously, the native call towards the method CallVoidMethod will surely trigger an exception NullPointerException being thrown out, so in the above code as the invocation of CallVoidMethod we call the JNI special method ExceptionOccurred to detect this exception. As is seen, in this case we've not made complicated management with this exception. Instead, we simply show a piece of debug info by calling the method ExceptionDescribe. And then, we call the method ThrowNew to throw out a new exception IllegalArgumentException which is just the exception to be detected by the Java application calling this native method.Figure 9 illustrates the running result of the above sample.

Figure 9: The running-time snapshot related to the exception handling sample

A last word is except for the few functions, such as ExceptionOccurred, ExceptionDescribe, and ExceptionClear, you are suggested to check, handle and clear the possible exceptions when calling other JNI functions.

Multi-threading programming in native methods

As is known, multi-threading programming is supported in Java. Though, in the native methods you have to bear in mind that you should modify any global variable related value because other threads contained in the Java application invoking the native method may also use this global variable.

1. JNI

There are several points worth noticing when things involve multithreading in native methods.

(1) The JNI interface pointer (JNIEnv*) is only valid in the current thread - it cannot be passed to other threads or used in them. Though the same thread that invokes the native method in Java will pass the same interface pointer (JNIEnv*) to the native method, other threads will pass different interface pointers to the native method.

(2) You cannot pass a local reference to another thread. When a different thread wants to use the same reference of a Java object you should define it as a global reference.

(3) Carefully check the use of global variables. Because multiple threads may simultaneously access these global variables you have to prevent multiple threads from modifying the values of the global variables simultaneously.

2. SynchronizationIn Java programs you can use the keyword synchronized to declare a segment of code, as follows: 

Synchronized (obj){

  //the synchronized block here...

}

Java virtual machine can ensure a thread, before running the above code, obtain the object obj related Monitor, so that the synchronized code can be run at most by one thread at anytime. 

 JNI provides two functions, i.e. MonitorEnter and MonitorExit, to make the code will be synchronously executed in the native methods. Their corresponding usage in the native methods is given below: 

//...

(*env)-> MonitorEnter(env,obj);

// the synchronized block here...

(*env)-> MonitorExit(env,obj);

//other code...

A thread, before executing the synchronized block, must enter the object obj related Monitor, and it can enter this Monitor for multiple times. Here the Monitor uses a counter to store the entering times of the special thread. As you may guess, when we invoke the method MonitorEnter the value of the counter will increase while invoking MonitorExit will decrease. Only the Monitor related counter value is equal to zero can other threads obtain the object obj related Monitor. 

3. Wait and Notify There are also other several methods, such as Object.wait, Object.notify and Object.notifyAll, with which to achieve the synchronization of the multiple threads. A fact is JNI does not provide such methods related functions. Though, we can rest upon the approaches mentioned in the preceding paragraphs to invoke these methods in Java applications.

Summary

On the whole, this tutorial has just scratched the surface of Java JNI related knowledge -there are still lots of good stuffs deserved to be further researched into. But this elementary tutorial is nearly enough for general NDK related programming under the Android environment. In the subsequent tutorial, we will focus upon the really interesting Android NDK programming. 



Posted on Utopian.io - Rewarding Open Source Contributors

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @laxam, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

Hey @folke I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Congratulation

Today one year ago you joined SteemIt
Thank you, for making SteemIt great and Steem on for more years to come!

(You are being celebrated here)

Congratulations @folke! You have received a personal award!

1 Year on Steemit
Click on the badge to view your own Board of Honor on SteemitBoard.

Upvote this notificationto to help all Steemit users. Learn why here!