Linux Embedded

Le blog des technologies libres et embarquées

Compilation de code (legacy) C/C++ pour Android

Introduction

La majorité des applications Android utilisent le langage de programmation Java. Ce langage très répandu a été créé en 1982 par James Gosling dans les laboratoires de SUN Microsystems (désormais ORACLE depuis 2009). Java a de nombreux avantages, en particulier sa large diffusion, sa syntaxe relativement simple et le fait qu'il utilise nativement une approche « objet ».

De nombreux projets industriels utilisent cependant le langage C/C++, beaucoup plus ancien et permettant le plus souvent d'obtenir des performances supérieures car il est compilé. Android utilise le C/C++ pour les couches système et nous avons décrit récemment l'utilisation de JNI (Java Native Interface) dont le but est l'utilisation de fonctions provenant de bibliothèques C/C++ dans une application Java.

Dans le cadre du développement (ou de la migration) d'un projet industriel sous ou vers Android, il n'est pas forcément possible d'utiliser Java pour l'ensemble du code. Nous aurons souvent une configuration mixte avec une application graphique en Java, des « services » (ou démons) écrit en C/C++ et éventuellement des pilotes (Linux) écrits en C. Il est également fréquent d'adapter du code « ancien » (on parle de code legacy) qui reste en C/C++ car utilisé sur d'autres plate-formes comme GNU/Linux, ou POSIX en général.

Dans cet article nous allons décrire le développement de programmes en C/C++ dans l'environnement Android. Il faut noter que ces programmes n'utiliseront pas d'IHM car dans ce dernier cas, l'API Java est bien entendu un meilleur choix.

Comme nous pouvons le voir ci-dessous, le système Android utilise lui-même quelques programmes en C/C++ (voir le début de la liste) même si la majorité du framework et des services sont écrits en Java (nommés com.android.*).

$ adb shell 
root@generic:/ # ps                                                

USER     PID   PPID  VSIZE  RSS     WCHAN    PC          NAME 
drm       55    1     10616  3352  ffffffff b6e65b40 S  /system/bin/drmserver 
media     56    1     28708  6272  ffffffff b6ee1b40 S  /system/bin/mediaserver 
install   57    1     1008   432   c02f5e30 b6f0387c S  /system/bin/installd 
keystore  58    1     3512   1256  c0253e80 b6f3fb40 S  /system/bin/keystore 
root      59    1     940    372   c00e68a4 b6ee9d10 S  /system/bin/qemud 
shell     64    1     924    460   c01eb6dc b6ecf87c S  /system/bin/sh 
root      65    1     4592   224   ffffffff 0001a1b0 S  /sbin/adbd 
...
u0_a40    371   54    208200 28696 ffffffff b6f25d10 S  com.android.systemui 
u0_a19    427   54    203584 22332 ffffffff b6f25d10 S  com.android.inputmethod.latin 
system    439   54    208872 19192 ffffffff b6f25d10 S  com.android.settings 
radio     453   54    219120 25240 ffffffff b6f25d10 S  com.android.phone

Utilisation du C/C++ sous Android

Même si le C/C++ n'est pas le langage de prédilection sous Android, il est possible de l'utiliser grâce au NDK (Native Development Kit) décrit dans l'article concernant JNI. En premier lieu nous allons tester le traditionnel exemple « Hello World » dans l'émulateur Android. Le source utilisé est celui-ci :

#include <stdio.h> 
#include <stdlib.h> 

int main(int ac, char **av) 
{ 
  printf("Hello world (Android.mk) !\n"); 

  exit (0); 
}

Si nous utilisons le NDK, le fichier helloworld.c doit être placé par défaut dans un répertoire jni (même si cela n'a rien à voir avec JNI!). Le répertoire contient également le fichier Android.mk et Application.mk.

$ ls jni/ 
Android.mk  Application.mk  helloworld.c

Comme nous l'avons vu dans l'article sur JNI, le fichier Android.mk correspond à un Makefile spécifique à AOSP. La dernière ligne utilise la macro BUILD_EXECUTABLE et non plus BUILD_SHARED_LIBRARY comme dans cas de la bibliothèque JNI.

LOCAL_PATH := $(call my-dir) 
include $(CLEAR_VARS) 
LOCAL_SRC_FILES = helloworld.c 
LOCAL_MODULE = HelloWorld 
LOCAL_MODULE_TAGS = optional
include $(BUILD_EXECUTABLE)

La compilation s'effectue par la commande suivante :

$ ~/Android/android-ndk-r9c/ndk-build      
[armeabi-v7a] Compile thumb  : HelloWorld <= helloworld.c 
[armeabi-v7a] Executable     : HelloWorld 
[armeabi-v7a] Install        : HelloWorld =>
libs/armeabi-v7a/HelloWorld 
[armeabi] Compile thumb  : HelloWorld <= helloworld.c 
[armeabi] Executable     : HelloWorld 
[armeabi] Install        : HelloWorld => libs/armeabi/HelloWorld
[x86] Compile        : HelloWorld <= helloworld.c 
[x86] Executable     : HelloWorld 
[x86] Install        : HelloWorld => libs/x86/HelloWorld 
[mips] Compile        : HelloWorld <= helloworld.c 
[mips] Executable     : HelloWorld 
[mips] Install        : HelloWorld => libs/mips/HelloWorld

Nous obtenons donc les différentes versions de l'exécutable pour chaque architecture cible supportée par le NDK.

Le test s'effectue simplement par :

$ adb push libs/armeabi-v7a/HelloWorld /data 
179 KB/s (9500 bytes in 0.051s) 
$ adb shell /data/HelloWorld 
Hello world (Android.mk) !

Android et POSIX

L'exemple précédent fonctionne mais ce n'est qu'un simple programme « Hello World ». Android n'utilise pas les mêmes composants que GNU/Linux en particulier au niveau de la bibliothèque libC qui est à la base de tous les développements C/C++. GNU/Linux utilise la GNU-libC particulièrement performante et conforme au standard POSIX (Portable Operating System Interfaces for uniX ) démarré en 1988. Contrairement à GNU/Linux, Android utilise BIONIC, une libC beaucoup plus légère mais également moins complète que la GNU-libC par rapport au standard POSIX.

Le but initial de POSIX était de standardiser le code source des applications afin qu'il fonctionne après compilation sur les différentes versions d'UNIX de l'époque (Solaris, HP/UX, IRIX, AIX, …) qui tournaient sur des architectures matérielles différentes (SPARC, PA-RISC, RS/6000, …). Outre les API de programmation (libC, threads, compteurs, etc.), POSIX standardise également l'interpréteur de commande « shell » (IEEE Std 1003.2-1992). L'utilisation de POSIX est désormais étendue à de nombreux systèmes n'ayant rien à voir avec UNIX, dont la grande majorité des RTOS comme VxWorks, LynxOS, RTEMS, etc. qui proposent cette API en plus de leur API « propriétaire ». L'intérêt de POSIX est évident puisque l'effort de portage du code source sera quasiment nul lors du passage d'un système compatible à un autre.

Android n'est pas réellement un « UNIX like » et comme nous l'avons déjà dit la majorité du framework fourni par Google est écrit en Java. Le système est donc faiblement teinté POSIX. En particulier, BIONIC est beaucoup plus légère que la GNU-libC. Ce point est facilement vérifiable en comparant la taille de BIONIC avec celle de la GNU-libC fournie par le compilateur croisée CodeSourcery. BIONIC est en effet presque 6 fois plus légère.

$ adb shell ls -l /system/lib/libc.so 
-rw-r--r-- root     root   306444 2014-02-13 11:13 libc.so 
$ ls -l ~/arm-2013.05/arm-none-linux-gnueabi/libc/lib/libc-2.17.so 
-rwxr-xr-x 1 pierre pierre 1704731 Apr 30  2013
/home/pierre/arm-2013.05/arm-none-linux-gnueabi/libc/lib/libc-2.17.so

Le fichier docs/system/libc/OVERVIEW.html fourni avec le NDK est suffisamment clair concernant les objectifs et les limitations de BIONIC :

« The core idea behind Bionic's design is: KEEP IT REALLY SIMPLE.

This implies that the C library should only provide lightweight wrappers

around kernel facilities and not try to be too smart to deal with edge cases »

Le suite de ce document décrit les limitations de BIONIC en particulier au niveau du support des threads POSIX. A titre d'exemple, la primitive pthread_cancel() très fréquemment utilisée dans les programmes C/C++ n'est pas supportée par BIONIC.

$ ~/Android/android-ndk-r9c/ndk-build 
[armeabi-v7a] Compile thumb  : cancel <= cancel.c 
jni/cancel.c: In function 'threadfunc': 
jni/cancel.c:21:26: error: 'PTHREAD_CANCEL_DISABLE' undeclared
(first use in this function) 
jni/cancel.c:21:26: note: each undeclared identifier is reported
only once for each function it appears in 
jni/cancel.c:34:30: error: 'PTHREAD_CANCEL_ENABLE' undeclared
(first use in this function) 
jni/cancel.c: In function 'main': 
jni/cancel.c:69:17: error: 'PTHREAD_CANCELED' undeclared (first use
in this function)

Il existe sur Internet de nombreux programmes permettant de tester la compatibilité POSIX d'un système. Nous avons utilisé un programme relativement simple nommé posixtst et dont la référence est disponible en bibliographie. Si nous compilons le programme en utilisant le NDK, l'exécution sur l'émulateur Android indique de nombreuses lacunes dans la compatibilité POSIX. Ces mêmes points sont parfaitement traités par GNU/Linux.

POSIX.4 ASYNCHRONOUS IO not Supported. 
POSIX.4 MEMLOCK RANGE not Supported. 
POSIX.4 MEMORY PROTECTION not Supported. 
POSIX.4 MESSAGE PASSING not Supported. 
POSIX.4 PRIORITIZED IO not Supported. 
POSIX.4 REALTIME SIGNALS not Supported. 
POSIX.4 SEMAPHORES not Supported. 
POSIX.4 SHARED_MEMORY_OBJECTS not Supported. 
...

Outre la libC, Google n'utilise pas – parfois à raison - certains composants fondamentaux d'UNIX comme les IPC System V (Inter Process Communication) qui sont remplacés par BINDER qui provient initialement du système d'exploitation BeOS. Le document docs/system/libc/SYSV-IPC.html explique les raisons du choix de Google.

Bref, si quelques programmes de test posent déjà des problèmes de compatibilité, il paraît donc assez difficile d'assurer la compatibilité POSIX d'un programme réel souvent constitué de centaines de milliers de lignes de code, sans compter les évolutions au fil des différentes versions. Nous pouvons ajouter à cela le point concernant le fichier Android.mk. En effet, ce format est totalement spécifique à Android qui ne fournit aucun support pour des outils standards comme GNU/Autotools ou CMake. Il est possible d'utiliser ces outils mais cela au prix de manipulations douteuses.

Utilisation d'une chaîne externe

Nous avons pu constater qu'Android était très différent de GNU/Linux au niveau des composants de l'espace utilisateur. Android utilise par contre un noyau Linux disposant du support des appels systèmes y compris ceux non utilisés par BIONIC. Google a en effet ajouté quelques pilotes spécifiques dans drivers/staging/android (Binder, Ashmem, …) mais n'a bien entendu rien retiré des fonctionnalités standards. Il est donc possible d'utiliser une chaîne de compilation croisée basée sur GNU-libC puis d'exécuter les programmes ainsi produits sur une cible Android. Le « seul » problème est la disponibilité des bibliothèques d'exécution sur la cible ( libc.so, libm.so, …). Il faudra donc les installer en plus des bibliothèques Android déjà présentes sur /system/lib, par exemples sur /lib. Dans le cas de ce court article, nous pouvons résoudre simplement le problème en compilant les programmes avec l'option -static.

Si nous reprenons l'exemple de pthread_cancel(), le programme compilé avec CodeSourcery se compile parfaitement et fonctionne de plus sur la cible Android.

$ arm-none-linux-gnueabi-gcc -static -o cancel cancel.c -lpthread 
$ adb push cancel /data 
3481 KB/s (741236 bytes in 0.207s) 
$ adb shell /data/cancel 
Create thread using the NULL attributes 
pthread_create= 0 
Entered secondary thread 
Secondary thread is looping 
I'm still here ! 
Secondary thread is looping 
I'm still here ! 
Secondary thread is looping 
I'm still here ! 
Cancel the thread 
pthread_cancel= 0 
Secondary thread is looping 
I'm still here ! 
Secondary thread is looping 
I'm still here ! 
Cancel state set to ENABLE 
Secondary thread is looping 
Inside cleanup handler 
Main completed

De même, après une modification mineure, on peut compiler le programme posixtst.c avec CodeSourcery et vérifier que le support manquant dans BIONIC est bien disponible si l'on utilise la GNU-libc et le noyau Linux/Android.

$ arm-none-linux-gnueabi-gcc -static -o posixtst_arm posixtst.c 
$ adb push posixtst_arm /data 
3309 KB/s (689980 bytes in 0.203s) 
$ adb shell /data/posixtst_arm
…
POSIX.4 ASYNCHRONOUS IO Supported. 
POSIX.4 MAPPED FILES Supported. 
POSIX.4 MEMLOCK RANGE Supported. 
POSIX.4 MEMORY PROTECTION Supported. 
POSIX.4 MESSAGE PASSING Supported. 
POSIX.4 PRIORITIZED IO Supported. 
POSIX.4 PRIORITY SCHEDULING Supported. 
POSIX.4 REALTIME SIGNALS Supported. 
POSIX.4 SEMAPHORES Supported. 
POSIX.4 FSYNC Supported. 
POSIX.4 SHARED_MEMORY_OBJECTS Supported. 
POSIX.4 SYNCHRONIZED IO Supported. 
POSIX.4 TIMERS Supported.

L'utilisation d'une chaîne externe pose cependant un problème de compatibilité avec le reste du système. En effet, une application Android (Java ou non) utilise BINDER dont le support n'est pas disponible par défaut dans la chaîne externe basée sur GNU-libC. De même les IPC System V ne sont pas accessibles depuis le framework Android. Une solution élégante est proposée par Karim Yaghmour dans son ouvrage Embedded Android (voir bibliographie). Il suffit en effet d'utiliser une API de communication commune aux deux environnement et l'utilisation de l'API socket est un bon choix dans ce cas. Les deux applications (AOSP et GNU-libC) pourront donc communiquer grâce aux appels système du noyau Linux.

Figure1. Communication AOSP/GNU-libC (schéma Karim Yaghmour)

Conclusion

Cet article nous a permis d'introduire le développement en C/C++ sous Android dans l'optique de portage de code existant vers cet environnement. Dans un prochain article, nous évoquerons les possibilités d'utiliser sous Android des programmes temps réel grâce à des « patch » du noyau Linux comme Xenomai ou PREEMPT-RT.

Bibliographie

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.