Welcome to BBTECH Lab

Technical Note for Developing Embedded Media Devices, Set-Top-Boxes, Broadband TV, DVR, etc.

[AndroidTV][3]Android Native C/C++ Libraries

[AndroidTV][3]Android Native C/C++ Libraries

I. Background knowledge

1.1 Android NDK vs Android SDK

Reasons to use NDK

  • Great for CPU intensive operations: mobile videogames, signal processing or physics simulations. Run computationally intensive applications.
  • Porting existing C/C++ code to Android.
  • Developing a multiplatform application (iOS, Windows). (For cross-platform development)
  • The native code is compiled to a binary code and run directly on OS, while Java code is translated into Java byte-code and interpreted by Virtual Machine.
  • Native code allows developers to make use of some processor features that are not accessible at Android SDK.
  • The opportunity to optimize the critical code at an assembly level.

Reasons to use SDK

  • Ensured device portability despite processor architecture.
  • Rich set of libraries.
  • Automatic memory management.

1.2     JNI and NDK

JNI is part of Dalvik VM such as framework connecting the world of Java to the native code, it allows native code to access Java environment.

The general framework of a C/C++ function call via a JNI and Java program (especially Android application) is as follows

  • The way of compiling native is declared in the Java class (C/C++ function).
  • The .java source code file containing the native method is compiled (Build project in Android).
  • The javah command generates an .h file, which corresponds to the native method according to the .class files.
  • C/C++ methods are used to achieve the local method
  • The recommended method for this step is first to copy the function prototypes into the .h file and then modify the function prototypes and add the function body. In this process, the following points should be noted:
    • The JNI function call must use the C function. If it is the C++ function, do not forget to add the extern “C” keyword;
    • The format of the method name should follow the following template: Java_pacakege_class_method, namely the Java_package name class name and function method name.
  • The C or C++ file is compiled into a dynamic library (under Windows this is a .dll file, under Unix/Linux a .so file).

NDK is a toolchain from Android official, originally for developers who writes native C/C++ code as JNI library.

  • Cross-compiler, linker to build for ARM, x86, MIPS, etc.
  • Provides a way to bundle dynamic library into your APK.
  • JNI headers, minimal C++ support headers, and android native app APIs.

NDK build process:

1. 3 Deep Into JNI World

[JNI tips]

  • General tips.
  • JavaVM and JNIEnv.
  • Threads
  • jclass, jmethodID, and jfieldID.
  • Local and global references.
  • UTF-8 and UTF-16 strings.
  • Primitive arrays.
  • Region calls.
  • Exceptions.
  • Native libraries.
  • 64-bit considerations.

1.3.1    JNIEnv Interface Pointer

[JNIEnv Interface Pointer]

JNI functions are available through an interface pointer. The JNIEnv interface pointer is pointing to thread-local data, which in turn points to a JNI function table shared by all threads.

  • Gateway to access all predefined JNI functions
  • Access Java fields
  • Invoke Java methods.
  • It point to the thread’s local data, so it cannot be shared.
  • It can be accessible only by java threads.
  • Native threads must call AttachCurrentThread to attach itself to VM and to obtain the JNIEnv interface pointer

Every native method receives a JNIEnv pointer as its first parameter; this pointer provides access to the JNI support functions.

We have two ways to RegisterNatives

  • Using javah tool
  • Using RegisterNatives function

1.3.2    Loading Native Libraries

Native code is usually compiled into a shared library and loaded before the native methods can be called.

All the native methods are declared with native key word in Java.

1 static {
2 //use either of the two methods below
3 System.loadLibrary(“nativelib");
4 System.load("//libNative.so");
5 }

1.3.3 Registering native methods using RegisterNatives function

01 typedef struct {
02 char *name;
03 char *signature;
04 void *fnPtr;
05 } JNINativeMethod;
06
07 //
08 // Return 0 to indicates success, otherwise negative value
09 //
10 jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
11

  • Env: JNIEnv interface pointer
  • The clazz argument is a reference to the class in which the native method is to be registered.
  • Methods:
    • Name indicates the native method
    • Signature is the descriptor of the method’s input argument data type and return value data type
    • fnPtr is function pointer pointing to the native method.
  • nMethods indicates the number of methods to register.

1.3.4    JNI_OnLoad

Will be invoked when the native library is loaded. (system.loadLibray(“nativelib”);)

It is the right and safe place to register the native methods before their execution.

01 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved)
02 {
03 JNIEnv env = NULL; 04 if ((jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
05 return -1;
06 }
07 // Write your own code
08 // -> Get jclass with env->FindClass.
09 // -> Register methods with env->RegisterNatives.
10
11 return JNI_VERSION_1_6;
12 }

1.3.5    JNI Datatypes and Data Structures

[JNI Types and Data Structures]

1.3.5.1   Datatypes

The following definition is provided for convenience.

#define JNI_FALSE  0

#define JNI_TRUE   1

The jsize integer type is used to describe cardinal indices and sizes:

typedef jint jsize;

Java TypeNative TypeDescription
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidN/A
1.3.5.2   References Types

In C, all other JNI reference types are defined to be the same as jobject (for example: typdef jobject jclass;)

In C++, JNI modules a set of dummy classed to enforce the subtyping relationship. For example:

1 class _jobject {};
2 class _jclass : public _jobject {};
3 …
4 typedef _jobject *jobject;
5 typedef _jclass *jclass;

1.3.5.3   Field and Method IDs

1 struct _jfieldID; /* opaque structure */
2 typedef struct _jfieldID *jfieldID; /* field IDs / 3 4 struct _jmethodID; / opaque structure */
5 typedef struct _jmethodID *jmethodID; /* method IDs */

1.3.5.4   Type Signatures

The JNI uses the Java VM’s representation of type signatures

Type Signature/Field descriptorJava Field Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L fully-qualified-class ;Has following type signature: Ljava/lang/String;fully-qualified-classexample: String
[ typeHas following type signature: [ Itype[]example: int[]
( arg-types ) ret-type 
has following type signature: (ILjava/lang/String;[I)J
method type
For example, the Java method:
long f (int n, String s, int[] arr);

1.3.6    JNI manipulating

[Oracle, JNI Functions]

1.3.6.1   Manipulating strings in JNI

Strings are complicated in JNI, because Java strings and C strings are internally different.

Java programming language uses UTF-16 to represent strings. If a character cannot fit in a 16-bit code value, a pair of code values named surrogate pair is used. [Modified UTF-8 Strings]

C strings are simply an array of bytes terminated by a null character.

[Oracle, String Operations]

1.3.6.2   Manipulating Object in JNI

[Oracle, Object Operations]

  • The clazz argument is a reference to the Java class of which we want to create an instance object. It cannot be an array class, which has its own set of JNI functions
  • methodID is the constructor method ID, which can be obtained using the GetMethodID JNI function.
1 jobject AllocObject(JNIEnv *env, jclass clazz);
2 jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, …);
3 jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
4 jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);1 jobject AllocObject(JNIEnv *env, jclass clazz);2 jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...);3 jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);4 jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

1.3.6.3   Manipulating Classes in JNI

[Oracle, Class Operations]

1 jclass DefineClass(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize bufLen);
2 jclass FindClass(JNIEnv *env, const char *name);
3 jclass GetSuperclass(JNIEnv *env, jclass clazz);


1.3.6.4   Accessing Java Static and Instance fileds in Native code
  • jfieldID data type: jfieldID is a regular C pointer pointing to a data structure with details hidden from developers. We should not confuse it with jobject or its subtypes. jobject is a reference type corresponding to Object in Java, while jfieldID doesn’t have such a corresponding type in Java. However, JNI provides functions to convert the java.lang.reflect.Field instance to jfieldID and vice versa.
  • Field descriptor: It refers to the modified UTF-8 string used to represent the field data type. (refer to type signatures)

[Oracle, Accessing Static Fields]

[Oracle, Accessing Fields of Objects]

1.3.6.5   Calling static and instance methods from the native code
  • jmethodID data type: Similar to jfieldID, jmethodID is a regular C pointer pointing to a data structure with details hidden from the developers. JNI provides functions to convert the java.lang.reflect.Method instance to jmethodID and vice versa.
  • Method descriptor: This is a modified UTF-8 string used to represent the input (input arguments) data types and output (return type) data type of the method. Method descriptors are formed by grouping all field descriptors of its input arguments inside a “()”, and appending the field descriptor of the return type. If the return type is void, we should use “V”. If there’s no input arguments, we should simply use “()”, followed by the field descriptor of the return type. For constructors, “V” should be used to represent the return type. The following table lists a few Java methods and their corresponding method descriptors:

[Oracle. Calling static methods]

[Oracle, Calling Instance Methods]

1.3.6.6   Handling exceptions in JNI

[Oracle, Exceptions]

1.3.6.7   Debug in JNI

For Production Builds

  • adb shell setprop debug.checkjni 1

For Engineering Builds:

  • adb shell stop
  • adb shell setprop dalvik.vm.checkjni true
  • adb shell start
1.3.6.8   Memory Issues

Using Libc Debug Mode

  • adb shell setprop libc.debug.malloc 1
  • adb shell stop
  • adb shell start

Supported libc debug mode values are

  • 1: Perform leak detection.
  • 5: Fill allocated memory to detect overruns.
  • 10: Fill memory and add sentinel to detect overruns.
1.3.6.9   Further

[Android Tips]

  • Native Threads usage
  • More about references
  • JNI Graphics using OpenGL
  • Audio using OpenSL apis.
  • Etc….

II Developing Android NDK application

2.1 Developing Android NDK Applications for Embedded Devices

NDK application development can be divided into five steps shown in following figure

2.1.1    Developing Android NDK Applications with Android Studio

Required

2.1.1.1   Step 1: Creating a HelloJNI project
  • Open Android Studio IDE in your computer.
  • Create a new project and Edit the Application name to “HelloJNI”. (Optional) You can edit the company domain or select the suitable location for current project tutorial. Then click next button to proceed.
  • Select Minimum SDK (API 15:Android 4.0.3 (IceCreamSandwich). I choose the API 15 because many android devices currently are support more than API 15. Click Next button.
  • Choose “Empty Activity” and Click Next button
  • Lastly, press finish button.

[Note : You must download NDK package in the SDK Manager to proceed.]

2.1.1.2   Step 2: Setup external tools

In your android studio menu go to File > Settings. Expand the Tools section you will see “External Tools” and Click it. After that create two external tools which are javah and ndk-build.

2.1.1.3   Step 3: Add a java class for Java Native Interface

Right click package name > new > Java class and name it as “helloStringJNI“. This class will add static and load the library which name is “nativelib”. The library name is followed by the so file, we will compile .so file later. And the native method is to get the method from the C and C++ source code.

Edit helloStringJNI.java class. Go to the file and copy the following code in your class.

01 package com.bbtechlab.hellojni;
02
03 public class helloStringJNI {
04 static {
05 System.loadLibrary(“nativelib”);
06 }
07
08 public native String getStringJNI();
09
10 }

2.1.1.4   Step 4: Edit build.gradle (Module:app)

Add ndk and sourceSets.main in the defaultConfig. NDK is to specific what module name you use, for example our module name will be “nativelib”. The moduleName will follow by the C or C++ files so we will create later. In SourceSets.main section the jni.srcDirs = [] mean disable auto and jniLibs.src are specify which jni library located.

Edit gradle-properties

You will occur an error if you do not add the following code:

android.useDeprecatedNdk=true

2.1.1.5   Step 5: Add JNI & Implement C/C++ for nativelib

From Android navigate to Project Files, after that right click main folder > New > Folder > JNI Folder. You will see a new JNI folder was added in.

Right click your folder name jni > New > New C/C++ Source File and name it as “nativelib.cpp“. This name must be same as ModuleName in build.gradle.

Generate header files for nativelib.cpp.

  • Go to your java folder and Right click helloJNI.java class > NDK >javah. It will automatically create a header file in your jni folder. For example, it will look like this.

01 /* DO NOT EDIT THIS FILE - it is machine generated */
02 #include <jni.h>
03 /* Header for class com_humaxdigital_hellojni_helloStringJNI */
04
05 #ifndef _Included_com_humaxdigital_hellojni_helloStringJNI
06 #define _Included_com_humaxdigital_hellojni_helloStringJNI
07 #ifdef __cplusplus
08 extern"C"{
09 #endif
10 /*
11  * Class:     com_humaxdigital_hellojni_helloStringJNI
12  * Method:    getStringJNI
13  * Signature: ()Ljava/lang/String;
14  */
15 JNIEXPORT jstring JNICALL Java_com_humaxdigital_hellojni_helloStringJNI_getStringJNI
16   (JNIEnv *, jobject);
17
18 #ifdef __cplusplus
19 }
20 #endif
21 #endif

Now, edit nativelib.cpp files. Copy the method header from Auto-generated header files and paste it into this file. After that, add the parameter variable and return a value by using C++ code style. You must include the header file.

01 //
02 // Created by bamboo on 1/22/2019.
03 //
04 #include "com_humaxdigital_hellojni_helloStringJNI.h"
05
06 JNIEXPORT jstring JNICALL Java_com_humaxdigital_hellojni_helloStringJNI_getStringJNI (JNIEnv *env, jobject obj) {
07     return(*env).NewStringUTF("hello JNI - Bamboo");
08 }

Compile nativelib.so (shared library) file by creating Android.mk

Right click jni folder > New > File and name it to Android.mk. Add the following code to your file.

[Android.mk]

1 LOCAL_PATH := $(call my-dir)
2 include  $(CLEAR_VARS)
3
4 LOCAL_MODULE := nativelib
5 LOCAL_SRC_FILES := nativelib.cpp
6 include $(BUILD_SHARED_LIBRARY)

LOCAL_PATH := $(call my-dir)

An Android.mk file must begin defining the LOCAL_PATH variable, this is where the source files are. The macro ‘my-dir’ is the path where the Android.mk file is located.

include $(CLEAR_VARS)

Since all the building and parsing is done in the same context the variables called LOCAL_XXX is global and need to be cleared.

LOCAL_MODULE := nativelib

This is where you set the name used as the identifier for each module. Later used in java when loading the module. The system will add ‘lib’ before the module name when compiling into the .so file. So nativelib will become lib nativelib.so. The only exception is if you add ‘lib’ first in your module name then the system will not add it.

LOCAL_SRC_FILES := nativelib.cpp

Here you add a list of the files you need to compile your module. You do not need to add headers or include files the system will take care of that for you.

include $(BUILD_SHARED_LIBRARY)

The NDK provides you with two make files that parse and build everything accordingly to your Android.mk file. The two once are BUILD_STATIC_LIBRARY for building static library and BUILD_SHARED_LIBRARY for building shared library. For the example project here we use the BUILD_SHARED_LIBRARY.

Right click jni folder > New > File and name it to Application.mk. Add the following code to your file.

[Application.mk]

1 APP_MODULES := nativelib
2
3 APP_ABI := all

Right-click main folder > NDK > ndk-build. You will see new so files will appear in your libs folder as the picture below. The folders separate by different CPUs name.

Figure 3‑14. nativelib.so outputs separated by different CPUs name.

2.1.1.6   Step 6: Access nativelib via main activity

Edit activity_main.xml layout

01 <?xml version="1.0"encoding="utf-8"?>
02 <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
03     xmlns:app="http://schemas.android.com/apk/res-auto"
04     xmlns:tools="http://schemas.android.com/tools"
05     android:layout_width="match_parent"
06     android:layout_height="match_parent"
07     tools:context=".MainActivity">
08
09     <TextView
10         android:layout_width="wrap_content"
11         android:layout_height="wrap_content"
12         android:id="@+id/textView"
13         android:text="Hello World!"
14         app:layout_constraintBottom_toBottomOf="parent"
15         app:layout_constraintLeft_toLeftOf="parent"
16         app:layout_constraintRight_toRightOf="parent"
17         app:layout_constraintTop_toTopOf="parent"/>
18
19 </android.support.constraint.ConstraintLayout>

Edit MainActivity.java class

01 package com.humaxdigital.hellojni;
02
03 import android.support.v7.app.AppCompatActivity;
04 import android.os.Bundle;
05 import android.widget.TextView;
06
07 publicclassMainActivity extends AppCompatActivity {
08
09     @Override
10     protectedvoidonCreate(Bundle savedInstanceState) {
11         super.onCreate(savedInstanceState);

12         setContentView(R.layout.activity_main);
13
14         TextView textView=(TextView)findViewById(R.id.textView);
15         helloStringJNI testStringJNI = newhelloStringJNI();
16         textView.setText(""+ testStringJNI.getStringJNI());
17     }
18 }

Now to try to run your project, you shall see the output like as below

2.2     Porting Existing Android NDK applications to Embedded Devices

All NDK applications can be divided into three types based on the following properties of the native code:

  • Consists of C/C++ code only that is not related to hardware
  • Uses a third-party dynamic linked library
  • Includes assembly code that is highly related to non-embedded platform.(example: non-Intel Atom platform)

Native code that consists of C/C++ code only that is not related to hardware

  • Recompile the native code to run the application on embedded platform successfully.
  • Open the NDK project and search for Android.mk file and add APP_ABI:=armeabi armeabi-v7a x86 in Android.mk and recompile the native code with ndk-build.
  • If the Android.mk file is not found, use the ndk-build APP_ABI=”armeabi armeabi-v7a x86″ command to build the project.
  • Package the application again with supported x86 platforms.

If native code uses a third-party dynamic linked library, the shared library must be recompiled into embedded platform version (example: x86 version for the Intel Atom platform).

If native code includes assembly code that is highly related to non-embedded platform (example: non-IA platforms), code must be rewritten with IA assembly or C/C++.

3. Integrate pre-built Native libraries to android projects

TBD.

bbtechlab