Build system modulaire avec Make

Make est un outil formidable pour construire votre projet logiciel mais il est souvent difficile d’allier un système de construction simple et performant. Bien entendu, l’architecture présentée ici ne règlera pas tous les cas possibles mais a pour but d’apporter une solution pour des problématiques souvent recherchées.

Cahier des charges

Afin de bien délimiter les contours de notre build system, c’est-à-dire ce qu’il fait et ce qu’il ne fait pas, listons quelques propriétés fondamentales désirées.

Système modulaire

Votre projet aura tout intérêt à s’architecturer autour d’une organisation de code modulaire, ou par composants. Chaque fonction est ainsi bien détourée, indépendante et accessible via des interfaces agnostiques. Il y a plusieurs avantages à cela :

  1. Vos composants seront réutilisables
  2. Les changements seront moins lourds et potentiellement moins invasifs (pas de code spaghettis)
  3. Il est plus facile de tester un module indépendant, surtout avec des outils automatisés

Bien entendu cela est de la théorie, en pratique certains composants sont très fortement liés entre eux et il est facile, au cours d’un gros développement, de “casser” cette belle architecture. Il faudra donc prévoir beaucoup de tests et de revue de code pour éviter cet écueil.

Notre architecture logicielle par composant se matérialisera par un répertoire pour chaque module logiciel. Ce répertoire doit pouvoir être délocalisable, c’est-à-dire qu’il doit pouvoir se trouver à n’importe quel endroit d’une arborescence sans le modifier, notamment les chemins où se trouvent les fichiers sources.

Multi-cibles

Un projet logiciel aura très souvent besoin de générer plusieurs fichiers de sorties à partir d’un même ensemble de code source. Dans le développement sur cible embarqué par exemple (langage C/C++), on trouve ainsi :

  • Un ou plusieurs exécutables (l’application)
  • Un bootloader
  • Un exécutable d’usine (tests, injection de clés de sécurité ou de paramètres spécifiques)
  • Un exécutable pour les tests automatisés (Jenkins …)
  • Un exécutable de simulation (BSP simulée ou fonctionnant sur ordinateur hôte)

Dès lors, vu que les cibles vont partager beaucoup de code en commun, il faudra un système de construction lui aussi modulaire capable de récupérer différents modules logiciels pour une cible donnée.

Outils et systèmes variés

Lors d’un développement de logiciel embarqué, on va utiliser un compilateur dit croisé, c’est-à-dire qu’il fonctionne sur un ordinateur hôte pour une cible matérielle différente. Exemple: on construit sur Linux (x86_64) un programme pour Arduino (AVR 8 bits).

Or, il est souvent rusé de pouvoir faire fonctionner ce code embarqué sur cible PC, avec les interfaces matérielles simulées qui vont bien. L’avantage est de tester un code dans un environnement complètement différent : compilateur, OS, matériel. Si votre BSP (Board Support Package) est bien détournée (indépendante et aux interfaces agnostiques), alors vous pouvez également tenter de construire votre application sur une autre microcontrôleur. Par exemple, si votre cible principale est du ARM Cortex-m3, le compilateur IAR et l’OS Segger, tentez de le faire fonctionner sur un PIC32 (MIPS) avec GCC et FreeRTOS…

Si cela fonctionne, alors vous avez parfaitement “wrappé” vos interfaces systèmes et votre code source a été bien nettoyé de warnings. En pratique cela demande vraiment beaucoup de temps et de maintenance, surtout que les compilateurs ont tous leur lot d’extension propriétaire (pragma …) et un format de linkage différent.

Simple!

Enfin, et ce n’est pas un moindre argument, gardons un système de construction simple ! Les systèmes à base de Makefile peuvent finir en véritable monstre impossible à maintenir. Pour y parvenir, ne tentons pas de résoudre tous les problèmes, surtout ceux liés à l’environnement du produit. S’il y a des spécificités, alors celles-ci seront gérées au niveau projet, pas au niveau du moteur.

Ajoutons des surcouches, et évitons d’ajouter des fonctions que très rarement utilisées qui n’apportent finalement pas grand chose. C’est là un bon exercice également, le développeur doit faire l’effort de distinguer, à tout moment :

  • Si la fonction est générique, alors mettons la en standard, sous une forme agnostique (nommage, define ou option générique)
  • Si la fonction est spécifique, la placer dans des fichiers propres au projet

Cela est valable pour les Makefile mais également pour le code source C. Le code spaghetti est généralement le résultat de cela : on retrouve de l’applicatif dans du protocole, du driver dans de l’applicatif… séparer, wrapper et interfacer, ça fonctionne !

Enfin, nous voulons également avoir une certaine simplicité dans l’écriture du makefile d’un module. Un seul fichier, simple à éditer pour ajouter des sources et c’est tout.

Architecture

Il existe deux grandes familles de moteur de construction basé sur Make :

  • Les récursifs : Un makefile principal lance des sous makefile, un process est créé à chaque fois. Avantage: les sous modules peuvent être construits indépendamment, inconvénient: les variables ne peuvent pas être transmises du Makefile enfant au parent
  • Les non-récursifs : le makefile principal inclue tous les sous makefile avant exécution

Nous allons nous orienter vers la deuxième famille de makefile, les non-récursifs. Ayant une petite expérience avec la première famille, il s’avère que les avantages sont peu nombreux par rapport à la complexité générale, un peu plus importante qu’avec les non-récursifs.

Organisation des répertoires

Voici ce que cela donne, en image :

tarotclub

Chaque module, ou composant, est un répertoire. Celui-ci peut être organisé comme il veut à l’intérieur (avec des sous répertoires), même si le mieux est de respecter un certain standard au sein de votre projet (répertoire inc, src, doc etc.). A la racine de celui-ci se trouve notre fichier Makefile inclut par le moteur. Il ne se nomme pas “Makefile” pour ne pas confondre avec le fichier racine du projet, mais “Module.mk”.

Le répertoire “build” à la racine du dépôt contient le build system proprement dit et sera l’emplacement des fichiers de sorties (objets, exécutables, map …).

Le Makefile chapeau est donc situé à la racine. Il faut garder en tête certains standards et habitudes : quelqu’un ne connaissant pas votre architecture aura tendance à machinalement taper “make” tout court dans le répertoire de base pour construire la version “release”. Il faut essayer de garder cette pratique même s’il est tout à fait valable et même conseillé d’ajouter d’autres cibles (make tests, make manuf, etc.).

Fichier module

Intéressons-nous au fichier make de chaque module. Comme on le souhaite, il se veut minimaliste afin de faciliter l’ajout de modules à un projet existant. La première chose à faire est de détecter dans quel chemin nous sommes. Pour cette fonctionnalité, nous avons piqué le code au projet Google Android qui a la même problématique que nous (en plus compliqué).

1
2
3
LOCAL_DIR = $(call my-dir)/

SOURCES += $(addprefix $(LOCAL_DIR),aes.c cipher.c cipher_wrap.c gcm.c)

La deuxième ligne contient l’ajout des fichiers sources à la variable ‘SOURCES’. Le moteur se chargera, en fonction de l’extension des fichiers, de les filtrer et d’appeler le bon programme pour les traiter (l’assembleur pour les .s, GCC pour les .c, G++ pour les .cpp par exemple).

Nous avons rempli notre cahier des charges au niveau de la simplicité, de la modularité et de la relocalisation des modules.

Si le module possède des sous répertoires, il suffit de les ajouter dans la variable ‘INCLUDES’ pour que les fichiers d’en-tête puissent être trouvés. Par défaut, le moteur ajoute chaque module d’un projet comme répertoire d’inclusion à la compilation ce qui permet d’utiliser des fichiers d’un autre module même si cela doit être utilisé avec parcimonie. En effet, il faut à tout pris casser le plus possible la dépendance entre les modules logiciels, sans ça il sera difficile de réutiliser un module sans ses petits copains.

Voici l’exemple avec des sous répertoires :

1
2
3
4
5
6
7
8
9
10
11
local_src= \
MyModule.cpp \
MyUtils.cpp \
ModuleMisc.cpp \
Yeah.cpp

LOCAL_DIR = $(call my-dir)/

INCLUDES +=  $(LOCAL_DIR)include

SOURCES += $(addprefix $(LOCAL_DIR)src/, $(local_src))

Fichier projet

Le fichier projet se nomme “Makefile” et il est préférable de le placer à la racine du répertoire du projet pour le trouver rapidement. Nous allons uniquement regarder les parties intéressantes de ce fichier. Comme nous l’avons expliqué, ce fichier peut être vu comme le fichier principal de votre projet, celui qui va décrire les cibles à construire à l’aide des différents modules.

Une des premières choses à faire est de déclarer nos modules, ou ensemble de modules si besoin, en fonction de leur emplacement dans l’arborescence. Ici tous les chemins sont relatifs ce qui permet de garder une bonne clarté dans le fichier.

1
2
3
LIB_BASE				:= lib/system lib/database lib/application lib/crypto lib/ip
LIB_CLIENT		  := lib/serial lib/util
LIB_BSP					:= arch/host

Examinons maintenant comment nous allons construire une cible donnée, d’abord il faut la définir tous les modules qui la compose :

1
2
3
4
5
6
7
ifeq ($(MAKECMDGOALS), server)

APP_MODULES 	:= src $(LIB_BASE) $(LIB_BSP) $(LIB_CLIENT)
APP_LIBPATH 	:=
APP_LIBS      :=

endif

Ici nous utilisons la variable ‘MAKECMDGOALS’ de make qui nous indique quelle est la variable utilisée pour construire la cible et ainsi initialiser les variables du moteur de build aux bonnes valeurs. En effet, les variables ‘APP_MODULES’, ‘APP_LIBPATH’ et ‘APP_LIBS’ sont réservées au moteur.

Enfin, on inclue le moteur de build proprement dit, le fichier ‘Main.mk’, puis nous décrivons comment doit se construire la cible finale.

1
2
3
4
include build/Main.mk

server: $(OBJECTS)
  $(call linker, $(OBJECTS), $(APP_LIBS), my_app_name)

Notez que l’on peut spécifier le nom de l’exécutable final en variable d’appel de la fonction ‘linker’. Le système est vraiment très simple d’écriture et permet de jouer avec les variables que vous voulez. Mais il n’est pas encore parfait !

Inconvénients

Dans le cadre d’un projet multi-cibles, il peut être intéressant de séparer le Makefile projet en deux sous fichiers. En effet dans l’exemple présenté ici on ne définit pas de cible par défaut, le ‘make all’. Dans l’idée il s’agit de tout construire, toutes les cibles, alors qu’ici il faut explicitement écrire quelle cible unique nous voulons générer. Dès lors, le Makefile projet principal appellera un autre sous Makefile projet ou l’inclura selon l’astuce trouvée.

Notre moteur est également très lié à GCC, il faut donc que l’on définisse un système de variable d’architecture, passée en paramètre, et des fichiers de configuration dédiés à chaque compilateur (dans le monde embarqué il en existe plusieurs).

Enfin, on tentera de le faire évoluer pour intégrer Qt, un framework que j’utilise beaucoup mais un peu particulier à compiler.

Mais gardez à l’esprit qu’un moteur de construction, même s’il faut éviter de trop toucher ses entrailles une fois stable, est du aussi du code qu’il faudra maintenir et faire évoluer réguièrement à l’instar de tout autre code source.

Conclusion

Nous voici donc dotés d’un système simple, reproductible et extensible pour nos projets modulaires. Nous l’utiliserons dans le cadre d’un prochain petit projet mixant code embarqué et logiciel PC.

Version mobile de TarotClub - partie 3

Je développe depuis quelques années un jeu de tarot libre appelé TarotClub, créé à l’origine pour combler le manque de jeux de cartes sur Linux. Il est pour le moment disponible uniquement sur les trois grands systèmes d’exploitation du monde PC. Cette série d’articles expliquera le cheminement, j’espère logique, du portage du jeu sur mobile mais également jouable sur une page Web.

POC (Proof of Concept)

N’étant pas particulièrement enthousiasmé par les solutions existantes (voir article précédent), je tente, pour Android tout d’abord, un essai complet sans utilisation de framework. Voici le cadre du POC.

La première chose à faire est de se créer un code d’exemple mimant notre architecture. Pour le GUI, réalisons un “Hello, World” classique, l’affichage d’une primitive en WebGL. Nous utilisons la librairie Three.js pour également tenter l’usage d’une librairie tierce. Notre moteur se contentera, en C++, de créer un serveur TCP/IP et attendra la connexion d’un client en utilisant le protocole WebSocket (encore un truc à tester).

Les données d’entrées sont donc :

  • Un code d’exemple de développement natif (NDK), appel d’un code C à partir de Java
  • Un code d’exemple d’utilisation du composant WebView (navigateur embarqué dans les Android)
  • Un max de forums et autres entrées Stackoverflow pour activer et tweeker notre application (accès à des librairies Javascript, activation du Websocket ….)

L’architecture visée est la suivante :

tarotclub

Normalement, cette architecture nous permttra de garder au moins 90% de code similaire entre les plateformes. Le code dédié à une plateforme sera donc : * Du code pour initialiser le widget principal, un WebView * Du code du côté C/C++ pour encapsuler les librairies ICL et TarotCore * Tout un tas de fichiers de configuration (XML, gradle …) afin de paramétrer convenablement le package généré * L’empaquetage des icônes et autres ressources

Concernant la plateforme d’Apple, un peu plus obscure pour moi ne possédant pas d’appareils de la marque, devrait se dérouler convenablement. Le nouveau langage maison, Swift, permet d’appeler du code C directement !

Etape 1: compilation de TarotCore et ICL

Je pars du programme d’exemple du NDK fourni par Google appelé “hello-libs”. Après compilation, impossible de lancer l’émulateur (sous Ubuntu 16.04). Je tente deux choses, je ne sais pas lequel fcontioone mais je l’écris pour l’histoire :

1
2
sudo apt-get install lib64stdc++6:i386
sudo apt-get install mesa-utils

Puis j’édite le script de lancement de AndroidStudio (~/android-studio/bin/studio.sh) et j’ajoute la ligne suivante avant toute autre instruction :

1
export LD_PRELOAD='/usr/lib/x86_64-linux-gnu/libstdc++.so.6'

Maintenant tout se passe bien j’arrive à lancer l’application sur émulateur. Maintenant, on va essayer d’ajouter le code source d’ICL, la librairie d’utilitaires utilisée par TarotClub.

Il y a deux fichiers à modifier : le projet Gradle, essentiellement pour lui dire quelle librairie standard C++ nous allons utiliser et pour passer des paramètres à CMake. Puis, on va modifier CMake pour lui indiquer quels codes sources modifier.

Dans le fichier ‘build.gradle’, on va utiliser la librairie libc++ et le compilateur Clang, tous deux offrent un très bon support de C++0x14 :

cmake {
                arguments '-DANDROID_PLATFORM=android-21',
                          '-DANDROID_TOOLCHAIN=clang',
                          '-DANDROID_STL=c++_shared'
                // Enables RTTI support.
                cppFlags "-frtti"
                // Enables exception-handling support.
                cppFlags "-fexceptions"
                // For TarotClub and ICL, specify the target platform
                cppFlags "-DUSE_UNIX_OS"
            }

Enfin, notre CMakeLists.txt ressemble à cela, nous avons ajouté ici qu’un seul fichier pour nos tests:

#[[
 CMake file to build ICL and TarotClub source files on Android
]]

cmake_minimum_required(VERSION 3.4.1)

# New path definition
set(ICL_DIR ~/git/tarotclub/lib/icl)

# Switch on the C++0x14 standard for the compiler
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED on)

add_library(hello-jnicallback SHARED
            hello-jnicallback.c)

# Add the ICL library and specify the source files to build
add_library(icl SHARED ${ICL_DIR}/util/Util.cpp)


# Include libraries needed for TarotClub lib
target_link_libraries(hello-jnicallback
                      android
                      icl
                      log)

# End of Cmake file

Bingo ! Notre code C++ avancé compile et la librairie semble bien liée à l’exécutable. On bouge notre projet dans l’arboresence du dépôt TarotClub et on le renomme proprement. Maintenant, on continue le travail laborieux d’ajout du code source complet d’ICL et de TarotClub.

Etape 2: un serveur de fichiers HTTP

Comme nous l’avons dit en introduction, le but est de limiter le code dédié à chaque plateforme. Le stockage des ressources étant complètement propriétaire, j’ai décidé de créer mon propre serveur de fichiers en HTTP. Quelque chose de très limité bien entendu, rien à voir avec un serveur Web complet, même embarqué du style Mongoose. Ma librairie ICL possède déjà une classe TCPServer à tout faire, j’ajoute j’ajoute quelques lignes de code pour encoder et décoder des en-têtes HTTP et c’est presque tout.

Les fichiers HTML et associés (CSS, Javascript…) sont passés dans une moulinette générant des tableaux constants en langage C: les fichiers sont donc liés au binaire final. C’est une pratique courante dans l’embarqué, mon domaine d’origine.

void ReadData(const tcp::Conn &conn)
{
    std::string resource = Match(conn.payload, "GET (.*) HTTP");

    std::cout << "Get file: " << resource << std::endl;

    if (resource == "/")
    {
        resource = "/index.html";
    }

    const char *data;
    const char *mime;
    size_t size;

    data = find_embedded_file(resource.c_str(), &size, &mime);

    std::string output;
    if (data != NULL)
    {
        std::cout << "File found, size: " << size << " MIME: " << mime << std::endl;

        std::stringstream ss;

        ss << "HTTP/1.1 200 OK\r\n";
        ss << "Content-type: " << std::string(mime) << "\r\n";
        ss << "Content-length: " << (int)size << "\r\n\r\n";
        ss << std::string(data, size) << std::flush;

        output = ss.str();
    }
    else
    {
        std::stringstream ss;
        std::string html = "<html><head><title>Not Found</title></head><body>404</body><html>";

        ss << "HTTP/1.1 404 Not Found\r\n";
        ss << "Content-type: text/html\r\n";
        ss << "Content-length: " << html.size() << "\r\n\r\n";
        ss << html << std::flush;

        output = ss.str();
    }
    tcp::TcpSocket::SendToSocket(output, conn.peer);
}

Et voilà, notre serveur de fichiers est réalisé en 42 lignes, sans faire exprès. Qui a dit que le C++ était verbeux ?

Etape 3: Paramétrage du WebView

Après avoir ajouté le composant WebView dans le fichier XML ‘activity_main.xml’, on l’initialise et on appelle l’adresse de notre serveur de fichiers:

public class MainActivity extends AppCompatActivity {


    private WebView mWebView;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initialize();

        mWebView = (WebView) findViewById(R.id.activity_main_webview);
        mWebView.clearCache(true);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.loadUrl("http://127.0.0.1:8000");
    }

    static {
        System.loadLibrary("tarotclub_server");
        System.loadLibrary("icl");
    }

    public native void initialize();
}

Essais

A l’heure de l’écriture de cet article l’émulateur Android ne permet pas d’afficher du WebGL dans un composant WebView. Essayons donc directement sur le téléphone :

tarotclub

Par contre, problème, le touch screen ne semble pas fonctionner, je ne sais pas pourquoi pour le moment. J’éditerai cet artcile lorsque j’aurai l’explication technique !

Conclusion

Cest terminé ! Le code dédié à Android a été réduit à son plus strict minimum. Bien sûr il grossira un peu avec l’ajout de paramétrages et peaufinages divers mais le reste, tout le jeu, sera développé en Javascript uniquement.

Pour le stockage des donnés (paramétrage), nous utiliserons le stockage offert par le navigateur, limité en taille mais suffisant pour notre application (essentiellement des fichiers de configuration en JSON).

Version mobile de TarotClub - partie 2

Je développe depuis quelques années un jeu de tarot libre appelé TarotClub, créé à l’origine pour combler le manque de jeux de cartes sur Linux. Il est pour le moment disponible uniquement sur les trois grands systèmes d’exploitation du monde PC. Cette série d’articles expliquera le cheminement, j’espère logique, du portage du jeu sur mobile mais également jouable sur une page Web.

Une plateforme pour les gouverner tous

Notre voyage commence par résumer le cahier des charges de manière plus syntétique, c’est-à-dire sous la forme d’un tableau récapitulatif.

Plateformes cibles

Tout d’abord, voici les différentes variantes et l’usaga des modules actuels de TarotClub :

  Windows MacOS Linux Android iOS WindowsPhone Web (1)
Moteur du jeu (C/C++) X X X X X X -
Outils(2) X X X - - - -
GUI (plateau de jeu) X X X X X X X
GUI (autres, Qt) X X X - - - -
IA (Javascript) (3) X X X X X X X

(1) La version Web ne sera qu’un front-end et fonctionnera en réseau uniquement

(2) Les outils sont l’éditeur de donne, le calcul de points etc. Ils sont utiles mais pas indispensables pour jouer.

(3) Pour la version Web, les scripts d’IA pourront être utiliser par le front-end comme une aide

Priorités

Les priorités sont les suivantes :

  1. Mutualiser le code du GUI (plateau de jeu, cartes …)
  2. Mobile first (gestion des résolutions d’écran)
  3. Pas de langage chelou
  4. S’amuser

Architecture

À la vue du tableau précédent, on peut se poser une question : est-ce que Qt a un intérêt pour les plateformes mobiles ? En effet, le moteur du jeu étant en pur C++ nous avons une certaine liberté. Les outils eux pevent rester en Qt et sont très orientés “Widget desktop” traditionels, tout du moins pour le moment. Une version mobile pour eux seraient aussi envisageable mais je laisse cette partie à d’autres aventures.

Concernant l’architecture, le code C++ tournera en tâche de fond et le dialogue avec le front-end se fera en TCP/IP ; pas besoin de s’embêter avec des wrappers pour les différents langages mobiles (Java, Objective-C).

Voici donc notre première ébauche de diagramme d’architecture.

tarotclub

Nous allons commencer par le haut, choisir la technologie du GUI, pour ensuite terminer par le choix du framework capable d’embarquer tout cela proprement. D’ailleurs la réponse peut être Qt mais nous nous autorisons tout autre technologie séduisante.

Choix du GUI

Désirant mutualiser le code GUI, partons de la plateforme a priori la plus restrictive : le Web. Avec un peu de recherche préalable, on voit que l’écosystème est maintenant à peu près mature pour transformer une appli Web en une application desktop.

Là, il n’y a qu’un seul langage standard, c’est le Javascript. Je n’ai pas de contre indication phylosophique avec ce langage, d’ailleurs je l’utilise pour les scripts d’IA. Je n’aime pas les transpileurs (surcouche qui génère du Javascript) mais si toutefois je change d’avis je sais qu’il en existe de très bons comme le langage TypeScript.

Regardons maintenant ce que nous offre le Web pour afficher des graphismes.

DOM, CSS et JS

Avec un CSS maintenant très puissant en terme de possibilité (animations et transformations), c’est peut-être un bon candidat. De plus, les cartes de TarotClub sont au format SVG, du vectoriel, dont l’interprétation est native pour tous les navigateurs.

Si je poursuis dans cette solution, je pourrais faire tourner le GUI en embarquant un navigateur dans mon application Desktop, comme le fait l’application Slack ou l’éditeur Atom. En effet ils se basent tous sur Electron pour développer une application de bureau avec les mêmes outils qu’un développement web (NodeJS notamment pour la partie exécution de code).

Au niveau du réseau, Javascript peut dialoguer en WebSocket qui est déjà intégré au sein de la stack de TarotClub, donc aucun problème de ce côté là.

Par contre, je dois avouer que faire du CSS ou du HTML s’apparente plus à une punition pour moi. En tout cas sur le papier, je ne vois pas de contre indication générale.

Voici ce que donne notre architecture potentielle :

tarotclub

HTML5 Canvas 2D

Le canvas 2D deu HTML5 offre une solution pour afficher des éléments 2D au niveau du pixel et offre un certain nombre de primitives pour aider le développement, surtout des jeux. Ok c’est un bon candidat, par contre il va falloir créer tout un système de menu et un HUD au dessus du jeu, mais c’est quelque chose d’assez simple à faire (quelques contrôles suffisent comme des boutons ou des checkboxes).

Pour embarquer du code Canvas 2D dans une application desktop, on reprend la même astuce que précédemment. Ou alors, autre astuce avec Qt, on dispose d’un package Canvas2D compatible avec l’API Web. Dès lors, on peut espérer obtenir un package final plus contenu en terme de taille. Par contre, ce n’est pas un environnement Web complet, donc impossible d’effectuer des requêtes DOM ou du WebSocket.

Voici notre nouvel arbre de solutions :

tarotclub

HTML5 WebGL

L’architecture ressemble au Canvas 2D sauf que … ben tout est en 3D! Les avantages et inconvénients sont à peu près les mêmes, voire plus contraignant car il faut, a priori, que la plateforme hôte supporte l’OpenGL.

Je ne mets pas à jour l’architecture, cela n’a pas bougé.

HUD et futures évolutions

Réfléchir sur l’affichage des graphismes du jeu uniquement ne suffit pas. On oublie souvent les accessoires : un système de menu et le HUD (Head Up Display), c’est à dire tout ce qui s’affiche, généralement en 2D, devant le jeu. On trouve tout un tas d’indications, des cartes, des menus …

tarotclub

Non, ce HUD est tout à fait clair, je ne vois pas de problème

Après quelques recherches, il s’avère que le meilleur moyen, lorsque le jeu est affiché sur un Canvas, de réaliser le HUD en HTML, par dessus. Tout simplement. Le gros avantage est que le HTML est exactement fait pour cela, afficher des données, des images et des boutons simplement.

Lorsque qu’un joueur se connecte à un serveur TarotClub, il entre dans un salon de jeu disposant d’un certain nombre de tables de jeux. Il peut alors clavarder (chatter) avec les autres membres, en publique ou en privé. Difficile de réaliser un tel chat en Canvas, généralement limité dans l’affichage des textes et autres … sans compter le travail à réaliser ; il va falloir recréer tout un tas de primitives et les adapter selon les résolutions !

Enfin, sur PC, tous les Widgets sont disponibles en Qt, donc nous ne seront pas bloqués de ce côté là.

Conclusion partielle

Mettons à jour notre architecture de possibilités. On se lance donc dans :

  • L’affichage du plateau de jeu et des cartes sur un Canvas (identique à toutes les plateformes)
  • Le HUD/réseau en HTML/Javascript pour la version Web
  • Le HUD Widgets natifs sur PC, et le réseau en C++

La version mobile sera l’application Web empaquetée avec le moteur du jeu. Tout ça reste bien entendu assez théorique pour le moment, de compliqués essais s’annoncent.

Voici donc l’architecture statique envisagée :

tarotclub

Idéalement, il nous faudrait à cet instant un diagramme de déploiement. Cela permettrait de visualiser les composants à embarquer pour telle ou telle plateforme. Seulement, ayant à ce stage une connaissance très parcellaire des frameworks, il est difficile d’aller plus loin pour le moment. Nous y repenserons après quelques tests, considérons nous pour le moment en phase de faisabilité.

Premiers essais (et échecs)

Avant de se lancer à coprs perdu dans le développement de la GUI, lançons quelques tests pour conforter notre choix et avoir une meilleur visilité sur les différents écosystèmes (ou framework).

Solutions rejetées

Commençons tout d’abord par lister ce que l’on ne veut pas.

Certains me lanceront au visage des Godot Engine, des Löve, Haxe ou autres bidules. Je n’en veux pas ! Ou ils ne gèrent pas le Web, ou ils se basent sur un écosystème totalement non-standard. L’argument OpenSource ne tient pas, car tout code peut très bien tomber dans l’oubli par manque de mainteneur. Javascript n’a pas ce problème. Et accessoirement je ne souhaite pas apprendre un nouveau langage.

Regardons donc ce que nous fournissent quelques solutions existantes, tout du moins celles qui semblent les plus populaires vu la rapidité des évolutions des frameworks dans le Web.

Qt WebEngine

Qt propose depuis plusieurs versions un composant appelé Qt WebEngine embarquant le moteur Chromimum que l’on retrouve dans le navigateur Chrome. Dès lors, n’importe quel programme fonctionnant sur un navigateur quelconque pourra être embarqué au sein d’une application. Attention par contre, impossible d’utiliser le compilateur MinGW car Chromimum ne compile pas avec. Il faudra donc installer Visual Studio Community, la version gratuite du compilateur de Microsoft.

Essayons le programme minimal fourni par Qt ; nous le compilons en “Release” et regroupons dans le même répertoire l’exécutable les DLL nécessaires à son fonctionnement. Voici le résultat :

tarotclub

Gasp, 80 Mo pour un programme qui ne fait rien

Le ticket d’entrée est cher : 80Mo utilisés uniquement par les DLL avec 60Mo uniquement pour le moteur Chromium. On peut espérer que les versions mobiles (iOS et Android) aient une emprunte plus faible car il est possible d’utiliser le moteur WebView natif des plateformes. D’ailleurs sur iOS nous n’avons pas le choix car il est interdit de faire autrement.

Pour une application Desktop, cela peut être acceptable vu la taille des disques durs mais cela fait quand même un peu mal pour un simple jeu de cartes. Venant du monde de l’embarqué où une application complète tient dans une centaine de kilo-octets, ça fait drôle.

Demi-échec. il faudrait pouvoir utiliser le navigateur fournit par le système d’exploitation plutôt que d’en embarquer encore un autre.

Support de GCC dans Android

GCC va être supprimé du NDK d’Android en faveur de Clang. Donc, notamment sur Windows et Linux, le compilateur fourni dans le NDK est GCC en version 4.9. Malheureusement pour moi, cette version ne supporte pas bien la norme C++0x11 et je ne peux pas compiler TarotClub avec cette version.

Donc, je ne peux tout simplement pas utiliser Qt pour la version Android ! Impossible de trouver des informations sur l’évolution de Qt au moment d’écrire cet article, mais de toutes façons Qt devra passer ses librairies Android à Clang tôt ou tard.

Nouvel échec.

Apache Cordova

Lorsque l’on recherche des frameworks mobiles, on tombe tôt ou tard sur “Apache Cordova”, soit tel quel, soit en tant que couche basse d’un framework plus complet (PhoneGap, Ionic…). Alors c’est quoi Cordova ? C’est donc un ensemble d’outils permettant d’abstraire la plateforme cible (PC, iOS, Android) pour le développement d’applicatif utilisant HTML/CSS/Javascript. Bon, ça a l’air d’être conforme à notre cahier des charges.

tarotclub

L’architecture de Apache Cordova

Cordova virtualise un système de fichiers ce qui permet de garder le code actuel (au niveau des fichiers de configuration), les ressources utilisées en lecture sont à charger à l’initialisation. Sur le papier ça va, ça a l’air justifié et assez simple.

Sauf que, il va falloir déployer du C++ en plus du GUI. Après un rapide essai non concluant en essayant de débugguer une application du mon téléphone, je commence à le mettre mentalement de côté. Il faut que je regarde comment lier une librairie C++ et l’appeler, au moins pour démarrer le serveur local, après tout se passe en TCP/IP.

Echec partiel.

CoCos2D-X

Quand je suis tombé sur le diagramme d’architecture, j’ai tout de suite pensé avoir trouvé le candidat idéal :

tarotclub

L’architecture de CoCos2D-X

Sur le papier, ça a l’air parfait. Reste à essayer un exemple simple avec du code C++ pour tester.

Emscripten

Au lieu de chercher une solution hybride (gérant le C++ et le Javascript), une autre possibilité est de transpiler le C++ en Javascript ce que permet Emscripten. Oubliez l’utilisation sous Windows. Sur Linux, j’abandonne rapidement après des erreurs de compilation ne venant pas de mon code, qui lui compile partfaitement avec Clang/LLVM.

Nouvel échec.

L’avenir : WebAssembly ?

On tient le candidat idéal : à partir de (presque) n’importe quelle source, on compile directement en langage optimisé WebAssembly, un espèce d’assembleur pour navigateur. A l’heure de l’écriture de ces lignes, c’est une technologie encore en phase de définition.

Conclusion

Voici donc, finalement, notre diagramme d’architecture qui indique, pour chaque plateforme, les modules utilisés.

Mon objectif de factoriser le code graphique est partiellement atteint. Je garde une certaine déception n’ayant pas trouvé le candidat idéal. Je vais donc me débrouiller par moi même pour déployer le code sur différents systèmes, mon espoir repose toujours sur Qt qui, à terme, est le seul à pouvoir me fournir la solution idéale, simplement, et standard.

Dans tous les cas notre architecture est plus claire, je peux commencer à travailler sur un exemple réel.