glib: portabilità e funzioni di utilità generale

glib è una libreria di portabilità C e funzioni di utilità generale per sistemi UNIX-like e Windows. Questo capitolo descrive alcune delle caratteristiche di questa libreria più comunemente usate in applicazioni GTK+ e Gnome. glib è semplice e i concetti sono familiari; quindi andremo avanti in fretta. Per una descrizione più completa di glib consultate glib.h oppure il manuale fornito con la libreria. (Non abbiate paura di utilizzare i header di glib, GTK+, o Gnome; sono scritti con cura, facili da leggere, utilissimi per veloci consultazioni. Non abbiate inoltre paura di guardare il codice sorgente, se avete dei dubbi specifici relativi alla implementazione.)

Le varie facilitazioni offerte da glib sono state concepite per avere una interfaccia coerente; lo stile di programmazione è semi-orientato agli oggetti, e gli identificatori hanno il prefisso con "g" per creare una sorta di namespace.

glib ha un unico file header, glib.h.

Fondamenti

glib fornisce sostituti per molti costrutti C standard e per quelli comunemente usati. Questa sezione descrive le definizioni dei tipi fondamentali, le macro, le routine di allocazione della memoria e le funzioni per maneggiare le stringhe.

Definizioni dei tipi fondamentali

Anzichè utilizzare i tipi standard del linguaggio C (int, long, ecc.) glib definisce i propri. Questi si rivelano utili in molti casi. Per esempio, abbiamo la garanzia che gint32 sia di 32 bit, proprietà che nessun tipo standard C può assicurare. guint è semplicemente più facile da digitare di unsigned. Alcuni dei typedef esistono solo per avere un interfaccia coerente; per esempio, gchar è sempre equivalente al tipo char.

glib definisce i seguenti tipi primitivi:

  • gint8, guint8, gint16, guint16, gint32, guint32, gint64, guint64 - questi forniscono tipi di dato con dimensione garantita. Non tutte le macchine dispongono di interi a 64 bit; se una macchina li possiede, glib definirà G_HAVE_GINT64. (Ovviamente, i tipi guint sono senza segno e i tipi gint sono con segno.)

  • gboolean è utile per rendere il codice più leggibile, poichè il C non ha un tipo bool.

  • gchar, gshort, glong, gint, gfloat, gdouble sono puramente cosmetici.

  • gpointer può essere più conveniente da scrivere di void *. gconstpointer è equivalente a const void*. (const gpointer non si comporterà come tipicamente ci si aspetta; se la ragione non è ovvia, leggete un buon libro di C.)

Macro frequentemente utilizzate

glib definisce alcune macro comunemente utilizzate in programmi C, elencate in Figura 1. Il significato di ciascuna di esse dovrebbe essere chiaro. MIN()/MAX() restituiscono il più piccolo o il più grande dei loro argomenti. ABS() restituisce il valore assoluto del suo argomento. CLAMP(x, low, high) significa x, se x sta tra low e high; se x è "sotto" l'intervallo, viene restituito low; se x è "sopra", viene restituito high. In aggiunta alle macro elencate in Figura 1, sono definite anche TRUE/FALSE/NULL come 1/0/((void*)0).

#include <glib.h>

MAX(a, b);

MIN(a, b);

ABS(x);

CLAMP(x, low, high);

Figura 1. Macro C di uso comune

Ci sono anche molte macro "uniche", come le conversioni portabili gpointer-to-gint e gpointer-to-guint mostrate in Figura 2.

La maggior parte delle strutture di glib è costruita per contenere un gpointer. Questo è il modo più corretto di memorizzare puntatori ad oggetti dinamicamente allocati. Avvolte avrete bisogno di memorizzare una semplice lista di interi, senza doverli allocare dinamicamente. Anche se il C standard non lo garantisce, è possibile memorizzare un gint o un guint in una variabile gpointer, sulle piattaforme su cui glib è stata "portata"; in alcuni casi è necessario un cast intermedio. Le macro elencate in Figura 2 sono un astrazione di questo cast.

Ecco un esempio:

   gint my_int;
   gpointer my_pointer;
    
   my_int = 5;
   my_pointer = GINT_TO_POINTER(my_int);
   printf("Memorizziamo %d\n", GPOINTER_TO_INT(my_pointer));

Attenzione; queste macro permettono di memorizzare un intero in un puntatore, ma memorizzare un puntatore in un intero non funzionerà. Per farlo in modo portabile, dovrete memorizzare in un long. (Che comunque non è una buona idea.)

#include <glib.h>

GINT_TO_POINTER(p);

GPOINTER_TO_INT(p);

GUINT_TO_POINTER(p);

GPOINTER_TO_UINT(p);

Figura 2. Macro per memorizzare interi in puntatori

Macro per il debug

glib ha un set di macro che potete usare per controllare le invarianze e le precondizioni nel vostro codice. GTK+ ne fa largo uso, ed è una delle ragioni per le quali è cosi stabile e facile da utilizzare. Spariranno tutte quando definirete G_DISABLE_CHECKS oppure G_DISABLE_ASSERT, quindi non c'e alcuna penalizzazione nel codice finale. Farne largo uso è, in generale, una buonissima idea. Vi aiuterà a trovare i bug molto più in fretta. Potete aggiungere asserzioni affinché essi non si ripresentino in futuro. I controlli sono utili specialmente nel caso in cui il vostro codice venga usato "a scatola chiusa" da altri programmatori; Gli utenti si accorgeranno immediatamente di aver utilizzato il codice in modo sbagliato.

Certamente, dovrete stare molto attenti ad assicurarvi che il vostro codice non dipenda da istruzioni eseguite soltanto in sessioni di debug. Le istruzioni che spariranno nel codice finale non dovrebbero avere mai effetti collaterali.

#include <glib.h>

g_return_if_fail(condition);

g_return_val_if_fail(condition, retval);

Figura 3. Controlli delle precondizioni

Figura 3 mostra i controlli delle precondizioni di glib. g_return_if_fail() stampa un messaggio di allarme e ritorna immediatamente dalla funzione corrente se la condizione condition è FALSE. g_return_val_if_fail() è simile ma permette di restituire il valore retval. Queste macro saranno incredibilmente utili---se le utilizzarete liberamente, specialmente in combinazione con il controllo dei tipi a run-time di GTK+, dimezzerete il tempo impiegato per cercare i puntatori impazziti ed errori di tipo.

Usare queste funzioni è semplice; ecco un esempio tratto dalla implementazione delle tabelle hash di glib:

void
g_hash_table_foreach (GHashTable *hash_table,
                      GHFunc      func, 
                      gpointer    user_data)
{
  GHashNode *node;
  gint i;
  
  g_return_if_fail (hash_table != NULL);
  g_return_if_fail (func != NULL);
  
  for (i = 0; i < hash_table->size; i++)
    for (node = hash_table->nodes[i]; node; node = node->next)
      (* func) (node->key, node->value, user_data);
}

Senza i controlli, passare NULL come parametro di questa funzione risulterebbe in un misterioso "Segmentation fault". La persona che utilizza la libreria dovrebbe quindi cercare l'errore con un debugger, e magari anche frugare dentro il codice di glib per capire cosa è andato storto. Con i controlli , vedranno apparire un messaggio di errore che spiega che argomenti come NULL non sono permessi.

#include <glib.h>

g_assert(condition);

g_assert_not_reached(void);

Figura 4. Asserzioni

glib ha anche delle macro di asserzione più tradizionali mostrate in Figura 4. g_assert() è praticamente identica a assert(), ma risponde a G_DISABLE_ASSERT e si comporta nello stesso modo su tutte le piattaforme. E' anche disponibile g_assert_not_reached(); è un asserzione che fallisce sempre. Tutte le asserzioni chiamano abort() per uscire dal programma e (se il sistema lo supporta) creano un file "core" per poter poi eseguire il debug.

Le asserzioni fatali dovrebbero essere utilizzate per controllare la consistenza interna di una funzione di libreria, mentre g_return_if_fail() dovrebbe assicurare valori corretti passati all'interfaccia pubblica del modulo. In particolare, se un asserzione fallisce, tipicamente andrete a cercare il bug nel modulo contenente l'asserzione; se un controllo g_return_if_fail() fallisce, è meglio iniziare a cercare il bug dal codice che invoca il modulo.

Questo codice, tratto dai calcoli di calendario di glb, mostra la differenza:

GDate*
g_date_new_dmy (GDateDay day, GDateMonth m, GDateYear y)
{
  GDate *d;
  g_return_val_if_fail (g_date_valid_dmy (day, m, y), NULL);
  
  d = g_new (GDate, 1);
  
  d->julian = FALSE;
  d->dmy    = TRUE;
  
  d->month = m;
  d->day   = day;
  d->year  = y;
  
  g_assert (g_date_valid (d));
  
  return d;
}

Il controllo della precondizione iniziale assicura che l'utente passi valori ragionevoli per il giorno, mese e anno; l'asserzione finale assicura la corretta costruzione dell'oggetto da parte di glib stessa.

g_assert_not_reached() dovrebbe essere utilizzata per marcare situazioni impossibili; un utilizzo molto comune è nelle istruzioni switch che non accettano tutti i possibili valori di un enumerazione:

  switch (val) 
    {
      case FOO_ONE:
        break;
      case FOO_TWO:
        break;
      default:
        /* Valore di enumerazione errato */
        g_assert_not_reached();
        break;
    }

Le macro di debug utilizzano la funzione g_log() per stampare messaggi di errore, questo significa che tutti i messaggi includono il nome della applicazione o libreria che li genera e che potete installare una funzione "rimpiazzo" per stampare i messaggi. Per esempio, potreste voler spedire tutti i messaggi di errore a una finestre di dialogo o ad un file di log anzichè stamparli in console.

Memoria

glib rimpiazza le funzioni standard malloc() e free() con le sue varianti g_, g_malloc() e g_free(), mostrate in Figura 5. Queste hanno molti piccoli pregi:

  • g_malloc() restituisce sempre un gpointer, mai un char*, quindi non c'e mai bisogno di utilizzare un cast per il valore di ritorno.

  • g_malloc() termina il programma se la malloc() interna fallisce, quindi non dovete controllare se il valore restituito è NULL.

  • g_malloc() si comporta bene nel caso in cui la size (dimensione) richiesta sia 0, restituendoNULL.

  • g_free() ignora tutti i puntatori NULL che gli vengono passati.

In aggiunta a queste piccole facilitazioni, g_malloc() e g_free() supportano diversi modi di debugging e profiling della memoria. Se utilizzate l'opzione --enable-mem-check dello script di configurazione di glib, la g_free() compilata vi avvertirà se tentate di deallocare due volte la memoria puntata dallo stesso puntatore. L'opzione --enable-mem-profile abilita il codice che si occupa delle statistiche di utilizzo della memoria, che possono essere stampate in console chiamando g_mem_profile(). Per finire, potete definire USE_DMALLOC perchè glib usi le macro di debug MALLOC(), ecc. disponibili in dmalloc.h su alcune piattaforme.

#include <glib.h>

gpointer g_malloc(gulong size);

void g_free(gpointer mem);

gpointer g_realloc(gpointer mem, gulong size);

gpointer g_memdup(gconstpointer mem, guint bytesize);

Figura 5. Allocazione di memoria di glib

E' importante far corrispondere g_malloc() con g_free(), malloc() con free(), e (se state utilizzando il C++) new con delete. In caso contrario, possono accadere cose imprevedibili; i differenti allocatori possono utilizzare campi di memoria differenti (inoltre new/delete chiamano costruttori e distruttori).

Ovviamente c'e anche una g_realloc() equivalente a realloc(). Esistono inoltre le funzioni g_malloc0(), che riempie la memoria allocata con zeri, e g_memdup() che restituisce una copia di bytesize bytes partendo da mem. g_realloc() e g_malloc0() accettano entrambe un size di 0, per coerenza con g_malloc(). Tuttavia, g_memdup() non accetta questo valore.

Chiaramente g_malloc0() riempie la memoria grezza con bit azzerati e non con valori 0 del tipo che desiderate inserire. A volte ci si aspetta di ottenere un array di numeri floating point inizializzati a 0.0; in questo caso non funzionerà.

Per finire, esistono macro di allocazione sensibili al tipo, mostrate in Figura 6. L'argomento type di ogni una di queste funzioni è il nome di un tipo, l'argomento count è il numero dei blocchi di dimensione del type da allocare. Queste macro permettono di risparmiare tempo nella stesura del codice, e qualche moltiplicazione, riducendo la possibilità di errore. Il cast verso il puntatore di tipo desiderato avviene automaticamente, mentre tentando di assegnare la memoria allocata a un puntatore di tipo errato dovrebbe provocare un warning del compilatore. (Se avete i warning abilitati: un programmatore responsabile dovrebbe averli!)

#include <glib.h>

g_new(type, count);

g_new0(type, count);

g_renew(type, mem, count);

Figura 6. Macro per la allocazione di memoria

Operazioni con le stringhe

glib fornisce molte funzioni di manipolazione delle stringhe; alcune sono esclusivamente di glib altre servono principalmente per risolvere problemi di portabilità. Tutte cooperano perfettamente con le routine di allocazione di memoria di glib.

Per quelli a cui interessa una stringa migliore di un gchar*, esiste il tipo GString. Non è descritto in questo libro, ma la documentazione è disponibile all'indirizzo http://www.gtk.org/.

#include <glib.h>

gint g_snprintf(gchar* buf, gulong n, const gchar* format, ...);

gint g_strcasecmp(const gchar* s1, const gchar* s2);

gint g_strncasecmp(const gchar* s1, const gchar* s2, guint n);

Figura 7. Funzioni per la portabilità

Figura 7 mostra i sostituti che glib fornisce per le estenzioni al C ANSI comunemente implementate, ma che non sono portabili.

Uno degli aspetti fastidiosi del C è che esso fornisce la funzione sprintf(), spesso causa di crash, problemi nella sicurezza, funzione generalmente da evitare; la funzione semi-sicura e comunemente implementata snprintf(), invece, è un sua estensione. g_snprintf() utilizza snprintf() sulle piattaforme che ne sono provviste e fornisce un implementazione sulle altre. Potrete dire addio a sprintf() per sempre. Meglio ancora: in genere snprintf() non garantisce di terminare la stinga con NULL, mentre g_snprintf() sì.

Le funzioni g_strcasecmp() e g_strncasecmp() implementano confronti case-insensitive di due stringhe, opzionalmente con una lunghezza massima. strcasecmp() è disponibile su molte piattaforme, ma non è universale; è consigliabile utilizzare la versione di glib.

Le funzioni in Figura 8 modificano le stringhe passate come parametro: le prime due convertono la stringa in caratteri minuscoli o maiuscoli, rispettivamente, mentre g_strreverse() inverte tutti i caratteri. g_strchug() rimuove gli spazi iniziali mentre g_strchomp() rimuove gli spazi finali. Queste ultime due restituiscono la stringa oltre a modificarla; in alcuni casi può essere conveniente utilizzare il valore restituito. La funzione g_strstrip() è una combinazione delle due precedenti (rimuove gli spazi iniziali e finali); La si utilizza come le funzioni individuali.

#include <glib.h>

void g_strdown(gchar* string);

void g_strup(gchar* string);

void g_strreverse(gchar* string);

gchar* g_strchug(gchar* string);

gchar* g_strchomp(gchar* string);

Figura 8. Manipolazione delle stringhe

Figura 9 mostra altre funzioni semi-standard offerte da glib. g_strtod è simile a strtod() - converte la stringa nptr in un valore double - in aggiunta, essa tenterà di convertire il double nel locale "C" se la conversione nel locale dell'utente fallisce. A *endptr viene assegnato il puntatore al primo carattere non convertito, ovvero un qualunque testo seguente la rappresentazione del numero. Se la conversione fallisce, a *endptr viene assegnato nptr. endptr può essere NULL, in questo caso sarà ignorato.

g_strerror() e g_strsignal() sono equivalenti alle fuzioni non-g_, ma sono portabili. (Restituiscono la rappresentazione in stringa di un errno o di un numero di segnale.)

#include <glib.h>

gdouble g_strtod(const gchar* nptr, gchar** endptr);

gchar* g_strerror(gint errnum);

gchar* g_strsignal(gint signum);

Figura 9. Conversioni di stringhe

Figura 10 mostra le funzioni glib per la allocazione di stringhe. Chiaramente, g_strdup() e g_strndup() producono una copia allocata di str oppure dei primi n caratteri di str. Per coerenza con le funzioni di allocazione di memoria, esse restituiscono NULL se ricevono un puntatore NULL come parametro. Le varianti di printf() restituiscono una stringa formattata. g_strescape inserisce il carattere di escape prima di ogni carattere \, in pratica, duplica ogni \ presente. g_strnfill() restituisce una stringa di dimensione length riempita con caratteri fill_char.

g_strdup_printf() ha bisogno di qualche spiegazione in più, è un modo semplificato di implementare il seguente codice:

  gchar* str = g_malloc(256);
  g_snprintf(str, 256, "%d printf-style %s", 1, "format");

Potete rimpiazzare l'esempio precedente con questo codice, e non dovervi preoccupare di calcolare la giusta lunghezza del buffer da creare:

  gchar* str = g_strdup_printf("%d printf-style %s", 1, "format");

#include <glib.h>

gchar* g_strdup(const gchar* str);

gchar* g_strndup(const gchar* format, guint n);

gchar* g_strdup_printf(const gchar* format, ...);

gchar* g_strdup_vprintf(const gchar* format, va_list args);

gchar* g_strescape(gchar* string);

gchar* g_strnfill(guint length, gchar fill_char);

Figura 10. Allocazione delle stringhe

glib fornisce alcune funzioni per concatenare stringhe, mostrate in Figura 11. g_strconcat() restituisce una nuova stringa creata concatenando tutte le stringhe passate nella lista degli argomenti. L'ultimo argomento della funzione deve essere NULL, in questo modo g_strconcat() saprà dove fermarsi. g_strjoin() è simile, ma le stringhe vengono concatenate inserendo fra loro la stringa separator. Se separator è NULL, nessun separatore viene inserito.

#include <glib.h>

gchar* g_strconcat(const gchar* string1, ...);

gchar* g_strjoin(const gchar* separator, ...);

Figura 11. Concatenazione delle stringhe

Infine, Figura 12 riassume alcune routine che manipolano gli array di stringhe terminati da NULL. g_strsplit() spezza la string a ogni delimiter, ritornando un array di stringhe allocato. g_strjoinv() concatena tutte le stringhe dell'array con un opzionale separator, restituendo una nuova stringa. g_strfreev() dealloca la memoria occupata da ogni stringa dell'array e l'array stesso.

#include <glib.h>

gchar** g_strsplit(const gchar* string, const gchar* delimiter, gint max_tokens);

gchar* g_strjoinv(const gchar* separator, gchar** str_array);

void g_strfreev(gchar** str_array);

Figura 12. Manipolazione di vettori di stringhe terminati da NULL