Эта статья о том, как подружить языки Java и C++ в одном приложение для операционной системы Android.

На С++ написано огромное количество кода. Мне хотелось бы как-то использовать этот код в своих приложения, но почему-то у меня процесс использования вызывал некий дискомфорт. Скорей всего, это, банально, было связано с тем, что основой моей рабочей средой является Android Studio, в которой работа с нативным кодом, скажем так, не самая лучшая. Но вот мысль о том, чтобы комфортно разрабатывать приложение с нативной частью не оставляла меня никогда. Поэтому я решил попробовать скрестить всю мощь библиотеки Qt и приложение, написаное на родном для Android языке — Java.

На данный момент экосистема Qt разрешает писать приложение полностью средствами этого фреймворка, даже больше — компилировать его для целого набора операционных систем, среди которых находится и Android. Но двигаясь этим путем очень сложно разработать интерфейс, который будет выглядеть и вести себя соответственно гайдам платформ. Да и становится невозможным использовать библиотеки, написаные на Java для оформление внешнего вида и поведения приложения. Именно поэтому хотелось бы испробовать модель разработки, при которой вся бизнес логика писалась на С++, а пользовательский интерфейс — средствами целевой платформы. В дальнейшем это обеспечит простой перенос приложения на другие платформы с внешним видом, характерным для целевой системы и функционалом, который уже проверен и отложен.

Итак, дальше вас ожидает инструкция о том, как написать приложение, которое будет иметь часть, написаную на Qt, и часть на Java.

Для работы нам понадобится библиотека Qt и ide Qt Creator, настроенные для работы с Android-проектами (это сделать довольно просто, подробно об этом можно прочитать по ссылке), Android Studio, а так же Android ndk.

1. Для начала нужно создать андроид проект (назовем его QTtests) и в качестве имени пакета зададим «penguin.in.flight.qttests». На следующих этапах мастера создания проектов можно выбирать любые, удобные для работы, настройки. Главным условием для того, чтобы можно было следовать дальнейшим инструкциям является соблюдение имени пакета.

Создание нового проекта в Android Studio
image

2. Когда Android проект уже есть, можно на время забыть о нем и перейти к созданию Qt части. Первым шагом будет создание (желательно в корне основного проекта) стандартного проекта типа библиотека. Дадим ему имя cpp_lib. При создании проекта в качестве целевой платформы нужно выбрать armeabi-v7.

Создание библиотеки




3. Шаблон проекта у нас есть, теперь самое время написать наш Hello Word на Qt.
cpp_lib.h

#ifndef CPP_LIB_H
#define CPP_LIB_H

#include "cpp_lib_global.h"
#include <jni.h>
#include <QString>

class CPP_LIBSHARED_EXPORT Cpp_lib
{

public:
    Cpp_lib();
    QString say(QString subject);
};

#ifdef __cplusplus
extern "C" {
#endif
    JNIEXPORT jstring JNICALL Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello(JNIEnv *env, jobject obj);
#ifdef __cplusplus
}
#endif

#endif // CPP_LIB_H


cpp_lib.cpp

#include "cpp_lib.h"


Cpp_lib::Cpp_lib()
{
}

QString Cpp_lib::say(QString subject)
{
    QString result = QString("Qt say \" %1 \"").arg(subject);
    return result;
}

JNIEXPORT jstring JNICALL Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello(JNIEnv *env, jobject obj){
    Cpp_lib *lib = new Cpp_lib();
    return  env->NewStringUTF(lib->say("Hello android!!!").toLatin1().data());
}

Как видно по коду, приложение у нас будет очень простое. Оно содержит две функции — say и Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello. Первая — простая и понятная. Они принимает на вход объект, который в фреймворка является текстовой строкой, и отдает новую строку. Намного интереснее следующая функция. В ней JNIEXPORT и JNICALL обозначаю то, что она может быть вызвана с кода, написаного на джава. В качестве возвращаемого типа она имеет jstring (который доступен нам благодаря этому инклуду #include <jni.h>). Этот тип является типом String в джава.

В завершение разберем аргументы метода. *env — это ссылка на структуру, которая содержит методы, необходимые для работы с сущностями джавы, а obj является обьектом, который вызвал метод.

3. После того, как код написан, нужно сделать так, чтобы наша библиотека корректно собиралась и мы могли использовать ее в своих проектах. Для этого нам нужно убрать вот эту строку TEMPLATE = lib в файле настроек проекта ( в нашем случае это cpp_lib.pro). Если этого не сделать то при сборке мы получим ошибку, вроде этой:

Internal Error: Could not find .pro file.
Error while building/deploying project cpp_lib (kit: Android для armeabi-v7a (GCC 4.9, Qt 5.4.1) )
When executing step «Build Android APK»

4. Дальше нужно сделать так, чтобы Qt Creator собирал библиотеку по таком адресу: {путь_к_проекту}/QTtests/app/src/main/qt_output:

Задание папки сборки



5. Там же, где мы указывали место сборки, нужно проверить настройки сборки приложения. А именно — целевую платформу сборки. Нужно выбрать последнюю (у меня это android 22). Далее поставить галочку напротив Build Qt libraries in APK. Это нужно для того, чтобы система сборки сгенерировала все необходимые библиотеки. Так же нужно поставить галочку напротив Use Gradle — для того, чтобы система сборки сгенерировала проект нужного нам типа.

Скриншот настроек



6. Теперь мы можем собрать библиотеку и получить все необходимые файлы.

Собрать проект



7. На этом работа с нативной библиотекой оканчивается. Мы возвращаемся назад в Android studio. Первое, что нужно сделать — отредактировать файл сборки, чтобы подключить Qt библиотеки. Для этого нужно добавить следующие строки в файле /QT_tests/app/build.gradle:

   sourceSets {
        main {
            java.srcDirs += ['/src/main/java']
            res.srcDirs += ['src/main/res','src/main/qt_output/android-build/res']
            jniLibs.srcDirs += 'src/main/qt_output/android-build/libs'
        }
    }

src/main/qt_output/android-build/res нужно для того, чтобы иметь доступ к ресурсам, в которые система сборки прописала имена библиотек, от которых зависит основной Qt проект. Путь src/main/qt_output/android-build/libs указывает на место, где лежат сами файлы библиотеки.

Так же модифицируем блок зависимостей следующим образом:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile files('src/main/qt_output/android-build/libs/QtAndroid-bundled.jar')
    compile files('src/main/qt_output/android-build/libs/QtAndroidAccessibility-bundled.jar')
    //остальные зависимости 
}


8. Теперь у нас есть собрание библиотеки и они даже подключены к проекту. Далее нужно подключить их во время старта самой программы. Для этого будем использовать следующий класс:

package penguin.in.flight.qttests.utils;

import android.content.Context;
import android.util.Log;

import penguin.in.flight.qttests.R;

/**
 * Created on 11.04.15.
 */
public class JavaNatives {
    public native static String sayHello();

    public static void init(Context context) {
        load(context, R.array.bundled_in_assets);
        load(context, R.array.qt_libs);
        System.loadLibrary("cpp_lib");
    }

    static void load(Context context, int arrayResourceId) {
        String[] libsToLoad = context.getResources().getStringArray(arrayResourceId);
        for (String lib : libsToLoad) {
            if (lib.indexOf('/') > -1) {
                lib = lib.substring(lib.indexOf('/'));
            }
            if (lib.indexOf("lib") == 0) {
                lib = lib.substring(3);
            }
            if (lib.endsWith(".so")) {
                lib = lib.substring(0, lib.length() - 3);
            }
            Log.i(JavaNatives.class.getSimpleName(), "loading " + lib);
            try {
                System.loadLibrary(lib);
            } catch (Throwable e) {
                Log.i(JavaNatives.class.getSimpleName(), "failed to load " + lib + " " + e);
                e.printStackTrace();
            }
            Log.i(JavaNatives.class.getSimpleName(), "Successfully loaded " + lib);
        }
    }

}

Как видно из кода, класс находится в пакете penguin.in.flight.qttests.utils и называется JavaNatives. Это важно для того, чтобы обеспечить связь между тем кодом, который мы написали в нативной части. Чтоб понять как, давайте вспомним метод с именем Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello. Его имя состоит из префикса Java_, потом идет имя пакета, где имена саб пакетов разделены символом "_" (penguin_in_flight_qttests_utils). Дальше идет имя класса и, наконец, имя метода. Детальнее о именование методов в нативном кодом можно почитать здесь.

9. Чтобы использовать написаный нами класс в onCreate в стартовой активити, нужно вставить эту строку:

JavaNatives.init(this);

10. Теперь осталось только использовать то, что мы написали. Для этого добавим кнопку и в обработке нажатия на нее вставим следующий код:

AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setMessage(JavaNatives.sayHello());
dialog.setPositiveButton("Ok", null);
dialog.show();

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

Всем спасибо за внимание, весь код примера доступен на github.com.

Комментарии (17)


  1. IrixV
    13.04.2015 16:42

    Добрый день. Прошу прощения за вопрос «не в кассу». Можно ли как-то в Android Studio вести несколько проектов с использованием общего кода. Есть ли в java нечто аналогично #ifdef и куда при этом пихать переменные препроцессора? Использовать статические переменные не получается, так как нужно подсоединять различные ресурсы strings.xml и color.xml. Наверняка многие сталкивались с такой проблемой.

    А вот по поводу «дружбы Java и C++ в одном приложение», очень интересно как обстоят дела с отладкой сишного кода из Android Studio? У меня как-то отладка кода из Cocos2dx так и не задалась, хотя компилировалось все и запускалось нормально.


    1. Braiko Автор
      13.04.2015 17:57
      +1

      По первой части вопроса — то можно что-то вроде этого сделать:

          sourceSets {
              main {
                  java.srcDirs = ['src']
                  resources.srcDirs = ['src']
                  aidl.srcDirs = ['src']
                  renderscript.srcDirs = ['src']
                  res.srcDirs = ['res']
                  assets.srcDirs = ['assets']
      
                  manifest.srcFile 'AndroidManifest.xml'
              }
              refcotor_list {
                  java.srcDirs += ['dev/refcotor_list']
                  res.srcDirs = ['dev/res']
              }
              old_list {
                  java.srcDirs += ['dev/old_list']
              }
          }
      


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

      Нащет отладки — то в описанной схеме она по прежнему не возможно, но используя среду Qt Creator можно параллельно сделать проекто-демку, который можно запускать на андроиде и без проблем отлаживать.


      1. IrixV
        13.04.2015 18:46

        Я вижу, что вы отделили ресурсы, но не вижу куда вы подключили refcotor_list и old_list?


        1. Braiko Автор
          13.04.2015 19:13
          +1

          Извините за неполный ответ)) Еще нужно дописать что-то вроде этого кода.

          buildTypes {
                  release {
                      minifyEnabled true
                      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                      signingConfig signingConfigs.release
                  }
                  refcotor_list {
                      debuggable true
                      signingConfig android.signingConfigs.debug
                  }
                  old_list {
                      debuggable true
                      signingConfig android.signingConfigs.debug
                  }
              }
          


          После этого можно будет собирать соответственный тип сборки



          или же выполнить соответствующую задачу, например assembleRefcotor_list


          1. IrixV
            13.04.2015 19:46

            Большое спасибо!


          1. silentnuke
            14.04.2015 04:13

            в данном случае правильние использовать flavor, а не build type.


            1. IrixV
              14.04.2015 10:32

              Согласна. Спасибо за ссылку. Просто, мне картинка выше помогла найти место переключения билдов. Как-то все это не очень очевидно по сравнению с ios), хотя может дело привычки.


              1. toxicdream
                14.04.2015 18:11

                Методом тыка нашел в первый день работы с Android Studio.


      1. silentnuke
        13.04.2015 23:10

        По первому вопросу прочитайте про flavor, flavorDimensions и build type. Начать можно остюда. developer.android.com/tools/building/configuring-gradle.html


  1. tywonka
    13.04.2015 19:15

    Я бы посоветовал использовать не такие ужасающие названия методов

    JNIEXPORT jstring JNICALL Java_penguin_in_flight_qttests_utils_JavaNatives_sayHello(JNIEnv *env, jobject obj)
    

    а сделать так:
    static JNINativeMethod methods[] = {
        {"someMethod", "()Ljava/lang/String;", (void *)someMethod}
    };
    

    и в методе
    JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
    

    зарегистрировать указанные нативные методы
    if (env->RegisterNatives(javaClass, methods, sizeof(methods) / sizeof(methods[0])) < 0)
            return JNI_ERR;
    


    В таком случае, вам не нужно будет переименовывать все эти методы, если вдруг понадобиться, например, изменить имя пакета или что-то другое, ну и исчезнут эти монструозные названия методов.


    1. Braiko Автор
      13.04.2015 19:47

      Спасибо большое за совет.


    1. ram2406
      14.04.2015 16:26

      Да действительно удобно.

      А вы пробовали SWIG?

      Позволяет избежать неприятных моментов несоответствия версий нативного и клиентского кода.
      Дополнительное удобство в обработке исключений, стоит нам дописать к методу " throw(std::exception)".
      Плюс — подготовленные шаблоны для vector, wstring.
      И самое главное он очень гибкий инструмент и не только под Java.

      Имхо, незаменимый инструмент для кросс-платформенной разработки на основе C/C++.


  1. Beltoev
    13.04.2015 23:53

    Qt под андроид всё также требует дополнительно качать 10-12 МБ (министро вроде, точно не помню) на телефон?


    1. bitterman
      14.04.2015 11:46

      можно так, можно эдак, но с Министро работает лучше. Меньше проблем с потоками, поверхностями, инициализацией. В процессе вылавливания проблем с чёрным экраном на ровном месте и теряющимися сообщениями (т.к. parent is in other thread) перешёл на Министро и начал спать спокойно.


  1. ram2406
    14.04.2015 16:32

    А как у вас дела с отладкой нативной библиотеки в таком случае обстоит?

    Эти сценарии мне не очень-то помогли:
    github.com/mapbox/mapbox-gl-native/wiki/Android-debugging-with-remote-GDB
    geekswithblogs.net/raccoon_tim/archive/2011/09/12/working-with-android-on-windows-and-without-cygwin.aspx
    comments.gmane.org/gmane.comp.lib.qt.android/3462

    Видимо руки кривые… :(


    1. VioletGiraffe
      14.04.2015 20:58

      Отладка нативного кода на Андроиде — это боль. У меня она регулярно ломается по непонятным причинам. Т. е. буквально: сегодня работает, прихожу утром на следующий день — не работает. И это в Эклипсе, под который больше гайдов и советов на SO.
      Ещё и банальные вещи вроде визуализации std::vector Эклипс не умеет.
      Извините, накипело.


      1. ram2406
        15.04.2015 10:29

        Ну примерно это я и ожидал услышать. Имхо, это настоящий ад)

        Вычитав первое предложение этой статьи, я рассчитывал увидеть как подружить Qt Creator + gradle + Java (о которой возможности было заявлено разработчиками)…

        Суть моей проблемы в том, что мне приходится использовать ту же Android Studio. IDE которую используют Android-разработчики, клиенты моей библиотеки. И в этом проекте используется gradle. А так же Google Maps SDK — где я переопределяю тайловый провайдер и заполняю его нативной начинкой. Т.е. локальный рендеринг карты. Да сценарий не самый лучший, но не я его инициатор.

        Саму библиотеку я разрабатываю под Qt Creator'ом — удобно, если привыкнуть (тот же alt+enter под Windows). И с отладкой в нем проблем нет, если на выходе приложение, а не .so модуль (std::vector вполне читаем).

        В итоге получается что на данный момент есть плавающий баг, который повторяется только Java-приложении с .so библиотекой. И разобраться в чем именно проблема не получается.

        Вообщем попробую Eclipse. Спасибо, за комментарий.