ReCpp, le futur de l'asynchrone en C++ ?
Bonjour et bienvenue dans ce nouvel article de la Chronique Réactive ! Je vais aujourd'hui vous présenter un projet sur lequel je travaille depuis quelques temps, j'ai nommé ReCpp, une librairie C++ de programmation réactive !
Petits rappel
La programmation réactive est un paradigme de programmation visant au développement de programmes plus robustes et performants.
Pour ce faire la programmation réactive traite les algorithmes comme des flux de données asynchrones où il est possible de spécifier la manière dont le programme doit réagir à ces flux.
Cette approche très déclarative permet ainsi une gestion simple et terriblement efficace de l'asynchrone, ainsi qu'une très haute résilience ce qui en fait un allié de choix pour tous types de programmes.
Plus d'informations dans mon précédent article !
ReCpp
Reactive Streams
ReCpp est tout d'abord basé sur un standard nommé Reactive Streams proposant une série d'interfaces pour faciliter l'interopérabilité entre librairie réactives.
Cette approche permet à un utilisateur averti de rajouter ses propres implémentations et de les faire fonctionner avec ReCpp.
Vous pouvez retrouver l'implémentation C++ par ici !
ReactiveX
ReCpp s'inspire du modèle ReactiveX basé sur le principe d'Observable, un objet représentant un flux de données asynchrone, et plus particulièrement de son implémentation Java proposant également des spécialisations d'Observable offrant certaines garanties sur le flux de données.
ReCpp propose donc :
- Une classe Observable représentant un flux de données asynchrone pour tout type d'utilisation.
- Une classe Completable, spécialisation d'Observable garantissant que le flux ne contient aucune donnée, ce qui est par exemple très utile pour des actions asynchrones ne nécessitant aucun type de retour.
- Une classe Single, spécialisation d'Observable garantissant que le flux contient un unique élément. Cet objet peut être vu comme un std::expected asynchrone.
- Une classe Maybe, spécialisation d'Observable pouvant soit être vide, soit contenir un unique élément.
Ces spécialisations permettent notamment de clarifier le comportement de certaines méthode, par exemple une méthode servant à la destruction d'une ressource pourrait être prototypée de la manière suivante avec RxCpp (implémentation officielle de ReactiveX en C++) :
rxcpp::observable<int> rxDeleteResource(std::string_view resourceId);
Son alternative ReCpp serait :
recpp::rx::Completable rxDeleteResource(std::string_view resourceId);
Cette approche permet à la fois de garantir à la personne utilisant cette méthode que le flux sera vide, lui évite de typer un observable qui ne doit contenir aucun type, mais permet également de lui fournir une API plus claire et lisible (un Completable ne disposera pas de méthodes permettant de manipuler ses données puisqu'il n'en dispose pas).
Gestion de l'asynchrone
ReCpp met également à disposition une série de classes permettant de contrôler le contexte d’exécution des flux de données. Il s'agit de Schedulers, des objets possédants une pile de Schedulables c'est à dire d'actions à effectuer à un instant T. Ces classes sont pour le moment au nombre de 3 et sont les suivantes :
- ThreadPool : Scheduler possédant plusieurs threads indifférenciées, une action poussée sur cet objet sera exécuté par la prochaine thread disponible ce qui en fait un choix idéal pour paralléliser et exploiter au maximum les performances d'une machine.
- WorkerThread : Scheduler possédant une unique thread de travail, cette classe est particulièrement adaptée aux calculs ne pouvant pas être parallélisés et devant s'exécuter sur une unique thread.
- EventLoop : Scheduler le plus permissif ne possédant pas de thread dédiée, il est nécessaire d'appeler directement sa méthode run ou runFor pour exécuter les tâches stockées sur sa pile depuis la thread courante. Cette classe est typiquement adaptée pour un thread principal ou pour l'interopérabilité avec une autre technologie.
Ces différents Schedulers peuvent ensuite être utilisés avec les méthodes subscribeOn et observeOn de la classe Observable et de ses dérivés (Single, Maybe et Completable), ce qui permet un contrôle assez fin du contexte d’exécution des différents événements du flux de données.
Plus d'infos sur la différence entre subscribeOn et observeOn par ici.
Exceptions
Contrairement à d'autres librairies réactives j'ai fait le choix avec ReCpp de ne pas intercepter nativement les exceptions, cette action entraine un surcoût et va (à mon sens) à l'encontre de l'approche fonctionnelle de la gestion d'erreur apportée par la programmation réactive. Avec ReCpp les exceptions doivent être gérées directement là où elles peuvent apparaitre, notamment lors d'appels à des fonctions et méthodes externes comme la stl. C'est pourquoi il est important de concevoir des APIs claires, et c'est également la raison pour laquelle je souhaite mettre à disposition d'autres extensions basées sur ReCpp pour faciliter certaines actions courantes en C++ comme la manipulation de système de fichiers, de réseau ou encore de bases de données.
Néanmoins ce point est susceptible de changer en fonction des différents avis qui peuvent m'être remontés.
Intégration continue et qualité du code
Plusieurs actions automatiques ont également été mises en place afin de garantir la meilleure qualité possible de la librairie et limiter les régressions lors de changements, notamment :
- La compilation sur de multiples plateformes, à l'heure où j'écris cet article il s'agit de Windows, Linux et OsX avec les compilateurs MSVC, GCC et Clang.
- L’exécution de tests automatiques sur toutes les plateformes visées.
- La vérification de la documentation. Cette dernière étant générée à partir du code et déployée automatiquement il est impératif d'assurer qu'aucun changement n'introduit de détérioration majeure de la documentation.
- La vérification de la norme de codage.
Documentation
Une documentation est également disponible à cette adresse et est automatiquement mise à jour lors de changements.
Prochaines étapes
ReCpp n'est pas encore en version stable, ne nombreuses fonctionnalités sont encore manquantes et je souhaite rester à l'écoute de potentielles remarques ou demandes. Une refonte des tests est également prévue afin de les renforcer, ainsi qu'une expérimentation pour trouver une alternative à l'utilisation de pointeurs opaques afin de potentiellement limiter ce surcoût.
Conclusion
J'espère que cette présentation vous aura plu et aura aiguisé votre curiosité sur le sujet, je suis pour ma part convaincu de l'utilité de telles librairies pour créer des programmes plus résilients en C++ et je compte travailler avec ReCpp sur l'un de mes prochains projets : un comparateur de performances de bases de données dans le même esprit que le classement TechEmpower (qui était le sujet de l'un de mes précèdent articles, disponible ici).
Références
- Le repo ReCpp : GitHub - pribault/ReCpp: Reactive Extensions for C++
- Le repo RsCpp : GitHub - pribault/RsCpp: Reactive-Streams implementation for C++ with Technology Compatibility Kit (TCK)
- Le manifeste réactif : Le Manifeste Réactif (reactivemanifesto.org)
- Le standard Reactive Streams : reactive-streams.org
- ReactiveX : ReactiveX
- RxMarbles, site proposant des diagrammes interactifs pour visualiser les différents opérateurs ReactiveX : RxMarbles: Interactive diagrams of Rx Observables