Исользование нативного кода, написанного на C или С++ — это тема которая затрагивается большинством разработчиков в лучшем случае поверхностно. И чаще всего это оправданно, так как использование нативного кода на порядок усложняет процесс разработки приложения и случаи, когда использование Android NDK действительно оправданно можно пересчетать на пальцах одной руки опытного токаря. Из этой статье вы сможете получить действительно базовый навыки работы с NDK, включая использование STL_PORT, полезной утилиты javah и пару моих мыслей о том, когда применение NDK себя оправдывает.

NDK or not to NDK?

Статьи о NDK принято начинать с описания случаев, когда использование NDK уместно. Не буду отходить от этой традиции с смысле последовательности, но отойду от ее содежательной части.

Часто рекомендуют использовать NDK для «сложных вычислительных процессов». Признаться честно, я не уверен, что кто-то занимается вычислением числа π при помощи своего Android-девайса. Поэтому я перечислю те случаи, которые, на мой взгляд, явлются «NDKable» в порядке возрастающей важности:

  • Работа с OpenGL ES
  • Использование кросс-платформенных игровых движков, например Cocos2Dx
  • Использование уже написанного на C/C++ кода (а его ох как дофига написано!). Часто, это работа с мультимедиа, например FFMPEG, libpng или наукоемкие вещи типа openCV

Таким образом, работа с NDK чаще всего представляем из себя процесс (часто — мучительный) сборки некой библиотеки под ARM и написиние оберток (wrappers) на нативные методы. В тоже время, сейчас есть возможность ваять приложение практически без использования Java, используя NativeActivity (API 9 и выше).

Если говорить о возможностях NDK, то они обширны. Мы можем вызывать Java-методы и обращаться к объектам в нативном коде, так же мы можем вызывать нативные методы из Java-кода. Нативный код можно дебажить, можно настроить Eclipse так, что работать с ним будет так же просто, как работать с Java-кодом. Но эти вещи тянут на отдельную статью и сегодня касаться их мы не будем.

Зато мы будем:

  • Вызывать нативные меоды из Java
  • Использовать в нативном коде C++ и STL_PORT
  • Писать make-файлы и использовать полезную утилиту javah

С чего начинается NDK

Забегая вперед, скажу сразу: забудьте о Windows, если хотите работать с NDK. Я не явлюясь противником технологий Microsoft (как и любых технологий в принципе), но сборка проекта под виндой с использованием Cygwin — это не самое веселое занятие, тем более наукой не зафиксированно ни одного случая успешной сборки того же FFMPEG в среде Windows (если вам известен такой случай, буду рад узнать подробности :) ).

Так что первый шаг — это поставить какой-нибудь UNIX.

Допустим, первый шаг уже выполнен, второй — это скачать сам NDK (естественно, вы уже должны располагать стандартным набором Android-инструментов: JDK, Android SDK, ADT, Eclipse).

Следующий шаг — это добавление папки с NDK в системные пути, на Mac OS X это делаюется добавлением следующих строчек в файл .profile:

export NDKROOT="/Users/yourusername/your/path/to/ndk/android-ndk-r8"

export PATH=$PATH:$NDKROOT

Для проверки запустите (или перезапустите терминал если он был запущен) и попробуйте ввести «ndk» после чего нажать клавишу tab, если при этой вы наблюдаете примерно следующий вывод в консоли:

mymacname:~ dmitrykunin$ ndk-

ndk-build ndk-gdb ndk-stack

то вы все сделали правильно :)

С установкой мы завершили, теперь я рекомендую вам зайти в папочку с NDK и ознакомится с файлом documentation.html, после чего можно глянуть папку samples с поучительными примерами кода.

Дорога из Java в натив и обратно. Некоторые правила движения по этой дороге

Следующий 2 раздела опишут правила описания нативных методов как со стороны Java, так и со стороны C/C++ кода.

Описание нативных методов в Java

В учебном проекте мы определим 3 нативных метода, заниматься они будут большей часть бесполезной но показательной работой, а именно:

public class NativeUtils {

static {
System.loadLibrary("tinymath");
}
/**
* Статический нативный метод суммирования
*
* @param arg0 первое слагаемое
* @param arg1 второе слагаемое
* @return сумма
*/
public static native double nativeSum(double arg0, double arg1);
/**
* Статический экземплярный метод проверки числа на простоту
*
* @param candidate число, которое нужно проверить на простоту
* @return true если число простое, иначе false
*/
public native boolean nativeIsPrime(int candidate);
/**
* Получение информации о возможностях CPU
*
*
* @return строку, с поддерживаемы системой технологиями
*/
public native String getCpuInfo();
}

Эти методы примечательны следующим:

  • Отсутсутвует реализация (тело метода)
  • При объявлении метода используется модификатор native

Жизненно важной часть является вот эта часть кода:

static {

System.loadLibrary("tinymath");
}

Этот код выполнит загрузку модуля «tinymath» — нативной библиотеки, в которой и будут реализованы методы. Название этого модуля задается в файле Android.mk, но об этом позже.

Описание нативных методов в C/C++

Самая интересная и самая жуткая (на первый взгляд!) часть работы.

Для того чтобы связать Java-методы с нативными создадим в папке проекта папку jni, в ней создайте следующие файлы:

  • Android.mk — основные параметры для сборки приложения с нативным кодом
  • Application.mk — дополнительные параметры
  • by_idev_jni_NativeUtils.h — хэдеры нативных функций (методов)
  • tinymath.cpp — реализация нативных методов на C++

Мы не будем углубляться в суть первых двух, я приведу лишь их содержимое с небольшими комментариями, но очень рекомендую почитать документацию к NDK о которой говорил выше.

И так, файл by_idev_jni_NativeUtils.h будет выглядеть так:

/* DO NOT EDIT THIS FILE - it is machine generated */

#include
/* Header for class by_idev_jni_NativeUtils */
#ifndef _Included_by_idev_jni_NativeUtils
#define _Included_by_idev_jni_NativeUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: by_idev_jni_NativeUtils
* Method: nativeSum
* Signature: (DD)D
*/
JNIEXPORT jdouble JNICALL Java_by_idev_jni_NativeUtils_nativeSum
(JNIEnv *, jclass, jdouble, jdouble);
/*
* Class: by_idev_jni_NativeUtils
* Method: nativeIsPrime
* Signature: (I)Z
*/
JNIEXPORT jboolean JNICALL Java_by_idev_jni_NativeUtils_nativeIsPrime
(JNIEnv *, jobject, jint);
/*
* Class: by_idev_jni_NativeUtils
* Method: getCpuInfo
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_by_idev_jni_NativeUtils_getCpuInfo
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif



Некисло… особенно если писать руками. Но мы рыками писать не будем, вы ведь заметили комментарий /* DO NOT EDIT THIS FILE - it is machine generated */? Так вот генерирует этот файл утилита javah (входит в стандартную поставку JDK), почитайте о ней вот тут, которой нужно скормить .class-файл. Вызов ее будет выглядеть вот так:

cd ПУТЬ_К_ПРОЕКТУ/bin/classes

javah -d КУДА_ЗАПИСАТЬ_СГЕНЕРИРОВАННЫЙ_ФАЙЛ ПОЛНОЕ_ИМЯ_КЛАССА

А вот конкретный пример:

cd $HOME/workspace/SimpleJNI/bin/classes

javah -d $HOME/workspace/SimpleJNI/jni by.idev.jni.NativeUtils

Получаем готовый файл и радуемся, что есть такая полезная утилита как javah.

Теперь давайте разберемся в содержимом этого файла.

#ifndef _Included_by_idev_jni_NativeUtils

#define _Included_by_idev_jni_NativeUtils
#ifdef __cplusplus
extern "C" {
#endif
//методы методы методы
#ifdef __cplusplus
}
#endif
#endif

Этот самый extern "C" нужем потому, что компилятор C++ любит менять имена объявленных функций. После его вмешательства приложение не будет, поэтому мы запрещаем компилятору заниматься самодеятельностью с именами функций при помощи extern "C"

JNIEXPORT jdouble JNICALL Java_by_idev_jni_NativeUtils_nativeSum

(JNIEnv *, jclass, jdouble, jdouble);
..
JNIEXPORT jboolean JNICALL Java_by_idev_jni_NativeUtils_nativeIsPrime
(JNIEnv *, jobject, jint);

JNIEXPORT — необходимый для JNI модификатор. Типы данных с префиксом «j»: jdouble, jobject, jstring etc — это «отражения» объектов и типов Java в C/C++.

Дам подсказку, если вы откроете файл jni.h, то узнаете много интересного, в частности об этих самых типах:

/*

* Primitive types that match up with Java equivalents.
*/
#ifdef HAVE_INTTYPES_H
# include /* C99 */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#else
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#endif

Из описания следует, что работать с примитивными типами Java можно также как с примитивными типами C/C++. С объектами и массивами другая история и для нее нужна другая статья :)

Обратим внимание, что в каждой функции в качесте аргумента имеется JNIEnv* — интерфейс для работы с Java, при помощи него можно вызывать Java-методы, создавать Java-объекты и делать еще много всяких полезных Java-штук. Второй обязательный параметр — jobject или jclacc — в зависимости от того, является ли метод статическим. Если метод статический, то аргумент будет типа jclass (ссылка на класс объекта, в котором объявлен метод), если не статический — jobject — ссылка на объект, у которого был вызван метод.

Перейдем к реализации и файлу tinymath.cpp:

#include 

#include <android/log.h>
#include
#include
#include
#include
using namespace std;
#define LOG_TAG "SimpleJni"
#define LOGI(x...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,x)
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jdouble JNICALL Java_by_idev_jni_NativeUtils_nativeSum(JNIEnv *env,
jclass clazz, jdouble arg0, jdouble arg1) {
LOGI("JNI: nativeSum called");
return arg0 + arg1;
}
JNIEXPORT jboolean JNICALL Java_by_idev_jni_NativeUtils_nativeIsPrime(
JNIEnv *env, jobject obj, jint candidate) {
LOGI("JNI: nativeIsPrime called");
if (candidate < 2)
return JNI_FALSE;
double srt = sqrt((float) candidate);
int lowerSrt = (int) floor(srt);
char *log = new char[64];
sprintf(log, "JNI: sqrt=%.4f floor=%d", srt, lowerSrt);
LOGI(log);
delete[] log;
for (int i = 2; i NewStringUTF("Not ARM");
}
cpu_features = android_getCpuFeatures();
if (cpu_features & ANDROID_CPU_ARM_FEATURE_ARMv7) {
a.append(" ARMc7 n");
LOGI("Arm7");
}
if (cpu_features & ANDROID_CPU_ARM_FEATURE_VFPv3) {
a.append(" ARM w VFPv3 support n");
LOGI("VFP3");
}
if ((cpu_features & ANDROID_CPU_ARM_FEATURE_NEON)) {
a.append(" ARM w NEON support n");
LOGI("NEON");
}
if (cpu_features & ANDROID_CPU_ARM_FEATURE_LDREX_STREX) {
a.append(" LDREX_STREX ");
LOGI("LDREX_STREX");
}
if (a == "") {
return env->NewStringUTF("Unknown");
}
return env->NewStringUTF(a.c_str());
}
}

Если вы знакомы и с C, и с С++, то наверное заметили, что код написан с испозованием С++ специфичных вещей (string — часть STL). Обычно примеры работы с NDK приводят на C-коде, может быть из соображения простоты. Но как вы увидите далее, для того чтобы использовать C++ с элементами STL (к сожалению он поддерживается не полностью) не нужно прикладывать много усилий.

Давайте посмотрим на Android.mk:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
# имя модуля, который будет вызываться в Java при помощи System.loadLibrary()
LOCAL_MODULE := tinymath
# Add all source file names to be included in lib separated by a whitespace
LOCAL_SRC_FILES := tinymath.cpp
# статические библиотеки, уже скомпиленные за нас
LOCAL_STATIC_LIBRARIES := cpufeatures
# добавим библиотеку для логирования
LOCAL_LDLIBS := -llog
LOCAL_CFLAGS := -g
include $(BUILD_SHARED_LIBRARY)
#
$(call import-module,cpufeatures)

И на Application.mk:

# без этой строчки никого STL (включая string) мы не дождемся

APP_STL := stlport_static

Надеюсь комментариев к коду будет достаточно, если у вас возникнут вопросы, вы запросто можете задать их на форуме или в комментариях к статье.

Вот почти и все, осталось собрать приложение.

Сборка нативного кода при помощи ndk-build

В начале статьи мы добавляли директорую с NDK в системные пути как раз для ускорения доступа к ndk-build.

Для сборки приложения в терменали выполните слеюдующие инструкции:

cd ПУТЬ_К_ПРОЕКТУ

ndk-build

В ответ должны получить примерно следующее:

Gdbserver      : [arm-linux-androideabi-4.4.3] libs/armeabi/gdbserver

Gdbsetup : libs/armeabi/gdb.setup
Compile++ thumb : tinymath libs/armeabi/libtinymath.so

Если так и есть — то все в порядке, вас можно поздравить с первым NDK :) в папке проекта должны появится директории obj и libs с набором .a (статические библиотеки) и .so (динамические библиотеки) файлов.

Живой пример

Вы можете скачать исходники приложения SimpleJNI.

Запустив его вы сможете лицезреть это:

Радость нативного программирования

На этом все. Вернее конечно не все. Очень многое осталось за кадром (в том числе очень много вкусного), но я обязательно постараюсь освятить еще несколько темных углов JNI и NDK в будущих статьях :)

Полезные и очень полезные ссылки

На пути рыцаря-NDK возникнет немало трудностей, с которыми проще будет справиться, если почитать инфу, доступную по ссылкам ниже :)

  • JNI 1.5 guide http://docs.oracle.com/javase/1.5.0/docs/guide/jni/
  • JNI Design http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/intro.html
  • JNI Tips for Android http://developer.android.com/guide/practices/design/jni.html
  • Референс по C и С++ http://www.cplusplus.com/reference/

Вакантное место: ведущий Android разработчик в компании Softeq Development

Что будете делать Вы?

  • разрабатывать приложения для мобильных Android устройств
  • работать с заказчиками мирового уровня

Что нужно от Вас?

  • опыт разработки под Android от одного года
  • отличное знание основ Java
  • отличное знание основ Android
  • хорошее знание С/С++ и опыт работы с NDK
  • знание принципов ООП и способность эффективно применять их в архитектуре приложения
  • знание и умение применять на практике шаблоны проектирования
  • английский язык: уметь уверенно читать и писать, разговорный приветствуется

Большим плюсом будет:

  • опыт кросс-платформенного программирования

Сайт: www.softeq.by, www.softeq.com

Cтраничка в Facebook: http://www.facebook.com/pages/Softeq/110374298801

Страничка в VK: http://vk.com/club21079655

Контактная информация:

Ждем Ваше резюме на jobs@softeq.by!

Если вы хотите узнать о вакансии и нашей компании больше, Вы можете связаться с менеджером по персоналу Ириной Протащик:

Skype: irina.protaschik

E-mail: irina.protaschik@softeq.com

Моб.: +375 29 175 47 00, +375 29 234 47 00

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *