Linux Embedded

Le blog des technologies libres et embarquées

Utilisation de JNI sous Android

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

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.