Le module core. (Partie 1)

Cette page est dédiée au module core de ODFAEG, le module core est le coeur de odfaeg est contient un tas de fonctions utilitaires, exactement comme la bibliothèque boost mais en privilégiant le nouveau standard c++14.

Je vais dans cet article tâché de vous présenter le module core de ma bibliothèque.

Le système de fonction de callback de odfaeg.

Il est souvent indispensable d’avoir un système de fonction de callback, celui-ci permet entre autre de pouvoir faire des tableaux sur des pointeurs de fonction, afin d’appeler la bonne fonction de callback, ceci est surtout utilisé dans le cadre de al programmation événementielle, les tableaux sur pointeurs de fonction pourraient être utilisé dans les cas suivant. (Les signaux et le slot de QT, toutes les interfaces du style ActionListener en java, etc…)

Le c++ 98 n’offrait malheureusement pas façon simple de faire un tableau de pointeur de fonctions sur des fonctions générique de même type (c’est à dure des fonctions ayant le même type de retour), heureusement, le c++14 à améliorer cela grâce aux templates variadiques et à la classe std::function!

Les différents type de passages possible :

Il y a différent type de passage possible pour passer des arguments à une fonction, le passage par valeur, le passage par pointeur et le passage par référence.

Il va falloir stocker tout les types dans un tuple pour ensuite les passer à la fonction de callback en effectuant le bon passage.

Pour cela je vais devoir utiliser un wrapper, le wrapper va juste stocker la valeur du pointeur, la référence ou bien la variable pour la passer à la fonction de callback.

Comme il y a plusieurs type de wrapper possible (wrapper sur une valeur, wrapper sur une référence)  je vais devoir utiliser un wrapper générique et une classe utilitaire avec une surcharge de constructeur pour instancier le bon type de wrapper en fonction du type de l’argument template de la fonction de callback, ainsi que une fonction get virtuelle pour passer la valeur de l’argument à la fonction de callback.

Lors du passage d’argument je n’aurai plus qu’à appeler la fonction get sur la classe utilitaire pour retourner un pointeur sur le wrapper générique et lors du passage d’argument à la fonction de callback je n’aurai plus qu’à appeler la fonction virtuelle get du wrapper générique.

J’ai appeler la classe du wrapper générique, IRefVal, ensuite j’ai fais deux sous classes qui dérivent de IRefVal pour tout les types de passage possible : Val pour le wrapper qui va effectuer un passage par valeur et Ref pour le wrapper qui va effectuer un passage par référence.

Ensuite j’ai créer une quatrième classe utilitaire RefVal, qui va choisir le bon type de wrapper à instancier en fonction du type de T.

Le passage par référence en c++ à un foncteur se fait via la classe std::reference_wrapper et son compagnon std::ref.

Il va aussi falloir utiliser une classe de trait pour déterminer le type à stocker dans le tuple en fonction du type du paramètre passé à la fonction de callback. (Doit t’on stocker un wrapper sur un type std::reference_wrapper<T> ou bien un wrapper sur un type T ?)

Voilà maintenant que j’ai un wrapper générique, et une classe de trait, il va être très simple de stocker des valeurs, références et pointeurs dans un tuple pour ensuite les passer à la fonction de callback lors de l’appel du foncteur.

Il faut maintenant un foncteur qui va stocker un pointeur sur notre fonction et appeler le pointeur sur notre fonction en lui passant les valeurs de nos arguments.

Création d’un wrapper générique sur notre foncteur.

Pour créer un foncteur générique en c++14 il existe la classe std::function qui s’utilise comme ceci :

std::function<returnType(C&, ArgType1, ArgType2, …)> f = &C::funct;

C object;

f(object, ArgVal1, ArgVal2, …);

Mais il y a un inconvénient avec cette classe :

En effet, imaginer que l’objet passé a votre foncteur ne soit pas du même type que la classe contenant votre fonction membre, ce qui est possible dans le cadre de l’héritage :

std::function<returnType(D*, ArgType1, ArgType2, …)> f = &D::funct;

D derived;

B* base = &derived;

f(base, ArgVal1, ArgVal2, …);

Le programme va crasher car le type B* est différent du type D*!

Dans ce cas là il faudrait alors faire un wrapper générique, en utilisant du type erasure, et faire un dynamic cast sur notre foncteur lors du passage des arguments à notre fonceur, et renvoyer une exception si le cast échoue.

Afin de récupérer un pointeur sur le type dynamique du foncteur avant d’appeler notre foncteur de type std::function, et si le dynamic-cast échoue, c’est à dire que si les types des arguments sont différents à la construction du foncteur et à l’appel de celui-ci, dans ce cas il faut lancer une exception  à l’exécution.

Il y a aussi, un autre cas ou il faudrait lancer une exception pour éviter un crash, c’est dans ce cas-ci :

std::function<returnType(B*, ArgType1, ArgType2, …)> f = &D1::funct;

D2 derived;

B* base = &derived;

f(base, ArgVal1, ArgVal2, …);

Là, ont envoie un object dont le type dynamique est D2, hors que le foncteur prend l’adresse d’une fonction membre dont le type de la classe est D1, cela va également mener à un crash!

Au lieu de laisse le programme crasher, avec des crash parfois difficile à détecter, il serait bien de lancer un exception dans ses cas là!

Et c’est ce que je fais dans ODFAEG à l’aide de la classe DynamicWrapper!

Je lance donc une exception si les types ne sont pas polymorphiques, ainsi, je n’appelle pas le foncteur de type std::function et le programme ne crash pas!

Ensuite il faut créer une seconde classe utilitaire qui dérive de std::function et qui va choisir le bon constructeur en fonction du cas dans lequel on se retrouve et passé le bon foncteur à la std::function.

Ainsi std::function va appeler l’opérateur() du wrapper dynamique ou bien directement celui de la classe std::function en fonction du cas dans lequel on se retrouve.

Rien de très compliqué donc jusque là.

Maintenant que j’ai un wrapper pour gérer tout les cas je vais devoir faire une classe qui va :

-Contenir le wrapper sur le foncteur.

-Contenir le tuple qui va se charger de stocker les valeurs des arguments pour les passer au foncteur.

-Changer les valeurs pour les paramètres du foncteur. (Pour pouvoir ré appeler le foncteur avec des paramètres différents.)

Cette classe est appelée un delegate. (Car elle délègue le travail à quelqu’un d’autre c’est à dire au foncteur dans notre cas.)

Pour pouvoir utiliser un tableau sur des pointeurs de fonctions générique de même type c’est à dire avec le même type de retour, il va falloir utiliser du type erasure, donc, je vais créer deux classes :

Une classe de base qui va juste prendre le type de retour du pointeur de fonction en paramètre template et renvoyé le résultat de la fonction, en redéfinissant l’operator()() du delegate. (Je vais appeler cette classe Delegate

Une classe dérivée qui va contenir un wrapper sur la fonction ainsi que les valeurs des arguments à passer au delegate. (Je vais appeler cette clase FastDelegate)

Et une classe utilitaire qui va instancier le delegate, et effectuer un cast pour changer les valeurs des arguments de la fonction de callback en fonction du type des paramètres passé au delegate .

Et c’est ici que je gère le 1er cas qui me posait problème, en effet, si le type de l’argument passé au constructeur du delegate est D*, et que le type de l’argument passé à setParams est de type B*, alors le cast échoue et je lance une exception!

Maintenant, il est facile de créer un tableau de pointeurs sur fonctions ayant le même type de retour en utilisant la classe FastDelegate :

O object;

FastDelegate<void> f1 (&O::func, &object);

FastDelegate<void> f2 (&O::func, &object, argVal1, argVal2);

std::map<std::string, FastDelegate<void>> funcPtr;

funcPtr[“func1”] = f1;

funcPtr[“func2”] = f2;

funcPtr[“func1”]();

funcPtr[“func2”]();

funcPtr[“func2”].setParam(ArgV1, ArgV2);

funcPtr[“func2”]();

Ecrit comme celà, vous aller me dire que tout cela est inutile mais vous verrez que cela va vite devenir indispensable dans le cadre de la gestion d’événement.

En plus, comme le delegate gère également les lambdas expressions, on pourra également créer des signaux et des slots sur des fonctions anonyme ce que ne permet pas de faire le framweork QT.

Faire un système de signal et de slot similaire à celui du framework QT.

Maintenant que je peux stocker des tableaux sur des pointeurs de fonctions ayant le même type de retour, je vais pouvoir faire un système de signaux et de slot similaire à celui de Qt, et je vais en profiter pour effectuer une classe pour rendre compatible cela avec les événements SFML, pour cela, je vais juste créer un BSP-TREE qui va pouvoir contenir plusieurs actions, chaque action contiendra un événement SFML, et, une action pourra être soit simple ou combinée et chaque feuille du BSP-TREE contiendra alors un événement SFML.

Il y a quatre sortes de combinaison possible exactement comme pour les conditions dans un bête if : && dans le cas ou l’action doit se déclancher si les deux événements SFML on été généré, || si l’action doit être déclenchée au cas ou un des deux événement SFML, ou bien les deux événements SFML ont été déclanchés, | dans le cas ou l’action doit se déclenché seulement si l’un des deux événements SFML à été déclenché et ! dans le cas ou l’action doit être déclenchée si l’événement SFML n’a pas été déclenché.

J’ai mis le code pour le joystick en commentaire car je ne possède malheureusement pas de joystick pour tester.

Maintenant, il faut une seconde classe, celle-ci va devoir stocker tout les événements SFML qui ont été générés à chaque tout de boucle par la fonction pollEvent, mapper des actions à des slots, testé si l’action est déclenchée et appeler le slot le cas échéant, ainsi que passer les paramètres au slot chaque tout de boucle.

Je vais aussi en profiter pour stocker un signal, un signal n’est rien d’autre qu’un foncteur renvoyant un booléen (vrai si le signal est déclenché, sinon faux), ceci est utile par exemple si vous voulez appeler le slot si la souris est dans une zone particulière d’une fenêtre.

On peut également passer une action, un signal et un slot par exemple si l’on veut appeler une fonction lorsque l’on clic sur un bouton et que la souris se trouve dans la zone du bouton.

Il va donc y avoir 3 constructeurs différents : un prenant une action et un slot, un autre prenant un signal et un slot, et enfin, un dernier prenant une action, un signal et un slot.

Je vais appeler cette classe, la classe Command.

La commande devra juste vérifier si l’action ou bien le signal ou bien les deux sont déclenché en fonction des événements SFML qui lui seront passé, et si oui, appeler le slot.

Il ne reste plus qu’à coder un listener, cette classe va juste servir de contexte pour les différents guis et va connecter les commandes à un ID (ici je vais utiliser un std::string, mais j’aurai pu utiliser n’importe quel type), on a le choix, on peut utiliser un theads pour exécuter les commandes ou bien on peut le faire sans thread.

Au cas ou on utilise un thread, il faut utiliser une primitive de synchronisation pour mettre le thread principal en pause jusqu’à ce que le thread secondaire ai fini de mettre à jour la scène suivante, sinon, vous aller avoir une scène qui se déchire à l’écran.

Il faut éviter les appels à lock trop souvent donc je vais également endormir le thread pendant un petit laps de temps, ceci économisera le CPU et optimisera le code.

Je vais faire une fonction pushEvent pour mettre un événement SFML dans la pile, et une autre fonction pour dire au thread quand il doit gérer les événements, ce qui est bien plus pratique et bien plus performant que le système classique de SFML.

Gestion des placeholders.

Ce système possède encore un défaut, c’est que si plusieurs valeurs sont les mêmes lors de l’appel du slot, il faut les passer plusieurs fois à setParams et si il y a des paramètres qui ont des valeurs par défaut il faut les passer à chaque appel de setParam également.

Pour régler ses deux problèmes, on utilise un système de placeholders, exactement comme le fais std::bind, malheureusement std::bind oblige d’appeler la fonction là ou on a déclarer la fonction de callback, on ne va donc pas pouvoir le ré-utiliser.

Pour adapter notre classe il va falloir changer quelques petites choses :

On va devoir, lors de l’appel à la méthode get de l’interface IRefVal, passer des paramètres supplémentaire, on ne connais pas le types de ses paramètres, il faut donc un type commun, on va donc utiliser un pointeur sur void*, le nombre de paramètres dépendra d’une macro que l’on va définir, voici cette macro :

#define FUN_PARAM void* p0 =nullptr, void* p1 =nullptr, void* p2 =nullptr, void* p3 =nullptr
#define FWD_PARAM p0,p1,p2,p3

La 1ère macro défini juste le nombre de placeholders supportés (4 ici) et le code va être insérer dans la fonction get de l’interface IRefVal.

La secondes macro va juste passer les pointeurs sur void à la fonction get, et ensuite, il faudra faire une conversion du pointeur sur void* vers le type réel. (celui passé au placeholder)

Le placeholder va contenir deux paramètres templates, le 1er est le numéro du placeholder (0, 1, 2, ou 3) et le second le type réel de l’argument à passer)

Ensuite il faut aussi rajouter une fonction bind qui va convertir, chaque placeholder en un nouvel objet de type RefVal.

Il ne faut pas oublier non plus de modifier les classes de traits, pour rajouter le cas avec le placeholder :

A ce stade il ne reste plus qu’à modifier les autres classe, dans la classe FastDelegateImpl, je vais juste rajouté une méthode pour parcourir chaque élément du tuple et appeler la méthode bind dessus. (je vais appeler cette méthode bindParams) :

Il faut aussi modifier l’interface du delegate en rajoutant cette méthode bind :

Et enfin il ne reste plus qu’à modifier la classe utilitaire fastDelegate en rajoutant aussi une méthode bind.

Ensuite il ne faut pas oublier de rajouter une méthode bind dans les classes Command et Listener.

Et voila mon de signal, de slot et d’action est fonctionnel. 😀 (Ce qui est fun c’est que l’on peut créer n’importe quelle gui avec et définir des interfaces pour chaque gui exactement comme en java, par exemple, comme ceci :

class Button {

bool isMouseInButton(sf::Vector2f mousePos) {

//On test si la position de la souris est dans le rectangle du bouton.

}

void setActionListener(ActionListener &al) {

Action a (odfaeg::Action::EVENT_TYPE::MOUSE_BUTTON_PRESSED_ONCE, sf::Mouse::Left);

Command cmd(a, FastDelegate<bool>(&Button::isMouseInbutton, this, sf::Vector2f(-1, -1)),    FastDelegate<void> (&ActionListener::actionPerformed, &al, buttonID);

listener.connect(buttonID,  command);

}

Listener listener;

}

Voilà c’est tout en ce qui concerne le système de signaux, de slot et d’actions de ODFAEG! L’avantage de se système est que l’on pourra passer les événements SFML dans une classe de base application dont pourront hériter toutes les applications ODFAEG, et paramétrer un délai d’attente entre le déclenchement de chaque événement ce qui est quand même important dans un jeux.

Une grande page de l’implémentation du module core de la librairie se tourne, sur la page suivante je vais vous présenter une grand fonctionnalité du module core qui est aussi présente dans la librairie boost : la sérialisation de données!

 

Leave a comment