Wednesday, May 2, 2012

Embedded cross-platform development for Android, iOS and Linux Embedded

I recently came across a new problem: cross-platform development of libraries on iOS, Android and Linux Embedded devices. What I wanted was a cross-platform way of writing a library letting me perform as much non-GUI work as possible.

The only reasonable solution I could think of was developing the library entirely in C/C++ language. This can be quite long, but simple to recompile for all these platforms. Linux Embedded should commonly support entire C/C++ toolchains (like Codesourcery toolchains), iOS supports standard C/C++ quite well and can be used very simply from Objective-C using Objective-C++ and Android can make use of C/C++ libraries through a JNI interface (not as much quick as the others, but possible).

Ok, then C/C++ is my choice. Anyway, would it possible to increase my productivity by using some kind of general purpose C/C++ library instead of relying only on the standard library? There are many possible solutions to this, one of which is the Neptune library.

During the last months anyway, I've come to be very close to the Qt libraries. I started to love the completeness, the flexibility and the comfortable API Qt provides.

I've been looking with interest for months to the new development of the Qt porting to Android (thanks to BogDan Vatra) and iOS but I never had the chance to try. Anyway, those ports make use of the QPA build of Qt, which started with the Lighthouse project to provide Qt portability on the GUI level to other platforms. This is not exactly what I needed: I'm not interested at the moment on the GUI development. What I need is simpler. Anyway, I found this very useful to start working.

The environment I'm currently working with allows me to create a common Qt Makefile to be parsed by qmake to generate a Makefile for any platform I need: for Android the toolchain provided by the NDK and for iOS the toolchain provided by Apple.

After this introduction, I explain the steps I followed to setup the environment.

Setting up for Android development

First thing to do is to download the Android NDK from the official website. Uncompress it somewhere.

Download and compile the Android port of Qt: it might be good to compile a specification file for the build, it is quite simple, but the Android's toolchain is not a complete toolchain. This implies that some modifications to the Qt sources are needed. I started to do it, but for the moment simply downloading from the git the last sources from Necessitas is good enough.

Now you have both the Android toolchain and the Qt sources compiled for Android. You can select the new Qt platform and compile directly from Qt Creator. This is damn good, cause it makes it quite simple to implement the JNI interface also, which I've never found a way to write comfortably from Eclipse.

Once the library is compiled, you can place it into an Android project as instructed in the manual of the Android NDK and all is done. You have C/C++ sources with Qt support ready to be used in an Android project. You can also implement the JNI interface and include the sources only when building for android using the qmake scopes.

Setting up for iOS development

It might seem a paradox, but I found it more difficult to compile for iOS than for Android... What you can do is use the QPA specification for iOS that can be found in <Qt sources>/mkspecs/qpa/macx-iphone*. By using qmake from the command line you can create a Makefile for iOS and then compile it on a Mac:

qmake -spec qpa/macx-iphonesimulator-g++


you can also directly include Objective-C and Objective-C++ sources directly into the Qt project file by using the variable:

OBJECTIVE_SOURCES += source_list


Just remember that only static libraries are allowed on iOS, so include something like:


ios {
CONFIG += static
}

 

to compile statically when on iOS.

When I tried this for the first time it didn't work. It seems that for some reason there is a difference between my XCode setup and the one used as a reference for the platform specification. What I had to do is to fix the qmake.conf specification for the iOS platform so that I compiles correctly. I found this article very useful to see what needed to be changed.
In particular, for recent versions of Xcode, I had to change the variable QMAKE_IOS_DEV_PATH (in mkspecs/qpa/macx-iphonesimulator-g++/qmake.conf) from:

QMAKE_IOS_DEV_PATH = /Developer/Platforms/iPhoneSimulator.platform/Developer


to:

QMAKE_IOS_DEV_PATH = /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer

and the QMAKE_IOS_SDK_VERSION (in mkspecs/qpa/common/g++-base-macx-iphone.conf) to 5.1.

Using Qt libraries on Android and iOS

There is one other point when implementing libraries using Qt: many Qt features require an event loop to be running and that an instance of QCoreApplication to be instantiated somehow. To do this, it might be sufficient to run the event loop and instantiate QCoreApplication in a thread of the library: in the init function, just spawn a new thread and exec the event loop.
This is an example of what I tried: first I created a Java class like this:

package com.lukeshq.android.test.qt;

/**
 * Author Luca Carlon
 * Date: 05.10.2012
 */

public class QtTest {
   // Static initializer.
   static {
      System.loadLibrary("QtCore");
      System.loadLibrary("AndroidTest");
   }

   public QtTest() {
      new Thread(new Runnable() {
         @Override
         public void run() {
            while (true) {
               try {
                  Thread.sleep(1000);
               }
               catch (InterruptedException e) {
                  e.printStackTrace();
               }
               mySlot();
            }
         }
      }).start();
   }

   // Native interface.
   public native boolean mySlot();
}

then, in my library, I created a simple object like this:

class MyQObject : public QObject
{
   Q_OBJECT
public:
   explicit MyQObject(QObject* parent = 0);

public slots:
   void mySlot();
};

and this is the implementation of the bindings:

/*----------------------------------------------------------------------
|    includes
+---------------------------------------------------------------------*/
#include <QString>
#include <QtConcurrentRun>
#include <QCoreApplication>
#include <QTimer>
#include <jni.h>
#include <android/log.h>
#include "myqobject.h"

/*----------------------------------------------------------------------
|    declarations
+---------------------------------------------------------------------*/
MyQObject* myQObject;

/*----------------------------------------------------------------------
|    runQCoreApplication
+---------------------------------------------------------------------*/
void runQCoreApplication()
{
   // Instantiate QCoreApplication.
   int i = 1;
   char* c[1] = {"MyLib"};
   QCoreApplication a(i, c);

   // Start the QTimer.
   __android_log_print(ANDROID_LOG_INFO, "LibTag", "Starting QTimer!");
   QTimer* t = new QTimer();
   myQObject = new MyQObject();
   QObject::connect(t, SIGNAL(timeout()), myQObject, SLOT(mySlot()));
   t->setInterval(1000);
   t->setSingleShot(false);
   t->start();

   // Start the event loop.
   a.exec();
}

/*----------------------------------------------------------------------
|    JNI_OnLoad
+---------------------------------------------------------------------*/
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved)
{
   Q_UNUSED(vm);
   Q_UNUSED(reserved);

   // Test QString.
   QString myString = QString("My QString!");
   __android_log_print(ANDROID_LOG_INFO, "LibTag", "%s", qPrintable(myString));

   // Start the QCoreApplication event loop.
   QtConcurrent::run(&runQCoreApplication);

   return JNI_VERSION_1_6;

}

/*----------------------------------------------------------------------
|    JNI_OnUnload
+---------------------------------------------------------------------*/
extern "C" JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM* vm, void* reserved)
{
   Q_UNUSED(vm);
   Q_UNUSED(reserved);

   __android_log_print(ANDROID_LOG_INFO, "LibTag", "Unloading!");
}

/*----------------------------------------------------------------------
|    Java_com_lukeshq_android_test_qt_QtTest_myslot
+---------------------------------------------------------------------*/
extern "C" JNIEXPORT void JNICALL
Java_com_lukeshq_android_test_qt_QtTest_mySlot(JNIEnv* env, jobject thiz)
{
   Q_UNUSED(env);
   Q_UNUSED(thiz);

   QMetaObject::invokeMethod(myQObject, "mySlot");
}