Основы Android NDK: работа с C/C++ кодом

Android NDK path

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

Предварительная настройка

Если у вас ещё не настроен Eclipse, то читаем как настроить Eclipse для работы с Android. Только помимо того, что в статье сказано, при установке ещё необходимо выбрать и установить NDK плагин.

Так же вам необходимо установить CDT для Eclipse. Под Виндой вам вроде как ещё понадобиться установить Cygwin.

Теперь необходимо создать проект и прописать пути.

В проекте будет создана папка jni, где вы должны размещать файлы с C++ кодом. В ранних версиях был баг, когда Eclipse не мог верно настроить пути до некоторых хэдеров из NDK. В последней версии всё нормально. Просто очистите проект (Clean project), а затем перестройте его (Build project).

Зачем нужен NDK?

Думаю, необходимо предварительно объяснить, когда вообще стоит (и стоит ли?) использовать ndk. Многие советуют использовать C++, когда требуются какие-то большие/сложные вычисления. Что значит сложно? =/ В общем, лучше назову конкретные случаи, когда использование NDK оправдано:

Возможности NDK огромны. Вы можете из Java вызывать C++ методы. В то же время, вам ничто не машет вызывать Java методы из C++. Даже есть возможность создавать приложение практически без использования Java, используя NativeActivity (API 9 и выше).

Java. Путешествие в Native (или туда и обратно).

Да простит меня профессор за упоминание его работы (: И так, рассмотреть всё в рамках одной статьи невозможно. Поэтому, для начала реализуем лишь вызов native методов из Java.

Перечислю кратко основные моменты при работе с native:

  • Создание файлов с C++ кодом.
  • Определение C++ методов для экспорта.
  • Создание .mk файлов.
  • Генерация библиотеки.
  • Подключение библиотеки в Java и вызов C++ методов.

Создание файлов с C++ кодом

В native определим всего 3 метода: передача строки, изменение строки, получение строки.

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

#include <android/log.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#ifdef __ANDROID__
#define LOG_TAG "MyNative"
#define STRINGIFY(x) #x
#define LOG_TAG    __FILE__ ":" STRINGIFY(__MyNative__)
#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)


#endif

Создадим файл MyNative.h и определим в нём спецификации методов для экспорта, чтоб вызывать их из Java кода потом.

#include <def.h>
#include <jni.h>

char  MyStr[80];

extern "C" {
    JNIEXPORT void Java_ru_suvitruf_androidndk_AndroidNDK_SetString(JNIEnv * env, jobject obj, jstring str);
    JNIEXPORT void Java_ru_suvitruf_androidndk_AndroidNDK_ChangeString(JNIEnv * env, jobject obj);
    JNIEXPORT jstring Java_ru_suvitruf_androidndk_AndroidNDK_GetString(JNIEnv * env, jobject obj);
}

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

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

Ну и создадим MyNative.cpp с реализацией методов.

#include <MyNative.h>

JNIEXPORT void Java_ru_suvitruf_androidndk_AndroidNDK_SetString(JNIEnv * env, jobject obj, jstring str){
	jboolean isCopy;
	const char * Str;
	Str = env->GetStringUTFChars(str, &isCopy);
	strcpy(MyStr,Str);
	LOGI("string = \"%s\"",MyStr);
}

void ChangeStr(){
	strcat(MyStr," and bb.");
}

JNIEXPORT void Java_ru_suvitruf_androidndk_AndroidNDK_ChangeString(JNIEnv * env, jobject obj){
	ChangeStr();
	LOGI("string after change = \"%s\"",MyStr);
}

JNIEXPORT jstring Java_ru_suvitruf_androidndk_AndroidNDK_GetString(JNIEnv * env, jobject obj){
	LOGI("returned string = \"%s\"",MyStr);
	return env->NewStringUTF(MyStr);
}

Работа с Application.mk

В этом файле описаны глобальные настройки для сборки либы.

# Без этой строчки ничего не будет работать (:
APP_STL:=stlport_static
# Список модулей/либ, которые нужна забилдить. Они будут такие же как в LOCAL_MODULE в Android.mk файле
APP_MODULES      := AndroidNDK
# Указываем под какой arm собирать. Не обязательный параметр.
APP_ABI := armeabi armeabi-v7a
# Платформа, под которую билдим. Не обязательный параметр.
APP_PLATFORM := android-10

Работа с Android.mk

Здесь указываем параметры/настройки по линковке и прочее, чтобы собрать либу.

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

# имя нашего модуля, который будет вызываться в Java при помощи System.loadLibrary()
LOCAL_MODULE    := AndroidNDK

LOCAL_DEFAULT_CPP_EXTENSION := cpp

#список файлов, который нужно собрать
LOCAL_SRC_FILES := MyNative.cpp

#список библиотек из ndk, которые надо включить в сборку
LOCAL_LDLIBS :=  -llog -landroid

include $(BUILD_SHARED_LIBRARY)

В Android.mk вообще есть не мало всяких флагов и прочего. Можно добавлять в сборку уже готовые библиотеки и т.д. В следующих статьях напишу, как это делается.

После того, как вы создали C++ файлы и .mk сделали, можно забилдить проект, тогда в папке obj появится библиотека libAndroidNDK.so.

Подключение библиотеки в Java и вызов C++ методов.

Native simple project

Теперь остаётся только написать Java код. Сделаем простенькое приложение. Разместим поле для ввода текста, три кнопки (передача текста в native, изменение текста в native и возврат изменённой строки из native) и поле для нового текста, который получили из native кода.

Для того, чтобы использовать native методы создадим класс AndroidNDK.

public class AndroidNDK {
	
	// Загрузка модуля «AndroidNDK» — нативной библиотеки, в которой реализованы методы. 
	// Название этого модуля задается в файле Android.mk.
	static {
		System.loadLibrary("AndroidNDK");
	}
	
	public static native void SetString(String str);
	public static native void ChangeString();
	public static native String GetString(); 
}

Ключевое тут:

static { System.loadLibrary("AndroidNDK"); }

Этот код выполнит загрузку нашей библиотеки, в которой реализованы методы. Обратите ещё раз внимание на этот класс и методы и вспомните наименование методов при экспорте в native коде: Java_ru_suvitruf_androidndk_AndroidNDK_ChangeString.

Java код по обработке нажатий на кнопки писать не буду, ибо это тривиальная задача. В любом случае, если понадобиться, можете посмотреть в исходниках к статье. В логе будет вот что:

native log

Для знакомства с native достаточно написал. Можете скачать исходники AndroidNDK.rar.