Wrapping

Oggi vorrei spendere due parole su come non wrappare una libreria di terza parti (si avete letto bene…).

Quasi tutti i programmatori sanno cos’è un wrapper e perché lo si usa. In due parole un wrapper è un’interfaccia per astrarsi dall’API di una certa libreria di terze parti…quindi per non dipendere da essa e per avere un codebase più uniforme possibile.

Purtroppo delle volte, dalle menti di alcuni programmatori, escono dei wrapper che non servono a nulla ma anzi introducono overhead (performance/memoria) e soprattutto bugs!… vedendone uno di questi, ci ho voluto riflettere e scrivere questo post.

Veniamo ad un esempio concreto: abbiamo una libreria audio di terze parti (ie fmod) che vogliamo usare per il nostro progetto. Pur essendo cross platform vogliamo wrapparla per quello che abbiamo detto prima e perchè la nostra nonnina ci ha sempre detto che <<è sempre meglio wrappare…>>

Ci studiamo la libreria e subito iniziamo il lavoro…dopo qualche settimana ci ritroviamo con il nostro wrapper.

Prima di descrivere il nostro wrapper vi faccio una piccolissima panoramica sulla libreria di fmod: i file audio raw vengono processati da un editor che crea dei banchi di suoni…ogni banco è un progetto. Dentro ogni progetto ci sono degli eventi che sono divisi in gruppi. Ogni evento può essere formato da uno o più suoni. Infine ad ogni evento è associato un canale. [scusatemi per la descrizione più che minimale ma non è il cuore del discorso!]

Ora prendiamo di esempio il wrapper di un channel di fmod:

class MyChannel
{
public:

   void setVolume(float volume){ impl_->setVolume(volume); }

   // other 100 similar methods here... :)

private:

   FMOD::Channel* impl_;
}

In sostanza quello che abbiamo fatto è una copia 1:1 dell’interfaccia di fmod.  A prima vista questo può sembrare giusto ma per molte API (tra cui fmod) [in realtà quasi tutte] è un approccio completamente sbagliato. Perchè? Semplicemente perchè appunto stiamo ricalcando l’API e non astraendola!.

Quel giorno che volessimo abbandonare fmod dovremmo buttare via tutto perchè difficilmente la nuova libreria che andremo a scegliere avrà la stessa struttura. In sostanza è tutto lavoro sprecato….centinaia se non migliaia di righe di codice da cestinare…purtroppo molto spesso non è solo tempo buttato ma vengono a nascere bugs e quasi sicuramente la nostra astrazione, pur non servendo a nulla, costa in termini di tempo e di spazio. Infatti, nell’esempio non si vede, ma c’è anche il problema della duplicazione delle informazioni…

Qual’é l’alternativa?

L’alternativa è localizzare/centralizzare l’uso della libreria per astrarla da tutto il nostro progetto. Per fare questo molto spesso bastano poche interfacce che espongano le funzioni che usiamo della libreria. Una felice conseguenza di questo è che esponiamo solo quelle funzionalità che effettivamente ci servono potendo quindi wrappare la libreria in modo incrementale…

(ie)

class AudioController
{
public:

   bool initSoundDevice();

   ??? getChannel(const char* const name);

   void setChannelVolume(??? channel, float volume);

   // and so on...

private:

   FMOD::System* fmod_;

};

 

Lo scopo è contenere fmod dentro il nostro AudioController. Qua sorge il problema più grosso: dobbiamo permettere la comunicazione tra il nostro progetto e fmod senza andare a wrappare le singole classi di fmod… In sostanza il problema è il seguente: cosa deve restituire AudioController::getChannel(const char* const) ? La risposta è un HANDLE (un int32?) che fa da bridge tra il mondo esterno e l’AudioController (quindi fmod). E’ l’AudioController che si tiene al suo interno un mapping tra l’handle e l’FMOD::Channel così da risolverlo quando serve.

 

class AudioController
{

public:

   typedef size_t ChannelHandle; // static_assert(sizeof(size_t) == sizeof(FMOD::Channel*));

   bool initSoundDevice();

   ChannelHandle getChannel(const char* const name)
   {
      FMOD::Channel* fmodChannel(...);
      return reinterpret_cast<size_t>(fmodChannel);
   }

   void setChannelVolume(ChannelHandle channel, float volume)
   {
      reinterpret_cast<FMOD::Channel*>(channel)->setVolume(volume);
   }

   // and so on...

private:

   FMOD::System* fmod_;

};

Concentrandoci sulla NOSTRA API, cioè sulle sole funzionalità di cui necessitiamo, e non su quella di fmod abbiamo un wrapper molto più veloce, snello, meno error prone e soprattutto più facilmente adattabile ad un cambio di libreria!

Ciao e alla prossima

void*

Riassunto: void* è il male…o meglio lo diventa quando viene usato dove non ha alcun senso e/o in modo non appropriato.

La storia: ho perso giorni per fixare diversi memory leaks … molti di questi erano stringhe che non venivano deallocate. Dopo ore e ore di ricerca la causa era proprio un void*…il classico userData void* membro di una classe. Il distruttore di questa chiamava la delete su questo raw pointer e poi vi era il classico setter; Questo:

class Foo
{
   Foo() : m_pUserData(NULL) { }
   ~Foo() { delete m_pUserData; }
   void SetUserData(void* pUserData) { m_pUserData = pUserData; }

   void* m_pUserData;
};

Gli errori di fondo (o meglio gli orrori di fondo) sono due:

1) La gestione comepletamente sbagliata del lifecycle di m_pUserData. Una data risorsa viene creata all’esterno (dove? da chi?) e poi distrutta nel distruttore di Foo (perchè?)!

2) La delete di un void* è undefined behaviour [C++ Standard 5.3.5/3]. Nel mio caso non era la delete del c++ ma comunque, per nessun motivo, può chiamare il distruttore di m_pUserData perchè non ne conosce il tipo.

Spero che questa brevissima “storia” possa impedire o almeno ridurre porcate di questo tipo.

Ciao

ps buon anno!

Pillola #2: Il context operator di VS

Oggi ho fatto una grande scoperta (almeno per me) di cui voglio subito rendervi partecipi.
Al lavoro molto spesso faccio uso della watch window di Visual Studio sia per visualizzare variabili sia per fare conti o invocare funzioni. Il problema che ho avuto fino a ieri ( :D ) è proprio sull’invocazione di alcuni metodi.

Infatti in un progetto formato da diversi moduli linkati dinamicamente il watcher non è in grado automaticamente di invocare metodi definiti in moduli diversi da quello in cui l’applicazione si è fermata. In sostanza, quando l’esecuzione è ferma nel modulo A (per un break o per un crash) e vuoi invocare una funzione che risiede nel modulo B, il watcher visualizzerà un’errore del tipo:

CXX0017: Error: symbol "FunctionNameInB" not found

 

Per risolvere esiste il così detto context operator di Visual Studio.

Nel caso specifico basta anteporre alla nostra espressione qualcosa come:

{,,DllName}

 

Eccovi uno screen di test che ho eseguito (funziona!):

 

Per approfondire la sintassi dell’operatore rimando alla pagina msdn.

Spero che possa risultare utile,

ciao e alla prossima!

Pillola #1

Ciao e ben ritrovati. Oggi vorrei proporre la seguente brevissima pillola.
Mi capita molto spesso di ritrovarmi delle funzioni con prototipi del tipo:

void GetBestPlayers(int numBestPlayers, std::vector<Player*>& outputBestPlayers);

sono decisamente sollevato di morale quando vedo quel prototipo e non il seguente (per ovvie ragioni…):

std::vector<Player*> GetBestPlayers(int numBestPlayers);

Quello che però vedo molto spesso fare (tornando alla prima funzione), e non lo sopporto, è il seguente:

void GetBestPlayers(int numBestPlayers, std::vector<Player*>& outputBestPlayers)
{
    outputBestPlayers.clear(); // <--------- !?!?!

    // ...
}

Lo so che avrò contro tutti i sostenitori del Defensive programming ma lo reputo un errore. Ecco tre motivazioni che mi saltano in mente:

1) Performance! trascurabile? sarà una cavolata? verrà messa inline? ne siete sicuri? perché aggiunge uno statement che non serve? (vedi gli altri due punti)

2) Viola una buona regola dell’OOP che dice che una funzione deve avere un solo scopo. E’ buona norma controllare sempre gli input di un funzione per assicurarsi che “il patto” tra chiamante e chiamato sia rispettato ma in questo caso NON è un controllo sull’input. La funzione non verrebbe pregiudicata dal suo corretto funzionamento anche se quel vettore non fosse vuoto!

3) Flessibilità: questo è forse il punto a cui tengo di più. Non forzando il parametro input/output ad essere vuoto si rende la funzione molto più flessibile e versatile. Mi spiego meglio con un esempio.
Poniamo di avere un’altra funzione che questa volta ci restituisca N giocatori peggiori:

void GetWorstPlayers(int numWorstPlayers, std::vector<Player*>& outputWorstPlayers);

Ora, se volessimo i 3 migliori giocatori e i 3 peggiori della partita da visualizzare nella nostra interfaccia?

std::vector<Player*> playersToShow;
playersToShow.reserve(6);
GetBestPlayers(3, playersToShow);
GetWorstPlayers(3, playersToShow);

// ...

Questa banalissima cosa non l’avremmo potuta fare se le due funzioni forzano la pulizia del nostro vettore!!
Ovviamente è un esempio molto semplice ma vi assicuro che lo stesso pattern si applica a moltissimi casi anche più articolati.

ciao e alla prossima!

Pillola #0

Ciao a tutti,

con questo nuovo post vorrei far partire un appuntamento fisso (o quasi, non prometto nulla) dove darò dei consigli molto brevi (pillole) ma spero per voi utili sulla programmazione o comunque su tutto quello che riguarda il lavoro di un programmatore.

L’idea mi è venuta lavorando su molto codice scritto da altri. Leggere codice non proprio, è molto importante sia perchè impari sempre cose nuove che non conoscevi o comunque che avresti implementato in modo differente sia perchè, dopo aver acquisito una certa esperienza, diventi come una sorta di critico d’arte dove ti accorgi di errori o scelte almeno discutibili. E’ qui che bisogna far tesoro degli errori degli altri (ovviamente anche dei propri) per non commetterli più cercando di “salire di livello” (sia che si parli di qualità del codice, di tools o più in generale di una pipeline di lavoro…).

Ok, cominciamo…

Vi sarà capitato di trovare codice del tipo:

obj1.Method1(obj2.Method2()).Method3(obj3.MethodFoo(), obj4.MethodBar());

A prima vista potrebbe essere considerato un method chain, questo però ha senso solo in pochissimi casi. Ad esempio è perfetto per l’operator<< dello cout o comunque quando i metodi della chain sono molto semplici (solitamente setters) con un unico parametro builtin…piccolo esempio di una classe data:

Date date;

date.SetYear(2011).SetMonth(12).SetDay(5);

anche se comunque preferisco decisamente la versione non chain. Detto questo torniamo a noi…mettiamo che il mega statement di prima non sia un method chain ma una concatenazione di metodi di diverse classi.

Questa porcata (avrei degli esempi bellissimi ma non posso riportarli) ha solo grossi svantaggi quando malauguratamente andremo a debuggare quel codice.

Infatti se dovessimo debuggare un singolo metodo, poniamo il terzo, dovremmo steppare-into/steppare-out nei primi due metodi ed è davvero tedioso! Molti potrebbero replicare che basta mettere il breakpoint direttamente nella funzione da debuggare…ma se questa è chiamata in altre decine(centinaia?) di punti differenti? impensabile..anche con l’aiuto di eventuali conditional breakpoint…Altri potrebbero invece obbiettare che basta mettere il breakpoint nella nostra funzione da debuggare subito dopo che il primo breakpoint sia scattato e poi premere F5 (evitando così di entrare nelle altre funzioni..). Si funziona ma vi sembra pratico quando il problema si risolverebbe semplicemente non creando quelle inguardabili concatenazioni?

Sarò l’unico che le odia anche perchè se ne vedono davvero molte…troppe…e quali vantaggi ha? forse scrivere qualche carattere in meno…per il resto son solo problemi….

Una buona campanello di allarme, molto old style, è controllare che il nostro statement non superi gli 80 caratteri…se accade spesso, io fossi in voi, mi preoccuperei :D

ciau

 

 

 

 

YAPE !

Ciao a tutti,

scrivo queste poche righe per segnalarvi YAPE. Yape (Yet Another Pacman Emulator) è un ennesimo emulatore del Pacman arcade che l’amico Filippo e io abbiamo realizzato. Il codice è open e sempre su codeplex vi è anche il binario già compilato per window. Ovviamente non troverete la rom per ovvi motivi…

Non aggiungo altro anche perchè sul sito di Filippo c’è già un breve post riassuntivo e sul forum di IndieVault una discussione.

Proprio ieri, invece, ci siamo messi a codare la cpu NMOS 6502 …. vi dice niente? è la cpu del NES … stay turned

Non giocare con le macro…

Settimana scorsa ho letto la seconda parte di un simpatico articolo sull’allocazioni su AltDevBlogADay (parte1, parte2). Ad un certo punto però ho visto uno riga di codice che mi ha fatto balzare sulla sedia. Mi riferisco a questo simpatico gioco con la macro:

#define malloc( uSize ) my_malloc( uSize, 4 )

Ho voluto subito commentare l’articolo scrivendo:

Hi, nice post but this:
#define malloc( uSize ) my_malloc( uSize, 4 )
is madness!
Prefer your macro (MY_MALLOC) or something similar (inline free function, OO custom allocator) but don’t play with those dangerous preprocessor redefinition!

L’autore dell’artico mi ha risposto così:

Hi Martin,

I agree, preprocessor redefinition is dicey (or madness).

While I did say to ensure malloc.h was not included, I’ve seen less experienced team members, at the last game studio I worked at where we used a different macro, forget that rule and start allocating memory outside the memory tracking system, and this caused some teeth gnashing during crunch time trying to figure out why we weren’t fitting into memory even though the tracking system said we should. So, my preference is to cause compilation failures and gain the benefit of early detection of errant memory allocations, rather than trying to track them down when the deadline or milestone looms.

My choice isn’t the best for everyone, and using a differently named macro will definitely lead to less preprocessor gymnastics.

Cheers,
-Paul

Sostanzialmente concorda con me dicendo che uno statement del genere è un po criminale ma dice che è meglio un errore di compilazione che un’allocazione non gestita dal nostro sistema di memory tracking.

Premesso che una qualsiasi ridefinizione del genere, come qualunque ridefinizione della STL è assolutamente da bannare, come ne parla il mio intelocutore sembra che il maggior pericolo di una pratica del genere possa essere un simpatico errore di compilazione. In sostanza dice: i problemi nascono quando includi malloc.h (memory.h, stdlib.h, … no?)* e in questo caso avverrà un errore di compilazione del tipo:

c:\program files (x86)\microsoft visual studio 10.0\vc\include\malloc.h(106): error C2059: syntax error : ‘constant’

Il tutto qundi sembra decisamente safe. Il problema è “il sembra”. Poniamo di avere una situazione del genere:

Utils.h

#ifndef UTILS_H
#define UTILS_H

#define malloc( uSize ) my_malloc( uSize, 4 )
#define free( ptr ) my_free(ptr)

inline void* my_malloc(size_t uSize, size_t align)
{
    return ... ; // don't use malloc here :) LOL
}

inline void my_free(void* ptr)
{
    //... // don't use free here :) LOL
}

#endif

e di avere una classe compilata in una libreria statica che noi andremo poi a linkare al nostro main project:

Foo.h

#ifndef FOO_H
#define FOO_H

#include "malloc.h"

class Foo
{
public:

    Foo();
    ~Foo();

private:

    void* mPtr;
};

inline Foo::Foo()
{
    mPtr = malloc(X);
}

#endif
#include "Foo.h"

Foo::~Foo()
{
    if(mPtr)
        free(mPtr);
}

Sfortunatamente il coder che ha creato questa utilissima classe si è scordato di eliminare malloc.h a favore di Utils.h . In più ha definito il costruttore nel .h mentre il distruttore nel .cpp . Il costruttore alloca memoria mentre il distruttore la libera. Tutto ok quindi? Bhè non proprio; vediamo perchè:

Abbiamo detto che la classe Foo è stata compilata a parte e in una .lib che linkiamo al nostro progetto. Questa avrà un behaviour safe, come ci aspettiamo, se non definiamo la nostra bella macro, nella pratica se prima di includere Foo.h non abbiamo incluso Utils.h . Se così non fosse abbiamo un undefined behaviour. Infatti il preprocessore andrà a sostituire lo statement malloc(X) con la chiamata my_malloc(X, 4) come del resto ci aspettiamo; il problema è che in questo caso la free è nel cpp, già compilato, e ovviamente non viene modificato. Abbiamo quindi un’allocazione con un nostro allocatore e la sua deallocazione con un’altro allocatore: BOOM, sei morto!

Con questo esempio seppur molto semplice, se vogliamo banale, ho voluto evidenziare un grosso difetto che può avere un uso sconsiderato delle macro e quindi del preprocessore. Nel caso trattato, come abbiamo visto, non si tratta di un semplice errore di compilazione facilmente risolvibile ma una possibile fonte di bug veramente difficile da scovare. Sempre in questo caso non c’è dubbio che la scelta più saggia sia quella di non ridefinire malloc/free ma di usare nostre keyword (sia che si tratti di macro, inline functions….etc) e di obbligare a non usare malloc/free ma le nostre funzioni. Il controllo è banale: basta andare in cerca nel codebase delle due keyword incriminate e, utilizzando l’history del reposity, risalire all’autore per linciarlo.

Ciao e alla prossima!

*solo quando l’include malloc.h è posteriore alla definizione della macro incriminata.

Letture

Rieccomi dopo più di un mese dal mio ultimo post. Oggi non tratterò nessun argomento tecnico ma vorrei scrivere due righe a ruota libera sugli ultimi libri che ho acquistato, letto o sto per leggere.

Settimana scorsa ho ordinato un altro libro su amazon.co.uk . Spedizione ultra veloce come sempre e dopo pochi giorni mi è arrivato. E’ Real-Time Rendering 3th edition. Il libro assomiglia alla sacra bibbia (della computer grafica però :D ); è bello grosso e pesante anche se il formato è da romanzo… Ho iniziato a leggerlo da qualche giorno e devo dire che si legge molto bene: sia per quanto riguarda l’inglese per il modo di spiegare degli autori. In realtà questo libro lo ebbi per le mani già un anno fa quando frequentai il MGD ma non ebbi molto tempo per leggerlo e quindi ho deciso di comprarlo. E’ costato un bel po ma sono sicuro che ne varrà la pena. Per gli amanti della pratica, avverto che il libro in questione è molto teorico. Al massimo ci troverete 20 righe di codice (hlsl) in più di 1000 pagine (!!!) ma penso che sia un’ottima cosa per porre basi solide per il futuro.

Peccato solo che sta arrivando l’estate e non è facile rimanere a casa con i riflessi del mare che ti colpiscono a pochi metri da casa. Sto pensando di fare un 2×1 e di leggerlo anche al mare anche se capite bene che non è una lettura così leggera… :)

Mesi fa invece ho terminato la lettura di CLR via C# 3th edition e devo dire che sono rimasto notevolmente sorpreso. Consiglio a tutti questo libro se volete padroneggiare C# e conoscere in profondità il funzionamento del CLR. Un must-have senza se e senza ma…

Di recente invece ho iniziato a leggere C# in depth 2th edition che mi era stato consigliato parecchi mesi fa (in realtà la prima edizione forse più di un anno fa). In questo caso devo dire che mi aspettavo di più anche SE la lettura di CLR via C# influisce molto su questa valutazione preliminare. Nonostante sia ancora ai primi capitoli avanzo qualche considerazione. L’autore ha un approccio un po particolare cioè parte dalle vecchie versioni del C# per arrivare all’ultima (C# 4.0) così da evidenziare proprio l’evoluzione delle features del linguaggio. Questo approccio è sicuramente interessante ma potrebbe risultare stancante alla lunga; spero che non sia così. In più rispetto a CLR via C# ho notato che si concentra molto di più sul linguaggio (e questo è un bene) ma, per forza di cose, trascurando buona parte di quello che ci sta attorno (CLR). In conclusione penso che i due libri siano in qualche modo complementari e non esclusivi.

 

Per ora è tutto. E voi, cosa state leggendo?

Stringhe “hardcodate”

Nello sviluppo di qualsiasi software non manca di certo l’uso delle stringhe (string litteral detta alla C++) direttamente nel codice sorgente per svariate usi: dal titolo della finestra della nostra applicazione al nome di una texture… Esse sono molto utili anche perchè a noi umani torna molto più facile leggerle. Laddove però abbiamo bisogno di performance non vanno più bene ed è quindi consuetudine convertire la stringa in una chiave hash e usare quest’ultima come indentificartore.

Immaginiamo ad esempio di avere una collezione di suoni e di dover mandare in play il suono hit.wav quando il nostro player viene colpito…il nostro codice assomiglierà a qualcosa del genere:

//...
soundManager->play("hit.wav");
//...

Il soundManager, nella nostra prima e rudimentale implementazione, avrà una semplice mappa con chiave la stringa che identifica il suono e con il valore, ad esempio, l’handle del buffer di openAL del suono. Questo però è poco efficiente poichè ad ogni lookup della mappa equivalgono N comparazioni tra stringhe (strcmp): possiamo e dobbiamo fare di meglio!.

Come ho già detto quindi si converte la stringa in una chiave hash (solitamente uint32) usando una delle tantissime funzioni hash (esempi). La strada più usata sembra essere la seguente: si wrappa la nostra stringa in una classe che si occupa di calcolare il valore di hash e si implementa opportunamente l’operator==. In debug si conserva comunque la stringa per comodità mentre essa “sparisce” in release.

#include <string>

class StringId
{
public:

  explicit StringId(const char* const str)
  {
#ifdef DEBUG
    mString = str;
#endif
    mHash = hash(srt,strlen(str));
  }

  bool operator==(const StringId& other)
  {
     return mHash == other.mHash;
  }

  // operatore di conversione, altri operatori, e tutto il resto...
  // ... lasciato come esercizio! :)

private:

#ifdef DEBUG
  std::string mString;
#endif
  uint32 mHash;
};

Tornando al nostro super-complesso esempio potremmo convertire il codice come segue:

/*static storage*/ StringId sound_hit_wav("hit.wav");
//...
soundManager->play(sound_hit_wav);

//soundManager->play(StringId("hit_wav")); // no! evitiamo la costruzioni di inutili oggetti temp e la ri-generazione dell'hash
//...

e ovviamente ri-implemetando in modo opportuno il soundManger: la chiave della nostra mappa sarà ora un StringId o, ancora meglio, uint32. In questo modo ci siamo assicurati ottime performance senza però rinunciare alla comodità delle stringhe per noi comuni mortali.

Ti ricordi quando ti ho detto che in release la stringa sparisce ? bhè non è per niente vero nella maggior parte dei casi e vediamo perchè.
In c++ le stringhe costanti (rvalue string literal) vengono memorizzate nello static storage cioè in un’area di memoria riservata per la sola lettura. Questo vuol dire che nonostante i nostri sforzi per la creazione della classe StringId abbiamo si incrementato le performance in termini di tempo ma non di spazio (anzi, ora abbiamo un uint32 in più :D ) In più il calcolo della generazione dell’hash viene eseguita a runtime (comunque prima dell’esecuzione del main se abbiamo variabili con storage statico)

Se infatti apriamo un hex editor e ispezioniamo il nostro eseguibile:

vediamo chiaramente la nostra bella stringa nonostante l’eseguibile sia stato compilato in release con tutte le ottimizzazioni abilitate.
Per evitare cioè abbiamo queste possibilità:

0) Non fare niente visto che si parla di pochi kb anche se abbiamo centinaia di stringhe.
1) Non fare niente fidandoci del compilatore (sembra che gcc riesca a eliminare le string litteral non usate):

// da http://www.gamedev.net/topic/550505-string-names-for-resources/

#include <string.h>
#include <stdio.h>
inline unsigned int hash(const char* str) // cookbook Adler32 sum
{
unsigned int s1 = 1;
unsigned int s2 = 0;
for (unsigned int n = 0; n < strlen(str); ++n)
{
s1 = (s1 + str[n]) % 65521;
s2 = (s2 + s1) % 65521;
}
return (s2 << 16) | s1;
}

int main()
{
printf("%u", hash("SomeTextureName"));
}

This compiles to... HOLY CRAP!
movl $807077383, 4(%esp)
movl $LC0, (%esp)
call _printf
xorl %eax, %eax
leave
ret
($LC0 holds .ascii "%u\0", the format string)

2) Usare il preprocessore con la magia nera delle MACRO
3) Usare lo stesso compilatore con la magia nera dei TEMPLATE (questo risolverebbe solo il problema della generazione a runtime dell’hash)
4) “Hardcodare” a mano o tramite qualche tool oltre alla stringa anche la stessa chiave hash che in release viene scarta grazie ad una semplice macro.

Personalmente preferisco la soluzione 4 anche perchè, escludendo le prime due, la 2 richiede una sintassi orribile:

// da http://bitsquid.blogspot.com/2010/10/static-hash-values.html
uint32 root_point = HASH_STR_10('r','o','o','t','_','p','o','i','n','t'))

e, come la 3, sono difficili da manutenere (se volessi cambiare algoritmo?) e allungano i tempi di compilazione (proporzionalmente con il numero di string literal).

La 4 ha due varianti.
La prima è quella “manuale”: quando il programmatore inserisce una nuova stringa, genera offline l’hash (con un piccolo tool) e l’ “hardcoda” direttamente nel sorgente:

// da http://bitsquid.blogspot.com/2010/10/static-hash-values.html
#ifdef _DEBUG
    inline uint32 hash(const char *s, uint32 value) {
        assert( hash(s, strlen(s)) == value );
        return value;
    }
#else
    #define hash(s,v) (v)
#end
 
//...
 
uint32 root_point = static_hash("root_point", 0x5e43bd96);

da notare che in debug viene controllata la consistenza tra l’hash hardcodato e quello generato a runtime con una assert mentre in release sparisce tutto.

Invece la soluzione automatizzata potrebbe essere quella suggeritaci da Julien Koenen in un commento:

What we do here at keen games is that we have .crc files (text files with one identifier in each line) that run through a ruby script in our maketool (before the compilation starts). This script creates a header file with defines for each symbol in the crc file and the hash (crc32 in our case) value as value. That worked like a charm for all our projects.

In sostanza si ha un file del tipo

//c++ var, string
sound_hit_wav, hit_wav
sound_fire_wav, fire_wav
...

che viene convertito dal tool in un header del tipo:

#ifndef AUTO_GENERATED_IDS_H
#define AUTO_GENERATED_IDS_H

// don't edit this file manually, use XXX tool instead !!!

const uint32 sound_hit_wav = 0x5953e937;
const uint32 sound_fire_wav =  0x701b653a;
//...

#endif

o una sorta di parser del codebase alla ricerca delle stringhe e la sostituzione come ci suggerisce Phil in un’altro commento:

Instead of storing the output in a separate file we just modify the source file in place.

What we do is to place every string in a macro like this:

H(“hello”, 0)

Which gets replaced with the appropriate hash in the second parameter. So immediately after the pre-parser has run on the code file it would look like this:

H(“hello”, 0×263262)

Penso che sia tutto almeno per ora, alla prossima, ciao!

Questioni di layout

In questi ultimi mesi sul web (blog, forum, etc) è diventato (tornato?) di moda il termine “Data Oriented Design (DOD)“. Per chi non sapesse di cosa si tratta, rimando alla domanda fatta su  StackOverflow “what-is-data-oriented-design” che appunto risponde molto brevemente.

Se dovessi io riassumere il DOD in una frase, scriverei:

progettare il software (o moduli di esso) focalizzandosi sugli insiemi di dati che lo compongono e sulla loro trasformazione.

Read the rest of this entry »