Introduction
Dans le précédent article nous avons créé un module minimal « Hello World » afin de tester le nouveau noyau compilé pour Android/AOSP. Cette exemple n'est cependant pas réellement conforme à la réalité puisque dans le cas général on utilise plus souvent un pilote (ou driver) qu'un module. Les caractéristiques d'un pilote sont assez simples à décrire :
-
Un pilote est avant tout un module, i.e. Il est conforme à l'API des modules Linux
-
Contrairement à un simple module, un pilote est accessible depuis l'espace utilisateur par un fichier spécial appelé device node et présent dans le répertoire /dev (exemples : /dev/console, /dev/ttyS0, …)
Le dernier point est n'est pas totalement exact puisqu'il concerne les pilotes en mode caractère ou bloc. La troisième catégorie de pilote (mode réseau) utilise une interface (exemple : eth0) et non un fichier spécial.
Dans le cas de GNU/Linux, un programme POSIX pourra exploiter un pilote après ouverture du fichier spécial (par l'appel système open()). A l'issue de l'ouverture, on pourra – entre autres - effectuer des lecture, écriture, etc. par les appels système read() et write(). A la fin du programme, on fermera l'accès par l'appel système close().
L'approche est très différente sous Android car même si les pilotes noyau sont « identiques » aux pilotes GNU/Linux, la majorité des applications sont écrites en Java, ce qui rend impossible l'accès direct au fichier spécial. Cependant, une rapide exploration du système Android montre que le répertoire /dev existe :
$ adb shell
root@generic:/ # ls -l /dev
-r--r--r-- root root 131072 2014-02-20 05:15 __properties__
crw-rw-r-- system radio 10, 55 2014-02-20 05:15 alarm
crw-rw-rw- root root 10, 60 2014-02-20 05:15 ashmem
crw-rw-rw- root root 10, 61 2014-02-20 05:15 binder
drwxr-xr-x root root 2014-02-20 05:15 block
crw------- root root 5, 1 2014-02-20 05:15 console
...
Comme l'indique le schéma ci-dessous, le principe d'utilisation d'un pilote noyau sous Android est donc quelque peu différent du cas de GNU/Linux car Android impose d'utiliser JNI (Java Native Interface) pour accèder aux couches C/C++.
Figure 1. Architecture Android vs GNU/Linux
Dans cet article, nous verrons donc comment créer une application Java capable d'accès à un fichier spécial en utilisant JNI. Rappelons brièvement que JNI est une fonctionnalité standard de Java permettant d'accéder à des fonctions C/C++ depuis du code Java. Pour utiliser JNI sous Android, il est nécessaire d'installer le NDK (Native Development Kit) dédié au développement C/C++.
Gestion des pilotes sous Android
Au niveau du noyau, la gestion des pilotes est - comme prévu - très similaire à celle de GNU/Linux. Comme nous l'avons vu, le module est chargé dans la mémoire par la commande insmod. Le module peut également être intégré à la partie statique du noyau, comme c'est le cas pour le noyau fourni par défaut avec AOSP. Fonctionnellement cela ne change rien, puisque le fichier spécial sera alors créé au démarrage du système. Notons qu'un téléphone réel sera fréquemment utilisé dans ce mode comme le montre l'exemple d'un NEXUS 4 :
$ adb -s 0076aa569a181282 shell lsmod
/proc/modules: No such file or directory
GNU/Linux utilise le service/démon UDEVd afin de créer automatiquement les fichiers spéciaux dans /dev lors de l'insertion d'un pilote dans le mémoire. Dans le cas d'Android, on utilise un service similaire nommé UEVENTd.
root@generic:/ # ps | grep uevent
root 43 1 576 308 c00c3c0c 000195c0 S /sbin/ueventd
Le programme ueventd utilise les fichiers de configuration /ueventd.*.rc afin de définir les droits d'accès aux fichiers spéciaux lors de la création.
root@generic:/ # cat /ueventd.goldfish.rc
# These settings are specific to running under the Android emulator
/dev/qemu_trace 0666 system system
/dev/qemu_pipe 0666 system system
/dev/ttyS* 0666 system system
/proc 0666 system system
Dans l'exemple qui suit, nous allons utiliser un pilote qui retourne une valeur numérique évoluant au cours du temps (par exemple une température). Le module peut être compilé dans l'environnement AOSP habituel puis copié sur la cible.
$ export ARCH=arm
$ export CROSS_COMPILE=arm-eabi-
$ make
$ adb push temper.ko /data
Après insertion du module, le fichier /dev/temper0 est automatiquement créé et la lecture retourne une valeur numérique.
root@generic:/ # insmod /data/temper.ko
root@generic:/ # ls -l /dev/temper0
crw------- root root 10, 50 2014-02-21 04:30 temper0
root@generic:/ # cat /dev/temper0
5500
Le fichier est créé par défaut appartenant à root avec les droits d'accès 0644. On peut modifier ce comportement en ajoutant cette ligne au fichier de configuration ueventd.goldfish.rc :
/dev/temper0 0666 root root
Dans un premier temps, on peut effectuer l'opération directement avec la commande chmod. Ce point est important car n'oublions pas que les applications Android ne fonctionnent pas en root !
root@generic:/ # chmod 666 /dev/temper0
Utilisation d'une application Java
L'accès au fichier par un shell n'est pas le mode de fonctionnement le plus courant sous Android. Nous allons donc créer une simple application Java capable d'afficher l'évolution de la valeur. Elle dispose d'un bouton Update permettant de lire la nouvelle valeur sur /dev/temper0. Cette application est construite avec le SDK (ADT).
La figure suivant présente l'allure (simpliste) de l'application.
Figure2. Application Java
Comme nous l'avons dit précédemment, il n'est pas possible d'accéder au fichier /dev/temper0 directement dans le code Java. Nous allons donc utiliser JNI pour créer un bibliothèque partagée libtemper.so permettant l'accès à ce fichier.
L'installation du NDK Android se résume à l'extraction d'un fichier .tar.bz2.
$ tar xf -C ~/Android ~/Téléchargements/android-ndk-r9c-linux-x86_64.tar.bz2
Le NDK contient les chaînes de compilation croisées ainsi que les différentes bibliothèques utilisées pour le développement C/C++ (dont la libC Bionic développée par Google). Le sous-répertoire docs contient la documentation HTML des principaux composants fournis. Le répertoire samples contient de nombreux exemples dont un répertoire hello-jni dont est inspiré l'exemple de l'article. L'ajout d'une bibliothèque JNI correspond simplement à un répertoire jni dans les sources de l'application Java. Ce répertoire contient les fichiers suivants :
$ ls -l
total 12
-rw-rw-r-- 1 pierre pierre 742 Nov 13 17:23 Android.mk
-rw-rw-r-- 1 pierre pierre 51 Jan 15 11:00 Application.mk
-rw-rw-r-- 1 pierre pierre 1381 Nov 27 19:26 temper.c
Le fichier temper.c contient le code source de la bibliothèque. Le principe de fonctionnement est très simple car à chaque requête de l'application le fichier /dev/temper0 est ouvert, lu puis fermé. Au niveau du code Java, le nom de la fonction est getTemp(). Coté JNI, le nom de la fonction doit respecter la syntaxe décrit ci-dessous.
#include <string.h>
#include <stdio.h>
#include <jni.h>
#define FILE_PATH "/dev/temper0"
/* Java_<package name>_<class name>_<method name> */
jstring Java_com_example_temper_MainActivity_getTemp ( JNIEnv* env, jobject thiz )
{
char buf[256];
FILE *fp = fopen (FILE_PATH, "r");
if (!fp)
return (*env)->NewStringUTF(env, "No data file !");
memset (buf, 0, sizeof(buf));
fread (buf, 1, sizeof(buf), fp);
fclose (fp);
return (*env)->NewStringUTF(env, buf);
}
Le fichier Android.mk n'est qu'un Makefile adapté au NDK Android et utilisant des macros gmake. Dans notre cas, nous utilisons une macro permettant la production d'une bibliothèque partagée.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := temper
LOCAL_SRC_FILES := temper.c
include $(BUILD_SHARED_LIBRARY)
Le fichier Application.mk est optionnel et permet de paramétrer la compilation. Dans notre cas nous avons spécifié la liste des architectures supportées (toutes).
#APP_ABI := x86
#APP_ABI := armeabi
APP_ABI := all
Pour compiler la bibliothèque, il suffit d'utiliser le script ndk-build si l'on est positionné dans le répertoire de l'application Java (contenant le répertoire jni).
$ ~/Android/android-ndk-r9c/ndk-build
[armeabi-v7a] Compile thumb : temper <= temper.c
[armeabi-v7a] SharedLibrary : libtemper.so
[armeabi-v7a] Install : libtemper.so => libs/armeabi-v7a/libtemper.so
[armeabi] Compile thumb : temper <= temper.c
[armeabi] SharedLibrary : libtemper.so
[armeabi] Install : libtemper.so => libs/armeabi/libtemper.so
[x86] Compile : temper <= temper.c
[x86] SharedLibrary : libtemper.so
[x86] Install : libtemper.so => libs/x86/libtemper.so
[mips] Compile : temper <= temper.c
[mips] SharedLibrary : libtemper.so
[mips] Install : libtemper.so => libs/mips/libtemper.so
Au niveau du code Java de l'application, la classe MainActivity doit contenir le code de chargement de la bibliothèque soit l'appel à System.loadLibrary(). La fonction update_temp() convertit la valeur retournée par getTemp() en fonction des caractéristiques du capteur de température et affecte le résultat au TextView.
public class MainActivity extends Activity {
private static final String TAG = "TEMPerActivity";
private TextView tv;
// Get temp fro JNI interface (C)
void update_temp()
{
tv = (TextView)this.findViewById(R.id.textView2);
Float f = Float.parseFloat(getTemp());
f = (f * 125) / 32000;
String s = String.format("%.1f °C", f);
tv.setText(s);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
update_temp();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
/* Called when the user clicks the Send button */
public void sendMessage(View view) {
update_temp();
}
public native String getTemp();
static {
System.loadLibrary("temper");
}
}
Après compilation de l'application Java dans le SDK, le répertoire bin contient le paquet à installer sur la cible par ADB.
$ adb install bin/TEMPer.apk
Conclusion
Cet article nous a permis d'étudier sur un exemple simple l'utilisation d'un pilote de périphérique depuis une application Java. Dans un autre article, nous verrons comment ce principe de fonctionnement est généralisé dans Android par l'utilisation de la HAL (Hardware Abstraction Layer) utilisée par les services Java standards (Wi-FI, Bluetooth, Lights, …).
Bibliographie