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
Windowsandroid-ndk-r4b-windows.zip45792835 bytes
Mac OS X (intel)android-ndk-r4b-darwin-x86.zip50586041 bytes
Linux 32/64-bit (x86)android-ndk-r4b-linux-x86.zip49464776 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;
Primitive types are copied between the VM and native code, and objects are passed by reference. The VM is required to track all links that are passed into the native code. All references passed to native code cannot be freed by the GC. But the native code, in turn, must inform the VM that it no longer needs references to the transferred objects.

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.
JNI allows you to use Java Exception. Most JNI functions return an error code and not the Exception itself, and therefore you have to process the code itself, and in Java you already throw an Exception. In JNI, you should check the error code of the called functions and then call ExceptionOccurred(), which in turn returns an error object:

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 //... JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption* options = new JavaVMOption; options.optionString = "-Djava.class.path=/usr/lib/java"; vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = 1; vm_args.options = options; vm_args.ignoreUnrecognized = false; JNI_CreateJavaVM(&jvm, &env, &vm_args); delete options; jclass cls = env->FindClass("Main"); jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V"); env->CallStaticVoidMethod(cls, mid, 100); jvm->DestroyJavaVM();
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;
The JNI_CreateJavaVM() method initializes the JavaVM and returns a pointer to it. The JNI_DestroyJavaVM() method unloads the created 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.
You can define your own variables in Android.mk, but they should not have the following syntax: LOCAL_, PRIVATE_, NDK_, APP_, my-dir. Google recommends naming your variables like MY_. For example:
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.
In version 5 of the NDK, a flag was introduced called NDK_DEBUG. If it is set to 1, a debug version is created. If the flag is not set, then ndk-build by default checks whether the android:debuggable="true" attribute is set in AndroidManifest.xml. If you are using ndk higher than version 8, then Google does not recommend using the android:debuggable attribute in AndroidManifest.xml (because if you use "ant debug" or build a debug version using the ADT plugin, then they automatically add the NDK_DEBUG=1 flag) .

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;
Primitive types are copied between the VM and native code, and objects are passed by reference. The VM is required to track all links that are passed into the native code. All references passed to native code cannot be freed by the GC. But the native code, in turn, must inform the VM that it no longer needs references to the transferred objects.

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.
JNI allows you to use Java Exception. Most JNI functions return an error code and not the Exception itself, and therefore you have to process the code itself, and in Java you already throw an Exception. In JNI, you should check the error code of the called functions and then call ExceptionOccurred(), which in turn returns an error object:

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 //... JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption* options = new JavaVMOption; options.optionString = "-Djava.class.path=/usr/lib/java"; vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = 1; vm_args.options = options; vm_args.ignoreUnrecognized = false; JNI_CreateJavaVM(&jvm, &env, &vm_args); delete options; jclass cls = env->FindClass("Main"); jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V"); env->CallStaticVoidMethod(cls, mid, 100); jvm->DestroyJavaVM();
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;
The JNI_CreateJavaVM() method initializes the JavaVM and returns a pointer to it. The JNI_DestroyJavaVM() method unloads the created 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.
You can define your own variables in Android.mk, but they should not have the following syntax: LOCAL_, PRIVATE_, NDK_, APP_, my-dir. Google recommends naming your variables like MY_. For example:
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.
In version 5 of the NDK, a flag was introduced called NDK_DEBUG. If it is set to 1, a debug version is created. If the flag is not set, then ndk-build by default checks whether the android:debuggable="true" attribute is set in AndroidManifest.xml. If you are using ndk higher than version 8, then Google does not recommend using the android:debuggable attribute in AndroidManifest.xml (because if you use "ant debug" or build a debug version using the ADT plugin, then they automatically add the NDK_DEBUG=1 flag) .

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
Add tags






2024 gtavrl.ru.