- Android NDK Advanced Tutorial
- Using 3rd party C++ libraries with Prebuilts and standalone toolchain.
- Intro
- Prerequisites
- Building Crypto++ with NDK toolchain
- Preparing NDK toolchain
- Supplying general compiler flags
- Choosing STL version
- Patch for Crypto++ sources
- Creating android app project and writing Java sources
- Writing C++ sources
- Wiring the JNI stuff
- Global Android.mk
- Application.mk
- Wiring modules
- Checkpoint
- Externalizing and importing modules
- Supporting multiple STL versions
- Введение в Android NDK
- Что такое Android NDK?
- Для чего используют NDK?
- Что такое JNI?
- Преимущества JNI
- Как устроен JNI
- Локальные и глобальные ссылки
- Обработка ошибок
- Примитивные типы JNI
- Ссылочные типы JNI
- Модифицированный UTF-8
- Функции JNI
- Пример использования функций JNI
- Потоки
- Первые шаги
- Android.mk
- Application.mk
- NDK-BUILDS
- Как собрать проект?
- Вызов нативных методов из Java кода
Android NDK Advanced Tutorial
Using 3rd party C++ libraries with Prebuilts and standalone toolchain.
Intro
Prerequisites
- Android SDK r21.x or higher and basic understanding of Android platform and Android app structure
- NDK r8e or higher and basic understanding of NDK and JNI (here is a good tutorial to learn them)
- Eclipse IDE with ADT installed is recommended, but it is also possible to use just Apache Ant
- Crypto++ 5.6.1 sources
- GNU make and basic understanding of Makefiles
- Basic knowledge of shell commands and concepts
- This tutorial was created while working on Linux on x86_64, but it should be easy to «port» it to other platforms on which Android SDK is available
Building Crypto++ with NDK toolchain
Preparing NDK toolchain
First we need to prepare a toolchain for the right architecture (mips. arm, x86) and Android version. It is probably the best idea to choose the lowest version of Andy you want to support (ie the same version you put as minSdkVersion in your AndroidManifest.xml file). If you intend to support multiple devices with different archs then you will have to build Cryptopp separately for each arch. In this tutorial we will build for armv5te (referred to as ‘armeabi’ in Andy docs). Code for this arch can be also run on armv7-a CPUs (referred to as ‘armeabi-v7a’) so it covers most of Android devices.
To the point: set and export NDK shell variable to point to the location of your NDK installation and then in your working dir issue the following command: Replace android-8 with the desired version for your app. Such prepared toolchain targets arm arch (both armv5te and armv7-a). If you want to prepare toolchain for another arch add —arch=mips or —arch=x86 accordingly.
Now add ndk-toolchain folder to your path and export CXX shell var to g++ from ./ndk-toolchain/bin folder (it will be named something-something-g++ ).
Supplying general compiler flags
Choosing STL version
By default NDK toolchain will link your C++ shared libs against a static version of GNU STL lib. However if you are using several shared libs it is not acceptable to link against the static version of STL as each of your shared lib will have its own copy of STL. This will result in several copies of global vars defined in STL and may lead to memory leak or corruption. To use the shared version of STL you just need to add -lgnustl_shared linker option: Also neither LDFLAGS nor LDLIBS var is originally used in the command that builds shared library in the GNUmakefile file so you need to append it at its end like this:
GNU STL is distributed under GPLv3 license which is not acceptable for some people. NDK provides also STLport and it is possible to use it instead, but it is a bit more complicated as standalone toolchain does not include it. In order to make it work you need to point the compiler to the right header files and linker to the location of the STLport lib: in the GNUmakefile define the LDFLAGS , LDLIBS and CXXFLAGS vars like this: -nostdinc++ tells the compiler not to include stdlib++ header files which belong to gnustl in our case. The -I flag points the compiler to the header files for STLport.
-nodefaultlibs tells the linker not to link against standard C and C++ libs that may belog to gnustl: it will link only against the platform minimal C runtime (crt* stuff) and the libraries specified by -l flags that we specify in LDLIBS : STLport, libc and the low level compiler stuff. Finally -L flag points the linker to the location of these libs. If you are building for different arch than armeabi you need to change the path in this flag accordingly.
The last problem is that Crypto++ seems to rely on implicit inclusion of header file containing definition of fd_set . This is not the case when using STLport so you need to add the following preprocessor command somewhere in wait.h file:
Patch for Crypto++ sources
Below is the patch for Crypto++ sources with all the changes described above.
DISCLAIMER:
Described changes to the Crypto++’s GNUmakefile are UGLY HACKS just to make it work ASAP and are DEFINITELY NOT A GOOD EXAMPLE HOW TO WRITE MAKEFILES in general. For example you should generally introduce a notion of a target platform rather than putting options for it into the host platform.
Creating android app project and writing Java sources
Let’s create a standard android app project and our main activity will just display a return value from a native method. Create a class named Native that will be responsible for loading native libs and will contain the native method. Libs have to be loaded in the order of their dependencies, ie if liba.so depends on libb.so you first need to load libb.so and then liba.so. Thus you need to start from loading the STL lib you decided to use, then load Crypto++, and finally our small lib that uses it (let’s call it libcrypt_user.so )
Writing C++ sources
Wiring the JNI stuff
Global Android.mk
Each shared lib will have its own subfolder and Android.mk file so the main Android.mk file from the jni folder just needs to include them:
Application.mk
We also need to specify some global stuff in Application.mk file in the jni folder: APP_ABI specifies the arch(s) which our libs should be built for. If you want to build apk for multiple archs enumerate them all separated by space. For example APP_ABI := armeabi armeabi-v7a x86 mips
APP_CPPFLAGS specifies CXXFLAGS to be used by NDK when building your shared libs. By default NDK compiles CPP sources without exception nor RTTI support. As we are using STL version that uses them we need to turn them on here. (Contrary to this, the standalone toolchain has exception and RTTI support turned on by default)
APP_STL specifies which version of STL to include in your apk.
Wiring modules
Now create a subfolder cryptopp in the jni folder. Inside it create subfolders named after each arch you want to build your shared libs for. Place your prebuilt libcryptopp.so files in them accordingly. Next to them (ie inside cryptopp folder) create a subfolder named include and place there all header files that you may need to include in the sources of your libs that use this prebuilt lib. In case of Crypto++ just copy all header files (*.h) from its source folder.
Finally create Android.mk file inside the cryptopp folder: The last line says that this module is a prebuilt shared library and the LOCAL_SRC_FILES points to the location of its binary. The TARGET_ARCH_ABI var will be substituted accordingly during the build process for the given arch.
LOCAL_EXPORT_C_INCLUDES specifies the location of header files for other libs that use this one.
The last step is to create subfolder named crypt_user , placing there the source file of our small cpp lib ( crypt_user.cpp ) we created previously and creating Android.mk file for it: LOCAL_SHARED_LIBRARIES specifies other shared lib modules this module depends on so NDK knows where to find include files and what to link against.
Checkpoint
Externalizing and importing modules
If you intend to use your prebuilt lib (or any other module) in several project it makes sense to define it as an external module and reuse it in any project that needs it.
Define env var NDK_MODULE_PATH to contain a path somewhere outside of your app project tree: you will store your external modules there. Next move cryptopp subfolder of jni folder there. In the main Android.mk file from jni folder append the following line at the end: The second argument cryptopp must correspond to the subfolder name of your external module in NDK_MODULE_PATH folder. Inside its Android.mk file you can actually define several modules that you can later use as LOCAL_SHARED_LIBRARIES or LOCAL_STATIC_LIBRARIES in your projects. None of them has to be named after the subfolder name.
Since Crypto++ relies on RTTI and exception support it’s a good idea to export appropriate flags to all users of this module: add the following line before PREBUILT_SHARED_LIBRARY in cryptopp module’s Android.mk file: As our only ‘local’ module crypt_user does not depend on these flags directly anymore, you can safely remove APPAPP_CPPFLAGS line from your Application.mk file. crypt_user needs to be compiled with these flags as well to be properly linked with cryptopp , but they will be imported for him thanks to the declared dependency in LOCAL_SHARED_LIBRARIES in its Android.mk file.
Supporting multiple STL versions
Different projects may need to use different STL versions. That’s why when you provide prebuilt lib as an external module it’s a good idea to provide binaries for each STL version that may be needed.
Inside your cryptopp folder create subfolders named after STL versions, for example stlport_shared and gnustl_shared and inside them create subfolders for supported archs and place prebuilt binaries there. Now change the definition of LOCAL_SRC_FILES in Android.mk file to look like this: Sometimes even a single app may need to have different builts with different STL versions. There’s a small problem here that in such case you don’t know which lib to load in your java code. The solution is simply to try to load all of them and catch the error when the wrong ones fail. Our Native class would look like this: Now it will all work just by changing APP_STL var in the Application.mk file.
In case of problems you can refer to multiple-stl branch of the reference app.
Источник
Введение в Android NDK
Для разработки приложений под ОС Android, Google предоставляет два пакета разработки: SDK и NDK. Про SDK существует много статей, книжек, а так же хорошие guidelines от Google. Но про NDK даже сам Google мало что пишет. А из стоящих книг я бы выделил только одну, Cinar O. — Pro Android C++ with the NDK – 2012.
Эта статья ориентирована на тех, кто ещё не знаком (или мало знаком) с Android NDK и хотел бы укрепить свои знания. Внимание я уделю JNI, так как мне кажется начинать нужно именно с этого интерфейса. Так же, в конце рассмотрим небольшой пример с двумя функциями записи и чтения файла. Кто не любит много текста, тот может посмотреть видео версию.
Что такое Android NDK?
Android NDK (native development kit) – это набор инструментов, которые позволяют реализовать часть вашего приложения используя такие языки как С/С++.
Для чего используют NDK?
Google рекомендует прибегать к использованию NDK только в редчайших случаях. Зачастую это такие случаи:
- Нужно увеличить производительность (например, сортировка большого объема данных);
- Использовать стороннюю библиотеку. Например, много уже чего написано на С/С++ языках и нужно просто заиспользовать существующий материал. Пример таких библиотек, как, Ffmpeg, OpenCV;
- Программирование на низком уровне (например, всё что выходит за рамки Dalvik);
Что такое JNI?
Java Native Interface – стандартный механизм для запуска кода, под управлением виртуальной машины Java, который написан на языках С/С++ или Assembler, и скомпонован в виде динамических библиотек, позволяет не использовать статическое связывание. Это даёт возможность вызова функции С/С++ из программы на Java, и наоборот.
Преимущества JNI
Основное преимущество перед аналогами (Netscape Java Runtime Interface или Microsoft’s Raw Native Interface and COM/Java Interface) является то что JNI изначально разрабатывался для обеспечения двоичной совместимости, для совместимости приложений, написанных на JNI, для любых виртуальных машин Java на конкретной платформе (когда я говорю о JNI, то я не привязываюсь к Dalvik машине, потому как JNI был написан Oracle для JVM который подходит для всех Java виртуальных машин). Поэтому скомпилированный код на С/С++ будет выполнятся в не зависимости от платформы. Более ранние версии не позволяли реализовывать двоичную совместимость.
Двоичная совместимость или же бинарная совместимость – вид совместимости программ, позволяющий программе работать в различных средах без изменения её исполняемых файлов.
Как устроен JNI
JNI таблица, организована как таблица виртуальных функций в С++. VM может работать с несколькими такими таблицами. Например, одна будет для отладки, вторая для использования. Указатель на JNI интерфейс действителен только в текущем потоке. Это значит, что указатель не может гулять с одного потока в другой. Но нативные методы могут быть вызваны из разных потоков. Пример:
- *env – указатель на интерфейс;
- оbj – ссылка на объект в котором описан нативный метод;
- i and s – передаваемые аргументы;
Примитивные типы копируются между VM и нативным кодом, а объекты передаются по ссылке. VM обязана отслеживать все ссылки которые передаются в нативный код. Все переданные ссылки в нативный код не могут быть освобождены GC. Но нативный код в свою очередь должен информировать VM о том что ему больше не нужны ссылки на переданные объекты.
Локальные и глобальные ссылки
JNI делит ссылки на три типа: локальные, глобальные и слабые глобальные ссылки. Локальные действительны пока не завершиться метод. Все Java объекты которые возвращает функции JNI являются локальными. Программист должен надеется на то что VM сама подчистит все локальные ссылки. Локальные ссылки доступны лишь в том потоке в котором были созданы. Однако если есть необходимость то их можно освобождать сразу методом JNI интерфейса DeleteLocalRef:
Глобальные ссылки остаются пока они явно не будут освобождены. Что бы зарегистрировать глобальную ссылку следует вызвать метод NewGlobalRef. Если же глобальная ссылка уже не нужна, то её можно удалить методом DeleteGlobalRef:
Обработка ошибок
JNI не проверяет ошибки такие как NullPointerException, IllegalArgumentException. Причины:
- снижение производительности;
- в большинстве функций C библиотек очень и очень трудно защитится от ошибок.
JNI позволяет использовать Java Exception. Большинство JNI функций возвращают код ошибок а не сам Exception, и поэтому приходится обрабатывать сам код, а в Java уже выбрасывать Exception. В JNI следует проверять код ошибки вызываемых функций и после них следует вызвать ExceptionOccurred(), которая в свою очередь возвращает объект ошибки:
Например, некоторые функции JNI доступа к массивам не возвращают ошибки, но могут вызвать исключения ArrayIndexOutOfBoundsException или ArrayStoreException.
Примитивные типы JNI
В JNI существуют свои примитивные и ссылочные типы данных.
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
Модифицированный UTF-8
JNI использует модифицированную кодировку UTF-8 для представления строк. Java в свою очередь использует UTF-16. UTF-8 в основном используется в С, потому что он кодирует \u0000 в 0xc0, вместо привычной 0x00. Изменённые строки кодируются так, что последовательность символов, которые содержат только ненулевой ASCII символы могут быть представлены с использованием только одного байта.
Функции JNI
Интерфейс JNI содержит в себе не только собственный набор данных, но и свои собственные функции. На их рассмотрение уйдёт много времени, так как их не один десяток. Ознакомится с ними вы сможете в официальной документации.
Пример использования функций JNI
Небольшой пример, что бы вы усвоили пройденный материал:
Разберём построчно:
- JavaVM – предоставляет интерфейс для вызова функций, которые позволяют создавать и уничтожать JavaVM;
- JNIEnv – обеспечивает большинство функций JNI;
- JavaVMInitArgs – аргументы для JavaVM;
- JavaVMOption – опции для JavaVM;
Метод JNI_CreateJavaVM() инициализирует JavaVM и возвращает на неё указатель. Метод JNI_DestroyJavaVM() выгружает созданную JavaVM.
Потоки
Всеми потоками в Linux управляет ядро, но они могут быть прикреплены к JavaVM функциями AttachCurrentThread и AttachCurrentThreadAsDaemon. Пока поток не присоединён он не имеет доступа к JNIEnv. Важно, Android не приостанавливает потоки которые были созданы JNI, даже если срабатывает GC. Но перед тем как поток завершиться он должен вызвать метод DetachCurrentThread что бы отсоединиться от JavaVM.
Первые шаги
Структура проекта у вас должна выглядеть следующим образом:
Как мы видим из рисунка 3, весь нативный код находится в папке jni. После сборки проекта, в папке libs создастся четыре папки под каждую архитектуру процессора, в которой будет лежать ваша нативная библиотека (количество папок зависит от количество выбранных архитектур).
Для того, чтобы создать нативный проект, нужно создать обычный Android проект и проделать следующие шаги:
- В корне проекта нужно создать папку jni, в которую поместить исходники нативного кода;
- Создать файл Android.mk, который будет собирать проект;
- Создать файл Application.mk, в котором описываются детали сборки. Он не является обязательным условием, но позволяет гибко настроить сборку;
- Создать файл ndk-build, который будет запускать процесс сборки (тоже не является обязательным).
Android.mk
Как упоминалось уже выше, это make файл для сборки нативного проекта. Android.mk позволяет группировать ваш код в модули. Модули могут быть как статические библиотеки (static library, только они будут скопированные в ваш проект, в папку libs), разделяемые библиотеки (shared library), автономный исполняемый файл (standalone executable).
Пример минимальной конфигурации:
Рассмотрим детально:
- LOCAL_PATH := $(call my-dir) – функция call my-dir возвращает путь папки в которой вызывается файл;
- include $(CLEAR_VARS) – очищает переменные которые использовались до этого кроме LOCAL_PATH. Это необходимо так как все переменные являются глобальными, потому что сборка происходит в контексте одного GNU Make;
- LOCAL_MODULE – имя выходного модуля. В нашем примере имя выходной библиотеки установлено как NDKBegining, но после сборки в папке libs создадутся библиотеки с именами libNDKBegining. Android добавляет к названию префикс lib, но в java коде при подключении вы должны указывать название библиотеки без префикса (то есть названия должны совпадать с установленными в make файлах);
- LOCAL_SRC_FILES – перечисление исходных файлов из которых следует создать сборку;
- include $(BUILD_SHARED_LIBRARY) – указывает тип выходного модуля.
В Android.mk можно определить свои переменные, но они не должны иметь такой синтаксис: LOCAL_, PRIVATE_, NDK_, APP_, my-dir. Google, рекомендует называть свои переменные, как MY_. Например:
Application.mk
NDK-BUILDS
Ndk-build из себя представляет обёртку GNU Make. После 4-й версии ввели флаги для ndk-build:
- clean – очищает все сгенеренные бинарные файлы;
- NDK_DEBUG=1 – генерирует отладочный код;
- NDK_LOG=1 – показывает лог сообщений (используется для отладки);
- NDK_HOST_32BIT=1 – Android имеет средства для поддержки 64-х битных версий утилит (например NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64 и т.д.);
- NDK_APPLICATION_MK — указывается путь к Application.mk.
В 5-й версии NDK был введён такой флаг как NDK_DEBUG. Если он установлен в 1 то создаётся отладочная версия. Если флаг не установлен то ndk-build по умолчанию проверяет стоит ли атрибут android:debuggable=«true» в AndroidManifest.xml. Если вы используете ndk выше 8-й версии, то Google не рекомендует использовать атрибут android:debuggable в AndroidManifest.xml (потому что если вы используете «ant debug» или строите отладочную версию с помощью ADT плагина то они автоматически добавляют флаг NDK_DEBUG=1).
По умолчанию устанавливается поддержка 64-х разрядной версии утилит, но вы можете принудительно собрать только для 32-х установив флаг NDK_HOST_32BIT=1. Google, рекомендует всё же использовать 64-х разрядность утилит для повышения производительности больших программ.
Как собрать проект?
Раньше это было мучением. Нужно было установить CDT плагин, скачать компилятор cygwin или mingw. Скачать Android NDK. Подключить это всё в настройках Eclipse. И как на зло это всё оказывалось не рабочим. Я первый раз когда столкнулся с Android NDK, то настраивал это всё 3 дня (а проблема оказалось в том что в cygwin нужно было дать разрешение 777 на папку проекта).
Сейчас с этим всё намного проще. Идёте по этой ссылке. Качаете Eclipse ADT Bundle в котором уже есть всё то что необходимо для сборки.
Вызов нативных методов из Java кода
Для того что бы использовать нативный код из Java вам сперва следует определить нативные методы в Java классе. Например:
Перед методом следует поставить зарезервированное слово «native». Таким образом компилятор знает, что это точка входа в JNI. Эти методы нам нужно реализовать в С/С++ файле. Так же Google рекомендует начинать именовать методы со слова nativeХ, где Х – реальное название метода. Но перед тем как реализовывать эти методы вручную, следует сгенерировать header файл. Это можно сделать вручную, но можно использовать утилиту javah, которая находится в jdk. Но пойдём дальше и не будет использовать её через консоль, а будем это делать при помощи стандартных средств Eclipse.
Теперь можете запускать. В директории bin/classes будут лежать ваши header файлы.
Далее копируем эти файлы в jni директорию нашего нативного проекта. Вызываем контекстное меню проекта и выбираем пункт Android Tools – Add Native Library. Это позволит нам использовать jni.h функции. Дальше вы уже можете создавать cpp файл (иногда Eclipse его создаёт по умолчанию) и писать тела методов, которые уже описаны в header файле.
Пример кода я не стал добавлять в статью, чтобы не растягивать её. Пример вы можете посмотреть/скачать с github.
Источник