Основы 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.

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

  1. Hero

    А будут ли дальше уроки по LibGDX? Очень хочу узнать как делать Runner на LibGDX.

  2. Alti

    Еще вопрос, вы развиваете эту тему как возможность использования с++ и сами эти пользуетесь в разработке приложений, или используете только яву? еще что очень интересно возможность интеграции с игровыми движками, например как мин3д, ну или libgdx, да впрочем с любыми…? Вопрос может не очень корректен, т.к. только начинаю изучать яву, начал с разбора движка мин3д…

    1. Suvitruf Автор записи

      Я сейчас портирую игрушки с iPhone, поэтому и стал статьи про ndk писать, ибо актуально. По поводу интеграции…вы хотите что-то свою дописать в библиотеку LibGDX? Или я вас не так понял? )

  3. Уведомление: Основы Android NDK: вызов Java-методов из C/C++ кода при помощи JNI | Suvitruf's Blog

    1. Suvitruf Автор записи

      Если вы либу собираете сами, то можно что угодно дописать.

      Если вы используете уже собранную С++ библиотеку, то сложно сказать. Можно попробовать создать свою библиотеку, в которую вложить чью-то готовую и вызывать в своей либе методы той готовой библиотеки.

      В принципе, я когда в native работаю с zip или OpenGL, то юзаю уже готовые библиотеки. Другое дело, что вам заголовочные файлы всё равно нужны.

  4. Уведомление: Основы Android NDK: доступ к ресурсам директории assets из C++ | Suvitruf's Blog

  5. Уведомление: Основы Android NDK: работа с OpenAL и форматами WAV, OGG | Suvitruf's Blog

  6. Валерий

    Извиняюсь, за глупый вопрос. не подскажете а вот библиотеки которые мы подгружаем в коде на С++, они как подгружаются? Мне просто необходимо openssl включить и возник данный вопрос. Буду благодарен за ответ, если конечно вы еще просматриваете эту тему.

    1. Саша

      openssl (немного урезанный) есть на каждом андроидном девайсе, так что если его вам достаточно, можно пользоваться системной библиотекой. В Андроиде работает обычный линуксовый dlopen(), но нельзя установить LD_LIBRARY_PATH.

  7. Александр

    Eclipse -> Window -> Preferences -> Android -> NDK
    У меня нет такой вкладки. У меня ADT Bundle, качал с офсайта неделю назад. Может с тех пор что-то изменилось? В PATH путь к NDK прописал.

    1. Suvitruf Автор записи

      У меня на работе такая же фигня была. Я удалил все компоненты ADT, CDT и все плагины для C++. Установил заново и всё заработало.

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

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