Récemment, j’ai eu l’occasion de travailler sur un wrapper JNI que l’on utilise dans le projet Lithium pour appeler du Java à partir du C++. Non seulement est-ce que j’ai été satisfait du résultat que ça a donné, mais j’ai eu un plaisir fou à le réaliser. Aujourd’hui, je veux partager ce thrill avec vous.
4. JNI (Java to Native Interface)
Java
C, C++,
C#,Fortran,
Haskell
(n’importe quel
language qui
supporte la C
calling
convention)
5. Exemple (Java)
class JniTest
{
/**
Implemented in c++
*/
native boolean nativeMethod(String s);
/**
Called from c++
*/
void javaMethod(int i, float f) {
System.out.println("Hello from native!");
}
static void main(String args) {
new JniTest().nativeMethod("Ni!");
}
static {
System.loadLibrary("MyNativeLibrary");
}
}
6. Exemple (C++)
JNIEXPORT jboolean JNICALL Java_JniTest_nativeMethod
(JNIEnv* env, jobject obj, jstring s)
{
std::cout << env->GetStringUTFChars(s,0) << std::endl;
jclass clazz = env->FindClass("JniTest");
jmethodID m = env->GetMethodID(clazz, “javaMethod", "(ID)V");
std::thread([=]
{
env->CallStaticObjectMethod(clazz, m, 42, "1.0", 3.14);
}).detach();
return JNI_TRUE;
}
Voyez-vous un bug ?
Et pourtant, ce code compile sans erreur et sans warning!
7. Solutions existantes
Générateurs de code
GIWS
JACE
Codemesh JunC++ion
Wrapper + générateur
android-cpp-sdk
Autres (appel de code natif à partir de Java)
SWIG
JNIWrapper
8. L’objectif
Sans générateur de code
Sans fichier de configuration
Sans code d’initialisation
Sans gras trans
myClass.invoke<void(jint, jfloat)>("javaMethod", 42, 1.0);
Type de la function
Sert à générer la signature JNI
ET à valider la liste des
arguments
Les arguments passés à
la fonction
Le nombre et les types sont
validés à la compilation
Le nom de la
méthode Java
Lance une exception si
elle n’existe pas
9. Plan de match
1. Génération de la signature JNI (“(IF)V”)
2. Interface d’invocation (invoke())
3. Invocation (env->CallStaticVoidMethod())
10. Génération de la signature
Projeter (mapper) un type en string
jint -> "I"
(type) (string)
Décomposition du function type
void(jint,jfloat) -> Ret = void
P1 = jint
P2 = jfloat
Bâtir la signature
void(jint, jfloat) -> "(IF)V"
11. Projeter un type en string
Solution: TypeTrait
template<class T>
struct JniTypeTrait;
template<> struct JniTypeTrait<jfloat>
{
static const char* signature() {return "F";}
};
template<> struct JniTypeTrait<jint>
{
static const char* signature() {return "I";}
};
// Exemple
const char* signature_int = JniTypeTrait<jint>::signature();
template<> struct JniTypeTrait<void>
{
static const char* signature() {return "V";}
};
// …ainsi de suite pour les autres types
12. Décomposition du fonction type
Des meta-fonctions ??? Une liste de types ???
typedef boost::mpl::at_c<ParameterTypes, 0>::type P1;
typedef boost::mpl::at_c<ParameterTypes, 1>::type P2;
typedef boost::function_types::parameter_types<F>::type ParameterTypes;
typedef boost::function_types::result_type<F>::type ReturnType;
typedef void(F)(jint, jfloat);
13. Template Metaprogramming
Effectuer des calculs par à la compilation
Les calculs se basent sur des types et des
constantes
Le résultat est une constante ou du code
Turing Complete!
Possible d’effectuer n’importe quelle computation
Pas de variable!
Pas de loops. Seulement de la récursion.
Familiarité avec le paradigme fonctionnel
16. Function Type to JNI Signature
std::string buf;
buf += "(";
buf += ")";
buf += JniTypeTrait<ReturnType>::signature();
[&] (auto t) {
buf += JniTypeTrait<decltype(t)>::signature();
}
boost::mpl::for_each<ParameterTypes>(
);
NB: Les polymorphic lambdas (avec auto) ont été introduits dans
C++14 (qui est actuellement en cours de standardisation). Ils sont
disponibles dansVS2013 CTP, GCC 4.9 et Clang 3.4
18. Invocation (interface)
Rappel de l’enjeux:Vérification compile-time
du nombre et du type des arguments
obj.invoke<void(jint, jfloat)>("func", 1); // Error!
obj.invoke<void(jint, jfloat)>("func", 1, 2.0, 3); // Error!
obj.invoke<void(jint, jfloat)>("func", "1", "2.0"); // Error!
obj.invoke<void(jint, jfloat)>("func", 1, 1.0); // OK
20. Question quiz
Sachant que boost::mpl::at_c<Seq, n> n’est
valide que pour des valeurs de n comprise entre 0
et boost::mpl::size<Seq>::value,
Comment se peut-il y avoir des overloads de
invoke() pour chaque nombre d’arguments sans
erreur de compilation ?
typedef boost::mpl::vector<int> TypeList;
typedef boost::mpl::at_c<TypeList, 2>::type foo; // Error!
21. Réponse: SFINAE
Substitution Failure Is Not An Error
If an invalid argument or return type is formed during the instantiation
of a function template, the instantiation is removed from the overload
resolution set instead of causing a compilation error.
http://boost.org/doc/libs/1_55_0/libs/utility/enable_if.html
Exemple:
int negate(int i) {
return -i;
}
template<class F>
typename F::result_type negate(const F& f) {
return -f();
}
24. Invocation (implementation)
OK, mais ce n’est pas tout!
CallIntMethod()
Et si ReturnType vaut autre
chose que jint ??
auto r = env_->CallIntMethod(object_, method, p1);
env_.checkException();
return r;
25. Invocation (variations sur ReturnType)
typedef JniTypeTrait<typename JniMethod<F>::ReturnType> Trait;
((*env_).*Trait::callFunction())
// pointer-to-member-function type (to simplify callFunction() declaration)
typedef jint (JNIEnv::*CallFunctionType)(jobject, jmethodID, ...);
// return the JNIEnv member function to call to return jint
static CallFunctionType callFunction() {return &JNIEnv::CallIntMethod;}
26. Invocation (cas de ReturnType=void)
Il reste un problème:
auto r = env_->CallVoidMethod(object_, method, p1);
>> ERROR! auto cannot deduce type from void
27. Invocation (cas de ReturnType=void)
template<class ReturnType, class ParameterTypes>
struct Invoker
{
ReturnType operator()()
{
auto r = ((*env_).*CallFunc_)(object_, method_);
env_.checkException();
return r;
}
};
template<class ParameterTypes>
struct Invoker<void, ParameterTypes>
{
void operator()()
{
env_->CallVoidMethod(object_, method_);
env_.checkException();
}
}; Et les paramètres dans tout ça ?
30. Voilà!
Autres fonctionnalités:
Gestion des références (locales vs globales)
Gestion des exceptions
Scope de fonction et de thread
Facilitateur pour les Strings
Surveillez les commits dans Lithium
(git@git.innobec.com:innobec/lithium.git)
obj.invoke<void(jint, jfloat)>("javaMethod", 42, 1.0);
Récemment, j’ai eu l’occasion de travailler sur un wrapper JNI que l’on utilise dans le projet Lithium pour appeler du Java à partir du C++. Non seulement est-ce que j’ai été satisfait du résultat que ça a donné, mais j’ai eu un plaisir fou à le réaliser. Aujourd’hui, je veux partager ce thrill avec vous.
Cette présentation n’est pas une présentation exhaustive du wrapper JNI. Il s’agit plutôt d’un survol partiel avec emphase sur le “comment ça marche” et un prétexte pour présenter certaines technique moderne de programmation c++ qui sont inconnue à plusieurs.
SVP éviter les questions du genre “pourquoi ne pas faire ceci au lieu de cela ?”, qui pourrait faire diverger rapidement la présentation déjà chargée. Je suis disponible pour en parler par après. Tous les commentaires et questions sont d’ailleurs les bienvenus.
JNI permet d’appeler du code natif à partir du Java et appeler du code Java à partir du code C++.
Le premier cas d’utilisation est plus fréquent et plus simple. Il permet à des organisations de rentabiliser un code base natif et l’intégrant à une application moderne Java.
Le second cas est plus rare (parce qu’en général non requis dans le premier cas d’utilisation) et aussi plus compliqué à cause de l’aspect “managed” de la JVM.
La fonction “nativeMethod” est implémentée par du code natif.
La fonction “javaMethod” est appelée par le code natif.
La signature de la fonction est générée par l’outil “javah” qui est inclus avec le JRE.
Il ne reste plus qu’à remplir le corps de la fonction et compiler. Exemples typiques:
Accéder le tableau de caractères d’une string Java
Charger une classe par son nom
Récupérer la méthode à appeler, identifiée par son nom et par sa signature
Appeler la méthode
Mais il y a des bugs dans cet exemple:
Leak de mémoire de la string UTF8
Erreur dans la signature
Erreur dans le nombre et le type des arguments
Mauvaise fonction d’appel (static + return type)
Initialization de l’environnement JNI pour les threads
Passage illégal de l’environnement et des références entre threads
Gestion d’erreurs
La majorité des solutions existantes comporte un générateur de code. C’est bien, mais c’est lourd, surtout dans un petit projet si l’on a qu’un ou deux appels à faire (on se connait: en tant que développeur, on va le faire manuellement plutôt que sortir la grosse machine.)
Voici l’objectif:
Syntaxe succinte, claire et simple
Pur C++
Sure (vérification statique) + exception en cas d’erreur runtime
Sondage:
Piece of cake ?
Possible, mais ne sait pas trop comment ?
What?
Si vous avez répondu b), cette présentation est pour vous!!
Pour cette présentation, je me limite seulement à l’aspect “appel d’une méthode Java en C++”. Il y a d’autres aspects, mais il faut faire des choix.
On commence en douceur…
On définit une classe “JniTypeTrait” pour laquelle on fournit une spécialisation pour int, float, void, etc.
La fonction membre signature() permet d’obtenir la string correspondant au type.
Simple, non ?
Ensuite, il faut extraire les types de la signature de la fonction
Boost.FunctionTypes offre des méta-fonctions qui permettent d’extraire le ReturnType et le type des arguments sont la forme d’une liste de type.
Boost.MPL permet ensuite d’extraire le type de chaque paramètre à partir de la liste avec la méta-fonction at_c<>
Le TMP a été “découvert accidentellement” durant la standardisation du c++. On réalise alors que non seulement on peut faire plein de chose avec les templates, mais qu’on peut faire n’importe quel calcul! (Turing complete-ness).
Comme le langage n’a pas été concu pour ça, la syntaxe est plus bizarre, mais on s’habitue.
Il y a 10 ans, les compilateurs suffoquaient à compiler des meta-programme moindrement complexe. Aujourd’hui, ce n’est plus un problème et l’évolution du langage fait de plus en plus de place à la métaprogrammation.
Tout le monde connait la suite de Fibonacci, alors on va l’implémenter dans un métaprogramme!
Cette façon de faire dite “pattern matching” est typique des langages fonctionnels.
La limitation: les arguments et le résultats sont des constantes à la compilation.
Boost.MPL fournit un algorithme, for_each, qui, comme son pendant STL, invoque un functor pour chaque élément d’une séquence. À la différence que la séquence en question est, dans le cas mpl::for_each, une séquence de types.
Convertir un type en string? Nous avons déjà vu ça! JniTypeTrait ! Trop facile…
Troisième et dernière étape de notre périple…
Voici la solution pour ReturnType=int. Rien de compliqué
La fonction checkException() vérifie qui une exception Java a été lancée et convertie celle-ci en exception C++. On se rappele que JNI est une API C et donc ne supporte pas les exceptions nativement. Les exceptions Java sont donc émulées par des attributs de JNIEnv que le programmeur se doit de vérifier.
On ramène la class de Trait définit plus tôt et on y ajoute une fonction qui permet d’obtenir la fonction JNI à appeler selon le type de valeur de retour. La fonction est retournée sous la forme d’un pointeur vers une fonction membre (dont un typedef est déclaré pour fin de lisibilité).
Impossible, même pour auto, de déclarer une variable de type void (qui, par définition, signifie absence de type)
Dans un premier temps, on refactor le corps de la fonction dans une classe “invoker”
NB: cette définition est complète: il manque le constructeur ainsi que les membres
Dans un 2e temps, on crée une spécialisation partielle pour ReturnType=void
Pas de spécialisation partielle des function templates
Ici, on peut se permettre d’accepter n’importe quoi parce que la validation a été faite au niveau de invoke()
Alors, on utilise simplement un variadic template
Voici un exemple type d’utilisation de variadic templates
L’équivalent de la fonction print en Python.
Il ne faut pas confondre les variadic templates (…) et les VARARGS (…). Les derniers ne sont pas sécuritaires et un héritage du langage C. Les variadic templates, en revanches, sont sécuritaires parce qu’ils n’existent qu’à la compilation.
À noter:
Le template parameter pack n’est pas un type! Il n’est pas manipulable au runtime. En fait, comme les templates, il n’existent carrément plus une fois le programme compilé…
Il est possible d’inclure une expression abitrairement complexe dans l’expansion du pack.