dimanche, août 23, 2009

Dans le Pied++

Je n'avais pas une grande expérience du C++ pendant les années où j'ai travaillé sur mon projet d'OS, mais j'avais suivi avec intérêt les investigations de DasCandy pour parvenir à faire fonctionner les objets globaux (cf. http://wiki.osdev.org/C_PlusPlus). Et j'en avais retenu une chose : n'utiliser ces choses qu'en cas d'urgence, et avec la prudence qui s'impose quand on manipule de la nitro.

D'un autre côté, pour pouvoir définir le comportement de mon appleman, il me faut beaucoup plus de contrôleurs, qui sont appelés en séquence pour définir le comportement réel pour un état. Au moment de la lecture du script qui définit le niveau, je vais donc traiter une série de lignes "using tracker(Bilou)" "using Momentum(5,-1)" "using walker()", chacune de ces lignes donnant le nom d'une classe de contrôleur à instancier. J'ai donc pour chaque classe dérivée de iGobController une classe dérivée de iGobFactory capable de construire le bon objet à partir de la chaîne de caractères entre parenthèses.

So i've started decomposing the former "big chunk" controller such as 'walk' and 'gravity' into 'micro-gob-controllers' and make sure they are published in the std::map used by GameScript to prepare the GobState objects according to the rules found in the script, such as "using tracker(Bilou) ; using Momentum(5,1)", "using walker". The corresponding output is a chain of controllers that each do a specific job, easing code reuse. The BerryBat and the Appleman can for instance share the same "tracker" controller that tell them where Bilou stands, while Bilou and the appleman share "Momentum" and "walker" controllers, but Bilou receives its input from "dpad" rather than from "tracker".

Bon. Mais il faut que ces factories -- qui sont du coup des singletons -- soient enregistrées dans une map pour que le script les retrouve (c'est pas du Java: exit le 'Class.forname("tracker")'). Et je voulais pouvoir ajouter encore et encore des contrôleurs à mon code sans devoir maintenir une grosse fonction du genre "register_all_controllers". Cyril proposait donc évidemment des objets globaux (qu'on appelle parfois aussi "objets statiques"), mais je n'étais pas chaud: avec eux, il y a toujours anguille sous roche ...

Comme je n'avais pas d'argument vraiment convaincant, j'ai essayé quand-même. Première surprise : les "printf" ne marchent plus de manière fiable! J'ai encore quelques messages imprimés quand j'utilise "printf" sans aucun argument, mais c'est tout... un problème que j'avais déjà rencontré.

Un peu de désassemblage, et je constate que les messages qui sont effectivement imprimés n'utilisent pas véritablement printf: ils ont été convertis en interne en puts. Conclusion: en appelant printf dans l'initialisation des objets globaux, qui a lieu avant ConsoleInit(), j'ai empêché l'initialisation correcte de la console et rien ne va plus.

I stumbled upon two issues while implementing those factories -- which i initially chose to build with static objects. The first problem is a "known issue" of my libnds version: if i ever invoke a printf() function before consoleInit(), i screw up the stdlib's internal state and all subsequent prints using this function won't output a single pixel >_<.

Je change d'approche: initialisation "en aveugle", et une petite méthode "test" indépendante, histoire de vérifier que mes contrôleurs sont bien présents, appelée juste avant la boucle principale du programme. Deuxième surprise: il y a bien des contrôleurs enregistrés mais pas tous. Pourtant, mon code est bien compilé, tel que je l'ai écrit. Et il avait été appelé, puisqu'il était capable de foutre en l'air l'affichage...

La map est une variable de classe, statique, initialement vide. "As plain as can be" ...
... statique ? ...

Eh là, une minute ... Et comment le compilateur sait-il que parmi ces objets statiques, la bonne initialisation des GobFactories dépend du fait que la map ait déjà été initialisée ? Bin il ne le sait pas, évidemment. Le code d'initialisation appelle tous les constructeurs dans un ordre qui lui convient, et si ça ne me plaît pas, c'est tant pis pour ma faute.

Once this was fixed, i realised that i was f00lish to use static objects to register things in a static map. When i say "static", here, i actually mean "some object defined at the top level that should be instanciated before main() is ever called". The problem with such object is that their initialization order is undefined. In other words, my factory F could very well inserts itself in the map and then the map itself initializes (and thus clears what F modified).

Retour temporaire à "register_all_controllers" pendant que j'évalue les options... Voilà qui donne un nouvel éclairage au dicton "In C++ it's harder to shoot yourself in the foot, but when you do, you blow off your whole leg." (Stroustrup himself ;)

3 commentaires:

sylvainulg a dit…

le simulateur OMNet++ a bien automatisé ce genre de chose, en passant notamment par classes.getInstance()->add(new cClassFactory(<name>,<factory-function>,<description>)

Ils ont aussi un objet "ExecuteOnStartup" à qui ont peut donner n'importe quelle fonction à traiter (en construisant à la main une liste liée de choses à faire), mais ce code sera vraisemblablement invoqué à partir de main().

tout ça à grand renfort de __UNIQUE_NAME__ ou typeid(...) pour automatiser la production du nom "stringifié". Malheureusement, moi, je fonctionne sans rtti. Loupé pour typeid, j'imagine.

Cyril a dit…

http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Nifty_Counter

sylvainulg a dit…

je vois qu'on mentionne d'autres approches sur stackoverflow, notamment le __attribute__((init_priority(300))) de g++ ... à suivre.