En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 14 de enero de 2017

Por un puñado de ifdef
cómo usar el preprocesador en C

Después de la juerga de Año Nuevo es mejor empezar con un post ligero. Hablaremos, entonces del preprocesador... bueno, en realidad si hablasimos del preprocesador en profundidad no sería un post muy ligero, por lo que nos limitaremos a un caso simple, es decir un uso interesante de la directiva #ifdef. Y os sugiero de seguir los consejos de Joe (el extranjero), ya que es uno que se enfada fácilmente...
...y quién no utiliza el #ifdef tendrá que tratar conmigo...
Por lo tanto: retomamos un viejo pedazo de código que se muestra aquí (en seguida a releerlo!), el del Socket Server. Asumiendo que tengáis muy claro cómo funciona, lo vamos a modificar siguiend un posible caso real, que sería el siguiente: supongamos que tenemos que compilar el nuestro código para dos entornos operativos diferentes, por ejemplo para un Linux Desktop/Server reciente, y para Linux Embedded  un poco anticuado (con compilador, kernel y glibc de hace unos años). Hemos decidido por diversas razones que el nuevo código tiene que crear un socket no bloqueante en la fase de accept, así que por ejemplo, se podría reemplazar la llamada a accept() con una a accept4() que tiene un argumento flags que se puede establecer a SOCK_NONBLOCK que es justo lo que necesitamos. Por desgracia, nuestro sistema embedded (anticuado, como se ha mencionado) utiliza un kernel más antiguo del 2.6.28 y una glibc anterior a la 2,10 (que son las dos condiciones mínimas para poder utilizar la accept4()). ¿Qué hacer? Hacemos dos versiones del código? ¡NO! ¿Por qué así tendremos que hacer  (en el futuro) doble mantenimiento, que es una situación a evitar. Vamos a mantener, sin embargo, sólo una versión con las #ifdef apropiadas para gestionar la compilación en los dos entornos operativos.

Entonces, el fragmento de código (extracto de ese post allí) que hay que cambiar es el siguiente: 
... 
// acepta conexiones de un client entrante
printf("%s: espera connexiones entrantes...\n", argv[0]);
socklen_t socksize = sizeof(struct sockaddr_in);
struct sockaddr_in client;          // (remote) client socket info
int client_sock;
if ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) < 0) {
    // error accept()
    printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
    return EXIT_FAILURE;
}
...
Y la nueva versión con la compilación condicional a través de #ifdef será la siguiente:
...
// acepta conexiones de un client entrante (en non blocking mode: my_socket es SOCK_NONBLOCK)
printf("%s: espera connexiones entrantes...\n", argv[0]);
socklen_t socksize = sizeof(struct sockaddr_in);
struct sockaddr_in client;          // (remote) client socket info
int client_sock;
#ifdef OLD_LINUX
if ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) < 0) {
#else
if ((client_sock = accept4(my_socket, (struct sockaddr *)&client, &socksize, SOCK_NONBLOCK)) < 0) {
#endif
    // error accept()
    printf("%s: accept failed (%s)\n", argv[0], strerror(errno));
    return EXIT_FAILURE;
}
#ifdef OLD_LINUX
else {
    // accept ejecutada: set socket a non-blocking
    int flags;
    if ((flags = fcntl(client_sock, F_GETFL, 0)) >= 0) {
        if (fcntl(client_sock, F_SETFL, flags | O_NONBLOCK) < 0) {
            // error accept()
            printf("%s: fcntl failed (%s)\n", argv[0], strerror(errno));
            return EXIT_FAILURE;
        }
    }
}
#endif
...
Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciones y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales.

En las partes incluidas en el #ifdef OLD_LINUX hay la versión del código que NO utiliza la accept4(), y que es, obviamente, un poco más complicada que la otra, porque hay que utilizar fcntl() para transformar en no-bloqueante el socket creado, mientras que la accept4() lo crea directamente usando, como se ha mencionado, el flag SOCK_NONBLOCK. Sin embargo, como se puede ver, el código resultante es bastante claro y legible y, después de todo, no se ve tan mal (¡el estilo primero!).

Alguien podría decir: la versión bajo #ifdef también trabaja con un Linux reciente, así que ¿por qué no dejar solo esa y quitar las #ifdef? ¡NO! ¡NO! y otra vez ¡NO! No hay que escribir código old-style para que sea retro-compatible: siempre hay que tratar de escribir de una manera moderna, y si es necesario (como en el ejemplo) hay que poner el material antiguo bajo #ifdef. Y cuando llegue el momento (cuando, por ejemplo, ya no vamos a usar un entorno operativo dual) limpiaremos y dejaremos sólo un bonito código moderno.

Obviamente la compilación condicional la efectuaremos mediante la introducción (o no introducción) de una instrucción -D OLD_LINUX  en la línea del nuestro makefile que genera lo files objeto, o directamente en la línea de comandos si no se utiliza un makefile. Bueno, por fin hemos escribito un código único para dos entornos operativos diferentes: ¡misión cumplida!

Sólo una última aclaración sobre el nuestro socket server modificado (y que no tiene nada que ver con las #ifdef): si el non-blocking socket nos necesita para ejecutar unas recv() no bloqueantes en la siguiente fase a la de accept, es mucho más fácil modificar adecuadamente el loop de recepción y pasar el flag MSG_DONTWAIT a la recv() en el argumento flags (el cuarto). Por lo que la recv() no bloquea en ambos entornos operativos, y todo ello sin el uso de #ifdef. Pero esa es otra historia...

¡Hasta el próximo post!

jueves, 8 de diciembre de 2016

El último Apache III - La venganza
cómo escribir un módulo Apache en C - pt.3

Entonces, hacemos el punto: en los últimos tres post hemos hablado de módulos lighttpd (aquí, aquí y también aquí). En el primero de los tres he mencionado que el tema no era nuevo: antiguamente había escrito dos post (aquí y aquí) que hablaban de módulos para Apache, que es un pariente cercano de lighttpd (familia Web Servers). Aquí, para cerrar el circulo os propongo una tercera entrega de "El último Apache" (¿o Mohicano?), con una (espero) interesante extensión de lo que se había escrito.
...en realidad soy un Mohicano, no un Apache...
Pues bien, en "El último Apache II - El regreso" (que acabáis de releer, supongo...) escribimos un agradable módulo elemental para Apache. Hacia poco (escribía solo una presentación en browser), pero era un buen comienzo para entrar en el mundo de los módulos para Web Servers. Vale, ahora vamos a reanudar ese módulo y les vamos a añadir una función que extrae los datos POST incluidos en una eventual petición HTTP de tipo POST que llega al módulo. ¿Por qué he elegido añadir esa funcionalidad? Bueno, obviamente porque es bastante normal que un módulo cuente con varios tipos de peticiones (GET, POST, etc.), y luego, en particular, recuerdo que la primera vez que he añadido esta funcionalidad en un módulo Apache que escribí, me di cuenta de que no estaban disponibles muchas indicaciones sobre este tema (y esto a pesar de la enorme cantidad de documentación de Apache disponible respeto a la de lighttpd).

Pues bien, sin molestarse en repetir todo el código y la forma de generarlo (hay de todo en "El último Apache II - El regreso"), sólo reescribiremos la función myapmodHandler() y añadiremos una nueva función getPost().  ¡Vamos con el código!
// handler del modulo
static int myapmodHandler(
    request_rec *reqrec)            // request data
{
    // test handler
    if (! reqrec->handler || strcmp(reqrec->handler, "myapmod"))
        return DECLINED;

    // set del apropiado content type
    ap_set_content_type(reqrec, "text/html");

    // test método http
    if (reqrec->method_number == M_GET) {
        // petición GET: escribe solo "Hello, World!"
        ap_rprintf(reqrec, "GET request: Hello, world!");
    }
    else if (reqrec->method_number == M_POST) {
        // petición POST: lee POST data
        char *data;
        if (readPost(reqrec, &data) != -1) {
            // escribe "Hello, World!" y enseña POST data
            ap_rprintf(reqrec, "POST request: Hello, world! postdata: %s", data);
        }
        else {
            // POST data no disponible: escribe solo "Hello, World!"
            ap_rprintf(reqrec, "POST request: Hello, world!");
        }
    }
    else
        return HTTP_METHOD_NOT_ALLOWED;

    // sale con OK
    return OK;
}

// función para leer POST data
static int readPost(
    request_rec *reqrec,            // request data
    char        **data)             // buffer de destinazion paraer POST data
{
    // setup del client para permitir que Apache lea el request body
    if (ap_setup_client_block(reqrec, REQUEST_CHUNKED_ERROR) == OK) {
        // determina si el client ha enviado datos
        if (ap_should_client_block(reqrec)) {
            // lee los datos de POST
            char argsbuffer[HUGE_STRING_LEN];
            int rsize, len_read, rpos=0;
            long length = reqrec->remaining;
            *data = (char*)apr_pcalloc(reqrec->pool, length +1);

            // loop de inserción datos en el buffer de destinación
            while ((len_read = ap_get_client_block(reqrec, argsbuffer, sizeof(argsbuffer))) > 0) {
                if ((rpos + len_read) > length)
                    rsize = length - rpos;
                else
                    rsize = len_read;

                // copia un bloque de datos
                memcpy((char *)*data + rpos, argsbuffer, rsize);
                rpos += rsize;
            }

            // POST data leido: return OK
            return 0;
        }
    }

    // sale con NOK
    return -1;
}
Ok, como se nota es ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciónes y/o grupos de instrucciones (¡leer los comentarios! ¡Están ahí para eso!), pero voy a añadir, solamente, algunos detalles estructurales.

la función original myapmodHandler() se ha convertido en una especie de función helloworld (bueno, en práctica lo era incluso antes), que distingue los tipos de petición y, en el caso POST, llama a la función readPost() y escribe  como respuesta "Hello, world!" mas los datos. En el caso de peticiones GET o POST sin datos se limita a escribir "Hola, mundo!", y para otros tipos de peticiones sale con error.

La función readPost() es simple, pero no es inmediata: en su tiempo la derivé de el único ejemplo interesante y bien hecho que encontré, y que estaba en el (óptimo) libro "Writing Apache Modules with Perl and C", que os recomiendo. Yo diría que en la última versión de Apache, la 2.4, se han introducido (y descritos aquí) otros métodos de extracción de datos de POST (¡os deseo buena lectura!), de todas maneras yo me la arreglé así, y el módulo funcionaba (y funciona todavía) muy bien.

¡Ah, el test, se me olvidaba! Testear con peticiones POST no es simple cómo hacerlo con las GET, adonde es suficiente (como dicho en el post anterior) escribir la URI del modulo en la barra de direcciones de Firefox o Chrome y esperar la respuesta. Para probar el nuestro nuevo módulo es mejor usar un buen plugin (como Poster, por ejemplo) que nos facilitará mucho la tarea.

Vale, de momento, creo que podemos parar por un tiempo el tema módulos de Apache/lighttpd. Juro que en el próximo post voy a hablar de otra cosa, no me gustaría que pensaran que el C se utiliza sólo para Web Servers...

¡Hasta el próximo post!