Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 28 novembre 2015

Mission: Impossible - XML Serializer
come scrivere un XML Serializer in C

La missione di oggi, in realtà, non è affatto impossibile: scrivere un XML Serializer in C, specialmente aiutandosi con una ottima libreria open-source come la libXML2 (licenza MIT), è relativamente semplice. Cosa è un serializzatore? È, semplicemente, una funzione che ci permette di trasformare una struttura dati, interna a un programma, nel suo equivalente in linguaggio XML.
fortuna che non era impossibile...
Andiamo al sodo: supponiamo di avere la seguente struttura dati (anzi, una struttura annidata, complichiamoci un po' la vita):
// tipo Film per test
typedef struct {
    char title[32];
    char director[32];
    int  year;
} Film;

// tipo Catalog per test
typedef struct {
    time_t t_lastupd;
    Film films[2];
} Catalog;
in un programma la possiamo riempire con un po' di dati (beh, in questo caso pochi, è solo un esempio) e con il nostro XML Serializer cerchiamo di ottenere un documento XML tipo questo:
<?xml version="1.0"?>
<CATALOG>
  <LASTUPDATE>28/11/15 12:26:04</LASTUPDATE>
  <FILM>
    <TITLE>Mad Max 2: The Road Warrior</TITLE>
    <DIRECTOR>George Miller</DIRECTOR>
    <YEAR>1979</YEAR>
  </FILM>
  <FILM>
    <TITLE>Mad Max: Fury Road</TITLE>
    <DIRECTOR>George Miller</DIRECTOR>
    <YEAR>2015</YEAR>
  </FILM>
</CATALOG>
Ok, visto che siete impazienti di sapere come si fa, passiamo subito al codice:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <libxml/parser.h>

// tipo Film per test
typedef struct {
    char title[32];
    char director[32];
    int  year;
} Film;

// tipo Catalog per test
typedef struct {
    time_t t_lastupd;
    Film films[2];
} Catalog;

// prototipi locali
char* serialize(char* document, const Catalog* catalog);

// main() del programma di test
int main(int argc, char* argv[])
{
    // test argomenti
    if (argc != 1) {
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s [e.g.: %s]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    /* libxml2: initializza la libreria e testa potenziali ABI mismatches
       tra la versione compilata e la attuale shared library usata */
    LIBXML_TEST_VERSION;

    // prepara dati per test
    char dest_document_xml[2048];
    Catalog catalog;
    catalog.t_lastupd = time(NULL);
    strcpy(catalog.films[0].title, "Mad Max 2: The Road Warrior");
    strcpy(catalog.films[0].director, "George Miller");
    catalog.films[0].year = 1979;
    strcpy(catalog.films[1].title, "Mad Max: Fury Road");
    strcpy(catalog.films[1].director, "George Miller");
    catalog.films[1].year = 2015;

    // serializza XML
    serialize(dest_document_xml, &catalog);

    // scrive i risultati in un file
    FILE* fp = fopen("catalog.xml", "w");
    fprintf(fp, "%s", dest_document_xml);
    fclose(fp);

    // esce con Ok
    return EXIT_SUCCESS;
}

// funzione serialize()
char* serialize(
    char*          document,        // documento destino serializzato
    const Catalog* catalog)         // struttura documento
{
    // crea documento con root node <CATALOG>
    xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0");
    xmlNodePtr root_node = xmlNewNode(NULL, BAD_CAST "CATALOG");
    xmlDocSetRootElement(doc, root_node);

    // aggiunge <LASTUPD> child-field al nodo <CATALOG>
    char str_lastupd[64];
    strftime(str_lastupd, sizeof(str_lastupd), "%d/%m/%y %H:%M:%S", localtime(&catalog->t_lastupd));
    xmlNewChild(root_node, NULL, BAD_CAST "LASTUPDATE", BAD_CAST str_lastupd);

    // loop per aggiungere i child-nodes <FILM> e child-fields al nodo <CATALOG>
    int i;
    for (i = 0; i < sizeof(catalog->films) / sizeof(Film); i++) {
        // aggiunge il child-node <FILM> al nodo <CATALOG>
        xmlNodePtr film_node = xmlNewChild(root_node, NULL, BAD_CAST "FILM", BAD_CAST NULL);

        // aggiunge i child-fields al nodo <FILM>
        xmlNewChild(film_node, NULL, BAD_CAST "TITLE", BAD_CAST catalog->films[i].title);
        xmlNewChild(film_node, NULL, BAD_CAST "DIRECTOR", BAD_CAST catalog->films[i].director);
        char s_year[8];
        sprintf(s_year, "%d", catalog->films[i].year);
        xmlNewChild(film_node, NULL, BAD_CAST "YEAR", BAD_CAST s_year);
    }

    // copia il document al documento destino
    xmlChar *xmlbuff;
    int buffersize;
    xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
    strcpy(document, (char *)xmlbuff);

    // libera risorse
    xmlFree(xmlbuff);
    xmlFreeDoc(doc);
    xmlCleanupParser();

    // return un pointer al document formattato
    return document;
}
Ok, come vedete il codice è (come sempre in questo Blog) ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò, solo (al solito), qualche dettaglio strutturale.

Il main() è solo funzionale all'esempio: inizializza la libreria (leggete il commento!), prepara i dati per il test, li passa al Serializer, scrive i risultati in un file.

Quello che conta è la funzione serialize(), che si può usare dove e come si vuole in un programma che usa le strutture dati descritte sopra, e che, soprattutto, può essere usata come esempio per scrivere funzioni analoghe per altre strutture dati: è molto semplice e si può adattare facilmente a qualsiasi altro caso. Come si nota usa varie funzioni della libreria libXML2, e il meccanismo che permette di riempire il documento destino è semplice: si crea un nodo di partenza (root-node) e si aggiungono figli (child-nodes) seguendo la forma della struttura dati originale.

Dimenticavo: per testare l'esempio su Linux (cosa che ho fatto, ovviamente...) bisogna installare prima la libXML2 (dal repository della distribuzione), e poi compilare il programma con:
gcc serxml.c -I/usr/include/libxml2 -o serxml -lxml2 
Nel prossimo post (se non cambio idea nel frattempo) vedremo la funzione inversa, un XML Deserializer (e qui ci starebbe bene un "Ohhhhhhh" di stupore).

Ciao e al prossimo post!