Communiquer avec D-Bus en Java avec JNIDBus

Posté par (page perso) . Édité par SeekDaSky, Davy Defaud, Ysabeau, Xavier Claude, Nÿco et BAud. Modéré par Ysabeau. Licence CC by-sa.
Tags :
30
24
sept.
2019
Java

Avec mes collègues chez Viveris, on s’est dit qu’on aimerait bien faire plus de logiciel libre. On a donc monté un « groupe opensource » dont le but est d’identifier les projets pour lesquels on peut publier tout ou une partie du code sous licence libre, et aussi de contribuer aux outils et bibliothèques qu’on utilise le plus.

Il y a quelques mois je vous présentais QTestFramework, depuis on a également pu contribuer au dissecteur 0MQ pour Wireshark et un outil pour le boundary scan JTAG.

On vient de publier il y a quelques jours une bibliothèque Java pour communiquer en D-Bus.

Sommaire

Contexte

D-Bus

D-Bus est un système de communication inter-processus utilisé sous GNU/Linux. Le projet a été lancé par des développeurs de Red Hat au sein de Freedesktop. Il a été intégré dans GNOME 2 et KDE 4, et aujourd’hui il est utilisé par de très nombreux composants d’un système GNU/Linux : systemd, NetworkManager et PulseAudio, par exemple. Il y a même une implémentation dans le noyau Linux lui‑même.

JNI

JNI (Java Native Interface) est une API qui permet d’interfacer du code tournant dans une machine virtuelle Java avec du code natif. Cela nécessite d’écrire (en C ou C++) des wrappers qui vont manipuler la pile de la JVM pour récupérer les arguments et pousser les valeurs de retour, et éventuellement accéder aux objets Java manipulés. Les méthodes ainsi implémentées peuvent ensuite être appelées depuis le code Java de façon transparente.

Solutions existantes et leurs limitations

Freedesktop propose DBus-Java, mais il n’y a pas eu de version publiée depuis 2009. La dernière version a besoin de Java 7 pour fonctionner. De plus, cette bibliothèque implémente le protocole D-Bus en Java, ce qui risque de poser des problèmes d’interopérabilité avec l’implémentation en C.

Il existe bien une version mise à jour de la bibliothèque qui corrige au moins le premier problème, cependant l’API n’utilise pas les nouvelles fonctionnalités de Java et c’est bien dommage.

JNIDBus

Historique

Dans le cadre d’une migration d’un de nos logiciels depuis Java 7, nous avons découvert que DBus-Java ne prenait pas en charge les versions plus récentes. Nous aurions pu nous contenter d’une mise à jour de cette implémentation, mais il y avait beaucoup de code à reprendre dedans et de toute façon, l’API ne nous convenait pas.

En effet, dbus-java représente les messages D-Bus par des « tuples » génériques, ce qui est assez peu pratique à utiliser et rend le code illisible. De plus, les API sont bloquantes et cela nous contraignait à utiliser une réserve (pool) de fils d’exécution qui complexifiait encore le logiciel.

L’ensemble des ces défauts et l’absence d’alternative viable nous ont poussé à développer notre propre alternative, en essayant de répondre a toutes les problématiques.

Afin de ne pas réimplémenter le protocole D-Bus nous voulions utiliser la bibliothèque libdbus-1. L’écosystème Java possède deux manières d’appeler du code natif : JNI et JNA, ce dernier étant écarté pour des raisons de performances et de complexité des bibliothèques de liaison (bindings) à écrire.

Enfin, la dernière contrainte était de réduire le code natif au strict minimum, afin de limiter la complexité de ce dernier qui est très difficile à tester unitairement.

Utilisation

La base de JNIDBus est la sérialisation d’objet Java. La signature du message est décrite dans une annotation.

Exemple pour un message contenant une chaîne de caractères et un entier :

@DBusType(
    /* Pour plus d’info sur le format de la signature, référez vous
     * à la documentation D-Bus
     */
    signature = "si",

    /* Donne le nom des propriétés contenant les données du message,
     * dans notre cas la chaîne de caractères est contenue dans la
     * propriété nommée « string » et l’entier dans le champs « integer »
     */
    fields = {"string","integer"}
)
public class StringMessage extends Message {
    /* Les champs seront accédés au travers de ses setters et
     * getters qui devront respecter la convention "setXxx"/"getXxx"
     */
    private String string;
    private int integer

    public String getString() { ... }
    public void setString(String string) { ... }

    public int getInteger() { ... }
    public void setInteger(int string) { ... }

}

L’appel de méthodes distantes est transparent grâce a l’utilisation de proxy Java, il suffit de décrire l’interface de l’objet et de donner le nom de son bus pour pouvoir l’appeler. Toute méthode distante retourne un PendingCall auquel on doit attacher un listener pour être notifié de l’arrivée du résultat.

JNIDBus permet également d’exposer des méthodes distante par le biais de handlers. Un handler est simplement une classe annotée décrivant et implémentant les méthodes distantes, les signatures sont inférées grâce aux types d’entrées et de sorties. Les handlers seront exécutés dans la boucle d’évènements, il est donc primordial d’éviter tout appel bloquant ou toute tâche lourde. Afin de tout de même pouvoir effectuer ces tâches lourdes, les handlers gèrent un type de retour asynchrone (Promise).

Exemple d’un handler pour un signal et un appel :

@Handler(
    /* pour plus d’information référez vous à la documentation D-Bus
     */
    path = "/some/object/path",
    interfaceName = "some.dbus.interface"
)
public class SomeHandler extends GenericHandler {
    @HandlerMethod(
        //le nom exposé a D-Bus peut être différent du nom de la méthode Java
        member = "someSignal",
        type = HandlerType.SIGNAL
    )
    //Ici notre signal n’a aucun paramètre, on utilise donc le singleton EmptyMessage
    public void someSignal(Message.EmptyMessage emptyMessage) { ... }

    @HandlerMethod(
        member = "stringSignal",
        type = HandlerType.METHOD
    )
    public SomeOutput someCall(SomeInput input) { ... }
}

Comment enregistrer le handler auprès de D-Bus :

//connection au bus
Dbus receiver = new Dbus(BusType.SESSION,"my.bus.name");
//instanciation
SomeHandler handler = new SomeHandler();
//ajout, JNIDBus lancera une exception si le handler n’est pas valide
this.receiver.addHandler(handler);

Le langage Kotlin est pris en charge par le biais d’un artefact Gradle supplémentaire définissant des extensions qui suspendent l’appel de fonction et offrant la possibilité d’avoir des handlers qui facilitent la mise en place de cette suspension :

//il faut enregistrer la classe qui se chargera d’invoquer les méthodes qui suspendent
KotlinMethodInvocator.registerKotlinInvocator()

@Handler(path = "...", interfaceName = "...")
class CallHandler : KotlinGenericHandler() {

    @HandlerMethod(member = "suspendingCall", type = MemberType.METHOD)
    suspend fun suspendingCall(emptyMessage: Message.EmptyMessage): SingleStringMessage {
        //coroutines are awesome
        delay(2000)
        return SingleStringMessage().apply { string = "test" }
    }
}

Aller plus loin

  • # libdbus-1

    Posté par (page perso) . Évalué à 7 (+6/-1).

    Afin de ne pas réimplémenter le protocole D-Bus nous voulions utiliser la bibliothèque libdbus-1.

    Pourquoi cette implémentation ?

    Elle est certes historique, mais les développeurs eux-même le déconseillent dès la première ligne :

    If you use this low-level API directly, you're signing up for some pain.

    Sinon, avez-vous demandé à apparaître dans la liste officielle des bindings ?

    • [^] # Re: libdbus-1

      Posté par (page perso) . Évalué à 6 (+4/-0).

      C'est effectivement une API bas niveau, ce qui serait pénible pour l'utiliser directement dans une application, mais qui est plutôt intéressant pour construire une API Java au-dessus en prenant en compte les besoins de nos applications. C'est souvent difficile de prendre une API haut niveau d'un langage pour l'exposer directement dans un autre.

      Pour la liste officielle des bindings, je m'en occuperai à mon retour de vacances la semaine prochaine.

  • # libdbus et multithreading

    Posté par (page perso) . Évalué à 3 (+1/-0).

    Salut, la dernière fois que j'avais eu à utiliser libdbus, j'avais noté que la bibliothèque n'était pas thread-safe:
    https://lists.freedesktop.org/archives/dbus/2013-July/015727.html

    En 2013, Simon McVittie de Collabora écrivait :
    https://lists.freedesktop.org/archives/dbus/2013-July/015728.html

    The executive summary is "threads + libdbus = anyone's guess".
    (…)
    I'm not convinced that it's possible to give libdbus well-designed
    multi-threading without a redesign and API break, at which point you
    might as well use GDBus instead.

    Est-ce que ça a changé entre temps ? Je suppose que le fait de ne pas être thread-safe n'est pas un soucis si libdbus est utilisé dans un seul thread.

    Quand j'avais regardé, l'implémentation de libdbus est un mélange entre code bloquant et code asynchrone assez surprenant.

    De mémoire, GDBus est une autre implémentation écrite proprement sous forme de code asynchrone avec l'event loop de la glib. Ca devrait moins poser de soucis de multithreading.

    • [^] # Re: libdbus et multithreading

      Posté par (page perso) . Évalué à 3 (+1/-0).

      Nous avons notre propre thread "event loop" qui centralise les accès à dbus. On s'arrange pour ne pas faire de traitements trop longs dedans, seulement distribuer les évènements vers d'autres threads.

      Il s'agit d'un thread Java qui appelle via JNI les fonctions de la libdbus native, ce qui fait qu'on a peu de code natif à maintenir.

      Utiliser GDBus nous aurait contraint à une boucle d'évènement native basée sur la GLib, et du coup à faire beaucoup plus de choses en C ou C++ (langages que j'apprécie beaucoup, mais là on parle d'un projet en Java). Avec JNIDbus, un projet Java peut utiliser DBus sans avoir besoin d'ajouter aucun code natif et de façon plutôt confortable (puisque les messages sont automatiquement convertis en objets Java). Je ne suis pas certain qu'on serait arrivé à la même chose avec une boucle d'évènement native, je pense que chaque application aurait nécessité d'intervenir dessus et du coup on ne pourrait pas vraiment dire que c'est une API Java s'il faut écrire du JNI pour s'en servir.

  • # implémentation java

    Posté par . Évalué à 2 (+1/-0).

    De plus, cette bibliothèque implémente le protocole D-Bus en Java, ce qui risque de poser des problèmes d’interopérabilité avec l’implémentation en C.

    Je comprends le point de vu, mais je trouve tout de même ça dommage. Au contraire la multiplication des implémentation aide à l’interopérabilité et l'utilisation d'une implémentation Java permet d'avoir un déploiement plus simple. Mais je comprends qu'arriver au même résultat en réimplémentant le protocole doit être une gageure.

    • [^] # Re: implémentation java

      Posté par . Évalué à 3 (+3/-0).

      Bien que je sois d'accord sur la diversité d'implémentation, les contraintes de temps (en nos connaissances limitées du protocole) ont fait que re-implémenter n'était pas une option. De plus Java n'est pas capable nativement d'exploiter les sockets UNIX, donc au final une re-implémentation aurait tout de même du utiliser du JNI, ça n'aurait donc pas facilité le déploiement de toute manière.

    • [^] # Re: implémentation java

      Posté par (page perso) . Évalué à 1 (+1/-1).

      Lors d'un projet où nous utilisions dbus pour faire communiquer java et erlang, l'implémentation la lib java gérait mal les problèmes d'endianness. De mémoire, alors qu'un message dbus indique au début l'endianness, cette info était tout simplement ignorée.

      Ceci dit, réimplémenter DBus en pur erlang a été extrêmement formateur pour ma part :)

  • # En pratique

    Posté par (page perso) . Évalué à 3 (+1/-0).

    En pratique, qu'est ce qu'il est pratique de faire du point de vue d'une application Java avec D-Bus?
    J'imagine que le cas d'utilisation doit être plus intéressant que d'afficher une notification dans Gnome, non?

    • [^] # Re: En pratique

      Posté par . Évalué à 3 (+2/-0).

      Sous Linux, les drivers Bluetooth/BLE utilisent D-bus ; c'est le seul moyen d'interragir avec eux (pour scanner les devices, se connecter, etc.)

    • [^] # Re: En pratique

      Posté par . Évalué à 1 (+0/-0).

      Tu peux utiliser linterface MPRIS pour controler un player audio par exemple, comme rhythmbox. Moi je controlais dleyna renderer, c etait cool

    • [^] # Re: En pratique

      Posté par . Évalué à 1 (+0/-0).

      Tu peux faire du RPC à pas cher, relativement rapidement et facilement utilisable par d'autres programmes/scripts sur ton système. Pour les exemples donnés dans les autres commentaires (et pleins d'autres choses), c'est vraiment approprié.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.