Introduction to Android NDK. Calling native methods from Java code
Android NDK is a tool for Android SDK, which allows developers Android applications build performance critically important parts your applications in native code. It is intended to be used in conjunction with the Android SDK only, so if you haven't already installed latest android SDK, please do this before downloading the NDP. Additionally, you should read up on what it is to get an understanding of what the NDT is offering and whether it will be useful to you.
Select download the package that suits your computer.
Platform | Package | Size | MD5 Checksum |
Windows | android-ndk-r4b-windows.zip | 45792835 bytes | |
Mac OS X (intel) | android-ndk-r4b-darwin-x86.zip | 50586041 bytes | |
Linux 32/64-bit (x86) | android-ndk-r4b-linux-x86.zip | 49464776 bytes |
Changes
The following sections provide information and notes about successive releases of the NDC, identified by revision number.
Android NDK, Revision 4b Android NDK, Version 4, b (June 2010):
Includes fixes for several issues in the NDK for creating and debugging scripts - if you are using the R4 NDK, we recommend downloading the R4b NDK. For getting detailed information changes in this release, read the CHANGES.txt document included in the downloaded NDK package.
General instructions:
Provides a simplified build system using the new NDK command builder.
Added support for easily debugging generated device manufacturing machine code via new team NDK-GDB.
Adding new Android-specific ABIs for ARM-based processor architectures, armeabi-v7a. The new ABIs extend the existing armeabi ABIs included in this set of extension processor instructions:
Thumb-2 hardware instructions VFP FPU instructions (VFPv3-D16)
Additional support for ARM SIMD extensions (neon) GCC and embedded VFPv3-D32. Supports devices such as Motorola's Verizon Droid, Google's Nexus, and others.
Adding new cpufeatures static library (with sources) that allows your application to discover the host device's processor at runtime. In particular, applications can check for ARMv7 support, as well as VFPv3-D32 and NEON support, and then provide individual code paths as needed.
Adds a sample application, hi-neon, that illustrates how to use the cpufeatures library to test the processor and then provide optimized code using NEON instrinsics if supported by the processor.
Allows you to generate machine code for one or both sets of instructions supported by the NDC. For example, you can build for ARMv5 and ARMv7 architectures and at the same time everything will be preserved.
APK application.
To ensure that applications are available to users only if their devices are capable of running them, Android Market has application filters based on information, a set of instructions included in the applications - action is required on your part in order to do the filtering. In addition, during installation, the Android system also checks itself, the application, and allows installation to continue only when the application provides a library that is compiled for the device's processor architecture.
Added support for Android 2.2, including new stable APIs for accessing raster object buffer pixels from native code.
Android NDK, Revision 3 Android NDK, Version 3 (March 2010)
General instructions:
Adds OpenGL ES 2.0 native library support.
Adds a sample application, hi-gl2, which illustrates the use of OpenGL ES 2.0 and top fragment shaders.
The executable toolchain has been updated for this version with GCC 4.4.0, which should generate slightly more compact and efficient machine code than the previous one (4.2.1). NDC also still provides 4.2.1 binaries that can optionally be used to generate machine code.
Android NDK, Revision 2 Android NDK, Revision 2 (September 2009)
Originally released as "Android 1.6 NDK Release 1".
General instructions:
Adds OpenGL ES 1.1 native library support.
Adds a sample application, San Angeles, that makes 3D graphics through the native OpenGL ES API, while managing the lifecycle of an activity with a GLSurfaceView object.
Android NDK, Revision 1 Android NDK, Revision 1 (June 2009)
Originally released as "Android 1.5 NDK Release 1".
General instructions:
Includes compiler support (CCS) for ARMv5TE instructions, including Thumb instructions.
Includes header systems for a stable native API, documentation, and sample applications.
To develop applications for Android OS, Google provides two development packages: SDK and NDK. There are many articles, books, and also good guidelines from Google about the SDK. But even Google itself writes little about NDK. And of the worthwhile books, I would single out only one, Cinar O. - Pro Android C++ with the NDK – 2012.
This article is aimed at those who are not yet familiar (or little familiar) with Android NDK and would like to strengthen their knowledge. I will pay attention to JNI, since it seems to me that we need to start with this interface. Also, at the end, let's look at a small example with two functions for writing and reading a file.
What is Android NDK?
Android NDK(native development kit) is a set of tools that allow you to implement part of your application using languages such as C/C++.What is NDK used for?
Google recommends using the NDK only in very rare cases. Often these are the following cases:- You need to increase productivity (for example, sorting a large amount of data);
- Use a third party library. For example, a lot of things have already been written in C/C++ languages and you just need to use existing material. Example of libraries such as, Ffmpeg, OpenCV;
- Low-level programming (for example, everything that goes beyond Dalvik);
What is JNI?
Java Native Interface– a standard mechanism for running code, under the control of the Java virtual machine, which is written in C/C++ or Assembler languages, and compiled in the form dynamic libraries, allows you to avoid using static linking. This makes it possible to call a C/C++ function from a Java program, and vice versa.Benefits of JNI
The main advantage over its analogues (Netscape Java Runtime Interface or Microsoft’s Raw Native Interface and COM/Java Interface) is that JNI was originally developed to ensure binary compatibility, for the compatibility of applications written in JNI for any virtual machines Java on a specific platform (when I talk about JNI, I am not tied to the Dalvik machine, because JNI was written by Oracle for the JVM which is suitable for everyone Java virtual cars). Therefore, the compiled code in C/C++ will be executed regardless of the platform. More early versions did not allow implementation of binary compatibility.Binary compatibility or binary compatibility is a type of program compatibility that allows the program to work in different environments without changing its executable files.
How JNI works
JNI table, organized as a table virtual functions in C++. The VM can work with several such tables. For example, one will be for debugging, the second for use. A pointer to a JNI interface is only valid in the current thread. This means that the pointer cannot walk from one thread to another. But native methods can be called from different threads. Example:
Jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s) ( const char *str = (*env)->GetStringUTFChars(env, s, 0); (*env)->ReleaseStringUTFChars(env, s, str ); return 10; )
- *env– pointer to the interface;
- obj– a link to the object in which the native method is described;
- i and s– passed arguments;
Local and global links
JNI divides references into three types: local, global, and weak global references. Locals are valid until the method completes. All Java objects that JNI functions return are local. The programmer must rely on the VM itself to clean up all local references. Local links are available only in the thread in which they were created. However, if there is a need, they can be released immediately using the JNI interface DeleteLocalRef method:Jclass clazz; clazz = (*env)->FindClass(env, "java/lang/String"); //your code (*env)->DeleteLocalRef(env, clazz);
Global references remain until they are explicitly released. To register a global reference, call the NewGlobalRef method. If the global reference is no longer needed, then it can be deleted using the DeleteGlobalRef method:
Jclass localClazz; jclass globalClazz; localClazz = (*env)->FindClass(env, "java/lang/String"); globalClazz = (*env)->NewGlobalRef(env, localClazz); //your code (*env)->DeleteLocalRef(env, localClazz);
Error processing
JNI does not check for errors such as NullPointerException, IllegalArgumentException. Causes:- decreased productivity;
- In most C library functions it is very, very difficult to protect against errors.
Jthrowable ExceptionOccurred(JNIEnv *env);
For example, some JNI array access functions do not return errors, but may throw ArrayIndexOutOfBoundsException or ArrayStoreException exceptions.
JNI primitive types
JNI has its own primitive and reference data types.Java Type | Native Type | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
JNI reference types
Modified UTF-8
JNI uses a modified UTF-8 encoding to represent strings. Java, in turn, uses UTF-16. UTF-8 is mostly used in C because it encodes \u0000 to 0xc0 instead of the usual 0x00. Modified strings are encoded so that a sequence of characters that contains only a nonzero ASCII characters can be represented using only one byte.JNI functions
The JNI interface not only contains its own set of data, but also its own functions. It will take a lot of time to review them, since there are more than a dozen of them. You can get acquainted with them in the official documentation.Example of using JNI functions
A small example to help you understand the material covered:#include
Let's look at it line by line:
- JavaVM– provides an interface for calling functions that allow you to create and destroy a JavaVM;
- JNIEnv– provides most of the JNI functions;
- JavaVMInitArgs– arguments for JavaVM;
- JavaVMOption– options for JavaVM;
Streams
All threads in Linux are managed by the kernel, but they can be attached to the JavaVM using the AttachCurrentThread and AttachCurrentThreadAsDaemon functions. Until the thread is attached, it does not have access to JNIEnv. Important, Android does not suspend threads that were created by JNI, even if the GC is triggered. But before the thread terminates, it must call the DetachCurrentThread method to detach from the JavaVM.First steps
Your project structure should look like this:As we can see from Figure 3, all the native code is located in the jni folder. After building the project, four folders will be created in the libs folder for each processor architecture, in which your native library will be located (the number of folders depends on the number of selected architectures).
In order to create a native project, you need to create regular Android project and take the following steps:
- In the root of the project you need to create a jni folder in which to place the sources of the native code;
- Create an Android.mk file that will build the project;
- Create an Application.mk file that describes the assembly details. He is not prerequisite, but allows you to flexibly customize the assembly;
- Create an ndk-build file that will launch the build process (also optional).
Android.mk
As mentioned above, this is a makefile for building a native project. Android.mk allows you to group your code into modules. Modules can be like static libraries(static library, only they will be copied to your project, in the libs folder), shared libraries, standalone executable file(standalone executable).Example minimal configuration:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= NDKBegining LOCAL_SRC_FILES:= ndkBegining.c include $(BUILD_SHARED_LIBRARY)
Let's look at it in detail:
- L OCAL_PATH:= $(call my-dir)– the call my-dir function returns the path of the folder in which the file is called;
- include $(CLEAR_VARS)– clears variables that were used before except LOCAL_PATH. This is necessary because all variables are global, because the build occurs in the context of one GNU Make;
- LOCAL_MODULE– name of the output module. In our example, the output library name is set to NDKBegining, but after the build, libraries named libNDKBegining will be created in the libs folder. Android adds the lib prefix to the name, but in the java code when connecting you must specify the name of the library without the prefix (that is, the names must match those installed in the makefiles);
- LOCAL_SRC_FILES– listing the source files from which the assembly should be created;
- include $(BUILD_SHARED_LIBRARY)– indicates the type of output module.
MY_SOURCE:= NDKBegining.c To access a variable: $(MY_SOURCE) Variables can also be concatenated, for example: LOCAL_SRC_FILES += $(MY_SOURCE)
Application.mk
This makefile defines several variables that will help make the build more flexible:- APP_OPTIM– an additional variable that is set to release or debug. Used for optimization when assembling modules. You can debug both release and debug, but debug provides more information for debugging;
- APP_BUILD_SCRIPT– points to an alternative path to Android.mk;
- APP_ABI– probably one of the most important variables. It indicates for which processor architecture the modules should be assembled. The default is armeabi, which corresponds to the ARMv5TE architecture. For example, to support ARMv7 you should use armeabi-v7a, for IA-32 - x86, for MIPS - mips, or if you need to support all architectures, then the value should be like this: APP_ABI:= armeabi armeabi-v7a x86 mips. If you are using ndk version 7 and higher, then you can not list all architectures, but set APP_ABI:= all.
- APP_PLATFORM– platform target;
- APP_STL– Android uses the libstdc++.so runtime library, which is stripped down and not all C++ functionality is available to the developer. However, the APP_STL variable allows extension support to be included in the build;
- NDK_TOOLCHAIN_VERSION– allows you to select the gcc compiler version (default 4.6);
NDK-BUILDS
Ndk-build is a wrapper for GNU Make. After version 4, flags for ndk-build were introduced:- clean– clears all generated binary files;
- NDK_DEBUG=1– generates debugging code;
- NDK_LOG=1– shows message log (used for debugging);
- NDK_HOST_32BIT=1– Android has tools to support 64-bit versions of utilities (for example NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64, etc.);
- NDK_APPLICATION_MK- the path to Application.mk is indicated.
By default, support for 64 is installed bit version utilities, but you can force compilation only for 32 by setting the NDK_HOST_32BIT=1 flag. Google still recommends using 64-bit utilities to improve the performance of large programs.
How to assemble a project?
It used to be a pain. It was necessary to install the CDT plugin, download the cygwin or mingw compiler. Download Android NDK. Connect this all in Eclipse settings. And how unfortunate it all turned out to be that it didn’t work. The first time I came across the Android NDK, it took me 3 days to set it up (and the problem turned out to be that in cygwin I had to give permission 777 to the project folder).Now everything is much simpler with this. Follow this link. Download the Eclipse ADT Bundle, which already contains everything you need for assembly.
Calling native methods from Java code
In order to use native code from Java, you first need to define native methods in Java class. For example:native String nativeGetStringFromFile(String path) throws IOException; native void nativeWriteByteArrayToFile(String path, byte b) throws IOException;
The method should be preceded by the reserved word “native”. This way the compiler knows that this is the entry point to JNI. We need to implement these methods in a C/C++ file. Google also recommends starting to name methods with the word nativeX, where X is the real name of the method. But before implementing these methods manually, you should generate a header file. This can be done manually, but you can use the javah utility that is in the jdk. But let's go further and will not use it through the console, but will do it using standard means Eclipse.
Now you can launch. The bin/classes directory will contain your header files.
Next, we copy these files to the jni directory of our native project. Calling context menu project and select the item Android Tools– Add Native Library. This will allow us to use jni.h functions. Then you can create a cpp file (sometimes Eclipse creates it by default) and write the bodies of methods that are already described in the header file.
I did not add a code example to the article so as not to lengthen it. You can view/download an example from github.
Tags: Add tags
Android NDK (Native Development Kit) is a very popular toolkit used for developing applications for mobile devices. Many apps in the Android Market app store use components developed using programming languages other than Java to achieve maximum performance. Based on this, the NDK is a toolkit that helps developers create components for their applications using compiled programming languages for various purposes, from achieving optimal performance to simplifying the code used.
Why and how is binary code used?
We all know that the Android application development process is closely related to the use of language Java programming, and also that the use of this language programming makes life much easier for developers because they can use Java's elegant object-oriented model. Applications or algorithms implemented in Java are converted into special bytecode that runs in the same way on all supported platforms. At the same time, the Java virtual machine or JVM (Java Virtual Machine), responsible for JIT compilation and execution of Java bytecode, is available for almost all existing platforms, from mainframes to mobile phones.
However, in case Android systems, which is used mainly on smartphones and tablet computers, the key factor becomes achieving maximum application performance on the used hardware. Java source code, as mentioned above, is first converted into bytecode. This is exactly the bytecode that executes with slight differences on platforms for which the Java virtual machine is available. Ultimately, the entire application runs within a virtual machine on an Android device.
When it comes to Android app development, the above factor is a minor drawback. But programming in Java can be quite difficult due to the constant complexity of the code and the difficulty of understanding it. Moreover, the use of multi-platform bytecode and a virtual machine requires significant computing resources on the device.
To others important factor What requires attention is the multi-platform code. If we need to write a program for multiple hardware platforms, we may end up rewriting much of the controller and display code for each platform, which is not a smart solution. But all code related to the controller must be ported to C and C++, as almost all mobile platforms support them; Thus, if we can implement the logic within libraries in C and C++ and subsequently use it on multiple platforms, we will be able to minimize the performance penalty of the application. In such cases, we will use C and C++ code along with regular Java code or "multi-platform code".
When using a compiled programming language, the source code is compiled directly into machine code for central processor, rather than into an intermediate representation such as in the Java language. This way, app developers can create apps with optimal performance for different Android devices. Fragments of compiled code can be structured within a single shared library, functions from which can be called from Java code. A separate shared library must be created for each of the supported CPU architectures. Most of it source code however, it may remain unchanged. The compiled shared libraries must be added to the .apk file of your application. With all that said, the fundamental model of Android apps won't change.
Using Android NDK
The Android NDK is a toolkit that allows you to implement parts of an Android application in compiled programming languages such as C and C++ and contains libraries for managing activities and accessing physical components of the device, such as various sensors and the display.
Android NDK is integrated with tools from the development kit software(Android SDK), as well as with an integrated development environment Android Studio or legacy environment Eclipse development A.D.T. However, NDK cannot be used alone.
How Android NDK works
The heart of the Android NDK package is the ndk-build script, which is responsible for automatic bypass Android project files (development of each new Android application using an integrated development environment such as Android Studio or Eclipse begins with the creation of new project files) and collects information about which components need to be compiled. This script is also responsible for generating binaries and copying these binaries to the application project files directory.
We can use keyword native so that the compiler knows that this fragment is implemented within the compiled code. For example:
Public native int numbers(int x, int y);
Also, during the project build process, shared libraries (Native Shared Libraries, with the .so extension) and static libraries (Native Shared Libraries, with the .a extension) are created, which can be linked with other libraries. The Application Binary Interface (ABI) uses shared libraries with the .so extension to execute machine code on the system while the application is running.
All compiled code is executed through an interface called Java Native Interface (JNI), which allows components to be connected to each other on Java languages and C/C++.
To build the project using the ndk-build script, we will have to create two files: Android.mk and Application.mk. Both of these files must be located in the JNI directory. The Android.mk file describes the module and its name, build flags, used libraries, source code files that must be compiled, and the Application.mk file describes the binary modules necessary for the application to work.
Installing and using Android NDK on Ubuntu
The Android NDK comes in a self-extracting archive format. For this reason, we will only have to set the execution bit and unpack it:
$ chmod +x android-ndk-r10c-linux-x86_64.bin $ ./android-ndk-r10c-linux-x86_64.bin
As a result, the NDK components will be saved in the current working directory.
Manual unpacking
Since the .bin file is nothing more than a self-extracting 7-Zip archive, we can extract its contents manually using next command:
$ 7za x -o/path/to/target/directories/android-ndk-r10c-linux-x86_64.bin
The 7-Zip archiver components package is available from the official Ubuntu repository and can be installed, for example, using the apt-get command:
$ sudo apt-get install p7zip-full
Installation using Android Studio
We can install Android NDK using SDK Manager component directly from Android Studio.
To do this, after opening the project, you should go to the main menu of the window Tools\u003e Android\u003e SDK Manager. After this, you need to check the boxes next to the names of the components LLDB, CMake and NDK. Next, you just need to apply the changes using the appropriate button.
Create or import a project with binary components
After Android settings Studio we can create new project with support for C/C++ programming languages. However, if we need to add or import existing code in these languages into the Android Studio project, we will be forced to follow the steps below.
The first step is to create new source code files using the mentioned programming languages and add them to a project opened in Android Studio. We can skip this step if the project already has similar files or we need to import a pre-compiled library into it.
The CMake build script allows you to tell the build system of the same name how to compile source code files and build the resulting binary library. This file is also required to import and link existing or bundled NDK libraries with our library. We can also skip this step without any consequences if our existing binary library already ships with a build script file CMakeLists.txt or if it uses the ndk-build component and ships with a build script file Android.mk .
Next, we need to tell Gradle about the existence of our binary library by specifying the path to the CMake or ndk-build build script file. Gradle uses the specified build script to import the source code into an Android Studio project and package the resulting binary library (a file with a .so extension) into an APK-format package file.
Important Note: if the project uses the outdated ndkCompile tool, we will have to open the build.poperties file and remove from it next line code before setting up Gradle to use CMake or ndk-build:
Android.useDeprecatedNdk = true
Now we can build and run our application by clicking on the Run button. Gradle will consider the CMake or ndk-build process as a dependency to be built, build the binary library, and package it into an APK file.
Once we run the application on the device or in the emulator, we can use the features of various integrated development environments such as Android Studio to debug it.
All this shows the importance of the Android NDK for developers of applications for the Android platform. For example, this set software components allows game engine creators to better optimize Android versions of their products, resulting in more impressive performance graphic effects, spending less system resources on them.
The process of creating a simple application Android based NDK does not come with any complications. However, every developer should understand one thing important point: The Android NDK software components were designed for specific use cases and should not be used for any application development.
Android NDK can both help in the application development process and make it as difficult as possible. It is no secret that the use of binary code on Android platform in some cases it does not significantly improve the performance of the application (although in most cases it does improve its performance), but in any case it increases the complexity of its code. Typically, application performance improvements are achieved by running code with CPU-specific instructions. But in general, it is recommended to use the NDK only when application performance is a critical parameter, and not when the developer is more comfortable writing code in C/C++.
As a conclusion, it should be said that there are no immutable rules governing possible cases of using NDK, so you should always turn to your knowledge, experience and intuition.
To develop applications for Android OS, Google provides two development packages: SDK and NDK. There are many articles, books, and also good guidelines from Google about the SDK. But even Google itself writes little about NDK. And of the worthwhile books, I would single out only one, Cinar O. - Pro Android C++ with the NDK – 2012.
This article is aimed at those who are not yet familiar (or little familiar) with Android NDK and would like to strengthen their knowledge. I will pay attention to JNI, since it seems to me that we need to start with this interface. Also, at the end, let's look at a small example with two functions for writing and reading a file.
What is Android NDK?
Android NDK(native development kit) is a set of tools that allow you to implement part of your application using languages such as C/C++.What is NDK used for?
Google recommends using the NDK only in very rare cases. Often these are the following cases:- You need to increase productivity (for example, sorting a large amount of data);
- Use a third party library. For example, a lot of things have already been written in C/C++ languages and you just need to use existing material. Example of libraries such as, Ffmpeg, OpenCV;
- Low-level programming (for example, everything that goes beyond Dalvik);
What is JNI?
Java Native Interface– a standard mechanism for running code under the control of the Java virtual machine, which is written in C/C++ or Assembler languages, and linked in the form of dynamic libraries, eliminating the need for static linking. This makes it possible to call a C/C++ function from a Java program, and vice versa.Benefits of JNI
The main advantage over its analogues (Netscape Java Runtime Interface or Microsoft's Raw Native Interface and COM/Java Interface) is that JNI was originally designed to provide binary compatibility, for the compatibility of applications written in JNI for any Java virtual machines on a specific platform (when I talking about JNI, I am not tied to the Dalvik machine, because JNI was written by Oracle for the JVM which is suitable for all Java virtual machines). Therefore, the compiled code in C/C++ will be executed regardless of the platform. Earlier versions did not allow binary compatibility.Binary compatibility or binary compatibility is a type of program compatibility that allows the program to work in different environments without changing its executable files.
How JNI works
JNI table, organized like a table of virtual functions in C++. The VM can work with several such tables. For example, one will be for debugging, the second for use. A pointer to a JNI interface is only valid in the current thread. This means that the pointer cannot walk from one thread to another. But native methods can be called from different threads. Example:
Jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s) ( const char *str = (*env)->GetStringUTFChars(env, s, 0); (*env)->ReleaseStringUTFChars(env, s, str ); return 10; )
- *env– pointer to the interface;
- obj– a link to the object in which the native method is described;
- i and s– passed arguments;
Local and global links
JNI divides references into three types: local, global, and weak global references. Locals are valid until the method completes. All Java objects that JNI functions return are local. The programmer must rely on the VM itself to clean up all local references. Local links are available only in the thread in which they were created. However, if there is a need, they can be released immediately using the JNI interface DeleteLocalRef method:Jclass clazz; clazz = (*env)->FindClass(env, "java/lang/String"); //your code (*env)->DeleteLocalRef(env, clazz);
Global references remain until they are explicitly released. To register a global reference, call the NewGlobalRef method. If the global reference is no longer needed, then it can be deleted using the DeleteGlobalRef method:
Jclass localClazz; jclass globalClazz; localClazz = (*env)->FindClass(env, "java/lang/String"); globalClazz = (*env)->NewGlobalRef(env, localClazz); //your code (*env)->DeleteLocalRef(env, localClazz);
Error processing
JNI does not check for errors such as NullPointerException, IllegalArgumentException. Causes:- decreased productivity;
- In most C library functions it is very, very difficult to protect against errors.
Jthrowable ExceptionOccurred(JNIEnv *env);
For example, some JNI array access functions do not return errors, but may throw ArrayIndexOutOfBoundsException or ArrayStoreException exceptions.
JNI primitive types
JNI has its own primitive and reference data types.Java Type | Native Type | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
JNI reference types
Modified UTF-8
JNI uses a modified UTF-8 encoding to represent strings. Java, in turn, uses UTF-16. UTF-8 is mostly used in C because it encodes \u0000 to 0xc0 instead of the usual 0x00. The modified strings are encoded so that a sequence of characters that contains only non-zero ASCII characters can be represented using only one byte.JNI functions
The JNI interface not only contains its own set of data, but also its own functions. It will take a lot of time to review them, since there are more than a dozen of them. You can get acquainted with them in the official documentation.Example of using JNI functions
A small example to help you understand the material covered:#include
Let's look at it line by line:
- JavaVM– provides an interface for calling functions that allow you to create and destroy a JavaVM;
- JNIEnv– provides most of the JNI functions;
- JavaVMInitArgs– arguments for JavaVM;
- JavaVMOption– options for JavaVM;
Streams
All threads in Linux are managed by the kernel, but they can be attached to the JavaVM using the AttachCurrentThread and AttachCurrentThreadAsDaemon functions. Until the thread is attached, it does not have access to JNIEnv. Important, Android does not suspend threads that were created by JNI, even if the GC is triggered. But before the thread terminates, it must call the DetachCurrentThread method to detach from the JavaVM.First steps
Your project structure should look like this:As we can see from Figure 3, all the native code is located in the jni folder. After building the project, four folders will be created in the libs folder for each processor architecture, in which your native library will be located (the number of folders depends on the number of selected architectures).
In order to create a native project, you need to create a regular Android project and follow these steps:
- In the root of the project you need to create a jni folder in which to place the sources of the native code;
- Create an Android.mk file that will build the project;
- Create an Application.mk file that describes the assembly details. It is not a prerequisite, but allows you to flexibly customize the assembly;
- Create an ndk-build file that will launch the build process (also optional).
Android.mk
As mentioned above, this is a makefile for building a native project. Android.mk allows you to group your code into modules. Modules can be either static libraries (static library, only they will be copied to your project, in the libs folder), shared libraries (shared library), standalone executable file.Minimal configuration example:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= NDKBegining LOCAL_SRC_FILES:= ndkBegining.c include $(BUILD_SHARED_LIBRARY)
Let's look at it in detail:
- L OCAL_PATH:= $(call my-dir)– the call my-dir function returns the path of the folder in which the file is called;
- include $(CLEAR_VARS)– clears variables that were used before except LOCAL_PATH. This is necessary because all variables are global, because the build occurs in the context of one GNU Make;
- LOCAL_MODULE– name of the output module. In our example, the output library name is set to NDKBegining, but after the build, libraries named libNDKBegining will be created in the libs folder. Android adds the lib prefix to the name, but in the java code when connecting you must specify the name of the library without the prefix (that is, the names must match those installed in the makefiles);
- LOCAL_SRC_FILES– listing the source files from which the assembly should be created;
- include $(BUILD_SHARED_LIBRARY)– indicates the type of output module.
MY_SOURCE:= NDKBegining.c To access a variable: $(MY_SOURCE) Variables can also be concatenated, for example: LOCAL_SRC_FILES += $(MY_SOURCE)
Application.mk
This makefile defines several variables that will help make the build more flexible:- APP_OPTIM– an additional variable that is set to release or debug. Used for optimization when assembling modules. You can debug either release or debug, but debug provides more information for debugging;
- APP_BUILD_SCRIPT– points to an alternative path to Android.mk;
- APP_ABI– probably one of the most important variables. It indicates for which processor architecture the modules should be assembled. The default is armeabi, which corresponds to the ARMv5TE architecture. For example, to support ARMv7 you should use armeabi-v7a, for IA-32 - x86, for MIPS - mips, or if you need to support all architectures, then the value should be like this: APP_ABI:= armeabi armeabi-v7a x86 mips. If you are using ndk version 7 and higher, then you can not list all architectures, but set APP_ABI:= all.
- APP_PLATFORM– platform target;
- APP_STL– Android uses the libstdc++.so runtime library, which is stripped down and not all C++ functionality is available to the developer. However, the APP_STL variable allows extension support to be included in the build;
- NDK_TOOLCHAIN_VERSION– allows you to select the gcc compiler version (default 4.6);
NDK-BUILDS
Ndk-build is a wrapper for GNU Make. After version 4, flags for ndk-build were introduced:- clean– clears all generated binary files;
- NDK_DEBUG=1– generates debugging code;
- NDK_LOG=1– shows message log (used for debugging);
- NDK_HOST_32BIT=1– Android has tools to support 64-bit versions of utilities (for example NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64, etc.);
- NDK_APPLICATION_MK- the path to Application.mk is indicated.
By default, support is installed for the 64-bit version of the utilities, but you can force it to build only for 32-bit by setting the NDK_HOST_32BIT=1 flag. Google still recommends using 64-bit utilities to improve the performance of large programs.
How to assemble a project?
It used to be a pain. It was necessary to install the CDT plugin, download the cygwin or mingw compiler. Download Android NDK. Connect this all in Eclipse settings. And how unfortunate it all turned out to be that it didn’t work. The first time I came across the Android NDK, it took me 3 days to set it up (and the problem turned out to be that in cygwin I had to give permission 777 to the project folder).Now everything is much simpler with this. Follow this link. Download the Eclipse ADT Bundle, which already contains everything you need for assembly.
Calling native methods from Java code
In order to use native code from Java, you first need to define native methods in the Java class. For example:native String nativeGetStringFromFile(String path) throws IOException; native void nativeWriteByteArrayToFile(String path, byte b) throws IOException;
The method should be preceded by the reserved word “native”. This way the compiler knows that this is the entry point to JNI. We need to implement these methods in a C/C++ file. Google also recommends starting to name methods with the word nativeX, where X is the real name of the method. But before implementing these methods manually, you should generate a header file. This can be done manually, but you can use the javah utility that is in the jdk. But let's go further and will not use it through the console, but will do it using standard Eclipse tools.
Now you can launch. The bin/classes directory will contain your header files.
Next, we copy these files to the jni directory of our native project. Call the project’s context menu and select Android Tools – Add Native Library. This will allow us to use jni.h functions. Then you can create a cpp file (sometimes Eclipse creates it by default) and write the bodies of methods that are already described in the header file.
I did not add a code example to the article so as not to lengthen it. You can view/download an example from github.
Tags:
- android programming
- android ndk
- jni