Compilation native
Google propose un Native Development Kit (NDK) permettant d'exécuter des applications codées en C ou C++.
Il est téléchargeable ici : http://developer.android.com/sdk/ndk pour différentes plateformes.
Après avoir installé tout cela, nous sommes en possession d'une chaîne de compilations pour un processeur ARM v5 ou v7. Les librairies compilées pour la version 5 sont compatibles avec tous les téléphones Android actuels. Les librairies compilées pour la version 7 ne sont compatibles qu'avec certains modèles comme le Google Nexus One. Une prochaine version permettra la compilation pour des processeurs Intel.
Tous les fichiers d'en-têtes et les librairies accessibles à la compilation sont stables. Cela garantit que le code fonctionnera dans les prochaines versions. Les fonctions non supportées par Google ne sont pas présentes dans les fichiers d'en-têtes pour éviter une utilisation malheureuse.
Ce NDK permet de coder des algorithmes exigeants en CPU, ou du code souhaitant s'interfacer avec des éléments systèmes non disponibles en Java comme les signaux et autres mémoires partagées. Cela complexifie l'application et la distribution, car il faut prévoir une compilation pour différentes plate-formes cibles.
Le framework déclenche toujours une JVM Dalvik en première intention. Le code java peut alors s'échapper à l'aide d'une interface JNI pour invoquer du code compilé.
Plusieurs librairies sont disponibles et pérennes :
- en-tête libc (C library)
- en-tête libm (math library)
- en-tête interface JNI
- en-tête libz (Zlib compression)
- en-tête liblog (Android logging)
- en-tête OpenGL ES 1.1 et OpenGL ES 2.0 (3D graphics libraries)
- en-tête libjnigraphics (Pixel buffer access pour Android 2.2+).
- en-tête minimal pour le support de C++
Les applications natives permettent la création ou l'adaptation de jeux existant à la plate-forme. La version 2.3 offre maintenant un accès aux différents senseurs et un nouveau type d'activité spécifique JNI (NativeActivity) permettant de gérer intégralement le cycle de vie en JNI.
Pour commencer une application JNI, il faut créer une application Android classique et ajouter un répertoire /jni possédant les sources C ou C++. Nous allons réaliser un petit « hello world » codé en C et l'invoquer à l'aide d'un wrapper JNI. Notre fichier source ressemble à celui-ci.
// Fichier Hello.c
#include <stdio.h>
#ifdef ANDROID
#include <android/log.h>
#define printf(X) __android_log_print(ANDROID_LOG_DEBUG,"stdout",X)
#endif
int
main(int argc, char *argv[])
{
printf(argv[1]);
}
Rien que du classique. Comme nous n'avons pas accès à printf(), nous ajoutons une petite macro pour adapter l'invocation.
Nous voulons pouvoir invoquer ce code depuis Java. Pour cela, nous allons rédiger un wrapper permettant d'invoquer la méthode main() de notre hello.c. Ce dernier doit tout d'abord être rédigé en Java. Nous déclarons une méthode native qui devra être implémentée en C en respectant l'interface JNI.
// Fichier JNIMain.java
package fr.prados.jni;
public class JNIMain
{
public static native int main(String[] args);
static
{
System.loadLibrary("jnimain");
}
}
Lors du chargement de la classe, nous demandons de la lier à une librairie partagée. Cette dernière sera présente dans le répertoire /libs/.
La méthode native JNIMain.main() doit être déclarée dans un fichier C, en respectant la convention de nom imposée par JNI. La fonction doit s'appeler Java_<nom_du_package>_<NomClass>_<Method>. Elle reçoit trois paramètres. Le premier est un pointeur vers tous les services exposés par la JVM Dalvik. Le deuxième, un pointeur vers l'instance Java this. Comme notre méthode est statique, ce pointeur est à null. Puis nous retrouvons les paramètres de la méthode, avec des types JNI.
// Fichier JNIMain.c
#include <string.h>
#include <jni.h>
jint
Java_fr_prados_jni_JNIMain_main(
JNIEnv* env,
jobject thiz,
jarray args)
{
...
}
Comme nous recevons en paramètre un pointeur sur un tableau Java, nous devons l'analyser pour construire en mémoire un tableau classique C.
int argc = (int)(jsize)(*env)->GetArrayLength(env, args);
char** argv=malloc(sizeof(*argv)*argc);
Nous récupérons les chaînes de caractères de tous les éléments du tableau Java. Après les avoir dupliquées en mémoire, nous alimentons notre tableau C avant de libérer les instances Java au fur et à mesure. Elles pourront alors être nettoyées si nécessaire.
int i;
int rc;
for (i=0; i<argc; ++i)
{
jstring* arg = (*env)->GetObjectArrayElement(env, args, i);
const jbyte* utf=(*env)->GetStringUTFChars(env, arg, &iscopy);
argv[i]=strdup((char*)utf);
(*env)->ReleaseStringUTFChars(env, arg, utf);
}
Il est temps d'invoquer la fonction main de hello.c.
rc=main(argc,argv);
Il ne faut pas oublier de nettoyer la mémoire avant de rendre la main à la JVM, le C ne possède pas de ramasse-miettes.
for (i=0;i<argc;++i)
{
free(argv[i]);
}
free(argv);
return rc;
Le code complet de notre wrapper est celui-ci :
#include <string.h>
#include <jni.h>
jint
Java_fr_prados_jni_JNIMain_main(
JNIEnv* env,
jobject thiz,
jarray args)
{
int argc = (int)(jsize)(*env)->GetArrayLength(env, args);
char** argv=malloc(sizeof(*argv)*argc);
int i;
int rc;
for (i=0; i<argc; ++i)
{
jstring* arg = (*env)->GetObjectArrayElement(env, args, i);
const jbyte* utf=(*env)->GetStringUTFChars(env, arg, &iscopy);
argv[i]=strdup((char*)utf);
(*env)->ReleaseStringUTFChars(env, arg, utf);
}
rc=main(argc,argv);
for (i=0;i<argc;++i)
{
free(argv[i]);
}
free(argv);
return rc;
}
Maintenant que tout est en place, nous devons rédiger le fichier Android.mk et le placer dans le répertoire /jni, à côté des deux fichiers C. Ce dernier est très simple. Après quelques variables imposées, il faut valoriser LOCAL_MODULE avec le nom de la librairie partagée. Un fichier lib<nom>.so sera généré dans le répertoire /lib/armeabi correspondant à l'architecture cible. Lorsqu'il sera possible de travailler avec plusieurs architectures, il y aura plusieurs sous-répertoires dans /lib. Lors de l'installation de l'application, Android sélectionne la version de la librairie qui correspond à son processeur et la copie dans le répertoire /lib du projet.
La variable LOCAL_SRC_FILES doit indiquer tous les fichiers C à compiler. Nous en avons deux. Enfin, la variable LOCAL_LDLIBS indique les librairies à ajouter lors de la compilation. Comme nous souhaitons bénéficier du mécanisme de Log d'Android, nous ajoutons la librairie correspondante.
Nous terminons avec un include imposé.
# Fichier Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jnimain
LOCAL_SRC_FILES := JNIMain.c hello.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
Il nous faut ensuite se positionner dans le répertoire jni du projet et à invoquer ndk-build.
$ ~/bin/android-ndk/ndk-build
Compile thumb : jnimain <= ~/JNIMain.c
Compile thumb : jnimain <= ~/hello.c
SharedLibrary : libjnimain.so
Install : libjnimain.so => ../libs/armeabi
Il nous reste à rédiger une petite activité pour invoquer notre traitement.
// Fichier Demo.java
package fr.prados.jni;
import android.app.Activity;
import android.os.Bundle;
public class Demo extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
JNIMain.main(new String[]{"hello","hello world"});
}
}
Il ne faut pas oublier de rafraîchir le projet Android dans Eclipse pour le régénérer, suite à la production de la librairie partagée.
Le déploiement de l'application permet d'obtenir un beau « hello world » dans LogCat.
Cela montre une troisième voie pour le développement d'applications Android. Comme tous les projets Android peuvent importer des librairies natives, les sécurités du langage ne servent à rien. Il est toujours possible de les contourner via un code C/JNI.
Nous avons effleuré le développement avec un langage compilé. C'est la troisième voie permettant de dépasser les limitations de l'approche HTML5 ou Java.
