Benchmark

Esordisco dicendo che ho intenzione di rilasciare questo lavoro sul mio account GitHub, ovviamente in forma gratuita e completamente riutilizzabile, per cui potete anche evitare di scopiazzare il codice dalle immagini. Al posto vostro, cercherei di capire, piuttosto, quello che si combina… Potete dare alle fiamme il codice che abbiamo scaricato dal file zip: serviva per farvi sporcare le mani. Ora scaricate questo da GitHub1, visto che avete imparato come si fa.

Voglio trascinarvi in una specie di gioco. L’idea è quella di scrivere la stessa classe due volte: una in GDScript e una in C++ con GDExtension. Creeremo un programma che non serve ad un emerito niente, tranne che a confrontare le prestazioni dei due linguaggi: sono proprio curioso di vedere cosa ne salta fuori. A scanso di equivoci, GDExtension è più il nome di una tecnologia che altro, per cui che io scriva GDExtension o C++, sapete che mi sto riferendo essenzialmente alla stessa cosa…

Idea di massima

Forse l’avrete capito già, ma lo dico lo stesso: mentre GDScript può creare classi ereditate da C++ con GDExtension, il contrario è del tutto impossibile. Si spiega molto facilmente: le classi GDExtension si legano, con un barbatrucco, direttamente al motore di Godot e ne diventano parte integrante, in un certo senso. GDScript, invece, è un linguaggio interpretato dallo stesso game engine, per cui e in linea di massima, non potete far ereditare una classe scritta in GDScript da una nuova classe scritta in C++. GDScript, in altre parole, è… fumo negli occhi. E’ qualcosa nascosto tra le righe del codice del game engine. Sia chiaro che stiamo parlando di ereditarietà e non di interoperabilità, cosa ben diversa…

Per questo motivo siamo costretti a partire, subito, con il C++. L’idea che passa nella mia mente buggata è quella di creare una classe che fornisca la struttura di base per un benchmark. Poi la erediteremo sia da classi GDScript che da classi C++. Ogni classe ereditata, farà un suo lavoro scritto ora in un linguaggio ora nell’altro. La classe progenitrice delle altre discenderà da Node3D perché ci voglio andare giù pesante con la grafica tridimensionale, in modo da vedere cosa succede.

La prima classe in C++

Voi siete dei provetti programmatori in C++, quindi sapete bene che i nomi dei files rispecchiano i nomi delle classi contenuti al loro interno, convenzionalmente. Sapete anche che una classe è, solitamente, divisa in due file diversi, uno di header che contiene i prototipi, con estensione .h o .hpp, ed un file .cpp che contiene la parte esecutiva, quella in cui vengono scritti i metodi per esteso. La classe GDExample è stata spazzata via, come anticipato. Al suo posto, ho creato una classe GDBenchmakWork che deriva da Node3D.

Approfitto per sottolineare un concetto solo in apparenza banale. Il C++ è un linguaggio disponibile per diverse piattaforme intese come sistemi operativi, ma anche come librerie del genere di .Net2, Mono3 o Qt4. Ognuna di queste tecnologie, si porta dietro una sua filosofia, magari condivisa con altre, ma non necessariamente. Qualunque cosa abbiate imparato attraverso .Net, Mono, Qt… come dire? Potete cestinarla. Quello che dovete salvare è l’ANSI C++, ovvero il linguaggio di programmazione vero e proprio, non le librerie che se lo portano dietro! Godot ha la sua tecnologia con le sue classi che sono, né più né meno, le stesse già utilizzate con GDScript.

Questo fatto implica che, per strano che possa sembrare, esiste una classe String che nel C++ canonico non c’è, ma sapete benissimo esistere in GDScript. Significa che esistono concetti come i segnali, mentre in altri contesti si tende a parlare di eventi passando per i delegati… Insomma: vi sembrerà di scrivere ancora in GDScript, ma in una versione un po’ più prolissa e contorta.

Questo strano C++

L’header

Vorrei iniziare a parlarvi delle stranezze del C++ di GDExtension, rispetto a quello più canonico. Iniziamo con il file header, gdbenchmarkwork.h (Fig.10, codice più a sinistra). In alto, ad un certo punto, compare questa strana specie di funzione che sembra starci come i cavoli a merenda:

GDCLASS(GDBenchmarkWork, Node3D)

Si tratta di una macro, ovvero qualcosa che, in fase di compilazione, verrà sostituito di sana pianta con un codice alternativo e molto più esteso. Dobbiamo prenderla larga. Siccome voi avete già scritto intere enciclopedie in C++, sapete benissimo che le interfacce dei vari IDE non crescono spontaneamente come funghi, ma discendono, in qualche modo, dalla gerarchia stessa delle classi di una libreria. Ovviamente .NET ha un suo metodo per ottenere questo effetto diverso da quello di Qt, visto che sono implementazioni totalmente diverse. Mono, metti che ci fosse ancora qualcuno che non lo sapesse, è la versione open source di .NET, praticamente il suo gemello omozigote. Ovviamente, anche a Godot serve un meccanismo del genere. Questa macro è proprio quella che permette di far conoscere questa classe all’IDE di Godot, archiviandone gli estremi in un database interno. Se la omettete, la classe non potrà esistere come elemento dell’IDE. Avrete capito che richiede due soli parametri: il nome della classe stessa e del suo diretto antenato.

Sempre nell’header, noterete che la classe è circondata da queste direttive al compilatore:

#ifndef GDBENCKMARKWORK_H
#define GDBENCKMARKWORK_H
...
#endif

In altri contesti potreste aver trovato qualcosa di simile a questo:

#pragma once

Fondamentalmente, queste istruzioni aiutano il preprocessore ad evitare l’inclusione multipla dei file di header in fase di compilazione. Il funzionamento è lo stesso, ma, neanche a dirlo, la seconda versione non funziona con Godot. Il termine GDBENCKMARKWORK_H è del tutto arbitrario, ma è prassi usare nomi che, seppur tutti in maiuscolo, corrispondano a quelli della classe nel file, per evitarne un riutilizzo accidentale, potenzialmente catastrofico.

Sempre nella parte header, più in basso, troviamo queste istruzioni, molto importanti perché squisitamente legate a Godot:

GDVIRTUAL0(_benchmark);		
GDVIRTUAL0R(String, _log_message);

Come dicevo sopra, Godot, come .Net e Qt, ha un suo meccanismo di class inspection, che proprio qui vediamo entrare in funzione. GDVIRTUAL0 e GDVIRTUAL0R sono due macro che registrano due metodi come accessibili attraverso l’interfaccia di GDScript e dell’IDE di Godot. Sono parte di una serie di macro che seguono una convenzione particolare. Il loro nome inizia sempre con GDVIRTUAL seguito da una o due cifre che rappresentano il numero di parametri accettati dal metodo in ingresso. Sia _benchmark che _log_message non accettano parametri in ingresso e, quindi, entrambi sono registrati con una macro del tipo GDVIRTUAL0. _log_message, però, restituisce un valore, da cui la R finale della macro adottata.

Esistono anche macro per metodi dichiarati const, per cui la corrispondente macro riporta una C finale e macro per metodi che devono obbligatoriamente subire un override, che terminano con _REQUIRED. Per tutte queste macro, il primo valore in ingresso è il tipo restituito, se esistente, seguito dal nome del metodo da registrare ed, infine, dai tipi dei valori in ingresso, nell’ordine corrispondente a quello del metodo sottostante. Riepilogando: GDVIRTUAL2R_REQUIRED è la macro che permette di registrare un metodo con due parametri in ingresso, un valore di ritorno di qualche tipo e richiede l’override obbligatorio del metodo.

Adesso è possibile leggere la seconda riga che, come si può notare, dichiara che si desidera registrare _log_message come classe di cui sia possibile fare l’override anche da GDScript, che restituirà un valore String.

Va detto che le classi registrate come virtuali, convenzionalmente, hanno nomi che cominciano con un underscore(_), convenzione non strettamente vincolante, ma che consiglio di seguire. GDVIRTUAL è solo una parte del problema: il restante lo troveremo, a breve, esaminando il resto del codice.

L’implementazione: zona protected

GDBenchmarkWork::_bind_methods()

Sempre in Fig.10, andiamo a dare un’occhiata al lato destro, al codice nel file gdbenchmarkwork.cpp. Siccome non ci stava in un’unica schermata, ho iniziato con il creare uno screenshot della parte alta, la zona protected della classe. Iniziamo con l’esaminare questo metodo:

void GDBenchmarkWork::_bind_methods()
{
    ClassDB::bind_method(D_METHOD("run"), &GDBenchmarkWork::run);             
    
    GDVIRTUAL_BIND(_benchmark);
    GDVIRTUAL_BIND(_log_message);
        
    ADD_SIGNAL(MethodInfo("job_ended", PropertyInfo(Variant::STRING, "message")));
}

Qui dentro c’è parecchio di interessante. Sostanzialmente, torna lo stesso discorso fatto per la macro GDCLASS, ma questa volta parliamo dei metodi e di un segnale esportati dalla classe. Il metodo _bind_methods, prima di tutto, non è una mia invenzione, ma una eredità delle classi di Godot. E’ un metodo statico, invocabile senza istanziare oggetti, ed è quello chiamato per collegare, ancora una volta, la classe C++ al database delle classi utilizzato per GDScript e l’IDE.

Come parametri, bind_method accetta una macro D_METHOD a cui è passato un nome di metodo, e un riferimento al metodo corrispondente nel codice C++ (attenzione all’operatore &). E’ chiaro che si potrebbe assegnare un nome diverso al metodo così come apparirebbe in GDScript rispetto al nome interno in C++, ma è altrettanto chiaro che sarebbe oltremodo stupido… Questo modo di registrare un metodo è strettamente legato ai metodi non sovrascrivibili in GDScript.

Seguono questi due:

GDVIRTUAL_BIND(_benchmark);
GDVIRTUAL_BIND(_log_message);

La loro funzione dovrebbe essere chiara, dopo la disquisizione sulla macro GDVIRTUAL. Se detta macro dichiara i metodi come virtual ed esterna la necessità di poterne fare l’override anche da GDScript, questa macro effettua il binding per metodi virtuali. GDVIRTUAL_BIND, in altre parole, nasconde una chiamata al metodo ClassDB::bind_virtual_method, rendendone meno complessa l’implementazione… Strano a dirsi, non ne esiste una corrispondente per ClassDB::bind_method che, perciò, siamo costretti a scrivere per esteso.

Allo stesso modo, la macro ADD_SIGNAL crea un segnale alla stregua di come succede in GDScript. MethodInfo accetta, come primo parametro, una stringa che diventerà il nome del segnale, mentre i successivi saranno istanze di PropertyInfo che associano ad un tipo di dato il nome con cui verrà identificato. Interessante il fatto che tutti questi valori sono sempre Variant::Qualcosa. In questo caso, quindi, avremo un segnale chiamato job_ended che si porta dietro un dato costituito da una stringa il cui nome identificativo nel codice è message.

GDBenchmarkWork::_benchmark_execute()

Esaminiamo, ora, il codice seguente:

uint64_t GDBenchmarkWork::_benchmark_execute()
{
    uint64_t elapsed_time = Time::get_singleton()->get_ticks_usec();
        _benchmark();
    elapsed_time = Time::get_singleton()->get_ticks_usec() - elapsed_time;    
    return elapsed_time;
}

Chiaramente, tutto quello che fa è prendere un tempo, lanciare il metodo _benchmark, fare la differenza tra il tempo precedente l’esecuzione e quello dopo la sua terminazione per restituirne la differenza. In poche parole, misura quanto tempo ci mette _benchmark ad essere eseguito. La parte che ci interessa di più, però, è questa:

uint64_t elapsed_time = Time::get_singleton()->get_ticks_usec();

E’ interessante perché, ancora una volta, si tratta di una caratteristica di Godot. Time è una delle numerose classi create sempre in un unico esemplare e messe a disposizione globalmente: un singleton, secondo lo strano mondo dei design patterns. Di singleton, in Godot, ce ne sono diversi, sicuramente già utilizzati in GDScript. Altri esempi sono le classi Engine e OS. Nel mondo GDExtension, tutti i singleton si usano allo stesso modo e cioè passando per il metodo statico standard get_singleton() che vi mette subito in contatto con loro. Trovate un altro esempio in corrispondenza del metodo GDBenchmarkWork::_benchmark() che, in questa classe, è solo un segnaposto che fa perdere 250 msec.

L’implementazione: zona public

GDBenchmarkWork::run()

Esaminiamo, ora, la parte pubblica. Il metodo run() è stato studiato con l’idea di mettere una serie di queste classi in un array e chiamarle in sequenza e, quindi, non può avere parametri.

All’interno noterete la chiamata al metodo _benchmark_execute() nonché quella che lancia un segnale al termine delle operazioni, lo stesso segnale di cui abbiamo parlato in precedenza.

Notate anche come viene formattata una stringa in GDExtension, attraverso il metodo globalmente disponibile vformat, che funziona in modo molto simile a come funziona la classica sprintf() del mondo C/C++.

GDBenchmarkWork::_benchmark() e GDBenchmarkWork::_log_message

Questi due metodi hanno in comune l’utilizzo delle macro GDVIRTUAL_IS_OVERRIDDEN e GDVIRTUAL_CALL e mostrano l’ultimo dei tre step necessari per rendere un metodo sovraccaricabile in GDScript. Potremmo dire, senza troppa esagerazione, che la scelta tra l’avvio del metodo originario e quello in GDScript è (passatemi il termine) manuale. Osserviamo:

String GDBenchmarkWork::_log_message()
{
	if (!GDVIRTUAL_IS_OVERRIDDEN(_log_message)) {		
		return "Processo di default. %d usec.";
	} else {
        String message;
        GDVIRTUAL_CALL(_log_message, message);
        return message;
	}
}

Ho riportato il codice del metodo _log_message() per maggiore chiarezza. Esaminandolo, viene fuori che occorre stabilire se il metodo è stato sovrascritto, in GDScript, con la macro GDVIRTUAL_IS_OVERRIDDEN che, intuitivamente, restituisce un valore booleano. Se la macro restituisce false allora occorre procedere con il codice di default del metodo. In caso contrario, si chiama il metodo sovrascrivente con GDVIRTUAL_CALL che può accettare uno o due parametri in ingresso: il primo è il sempre presente nome del metodo da chiamare, l’eventuale secondo è una variabile d’appoggio in cui verrà depositato il risultato. Ovviamente i tipi devono corrispondere a quelli del metodo originario. Nel caso di metodi che non restituiscono valori, la chiamata prevede solo il primo parametro.

Bonus

Questo paragrafo è stato appiccicato qui a posteriori, sinceramente, perché mi sono reso conto che, dato il progettino che mi sono comandato di creare, non serviranno l’uso di metodi con parametri in ingresso né proprietà. Ignorare queste casistiche, però, avrebbe comportato un bel buco nella trama, per così dire. Quindi, del tutto fuori dal discorso generale, andiamo ad illustrare come si procede in questi casi.

In Fig. 11 bis, abbiamo una classe chiamata GDFakeClass del tutto inutile.

Parto da una considerazione personale: ritengo che, quando si parla di proprietà che debbano essere visibili direttamente da IDE, generalmente queste non debbano essere né di sola lettura né di sola scrittura. Parametri read only o write only, come siamo abituati a chiamarli, hanno senso solo a livello di codice, nella maggior parte dei casi.

Dichiarare una proprietà

Diamo un’occhiata al codice nell’header. Compaiono queste due righe:

void setter_method(float valore);
float getter_method() const;

Sono il metodo setter ed il metodo getter della proprietà che stiamo per andare ad esportare. Nulla vieta, ovviamente, di dichiararli virtual e procedere come detto nel paragrafo precedente per renderli disponibili all’overriding.

Ora guardiamo questo spezzone di codice del motodo _bind_methods() nel file sulla destra:

 // Property with Setter and Getter
    ClassDB::bind_method(D_METHOD("setter_method", "valore"),&GDFakeClass::setter_method);
    ClassDB::bind_method(D_METHOD("getter_method"), &GDFakeClass::getter_method);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "set_get_param"), 
                                "setter_method", "getter_method");

Vedete come viene effettuato il binding del metodo setter, che accetta un parametro in ingresso: al nome del metodo, si affianca una stringa con il nome del parametro in ingresso, in questo caso “valore”.

Poi c’è la macro ADD_PROPERTY che accetta tre parametri: il primo è un’altra macro, PropertyInfo che vuole per sé come primo parametro in ingresso unVariant che corrisponda al tipo accettato e restituito dalla proprietà e come secondo parametro una stringa con il nome che assumerà la proprietà sia a livello di codice che nell’IDE di Godot. Variat::FLOAT è una costante dichiarata a livello di classe, ovviamente.

Come secondo e terzo parametro, ADD_PROPERTY si aspetta il nome del metodo setter e quello del metodo getter, in quest’ordine.

Dichiarare un metodo virtuale con parametri in ingresso.

Torniamo a guardare Fig. 11 bis, in particolare il codice header sul lato sinistro. Troviamo queste due righe di codice:

virtual float _vattelappesca(int numero, int altro);

GDVIRTUAL2R(float, _vattelappesca, int, int);	

Il metodo è dichiarato virtual, come nei casi del paragrafo precedente, ma ci sono due parametri di tipo int in ingresso. Guardate, ora, la macro che lo segue: GDVIRTUAL2R. Come da convenzione, il 2 finale rappresenta il numero di parametri in ingresso, mentre la R finale indica che ritorna un valore. La parte più interessante della macro sta, però, nel modo in cui è scritta: il primo parametro è il tipo di dato restituito, il secondo è il nome del metodo; segue l’elenco dei tipi dei parametri in ingresso nello stesso ordine in cui compaiono nel metodo! Cosa un po’ strana forse, ma non troppo, visto che le macro agiscono a livello di precompilatore e non hanno nulla a che fare con il linguaggio vero e proprio, tutte le voci passate alla macro non sono racchiuse tra apici e non contengono riferimenti a tipi di dato o robe del genere… Se il metodo restituisse void, come visto in precedenza, mancherebbe il primo parametro mentre tutto il resto rimarrebbe identico.

Andiamo, ora, ad esaminare il contenuto del metodo _bind_methods(), nel file .cpp. Troviamo questa riga:

GDVIRTUAL_BIND(_vattelappesca, "numero", "altronumero");

GDVIRTUAL_BIND questa volta si aspetta il nome del metodo, come nei casi precedenti, seguito dai nomi dei parametri in ingresso tra apici. Cosa sono questi nomi? Sono quelli che utilizzerà GDScript se gli chiederete di fare l’override di questo metodo. Solito discorso: non è necessaria una corrispondenza 1:1, ma è un’ottima prevenzione contro la pazzia…

Andiamo avanti nello stesso file e troviamo:

float GDFakeClass::_vattelappesca(int numero, int altro)
{
    if (!GDVIRTUAL_IS_OVERRIDDEN(_vattelappesca))
    {
        return 1.0f;
    }
    else
    {
        float ritorno;
        GDVIRTUAL_CALL(_vattelappesca, numero, altro, ritorno);
        return ritorno;
    }
}

Guardate come viene invocata la macro GDVIRTUAL_CALL: primo parametro il nome del metodo, seguono in ordine i nomi dei parametri in ingresso e, sempre per ultimo se presente, il valore di ritorno. Tutto senza apici, perché, questa volta, i parametri sono gli stessi che arrivano in ingresso al metodo C++ passati, pari pari, alla macro.


  1. Repository del progetto su GitHub. Naturalmente, l’idea è che dovreste usare git per scaricarlo e non il web… ↩︎
  2. Link ufficiale a .NET ↩︎
  3. Link ufficiale a Mono ↩︎
  4. Link ufficiale a Qt ↩︎

Benchmark

Esordisco dicendo che ho intenzione di rilasciare questo lavoro sul mio account GitHub, ovviamente in forma gratuita e completamente riutilizzabile, per cui potete anche evitare di scopiazzare il codice dalle immagini. Al posto vostro, cercherei di capire, piuttosto, quello che si combina… Potete dare alle fiamme il codice che abbiamo scaricato dal file zip: serviva per farvi sporcare le mani. Ora scaricate questo da GitHub1, visto che avete imparato come si fa.

Voglio trascinarvi in una specie di gioco. L’idea è quella di scrivere la stessa classe due volte: una in GDScript e una in C++ con GDExtension. Creeremo un programma che non serve ad un emerito niente, tranne che a confrontare le prestazioni dei due linguaggi: sono proprio curioso di vedere cosa ne salta fuori. A scanso di equivoci, GDExtension è più il nome di una tecnologia che altro, per cui che io scriva GDExtension o C++, sapete che mi sto riferendo essenzialmente alla stessa cosa…

Idea di massima

Forse l’avrete capito già, ma lo dico lo stesso: mentre GDScript può creare classi ereditate da C++ con GDExtension, il contrario è del tutto impossibile. Si spiega molto facilmente: le classi GDExtension si legano, con un barbatrucco, direttamente al motore di Godot e ne diventano parte integrante, in un certo senso. GDScript, invece, è un linguaggio interpretato dallo stesso game engine, per cui e in linea di massima, non potete far ereditare una classe scritta in GDScript da una nuova classe scritta in C++. GDScript, in altre parole, è… fumo negli occhi. E’ qualcosa nascosto tra le righe del codice del game engine. Sia chiaro che stiamo parlando di ereditarietà e non di interoperabilità, cosa ben diversa…

Per questo motivo siamo costretti a partire, subito, con il C++. L’idea che passa nella mia mente buggata è quella di creare una classe che fornisca la struttura di base per un benchmark. Poi la erediteremo sia da classi GDScript che da classi C++. Ogni classe ereditata, farà un suo lavoro scritto ora in un linguaggio ora nell’altro. La classe progenitrice delle altre discenderà da Node3D perché ci voglio andare giù pesante con la grafica tridimensionale, in modo da vedere cosa succede.

La prima classe in C++

Voi siete dei provetti programmatori in C++, quindi sapete bene che i nomi dei files rispecchiano i nomi delle classi contenuti al loro interno, convenzionalmente. Sapete anche che una classe è, solitamente, divisa in due file diversi, uno di header che contiene i prototipi, con estensione .h o .hpp, ed un file .cpp che contiene la parte esecutiva, quella in cui vengono scritti i metodi per esteso. La classe GDExample è stata spazzata via, come anticipato. Al suo posto, ho creato una classe GDBenchmakWork che deriva da Node3D.

Approfitto per sottolineare un concetto solo in apparenza banale. Il C++ è un linguaggio disponibile per diverse piattaforme intese come sistemi operativi, ma anche come librerie del genere di .Net2, Mono3 o Qt4. Ognuna di queste tecnologie, si porta dietro una sua filosofia, magari condivisa con altre, ma non necessariamente. Qualunque cosa abbiate imparato attraverso .Net, Mono, Qt… come dire? Potete cestinarla. Quello che dovete salvare è l’ANSI C++, ovvero il linguaggio di programmazione vero e proprio, non le librerie che se lo portano dietro! Godot ha la sua tecnologia con le sue classi che sono, né più né meno, le stesse già utilizzate con GDScript.

Questo fatto implica che, per strano che possa sembrare, esiste una classe String che nel C++ canonico non c’è, ma sapete benissimo esistere in GDScript. Significa che esistono concetti come i segnali, mentre in altri contesti si tende a parlare di eventi passando per i delegati… Insomma: vi sembrerà di scrivere ancora in GDScript, ma in una versione un po’ più prolissa e contorta.

Questo strano C++

L’header

Vorrei iniziare a parlarvi delle stranezze del C++ di GDExtension, rispetto a quello più canonico. Iniziamo con il file header, gdbenchmarkwork.h (Fig.10, codice più a sinistra). In alto, ad un certo punto, compare questa strana specie di funzione che sembra starci come i cavoli a merenda:

GDCLASS(GDBenchmarkWork, Node3D)

Si tratta di una macro, ovvero qualcosa che, in fase di compilazione, verrà sostituito di sana pianta con un codice alternativo e molto più esteso. Dobbiamo prenderla larga. Siccome voi avete già scritto intere enciclopedie in C++, sapete benissimo che le interfacce dei vari IDE non crescono spontaneamente come funghi, ma discendono, in qualche modo, dalla gerarchia stessa delle classi di una libreria. Ovviamente .NET ha un suo metodo per ottenere questo effetto diverso da quello di Qt, visto che sono implementazioni totalmente diverse. Mono, metti che ci fosse ancora qualcuno che non lo sapesse, è la versione open source di .NET, praticamente il suo gemello omozigote. Ovviamente, anche a Godot serve un meccanismo del genere. Questa macro è proprio quella che permette di far conoscere questa classe all’IDE di Godot, archiviandone gli estremi in un database interno. Se la omettete, la classe non potrà esistere come elemento dell’IDE. Avrete capito che richiede due soli parametri: il nome della classe stessa e del suo diretto antenato.

Sempre nell’header, noterete che la classe è circondata da queste direttive al compilatore:

#ifndef GDBENCKMARKWORK_H
#define GDBENCKMARKWORK_H
...
#endif

In altri contesti potreste aver trovato qualcosa di simile a questo:

#pragma once

Fondamentalmente, queste istruzioni aiutano il preprocessore ad evitare l’inclusione multipla dei file di header in fase di compilazione. Il funzionamento è lo stesso, ma, neanche a dirlo, la seconda versione non funziona con Godot. Il termine GDBENCKMARKWORK_H è del tutto arbitrario, ma è prassi usare nomi che, seppur tutti in maiuscolo, corrispondano a quelli della classe nel file, per evitarne un riutilizzo accidentale, potenzialmente catastrofico.

Sempre nella parte header, più in basso, troviamo queste istruzioni, molto importanti perché squisitamente legate a Godot:

GDVIRTUAL0(_benchmark);		
GDVIRTUAL0R(String, _log_message);

Come dicevo sopra, Godot, come .Net e Qt, ha un suo meccanismo di class inspection, che proprio qui vediamo entrare in funzione. GDVIRTUAL0 e GDVIRTUAL0R sono due macro che registrano due metodi come accessibili attraverso l’interfaccia di GDScript e dell’IDE di Godot. Sono parte di una serie di macro che seguono una convenzione particolare. Il loro nome inizia sempre con GDVIRTUAL seguito da una o due cifre che rappresentano il numero di parametri accettati dal metodo in ingresso. Sia _benchmark che _log_message non accettano parametri in ingresso e, quindi, entrambi sono registrati con una macro del tipo GDVIRTUAL0. _log_message, però, restituisce un valore, da cui la R finale della macro adottata.

Esistono anche macro per metodi dichiarati const, per cui la corrispondente macro riporta una C finale e macro per metodi che devono obbligatoriamente subire un override, che terminano con _REQUIRED. Per tutte queste macro, il primo valore in ingresso è il tipo restituito, se esistente, seguito dal nome del metodo da registrare ed, infine, dai tipi dei valori in ingresso, nell’ordine corrispondente a quello del metodo sottostante. Riepilogando: GDVIRTUAL2R_REQUIRED è la macro che permette di registrare un metodo con due parametri in ingresso, un valore di ritorno di qualche tipo e richiede l’override obbligatorio del metodo.

Adesso è possibile leggere la seconda riga che, come si può notare, dichiara che si desidera registrare _log_message come classe di cui sia possibile fare l’override anche da GDScript, che restituirà un valore String.

Va detto che le classi registrate come virtuali, convenzionalmente, hanno nomi che cominciano con un underscore(_), convenzione non strettamente vincolante, ma che consiglio di seguire. GDVIRTUAL è solo una parte del problema: il restante lo troveremo, a breve, esaminando il resto del codice.

L’implementazione: zona protected

GDBenchmarkWork::_bind_methods()

Sempre in Fig.10, andiamo a dare un’occhiata al lato destro, al codice nel file gdbenchmarkwork.cpp. Siccome non ci stava in un’unica schermata, ho iniziato con il creare uno screenshot della parte alta, la zona protected della classe. Iniziamo con l’esaminare questo metodo:

void GDBenchmarkWork::_bind_methods()
{
    ClassDB::bind_method(D_METHOD("run"), &GDBenchmarkWork::run);             
    
    GDVIRTUAL_BIND(_benchmark);
    GDVIRTUAL_BIND(_log_message);
        
    ADD_SIGNAL(MethodInfo("job_ended", PropertyInfo(Variant::STRING, "message")));
}

Qui dentro c’è parecchio di interessante. Sostanzialmente, torna lo stesso discorso fatto per la macro GDCLASS, ma questa volta parliamo dei metodi e di un segnale esportati dalla classe. Il metodo _bind_methods, prima di tutto, non è una mia invenzione, ma una eredità delle classi di Godot. E’ un metodo statico, invocabile senza istanziare oggetti, ed è quello chiamato per collegare, ancora una volta, la classe C++ al database delle classi utilizzato per GDScript e l’IDE.

Come parametri, bind_method accetta una macro D_METHOD a cui è passato un nome di metodo, e un riferimento al metodo corrispondente nel codice C++ (attenzione all’operatore &). E’ chiaro che si potrebbe assegnare un nome diverso al metodo così come apparirebbe in GDScript rispetto al nome interno in C++, ma è altrettanto chiaro che sarebbe oltremodo stupido… Questo modo di registrare un metodo è strettamente legato ai metodi non sovrascrivibili in GDScript.

Seguono questi due:

GDVIRTUAL_BIND(_benchmark);
GDVIRTUAL_BIND(_log_message);

La loro funzione dovrebbe essere chiara, dopo la disquisizione sulla macro GDVIRTUAL. Se detta macro dichiara i metodi come virtual ed esterna la necessità di poterne fare l’override anche da GDScript, questa macro effettua il binding per metodi virtuali. GDVIRTUAL_BIND, in altre parole, nasconde una chiamata al metodo ClassDB::bind_virtual_method, rendendone meno complessa l’implementazione… Strano a dirsi, non ne esiste una corrispondente per ClassDB::bind_method che, perciò, siamo costretti a scrivere per esteso.

Allo stesso modo, la macro ADD_SIGNAL crea un segnale alla stregua di come succede in GDScript. MethodInfo accetta, come primo parametro, una stringa che diventerà il nome del segnale, mentre i successivi saranno istanze di PropertyInfo che associano ad un tipo di dato il nome con cui verrà identificato. Interessante il fatto che tutti questi valori sono sempre Variant::Qualcosa. In questo caso, quindi, avremo un segnale chiamato job_ended che si porta dietro un dato costituito da una stringa il cui nome identificativo nel codice è message.

GDBenchmarkWork::_benchmark_execute()

Esaminiamo, ora, il codice seguente:

uint64_t GDBenchmarkWork::_benchmark_execute()
{
    uint64_t elapsed_time = Time::get_singleton()->get_ticks_usec();
        _benchmark();
    elapsed_time = Time::get_singleton()->get_ticks_usec() - elapsed_time;    
    return elapsed_time;
}

Chiaramente, tutto quello che fa è prendere un tempo, lanciare il metodo _benchmark, fare la differenza tra il tempo precedente l’esecuzione e quello dopo la sua terminazione per restituirne la differenza. In poche parole, misura quanto tempo ci mette _benchmark ad essere eseguito. La parte che ci interessa di più, però, è questa:

uint64_t elapsed_time = Time::get_singleton()->get_ticks_usec();

E’ interessante perché, ancora una volta, si tratta di una caratteristica di Godot. Time è una delle numerose classi create sempre in un unico esemplare e messe a disposizione globalmente: un singleton, secondo lo strano mondo dei design patterns. Di singleton, in Godot, ce ne sono diversi, sicuramente già utilizzati in GDScript. Altri esempi sono le classi Engine e OS. Nel mondo GDExtension, tutti i singleton si usano allo stesso modo e cioè passando per il metodo statico standard get_singleton() che vi mette subito in contatto con loro. Trovate un altro esempio in corrispondenza del metodo GDBenchmarkWork::_benchmark() che, in questa classe, è solo un segnaposto che fa perdere 250 msec.

L’implementazione: zona public

GDBenchmarkWork::run()

Esaminiamo, ora, la parte pubblica. Il metodo run() è stato studiato con l’idea di mettere una serie di queste classi in un array e chiamarle in sequenza e, quindi, non può avere parametri.

All’interno noterete la chiamata al metodo _benchmark_execute() nonché quella che lancia un segnale al termine delle operazioni, lo stesso segnale di cui abbiamo parlato in precedenza.

Notate anche come viene formattata una stringa in GDExtension, attraverso il metodo globalmente disponibile vformat, che funziona in modo molto simile a come funziona la classica sprintf() del mondo C/C++.

GDBenchmarkWork::_benchmark() e GDBenchmarkWork::_log_message

Questi due metodi hanno in comune l’utilizzo delle macro GDVIRTUAL_IS_OVERRIDDEN e GDVIRTUAL_CALL e mostrano l’ultimo dei tre step necessari per rendere un metodo sovraccaricabile in GDScript. Potremmo dire, senza troppa esagerazione, che la scelta tra l’avvio del metodo originario e quello in GDScript è (passatemi il termine) manuale. Osserviamo:

String GDBenchmarkWork::_log_message()
{
	if (!GDVIRTUAL_IS_OVERRIDDEN(_log_message)) {		
		return "Processo di default. %d usec.";
	} else {
        String message;
        GDVIRTUAL_CALL(_log_message, message);
        return message;
	}
}

Ho riportato il codice del metodo _log_message() per maggiore chiarezza. Esaminandolo, viene fuori che occorre stabilire se il metodo è stato sovrascritto, in GDScript, con la macro GDVIRTUAL_IS_OVERRIDDEN che, intuitivamente, restituisce un valore booleano. Se la macro restituisce false allora occorre procedere con il codice di default del metodo. In caso contrario, si chiama il metodo sovrascrivente con GDVIRTUAL_CALL che può accettare uno o due parametri in ingresso: il primo è il sempre presente nome del metodo da chiamare, l’eventuale secondo è una variabile d’appoggio in cui verrà depositato il risultato. Ovviamente i tipi devono corrispondere a quelli del metodo originario. Nel caso di metodi che non restituiscono valori, la chiamata prevede solo il primo parametro.

Bonus

Questo paragrafo è stato appiccicato qui a posteriori, sinceramente, perché mi sono reso conto che, dato il progettino che mi sono comandato di creare, non serviranno l’uso di metodi con parametri in ingresso né proprietà. Ignorare queste casistiche, però, avrebbe comportato un bel buco nella trama, per così dire. Quindi, del tutto fuori dal discorso generale, andiamo ad illustrare come si procede in questi casi.

In Fig. 11 bis, abbiamo una classe chiamata GDFakeClass del tutto inutile.

Parto da una considerazione personale: ritengo che, quando si parla di proprietà che debbano essere visibili direttamente da IDE, generalmente queste non debbano essere né di sola lettura né di sola scrittura. Parametri read only o write only, come siamo abituati a chiamarli, hanno senso solo a livello di codice, nella maggior parte dei casi.

Dichiarare una proprietà

Diamo un’occhiata al codice nell’header. Compaiono queste due righe:

void setter_method(float valore);
float getter_method() const;

Sono il metodo setter ed il metodo getter della proprietà che stiamo per andare ad esportare. Nulla vieta, ovviamente, di dichiararli virtual e procedere come detto nel paragrafo precedente per renderli disponibili all’overriding.

Ora guardiamo questo spezzone di codice del motodo _bind_methods() nel file sulla destra:

 // Property with Setter and Getter
    ClassDB::bind_method(D_METHOD("setter_method", "valore"),&GDFakeClass::setter_method);
    ClassDB::bind_method(D_METHOD("getter_method"), &GDFakeClass::getter_method);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "set_get_param"), 
                                "setter_method", "getter_method");

Vedete come viene effettuato il binding del metodo setter, che accetta un parametro in ingresso: al nome del metodo, si affianca una stringa con il nome del parametro in ingresso, in questo caso “valore”.

Poi c’è la macro ADD_PROPERTY che accetta tre parametri: il primo è un’altra macro, PropertyInfo che vuole per sé come primo parametro in ingresso unVariant che corrisponda al tipo accettato e restituito dalla proprietà e come secondo parametro una stringa con il nome che assumerà la proprietà sia a livello di codice che nell’IDE di Godot. Variat::FLOAT è una costante dichiarata a livello di classe, ovviamente.

Come secondo e terzo parametro, ADD_PROPERTY si aspetta il nome del metodo setter e quello del metodo getter, in quest’ordine.

Dichiarare un metodo virtuale con parametri in ingresso.

Torniamo a guardare Fig. 11 bis, in particolare il codice header sul lato sinistro. Troviamo queste due righe di codice:

virtual float _vattelappesca(int numero, int altro);

GDVIRTUAL2R(float, _vattelappesca, int, int);	

Il metodo è dichiarato virtual, come nei casi del paragrafo precedente, ma ci sono due parametri di tipo int in ingresso. Guardate, ora, la macro che lo segue: GDVIRTUAL2R. Come da convenzione, il 2 finale rappresenta il numero di parametri in ingresso, mentre la R finale indica che ritorna un valore. La parte più interessante della macro sta, però, nel modo in cui è scritta: il primo parametro è il tipo di dato restituito, il secondo è il nome del metodo; segue l’elenco dei tipi dei parametri in ingresso nello stesso ordine in cui compaiono nel metodo! Cosa un po’ strana forse, ma non troppo, visto che le macro agiscono a livello di precompilatore e non hanno nulla a che fare con il linguaggio vero e proprio, tutte le voci passate alla macro non sono racchiuse tra apici e non contengono riferimenti a tipi di dato o robe del genere… Se il metodo restituisse void, come visto in precedenza, mancherebbe il primo parametro mentre tutto il resto rimarrebbe identico.

Andiamo, ora, ad esaminare il contenuto del metodo _bind_methods(), nel file .cpp. Troviamo questa riga:

GDVIRTUAL_BIND(_vattelappesca, "numero", "altronumero");

GDVIRTUAL_BIND questa volta si aspetta il nome del metodo, come nei casi precedenti, seguito dai nomi dei parametri in ingresso tra apici. Cosa sono questi nomi? Sono quelli che utilizzerà GDScript se gli chiederete di fare l’override di questo metodo. Solito discorso: non è necessaria una corrispondenza 1:1, ma è un’ottima prevenzione contro la pazzia…

Andiamo avanti nello stesso file e troviamo:

float GDFakeClass::_vattelappesca(int numero, int altro)
{
    if (!GDVIRTUAL_IS_OVERRIDDEN(_vattelappesca))
    {
        return 1.0f;
    }
    else
    {
        float ritorno;
        GDVIRTUAL_CALL(_vattelappesca, numero, altro, ritorno);
        return ritorno;
    }
}

Guardate come viene invocata la macro GDVIRTUAL_CALL: primo parametro il nome del metodo, seguono in ordine i nomi dei parametri in ingresso e, sempre per ultimo se presente, il valore di ritorno. Tutto senza apici, perché, questa volta, i parametri sono gli stessi che arrivano in ingresso al metodo C++ passati, pari pari, alla macro.


  1. Repository del progetto su GitHub. Naturalmente, l’idea è che dovreste usare git per scaricarlo e non il web… ↩︎
  2. Link ufficiale a .NET ↩︎
  3. Link ufficiale a Mono ↩︎
  4. Link ufficiale a Qt ↩︎

Ultima modifica: