Mentionner JNI provoque chez de nombreux programmeurs une peur inexplicable. JNI semble suspectement difficile, et à première vue, son mécanisme relève de la magie. Cependant, ceux qui s’y sont penchés de plus près apprécient grandement ses propriétés.
Si vous n’avez pas encore entendu parler de cette technologie, Java Native Interface ou JNI est un mécanisme Java standard qui permet au code Java d’interagir avec le code C et C++. Wikipedia stipule que « JNI permet aux programmeurs d’écrire des méthodes natives pour gérer des situations où une application ne peut pas être entièrement écrite dans le langage de programmation Java, par exemple lorsque la bibliothèque de classes Java standard ne prend pas en charge les fonctionnalités spécifiques à la plateforme ou la bibliothèque de programmes », ce qui signifie que dans une application Android, nous pouvons utiliser une bibliothèque C++ dont nous avons besoin et interagir avec elle directement depuis le code Java, et vice-versa.
Ça sonne bien, n’est-ce pas ?
Voici donc trois raisons pour lesquelles nous aimons JNI :
- JNI rend possibles certains processus qui ne sont pas implémentés en Java. Comme les commandes sensibles au matériel ou les commandes directes de l’API système, par exemple. Pour les développeurs Android, cela ouvre de nombreuses opportunités en dehors de Dalvik. Le code C/C++ compilé fonctionnera sur n’importe quel appareil Java car JNI est indépendant de Dalvik, ayant été conçu pour la JVM.
- La possibilité d’améliorer les performances de l’application grâce à des bibliothèques de bas niveau pour des choses comme les graphiques, les calculs, différents types de rendu, etc.
- Un grand nombre de bibliothèques ont déjà été écrites pour toutes sortes de tâches. Et pouvoir réutiliser le code sans le réécrire dans un autre langage facilite grandement la vie d’un développeur. Ainsi, des bibliothèques populaires comme FFmpeg et OpenCV deviennent disponibles pour les développeurs Android.
Mais examinons cette technologie de plus près. Comme l’a dit Linus Torvalds : « Parler ne coûte rien. Montrez-moi le code. »
Le schéma d’interaction ressemble à ceci :

Pour appeler une méthode C++ depuis Java, vous devez :
- Créer une méthode dans une classe Java
private native void NomDeLaFonction( parametres ); - Créer un fichier d’en-tête et un fichier cpp dans un dossier jni. Il contiendra le code C++ appelé par la fonction native mentionnée ci-dessus.
- Dans l’en-tête, nous définissons sa signature comme suit :
extern "C" { JNIEXPORT void JNICALL Java_mon_package_ClasseAppelsNatives_maFonction(JNIEnv *, jclass); }- extern “C” est requis pour empêcher le compilateur C++ de modifier les noms des fonctions déclarées ;
- JNIEXPORT est un modificateur nécessaire pour JNI ;
- Les types de données avec le préfixe “j” : jdouble, jobject, jstring, etc. – reflètent les objets et types Java en C/C++ ;
- JNIEnv* est une interface pour Java. Elle permet d’appeler des méthodes Java, de créer des objets Java et d’effectuer d’autres opérations Java très utiles ;
- Le deuxième paramètre crucial est jobject ou jclass, selon que la méthode est statique ou non. Si elle l’est, l’argument sera jclass (un lien vers une classe dans laquelle le code est déclaré) et si elle n’est pas statique, ce sera jobject (un lien vers un objet dans lequel la méthode a été appelée).
En réalité, vous n’avez pas à écrire le code manuellement. Vous pouvez utiliser l’utilitaire javah, mais j’ai trouvé plus facile et plus clair de le faire moi-même. La fonction elle-même est réalisée dans un fichier .cpp ;
- Retournez dans Java où nous avons défini la fonction native et ajoutez :
System.loadLibrary( "Nom de la bibliothèque par laquelle nous compilons les fichiers cpp mentionnés ci-dessus" );au tout début du fichier, au-dessus de la déclaration de la méthode native. Un nom de bibliothèque est conservé dans Android.mk.
LOCAL_MODULE:= Nom # Si nous assemblons avec des fichiers .mk - J’aimerais également attirer votre attention sur des fichiers comme Android.mk et Application.mk. Dans Android.mk, nous stockons les noms de tous les fichiers .cpp du dossier jni que nous allons compiler, les indicateurs spécifiques ainsi que les chemins vers les en-têtes et les bibliothèques supplémentaires, en d’autres termes, certains paramètres de liaison et réglages, et d’autres éléments nécessaires à l’assemblage d’une bibliothèque.
Dans Android.mk, nous conservons des paramètres d’assemblage supplémentaires tels que la version de la plateforme requise, le type d’architecture, etc.
Permettez-moi de résumer. Appeler C++ depuis Java :
- Nous créons une fonction avec le modificateur native et l’appelons depuis n’importe quelle méthode Java ;
- Le compilateur Java génère le bytecode ;
- Le compilateur C/C++ crée une bibliothèque dynamique .so ;
- Lorsque nous exécutons l’application, l’appareil Java commence à traiter le bytecode ;
- Lorsqu’il rencontre l’appel loadLibrary, il ajoute un fichier .so au processus ;
- Lorsqu’il rencontre l’appel de la méthode native, il recherche une méthode dans les fichiers .so ouverts par sa signature ;
- Si la méthode est présente, elle sera appelée. Sinon, l’application plante.
Mais que faire si nous devons faire l’inverse ? (appeler une méthode Java depuis du code C/C++)
Parfois, nous devons appeler une méthode depuis un natif Java. Par exemple, lorsqu’il y a une opération de longue durée dans le natif et que nous devons suivre sa progression. C’est ce qu’on appelle un rappel (callback).
La logique d’un rappel :
Le code Java appelle une méthode C++ ->
Une fois traitée, la méthode appelle son SDK et envoie les informations nécessaires. Pour envoyer ces informations à l’application, vous devez faire ce qui suit :
- Dans le dossier jni, vous créez une classe AndroidGUIHandler qui étend IGUIHandler. Ses méthodes reçoivent des paramètres du SDK (wstring et autres), les convertissent dans un format compatible Java et appellent une méthode Java en envoyant ces paramètres.
- (Les méthodes de la classe AndroidGUIHandler n’auront pas de signature et ressembleront à des méthodes C++).
- Avant d’appeler des méthodes Java, vous devez d’abord vous connecter à un thread Java à l’aide d’une classe d’encapsulation (wrapping class). Ensuite, vous devez utiliser une autre classe d’encapsulation pour les rappels. Dans cette classe, en utilisant les méthodes jni GetObjectClass, GetMethodID, vous recherchez les méthodes Java que vous devez appeler (un mécanisme de réflexion est utilisé lors de la recherche de classe et de méthode). Et ensuite, vous appelez des méthodes jni standard comme CallIntMethod, CallVoidMethod pour appeler les méthodes précédemment trouvées depuis Java et leur envoyer toutes les informations nécessaires du SDK.
Et c’est la base de cette technologie. Je dirais que si vous n’aimez pas JNI, c’est probablement que vous ne le connaissez pas assez. Mais, comme toujours, il y a des défauts :
- JNI ne gère pas les exceptions comme NullPointerException ou IllegalArgumentException en raison de la baisse de performance et du fait que la majorité des fonctions des bibliothèques C ne peuvent pas gérer ce type de problèmes ;
- MAIS : JNI permet d’utiliser des exceptions Java. Nous pouvons donc presque annuler ce défaut en traitant le code JNI manuellement et en vérifiant les codes d’erreur, puis en lançant des exceptions vers Java ;
- La difficulté de travailler avec JNI depuis des threads natifs. Pour simplifier l’interaction, vous devez écrire une classe d’encapsulation qui effectuera toutes les manipulations requises ;
- Augmentation de la taille du fichier apk ;
- Transition « coûteuse » du code Java vers le natif et vice-versa ;
- Le débogage du code C++ est un problème en soi ;
- Dans certains cas, travailler avec JNI pourrait être BEAUCOUP plus lent qu’avec un analogue Java ;
- Mais le principal inconvénient de JNI est que tout code natif lie fermement votre application Java à une plateforme particulière. Et son utilisation ruine le concept « Écrire une fois, exécuter partout » (Write Once or Run Anywhere). Et c’est ce que vous devrez gérer.
Et, malgré tous ses défauts (personne n’est parfait), cette technologie est aimée et appréciée.
Découvrez comment nous avons développé un navigateur mobile avec une Omnibox avancée en utilisant Java et C++
