Qualche limite indigesto nelle struct del C…

Il C non ha certo bisogno di presentazioni, essendo uno dei linguaggi che ha avuto più successo ed è ancora fra i più utilizzati, nonostante si avvicini ai 40 anni d’età. Complice il successo di Unix (scritto in C), si è fatto strada anche grazie al sua relativamente ridotta e succinta sintassi, e alcuni lo preferiscono al C++ proprio per questa ragione, nonostante quest’ultimo sia sostanzialmente un suo superset.

E’ stato utilizzato per lo sviluppo di applicazioni fra le più disparate, ma essendo considerato di medio-basso livello il suo campo d’elezione è la programmazione di sistema. Sistemi operativi (ovviamente) e driver sono usualmente scritti C, ma anche emulatori e macchine virtuali sono un naturale ambito d’utilizzo, per citare altri esempi molto noti.

Nonostante non sia fra i miei linguaggi preferiti, a causa della sua diffusione e della mia passione per le ultime due cose citate mi è capitato sovente di doverlo utilizzare. E poiché compilatore e VM di Python (che, al contrario, adoro) più diffusi sono scritti in C, il mio smanettarci (il progetto WPython è uno dei risultati di tale lavoro, di cui in futuro parlerò più approfonditamente) negli ultimi anni mi ha riportato a lavorare spesso in C (la notte mi è testimone).

Chiaramente più ci si lavora e più si prende coscienza dei pregi e dei difetti del linguaggio nella vita di tutti i giorni, confrontandosi con le esigenze che saltano fuori in progetti concreti. Studiare la sintassi e la semantica, insomma, è soltanto il primo, importante, passo di un lungo cammino che porterà al formarsi della tipica mentalità del programmatore di quel particolare linguaggio, nonché prendere atto delle sue problematiche e dei limiti.

M’era già capitato altre volte, ma con CPython (si chiama così la versione “mainstream” di Python) qualcuno abbastanza seccante è saltato nuovamente fuori, e riguarda la definizione di una gerarchia di “tipi” tramite delle strutture dati, con la quale modellare i tipi, appunto, utilizzati nella VM.

CPython ne definisce una moltitudine per diversi scopi, ma vedere in che modo ne è rappresentato qualcuno aiuterà a comprendere velocemente la problematica in oggetto. Prendiamo, ad esempio, il tipo più utilizzato: l’intero (con segno e a 32 o 64 bit, a seconda della piattaforma su cui gira). Esso è rappresentato da una precisa struttura:

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

Fin qui sembra tutto a posto e la definizione è pure molto semplice. ob_ival è il campo che racchiude il valore intero vero e proprio, ma PyObject_HEAD non ha certo l’aspetto di un campo. Infatti andando a vedere com’è definito salta fuori la sua vera natura:

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD			\
    _PyObject_HEAD_EXTRA		\
    Py_ssize_t ob_refcnt;		\
    struct _typeobject *ob_type;

Si tratta di una macro (la parte del nome in maiuscolo forniva già qualche indicazione in merito, data la generale convenzione di utilizzarlo per indicare tutto o parte dell’identificatore), che definisce un altro paio di campi: ob_refcnt è un valore intero (in realtà è un ssize_t) usato per tenere conto del cosiddetto reference counting, mentre ob_type è un puntatore a un’altra struttura che identifica il tipo a cui appartiene l’oggetto in questione.

_PyObject_HEAD_EXTRA, come possiamo intuire ormai, è un’altra macro:

#define _PyObject_HEAD_EXTRA

In pratica non definisce nulla. In realtà se risulta abilitata una particolare opzione, Py_TRACE_REFS (in modalità di debug), prende la seguente forma:

/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA		\
    struct _object *_ob_next;	\
    struct _object *_ob_prev;

Quindi ci sono ancora un altro paio di campi definiti per tenere traccia di tutte le istanze tramite una lista doppiamente concatenata.

In definitiva, “espandendo” le macro, un PyIntObject avrebbe la seguente struttura:

typedef struct {
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
    long ob_ival;
} PyIntObject;

oppure, con Py_TRACE_REFS abilitato:

typedef struct {
    struct _object *_ob_next;
    struct _object *_ob_prev;
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
    long ob_ival;
} PyIntObject;

Se il C permettesse di derivare una struttura da un’altra (o, viceversa, estendere una struttura esistente, a seconda di come la si voglia vedere), il problema sarebbe stato risolto in maniera molto più semplice, elegante, leggibile, e manutenibile:

typedef struct {
#ifdef Py_TRACE_REFS
    struct _object *_ob_next;
    struct _object *_ob_prev;
#endif
} _PyObject_HEAD_EXTRA;

typedef struct(_PyObject_HEAD_EXTRA) {
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject_HEAD;

typedef struct(PyObject_HEAD) {
    long ob_ival;
} PyIntObject;

Ovviamente con la possibilità di “rilassare” il passaggio di queste strutture senza ricorrere a cast (downcast, per la precisione). Ad esempio, per accedere ai campi “comuni” ob_refcnt e ob_type si fa ricorso alle seguenti macro:

#define Py_REFCNT(ob)    (((PyObject*)(ob))->ob_refcnt)
#define Py_TYPE(ob)      (((PyObject*)(ob))->ob_type)

di cui si potrebbe fare tranquillamente a meno, utilizzando direttamente la classica notazione ob->ob_refcnt e ob->ob_type dove servisse.

Non pensiate che si tratti di casi rari, perché l’esigenza di definire gerarchie di tipi oppure di avere delle strutture “base” per altre più complesse è, invece, abbastanza comune, persino in un s.o..

Ad esempio un elenco di processi, file, o di risorse in generale si potrebbe (e in genere si fa così) gestire tramite una lista che faccia uso di una struttura nodo “classica” a cui vengono poi aggiunte le informazioni specifiche relative a quella particolare informazione. Ovviamente il tutto riutilizzando le funzioni di inserimento, cancellazione, ricerca, ordinamento, ecc., di un nodo in una lista.

Sempre riguardo alle strutture, non so quante volte sarà capitato di accedervi per manipolare dei campi. Ad esempio:

static int
compiler_push_fblock(struct compiler *c, enum fblocktype t, basicblock *b)
{
    struct fblockinfo *f;
    if (c->u->u_nfblocks >= CO_MAXBLOCKS) {
        PyErr_SetString(PyExc_SystemError,
            "too many statically nested blocks");
        return 0;
    }
    f = &c->u->u_fblock[c->u->u_nfblocks++];
    f->fb_type = t;
    f->fb_block = b;
    return 1;
}

Qui è stato necessario utilizzare la variabile f di “appoggio” per evitare di scrivere linee di codice particolarmente lunghe. I pascaliani avrebbero fatto ricorso al costrutto with:

static int
compiler_push_fblock(struct compiler *c, enum fblocktype t, basicblock *b)
{
    with (c->u) {
        if (u_nfblocks >= CO_MAXBLOCKS) {
            PyErr_SetString(PyExc_SystemError,
                "too many statically nested blocks");
            return 0;
        }
        with (*u_fblock[u_nfblocks++]) {
            fb_type = t;
            fb_block = b;
            return 1;
        }
    }
}

Oppure, senza introdurre nuove keyword (per evitare problemi di compatibilità):

static int
compiler_push_fblock(struct compiler *c, enum fblocktype t, basicblock *b)
{
    c->u.{
        if (u_nfblocks >= CO_MAXBLOCKS) {
            PyErr_SetString(PyExc_SystemError,
                "too many statically nested blocks");
            return 0;
        }
        u_fblock[u_nfblocks++]-> {
            fb_type = t;
            fb_block = b;
            return 1;
        }
    }
}

In pratica si tratta di “aprire” un nuovo scope che “punti” all’interno della struttura dell’oggetto, lasciando al compilatore di smazzarsi il compito di referenziare correttamente tutti i campi e di apportare anche le ottimizzazioni che ritiene più opportune per velocizzare l’operazione.

Chi ha lavorato col C sa che si tratta di esigenze comuni; molto comuni.

Purtroppo il C è un linguaggio che si evolve molto lentamente, ma ciò non ha impedito, ad esempio, di introdurre il supporto ai numeri complessi con l’ISO C99. Non so quanto possa essere utile il supporto ai complessi, ma penso che il comitato dovrebbe prestare più attenzione a risolvere problemi che hanno un maggior impatto nella vita dei programmatori.

Ovviamente non si pretende di estendere il linguaggio infilando tutte le idee che in questi ultimi decenni sono saltate fuori. Il C ormai ha una sua “struttura”, una sua “filosofia”, ed è anche per questo che continua a essere apprezzato.

Credo, però, che qualche sistemata per migliorare l’uso di quanto già esista non possa che giovare, specialmente a chi ci lavora…

Press ESC to close