Je travaille chez IsCool Entertainement en tant que développeur C++ sur des jeux pour téléphones portables. Aujourd'hui nous publions une partie de notre code en open source sur GitHub, ce qui est une bien bonne chose à mes yeux. La licence utilisée est Apache 2.
Au risque d'apparaître banal, nous essayons de partager autant de code que possible entre nos jeux sous la forme de divers modules plus ou moins indépendants et plus ou moins spécifiques au domaine. Ceux libérés sont les modules les plus généraux, les utilitaires. Parmi ces 35 modules vous trouverez des choses relatives aux plates-formes mobiles telles que l'émission de notifications, de quoi partager des fichiers nativement ou encore des facilités pour appeler du code Java depuis le C++ et inversement.
Dans les modules destinés à toutes les plates-formes vous trouverez de quoi émettre et gérer des erreurs, construire des factories, un itérateur qui boucle, de quoi écrire des logs, faciliter l'usage de pimpl et bien d'autres choses encore.
Certains modules se présentent comment des interfaces pour un sous-système qui doit être défini avant utilisation. Par exemple le module audio contient des fonctions permettant d'émettre des sons ou de jouer des musiques en relayant l'appel à un sous-système à définir. De même pour le module http qui gère les requêtes mais laisse la gestion de la connexion à un tiers. Ainsi nous gardons une interface unique cohérente sans trop lier nos applications à un outil spécifique.
Certains modules dépendent de bibliothèques tierces. Boost est notamment beaucoup utilisé. Le module Json s'appuie sur JsonCpp et offre de nombreuses fonctions pour manipuler des instances de Json::Value
, notamment un json_cast< T >
pour transtyper vers des types du C++. Le module i18n, en particulier la fonction ic_gettext
, s'appuie sur moFileReader, qui était l'outil le plus simple pour charger des fichiers .mo sur mobile.
Certains modules vous apparaîtront comme des redites d'autres bibliothèques, notamment any
, optional
et signals
qui ressemblent à Boost.Any, Boost.Optional / std::optional
et Boost.Signals respectivement. Nous avions initialement utilisé les versions de Boost pour cela, pendant un long moment, jusqu'à ce que nous nous penchions sur les problèmes de temps de compilation de nos jeux. Il s'est avéré que l'inclusion d'entêtes de Boost jouait beaucoup sur le temps de compilation et lors de l'édition des liens. À titre d'exemple, la compilation d'un fichier ne contenant que l'inclusion de boost/optional.hpp
prend 300 ms. sur ma machine contre 30 ms. pour iscool/optional.hpp
. Le même exercice avec boost/signals2/signal.hpp
prend 1500 ms. contre 600 ms. pour iscool/signals/signal.h
. Quand les entêtes sont inclus dans plusieurs centaines de fichiers l'impact est flagrant. En réécrivant une version épurée de ces bibliothèques nous avons gagné plusieurs minutes sur le temps de build.
Il n'y a volontairement pas de fichiers de système de build pour l'instant, ce qui implique que celui qui souhaite utiliser un module devra gérer le build lui-même. La raison à cela est que nos fichiers de builds (en premake) sont trop liés à nos jeux pour pouvoir être simplement extraits et je ne souhaite pas attendre d'avoir écrit tous les nouveaux scripts CMake avant de diffuser le code.
En navigant dans le dépôt vous remarquerez que le code est exempt de documentation. C'est aussi volontaire. Après avoir remarqué que nous lisions le code même quand une documentation était disponible, juste pour être sûr au cas où la doc serait erronée ou devenue obsolète, nous avons choisi de tout supprimer. Ainsi nous ne perdons plus de temps à lire la doc avant de lire le code, les fichiers sont épurés et clairs, et nous n'avons plus à maintenir maladroitement la doc en même temps que le code. Je suis bien conscient que cela n'aide pas à comprendre les modules pour un nouveau venu, c'est pourquoi je cherche une solution pour faciliter la compréhension du dépôt sans pour autant rédiger deux cent pages de doc. En attendant, les tests unitaires présents dans le dépôt sont vos amis.
Pour finir je vous invite à regarder nos jeux sortis (non libres) desquels est extrait ce code : Bazoo et Jardin des Mots (iOS, Android). Si vous vous intéressez à la création de jeu vidéo vous pouvez aussi lire les détails du développement de Bazoo dans un billet que j'avais rédigé lors de sa sortie.
# Merci pour ce partage - mais la doc fouque
Posté par Christie Poutrelle (site web personnel) . Évalué à 10. Dernière modification le 15 février 2018 à 14:06.
Merci pour ce partage, je n'ai pas grand chose à y redire sauf que c'est bien. Par contre, un point m'a fait pleurer (histoire de religion, goûts et couleurs tout ça).
On dirait un bien mauvais troll.
Chez nous on dit tout l'inverse, du code qui n'est pas documenté est, à mon avis, du code mort. Quand on tombe sur des milliers de lignes de code sans au moins avoir une doc meta qui explique l'architecture de la solution, sans besoin d'aller dans le détail, on passe notre chemin et on ne considère pas que c'est du logiciel libre, on ne considère que c'est pas du logiciel tout court.
Dans les milliards de lignes de code qu'on trouve sur internet, comment veux-tu qu'on s'y retrouve si on a pas au moins un synopsis, un motto, une courte description ? Du code sans doc, c'est comme si il était déjà dans une poubelle.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par Julien Jorge (site web personnel) . Évalué à 8.
Je te rejoins sur l'intérêt d'une doc meta et c'est ce que j'ai en tête quand je dis que je cherche une solution pour faciliter la compréhension du dépôt sans pour autant rédiger deux cent pages de doc. L'idée serait d'avoir une description générale du fonctionnement des modules, sans aller dans la description des détails dans le code.
Ce que nous avons supprimé ce sont les commentaires du type
Et autres descriptions de classes et fonctions qui ne font que reformuler ce que dit le nom de la classe ou de la fonction. Ce genre de commentaire se trouve malheureusement assez facilement, même dans des projets de sources respectables (un exemple, un autre et il nous semble que cela est plus gênant qu'autre chose. Du coup le commentaire systématique de chaque identifiant, nous le faisions il y a quelques années et nous avons laissé tomber.
Là il n'y a pas de doc globale simplement parce qu'en pratique nous n'en avons pas besoin : tous les développeurs font les revues de tous les autres, y compris les nouveaux venus. Si nécessaire on échange sur les points peu clairs. À notre échelle ça suffit mais je suis bien conscient que ça ne suffit pas pour le grand public ou si tous nos développeurs partaient en même temps.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par Lutin . Évalué à 1.
La doc type doxygen ne pourrait pas vous convenir ?
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par liberforce (site web personnel) . Évalué à 2.
C'en est.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par Julien Jorge (site web personnel) . Évalué à 6.
Justement non :) c'est le type de doc que je trouve plus polluante qu'autre chose. Je pensais plutôt à des trucs comme la doc de Boost, hors du code, bien structurée et pédagogique, avec un point de vue global sur la lib.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par Lutin . Évalué à 1.
Pourtant c'est quand même le minimum vital; tu as ce que doit faire la fonction, ce qu'elle prend comme argument, ce qu'elle renvoie. Et ça permet aussi de générer des graph de classes qui peuvent être utile quand on découvre un nouveau code.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par pulkomandy (site web personnel, Mastodon) . Évalué à 3.
Si ta fonction et tes arguments et leurs types sont nommés correctement, alors tu n'as pas besoin de répéter ces informations évidentes dans un cartouche de documentation.
En exagérant un peu, du code qui a besoin de documentation, c'est du code pas clair et mal écrit. Il vaut mieux passer du temps à nettoyer ton code qu'à le documenter. En suivant ce principe, on devrait pouvoir se faire une idée de l'architecture globale en regardant l'organisation des fichiers, ou les scripts de build.
Maintenant, c'est vrai que ce n'est pas facile d'en arriver là, et donc avoir une documentation peut être pertinent. La rédiger sous forme d'une TODO list (avec la liste de tous les trucs pas clairs) peut être un bon moyen de se souvenir que c'est comme ça qu'elle fonctionne, et que le but du développeur est de faire que la documentation ne soit plus nécessaire, parce que le code parle de lui-même.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par Dr BG . Évalué à 4. Dernière modification le 19 février 2018 à 14:09.
C'est un peu contraire à ce qu'on nous apprend à l'école, mais ce sont les conseils promulgués par des livres comme Clean Code et autres ouvrages écrits par des programmeurs expérimentés. Ces lectures m'ont fait changer d'avis sur les commentaires et la doc.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par lasher . Évalué à 2.
Je réponds très tard, mais selon moi, ce qu'on apprend sur la documentation de code, etc., vient principalement pour 2 raisons:
La combinaison des deux points précédents peut être fatale je pense.
J'exagère bien sûr. ↩
Oui, ça veut aussi dire que ce sont des gens qui n'ont pas bien configuré leur IDE/ViM/Emacs/etc., ou bien qui ne savent pas encore s'en servir, ou… D'un point de vue pragmatique, ce n'est pas très important. ↩
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par mzf (site web personnel) . Évalué à 2.
Pour détecter les morceaux de code pas clairs, ne pas hésiter à faire relire son code (revue de code).
Un regard extérieur permet de trouver plus rapidement ce qui n'est pas clair ou pas logique, contrairement au développeur original dont la lecture est biaisée par le contexte de la création du code.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 4.
Ça ne s'applique que lors de la création parce que quand on revient sur du code que l'on a écrit 5 ou 6 ans plus tôt, on se retrouve dans la position de l’œil extérieur et généralement, c'est là qu'on regrette de ne pas avoir été clair
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par barmic . Évalué à 2.
ans → semaines ;)
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par barmic . Évalué à 10.
C'est bien plus compliqué que ça.
Avoir comme objectif de réduire au minimum la quantité de commentaire (comme de réduire la quantité de code non testé) c'est bien, mais il ne faut pas en conclure qu'il faut se l'interdire ou imaginer que c'est une mauvaise pratique.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par Lutin . Évalué à 4.
Sur une équipe de 5 personnes qui écrivent du code toute la journée, même avec des relectures, tu auras forcément des ambiguïtés sur un nom de fonction, classe ou variable qui vont passer, personne n'est infaillible.
De plus, lire du code pas trivial du tout (ou au contraire très trivial), ça peut vite être pénible, alors qu'on cherche juste à savoir c'est ce qu'un bout de code fait. Si mon collègue a fait un truc qu'il trouve super ingénieux ou super complexe, qui mérite de l'attention en tout cas, il m'en aura parlé et je pourrais jeter un œil dessus, en dehors de ça c'est rarement super intéressant.
[^] # Re: Merci pour ce partage - mais la doc fouque
Posté par groumly . Évalué à 9.
J’ai tendance à être de ton avis, mais après zieuté la partie iOS, t’as clairement besoin de doc.
get_root_viewcontroller est soit méchamment bugee, soit plutôt inutile, voire dangereuse. Pour faire simple, iOS présente les alertes dans une nouvelle UIWindow (qui devient donc la key window). Ta fonction renvoie donc un controller d’alerte dans ce cas, là où je pense que tu voudrais renvoyer le UIApplication.shared.delegate.window.rootViewController. Problème c’est qu’on pas la moindre idée de ce que tu voulais renvoyer au final, vu que ton nom de méthode est super générique et que t’as pas comment l’intentention.
Idem sur present_view_controller dont l’implémentation est super chelou (le respondsToSelector, vous targetez vraiment encore iOS 7?), et aussi super dangereux (popoverPresentationController peut être nil, auquel cas ça va te faire bizarre quand tu vas lui assigner une source view). Je passe sur la réutilisation d’un nom de method uikit mais avec une semantique très différente.
Au final, je suis vraiement pas convaincu d’avoir même une vague idée de ce qu’est censée faire cette fonction.
Bref, si tu veux suivre la philosophie “no docs, va falloir sérieusement revoir tes noms de fonctions, ou admettre qu’écrire un peu de doc ne fait pas de mal.
Linuxfr, le portail francais du logiciel libre et du neo nazisme.
# Sans documentation ou tutoriel...
Posté par cppuser . Évalué à 2.
Bonjour,
c'est très bien, et je suis d'accord aec la licence…sauf que la documentation comme précisée est inexistante.
Je doute donc que du coup cela soit beaucoup utilisé, même s'il est possible que le code soit de qualité.
Personnellement quand j'utilise une bibliothèque je regarde : les possibilités, la documentation et la licence. Je préfère prendre une bibliothèque moins bonne mais mieux documentée qu'un excellente sans document.
[^] # Re: Sans documentation ou tutoriel...
Posté par Marotte ⛧ . Évalué à 3. Dernière modification le 16 février 2018 à 00:19.
Si tu es capable d’utiliser celle qui est excellente il vaut mieux choisir celle-ci quitte à la documenter toi-même.
Pour une bibliothèque, qui n’a donc pas d’interface utilisateur, la doc peut en effet très bien se limiter à une courte introduction de deux lignes, typiquement dans un fichier README. Comme tu le dis il faut aussi connaître la licence et le langage ciblé. Sur ce point je dirais que si tu as téléchargé le code source, tu as déjà ces informations. Par exemple ici, rien qu’avec ce journal tu connais le langage et la licence est à deux clics d’ici… Ensuite, si chaque classe, chaque fonction, et, éventuellement, c’est bien sûr à éviter, chaque truc un peu tordu est documenté, ça suffit à utiliser le code source, à apprendre à s’en servir. L’utilisation d’un linter pour s’assurer qu’on a bien commenté chaque objets est pratique.
C’est sûr que parfois, le nom d’une fonction avec le nom de ses arguments ne peut pas être reformulé en anglais ou en une autre langue naturelle de manière plus précise, concise et pertinente :)
En Python je profite souvent du commentaire pour indiquer le type d’argument attendu ou le type retourné. Je ne considère pas qu’il soit souhaitable de préfixer tous ses identifiants avec leur type, mais c’est le genre d’info utile qui a bien sa place ici, juste à côté du code, à la ligne suivante… C’est mieux qu’au fin fond d’un wiki ou d’un document de traitement de texte… simplement à un autre endroit…
[^] # Re: Sans documentation ou tutoriel...
Posté par abriotde (site web personnel, Mastodon) . Évalué à 0.
Il n'y a pas besoin de beaucoup forcément mais au moins un fichier "readme" à la racine qui présente globalement. Ne serais-ce que ce que tu as mis sur linuxfr. C'est déjà pas mal.
Sous licence Creative common. Lisez, copiez, modifiez faites en ce que vous voulez.
# Bien joué
Posté par Marotte ⛧ . Évalué à 8.
Ça peut être une excellente chose d’avoir fait ainsi.
Non, mais vous avez à répartir de zéro !
Le fichier README est déjà présent et non vide, c’est déjà quelque chose ! Prendre le temps de réfléchir, les semaines ou les mois qui viennent, à rédiger un petit paragraphe d’introduction pourrait peut-être être envisagé ? Si vous avez les ressources nécessaires pour cela bien sûr ! ;)
En fait ce journal colle au journal sur la traduction, quelqu’un devrait traduire l’un des paragraphes de ton journal et faire un pull request d’un ajout au fichier README :)
# NIH ?
Posté par rewind (Mastodon) . Évalué à 5.
Je trouve que dans beaucoup d'endroits, il y a plein de choses qui sont en doublon par rapport à la bibliothèque standard ou qui sont réimplémentées un peu différemment et du coup, on perd de l'intérêt ou des fonctionnalités.
Dans le module
meta
, je prend l'exemple de remove_const_reference qui aurait pu être implémenté plus simplement:Et en plus, vous aviez la gestion des rvalue references (celles avec un &&) alors que l'implémentation proposée n'enlève pas la référence dans ce cas.
Il y a aussi
indices
etmake_indices
dont on voit mal la différence avecstd::integer_sequence
etstd::make_integer_sequence
.Et dans le module
memory
, c'est un peu pareil.make_unique
est réimplémenté mais sans prendre en compte les tableaux. Et donc, si je faisiscool::memory::make_unique<int[]>(4)
, ça ne compile même pas (alors que la version dans la bibliothèque standard compile et fait bien ce qu'on attend d'elle).Et dans le module
random
,random_generator
fait quasiment la même chose questd::discrete_distribution
sauf qu'en plus, ça ne peut pas prendre n'importe quelle source de hasard (alors que la bibliothèque standard est plutôt bien conçue à ce niveau).Je pourrais continuer mais je vais m'arrêter là. Tout ça me laisse une impression plus que mitigée à la lecture du code. Ça laisse penser que le syndrôme NIH n'est pas loin et donc, qu'il n'y a que peu d'intérêt à utiliser cette bibliothèque.
[^] # Re: NIH ?
Posté par Julien Jorge (site web personnel) . Évalué à 6.
Oui beaucoup d'éléments sont déjà présents dans d'autres libs. Pour les exemples
std::integer_sequence
etstd::make_unique
que tu cites c'est tout simplement que notre développement est calé sur c++ 11 et tout cela n'est dispo qu'à partir de c++ 14, donc indisponible pour nous. Nous aurions pu utiliser les versions de Boost ou autres mais comme dit dans le journal nous avons eu de mauvaises surprises sur les temps de compilation avec Boost, donc nous sommes parti sur une version maison maîtrisée. L'idée étant d'avoir cette implémentation en attendant de faire le passage à c++ 14 ou plus, après quoi nous les supprimerons ou les remplacerons par des alias vers la STL.Les tableaux ne sont pas gérés par
make_unique
simplement parce que nous n'en avions pas besoin. Si nous l'avions implémenté ça aurait été l'équivalent d'un code mort pour nous, qui aurait coûté du temps de compilation et de maintenance pour aucun gain.Le dépôt GitHub a été créé avec le code dans l'état dans lequel nous l'utilisons, ce qui ne colle pas toujours à un besoin général, j'en suis conscient. Il y a un paquet d'autres trucs moyens que je ne réutiliserais probablement pas à titre perso et d'autres que j'apprécie assez (
heterogeneous_map
, le module jni, par exemple). Cela dit si nous avions pris le temps de tout rafraîchir et d'améliorer tous les points faibles que nous voyons nous n'aurions jamais été suffisamment satisfaits pour le sortir.[^] # Re: NIH ?
Posté par rewind (Mastodon) . Évalué à 3.
De nos jours, les grands compilateurs sont compatibles avec C++14, qui est une petite évolution par rapport à C++11. Mais bon, je peux comprendre. En revanche, pourquoi ne pas avoir utilisé des morceaux de libc++ par exemple (qui a une licence permissive) plutôt que de réimplémenter ?
[^] # Re: NIH ?
Posté par Julien Jorge (site web personnel) . Évalué à 5.
C'est pour moi toute la difficulté de la gestion des dépendances. Faut-il lier une bibliothèque externe, la gérer comme un bloc immuable et ainsi profiter de l'expérience des autres ? Faut-il la copier-coller dans le projet pour en faire la base d'un outil interne que l'on peut modifier librement au risque de compliquer ses mises à jour ?
Il y a des outils que l'on prend tels quels, Boost bien sûr, Google Breakpad, Google Test, JsonCPP, entre autres. Ce sont des outils assez gros et nous ne ferions probablement pas mieux en les réécrivant. Néanmoins si nous avions au moins fait une interface à JsonCPP nous aurions sans doute pu simplement changer pour rapidjson ou autre le jour où nous l'avions envisagé. Celui là a par exemple été à la fois un accélérateur et une gêne.
À côté de ça il y a des outils que l'on aurait bien voulu prendre tels quels, Cocos2d-x, MoFileReader, Spine, Soomla, mais le besoin a appelé des modifs et les mises à jour sont de plus en plus difficiles.
Alors est-ce qu'on aurait vraiment gagné du temps ou de la performance en piochant dans libc++ ? Pour l'inclusion d'optional par exemple, on part sur Boost, la compilation est lente (300 ms. sur mon pc pour un
g++ -c test.cpp
oùtest.cpp
ne contient que#include <boost/optional.hpp>
). Je teste avec la STL en c++17 : 250 ms. bof. Alors nous l'implémentons à notre sauce, ça coûte quelles que heures de dev, je teste la même compilation : 30 ms. Et quand je teste en essayant de copier la version de libc++ ça ne compile même pas car il manque des dépendances :/Dès fois c'est plus simple de prendre un outil existant tel quel, d'autres fois c'est la plaie. Parfois, comme avec libc++ ici, il faut passer du temps à gérer la compilation ou à extraire la seule partie qui nous intéresse. Pour moi le problème est loin d'être simple. Et encore là je parle de compilation desktop mais en pratique nous faisons des builds iOS, Android, OSX et Linux.
En ce moment je regarde d'ailleurs la gestion de dépendance avec Conan et je dois me faire violence pour plonger dedans plutôt que de bêtement compiler des .a et les archiver sur un bucket S3. Il me semble que tu avais regardé ces outils de gestion de dépendance en c++, en utilises-tu ?
[^] # Re: NIH ?
Posté par rewind (Mastodon) . Évalué à 2.
Globalement, je suis d'accord avec tout ce que tu dis ici, et je suis souvent confronté à ce même problème. J'ai parfois fait des choix sur lesquels je suis revenu ensuite : par exemple, l'UI en mode immédiat, au départ j'ai utilisé un truc simple que j'avais pioché dans un projet quasi-mort mais qui marchait, mais il manquait des fonctionnalités et au final, ça me semblait être une tâche trop ardue d'implémenter ça correctement et rapidement. Au final, j'ai intégré Nuklear qui avait bien plus de fonctionnalités et qui correspondait mieux à ce que je voulais. Mais la question se pose à chaque fois. Je me suis aussi retrouvé à devoir implémenter des petits trucs qui vont venir dans la bibliothèque standard, comme
std::string_view
.De toute façon, si je grossis le trait, j'ai fait une implémentation de SFML en prenant SDL comme base, mais j'ai repris beaucoup de code de SFML. Ce qui veut dire qu'il faut suivre les évolutions de SFML (et/ou les corrections de bugs) tout en ajoutant ce dont j'ai besoin. Et je ne parle même pas des morceaux de SFML que j'ai modifiés à ma sauce parce que je trouvais l'implémentation originale moyenne.
Pour répondre à ta question sur la gestion des dépendances, j'essaie d'en avoir le moins possible. Mais ce n'est pas toujours gagné. J'ai deux sortes de dépendances. Premièrement, des dépendances que j'importe directement (via git submodule). Ça concerne en particulier des bibliothèques qui ne sont que des en-têtes et qu'il est plus facile d'utiliser de cette manière (on peut planquer l'implémentation dans un fichier source sans la mettre dans une interface). J'ai stb et Nuklear qui sont comme ça. Ensuite, j'ai des parsers de formats particuliers que je préfère intégrer directement de cette manière plutôt que de passer par une dépendance affichée, je cherche alors des bibliothèques simples (un seul fichier source). J'ai TinyXML et RapidJson dans cette catégorie. Et j'envisage sérieusement de supprimer ces deux dépendances bientôt et de ne passer que par un seul format d'entrée (j'en parlerai peut-être dans un journal bientôt). Et enfin, j'ai GameControllerDB qui contient des données uniquement et Google Test où c'est la manière recommandée de procéder.
Deuxièmement, j'ai des dépendances externes affichées. Là, j'essaie d'en avoir le minimum. J'ai SDL2, Freetype, Boost (seulement certaines parties) et récemment j'ai ajouté Zlib. Pour celles là, j'utilise mon gestionnaire de paquet préféré quand je suis sur Linux et j'utilise l'excellent vcpkg quand je suis sur Windows (j'inclus les plateformes de CI dans ces descriptions). Et ça marche plutôt bien. Je trouve vcpkg très bien foutu, en une seule ligne, on a tout ce qu'il faut prêt à l'emploi. On a une impression d'être à la maison (Linux) tellement c'est simple. En revanche, je déteste Conan cordialement. Je trouve le concept fumeux de base.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.