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...

martes, 25 de abril de 2017

The FileCopy
cómo escribir una función de File Copy en C - pt.2

Ok, re-empezamos de donde nos dejamos en el último post (¿lo habeis leido verdad?) Y, como prometido, en esta ocasión el tema será una versión con buffered I/O de la función cpFile(). Es deber, antes, proporcionar otra imagen de The Thing, si no, de lo contrario, podría parecer que esto es sólo un blog de programación, mientras que, como bien sabéis, es un blog para programadores cinefilos...
...con un sombrero así se programa mejor...
Vale, repetimos: I/O buferizado (y por lo tanto, por ejemplo, fread(3) en lugar de read(2), porque el objetivo esta vez es la portabilidad ¿y que hay más portátil (en C) que usar el contenido de stdio.h? El código que veremos en un momento utiliza (casi) el mismo main() de la versión unbuffered y los únicos cambios son internos a la función cpFile(). De hecho, incluso la cpFile() es casi idéntica desde un punto de vista lógico, ya que las funciones buffered tienen una sintaxis de uso y un funcionamiento muy similar a las equivalentes versiones unbuffered. Y claro, si la versión buffered os sale muy diferente (visualmente y lógicamente) de la versión unbuffered es que hay algo que no funciona... bueno, sobre este punto voy a hacer un pequeña digresión al final del post. Por ahora: ¡vamos con el código!
#include <stdio.h>
#include <stdlib.h>

// prototipos locales
static int cpFile(const char* src, const char* dest);

// función main()
int main(int argc, char *argv[])
{
    // test argumentos
    if (argc != 3) {
        // error args
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s srcfile destfile [e.g.: %s try.c try.save]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // ejecuta copia
    int retval;
    if ((retval = cpFile(argv[2], argv[1])) < 0) {
        // enseña error y sale con error
        fprintf(stderr, "%s: error: %d\n", argv[0], retval);
        exit(EXIT_FAILURE);
    }

    // sale con Ok
    return EXIT_SUCCESS;
}

// función cpFile()
static int cpFile(
    const char *dest,               // file destinación
    const char *src)                // file fuente
{
    // abre el file fuente
    FILE *fp_in;
    if ((fp_in = fopen(src, "r")) == NULL) {
        // return con error
        return -1;
    }

    // abre el file destinación
    FILE *fp_out;
    if ((fp_out = fopen(dest, "w")) == NULL) {
        // cierra el file y return con error
        fclose(fp_in);
        return -2;
    }

    // r/w loop para la copia usando buffered I/O
    size_t n_read;
    char buffer[BUFSIZ];
    while ((n_read = fread(buffer, 1, sizeof(buffer), fp_in)) > 0) {
        if (! ferror(fp_in)) {
            // write buffer
            fwrite(buffer, 1, n_read, fp_out);
            if (ferror(fp_out)) {
                // cierra los file y return con error
                fclose(fp_in);
                fclose(fp_out);
                return -3;
            }
        }
        else {
            // cierra los file y return con error
            fclose(fp_in);
            fclose(fp_out);
            return -4;
        }
    }

    // cierra los file
    fclose(fp_in);
    fclose(fp_out);

    // return con Ok
    return 0;
}
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. El main(), como se había anticipado, es prácticamente idéntico, mientras que en la cpFile() se repiten, exactamente, las operaciones de la version unbuffered, pero usando fopen(3) en lugar de  open(2), fclose(3) en lugar de close(2), etc. ¿Donde encontramos algunas (pequeñas) diferencias? Sólo en el test de (eventuales) errores de lectura/escritura, que en la versión unbuffered estaban implícitas en las operaciones de read/write (probando si el resultado era igual a -1), mientras que en este caso hay que usar una función a parte, ferror(3), y esto es debido a que:
    On  success,  fread()  and  fwrite() return the number of items read or
    written.  This number equals the number of bytes transferred only  when
    size  is 1.  If an error occurs, or the end of the file is reached, the
    return value is a short item count (or zero).
    fread() does not distinguish between end-of-file and error, and callers
    must use feof(3) and ferror(3) to determine which occurred.
Lo anterior es lo que trae la man-page de fread(3)/fwrite(3), y creo que es una justificación suficiente de por qué he escrito el código asì (y, por lo mismo, no podemos usar strerror(3) en el main() para enseñar los errores). Así que, como se había anticipado, las versiones buffered y unbuffered deben ser casi sobreponibles, y creo que es exactamente el resultado conseguido.

Y ahora la digresión prometida: me ha pasado de encontrar en la red (incluso en apreciados blogs/webs de programación) ejemplos de buffered-copy de file que utilizan unos loop de este tipo:
while (!feof(fp_in)) {
    // lee y escribe buffer
    ...
}
vale, ¿cómo se puede comentar esto? Con una sola palabra:

NO

Si uno escribe el loop de esta manera significa que no ha leído la man-page de fread(3)/fwrite(3) o que la ha leido y no ha entendido el contenido. No hay necesidad de reinventar la rueda, repito: fread(3)/fwrite(3) funcionan casi de la misma manera de read(2)/write(2), por lo que, si el ejemplo de cpFile() unbuffered del post anterior era bueno (¡y lo era!), entonces el ejemplo del post actual debería ser (casi) idéntico. El loop con un while() que testea feof(3) es sintácticamente correcto, pero no lo es lógicamente, ya que comienza testeando algo que todavía no es utilizable (uhmm, ¿una prueba predictiva?) y que, además, no hay necesidad de testear. Bah, no quiero extender demasiado este argumento y os remito a la excelente análisis contenida ahí (en el siempre optimo stackoverflow.com).
Obviamente espero no haber ofendido a nadie (con la digresión anterior): Recordar que errare humanum est... y, por supuesto, también en este blog habré escrito en el pasado algunas tonterías (espero no graves como aquella enseñada hace un momento). Os aseguro, sin embargo, que yo siempre soy muy cuidadoso de no proponer soluciones que no he tenido tiempo para escribir y probar de manera curada, o de lo contrario en lugar de un blog de programación artística este sería un blog de programación a la esperamos que funciona...

¡Hasta el próximo post!

martes, 14 de marzo de 2017

The FileCopy
cómo escribir una función de File Copy en C - pt.1

Ok, este post no tiene nada que ver con The Thing, la inmortal obra maestra de John Carpenter (aparte de la pequeña similitud del nombre). De hecho, es sólo una excusa para celebrarlo, ya que lo he visto (por milésima vez) recientemente. Sin embargo, ya que estamos, hablaremos también un poco de C...
inmortal obra maestra
Con este post veremos cómo escribir una función para realizar una copia de un file, ya que los sistemas POSIX (como Linux) no proporcionan una función específica de librería para hacerlo. Hay, por supuesto, mil formas de escribirla, y esta vez he pensado en dos versiones. ¡Vamos con la primera!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/sendfile.h>

// prototipos locales
static int cpFile(const char* src, const char* dest);

// función main()
int main(int argc, char *argv[])
{
    // test argumentos
    if (argc != 3) {
        // error args
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s srcfile destfile [e.g.: %s try.c try.save]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    // ejecuta copia
    if (cpFile(argv[2], argv[1]) == -1) {
        // enseña error y sale
        fprintf(stderr, "%s: error: %s\n", argv[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    // sale
    return EXIT_SUCCESS;
}

// función cpFile()
static int cpFile(
    const char *dest,               // file destinación
    const char *src)                // file fuente
{
    // abre el file fuente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // abre el file destinación
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // cierra el file y return con error
        close(fd_in);
        return -1;
    }

    // r/w loop para la copia usando unbuffered I/O
    size_t n_read;
    char buffer[BUFSIZ];
    while ((n_read = read(fd_in, buffer, sizeof(buffer))) > 0) {
        // write buffer
        if (write(fd_out, buffer, n_read) == -1) {
            // cierra el file y return con error
            close(fd_in);
            close(fd_out);
            return -1;
        }
    }

    // cierra los file
    close(fd_in);
    close(fd_out);

    // sale con el ultimo resultado de read() (0 o -1)
    return n_read;
}
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. El main(), en este caso, sólo sirve para probar la función de copia y el programa generado se comporta (a nivel básico) como la función POSIX cp(1), que es precisamente lo que queremos emular usando la nuestra nueva función de librería.

La función que hace el trabajo la he llamada cpFile() y es bastante simple, como se ve. Utiliza el I/O sin búfer (así, por ejemplo, read(2) en lugar de fread(3)) y, aunque es muy compacta y eficiente, también trata de forma integral los errores y està escrita para ser una función de librería, por lo que no escribe nada en stderr y stdout sino simplemente realiza el trabajo y devulve un código de retorno (0 ó -1), que puede ser tratado por el llamante (en este caso el main()) para mostrar eventuales errores utilizando strerror(3) y errno. Todo el trabajo se realiza en un loop que lee un buffer en el file fuente y lo escribe en el file de destino, hasta el final del file. El resto del código es la abertura/cierre de los file y manejo de los errores. Teniendo en cuenta que utilizamos el unbuffered I/O he dimensionado el búfer de lectura/escritura utilizando la define BUFSIZ del sistema que deberia garantizar un tamaño óptimo para las operaciones de I/O

He dicho que iba a proponer dos versiones: sin modificar la función main() (que está bien para ambos casos) la versión alternativa es la siguiente:
// función cpFile()
static int cpFile(
    const char* dest,               // file destinación
    const char* src)                // file fuente
{
    // abre el file fuente
    int fd_in;
    if ((fd_in = open(src, O_RDONLY)) == -1) {
        // return con errore
        return -1;
    }

    // abre el file destinación
    int fd_out;
    if ((fd_out = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 00644)) == -1) {
        // cierra el file y return con error
        close(fd_in);
        return -1;
    }

    // copia en kernel-space usando la función sendfile()
    off_t bytesCopied = 0;
    struct stat fileinfo = {0};
    fstat(fd_in, &fileinfo);
    int result = sendfile(fd_out, fd_in, &bytesCopied, fileinfo.st_size);

    // cierra los file
    close(fd_in);
    close(fd_out);

    // sale con el resultado de sendfile()
    return result;
}
Como se puede ver, casi se puede sobreponer a la anterior, pero es diferente, precisamente, en la parte que realiza el trabajo de copia: en lugar del loop se utiliza la función sendfile(2), lo que nos permite realizar una copia directa y super-eficiente a nivel de kernel-space (mientras que la primera versión trabajaba en user-space).

Sin entrar en los detalles profundos que todo esto conlleva (kernel-space y user-space de los sistemas de la familia UNIX), me limitaré a señalar que esta segunda versión es mejor que la primera, pero es menos portátil, ya que la sendfile(2) tiene un comportamiento diferente en función del sistema (por ejemplo, sobre Linux se puede utilizar sólo con dal Kernel 2.6.33 hacia adelante, mientras que en macOS, por el contrario, sólo funciona hasta la versión 10.8). Y ya que estamos especificamos mejor: también la primera versión no es totalmente portátil, ya que en algunos sistemas (como el que se inicia con W y que prefiero no nombrar ni siquiera) la system call read(2) no está disponible.

Ok, entonces ya se puede adivinar que el tema del próximo post será una versión con buffered I/O de la función cpFile(), o sea una versión intrínsecamente portátil, ya que utilizará el I/O standard del C (lo que está contenido en stdio.h, para entendernos).

Y, por favor,  no contengáis la respiración esperando...

¡Hasta el próximo post!

sábado, 18 de febrero de 2017

Toma el makefile y corre
cómo escribir un makefile universal

Este es un post rápido. Y no es exactamente un post sobre el C. El consejo es tomar la información, huir y mantenerla celosamente para el futuro, ya que podría ser muy útil un dia. Y que no os capturen, de lo contrario pueden acabar como Virgil Starkwell.

cara de "pero he solo rubato un makefile..."
Así, supongamos que tenemos que hacer un proyecto (que llamaremos, por ejemplo, pluto) y, por diversas razones, no queremos (somos de la vieja escuela) o no podemos (no hay uno adapto) utilizar un IDE. Organizamos nuestros file de una manera clásica, en tres directory: pluto, lib y include. Obviamente escribiremos el código en C y colocaremos los file de una manera lógica (obviamente el file con el main() irá en la directory pluto). Los file son muchos, y cada vez que volvemos a compilar no quieremos reescribir el comando manualmente, y queremos volver a compilar sólo lo que lo necesita (sólo los fuentes modificados) satisfaciendo de forma automática las dependencias de los header (re-compilar sólo aquellos fuentes que dependen de un header modificado)... ¡Entonces nos necesita un makefile! Ok, vosotros ya sabéis lo que es un makefile, pero... ¿ya sabéis escribir uno muy simple y, al mismo tiempo, muy funcional y, sobre todo, genérico y universal? Si la respuesta es NO esto es el vuestro post (y si la respuesta es SI, entonces ¡Hasta el próximo post!).

Acabamos con las chácharas: si estáis leyendo esta línea habéis respondido NO a la pregunta anterior, ¡entonces vamos con el ejemplo!
# variables
CC = gcc
SRCS = $(wildcard *.c)
SRCS_LIB = $(wildcard ../lib/*.c)
OBJS = $(SRCS:.c=.o)
OBJS_LIB = $(SRCS_LIB:.c=.o)
DEPS = $(SRCS:.c=.d)
DEPS_LIB = $(SRCS_LIB:.c=.d)

# creación del target file ejecutable
pluto: $(OBJS) $(OBJS_LIB)
    $(CC) $^ -o $@ -lcurl

# creación de los object files
%.o: %.c
    $(CC) -MMD -MP -I../include -c $< -o $@ -g -Wall -std=c11 -D SIMULATION

# directivas phony
.PHONY: clean

# limpieza proyecto ($(RM) es de default "rm -f")
clean:
    $(RM) $(OBJS) $(OBJS_LIB) $(DEPS) $(DEPS_LIB)

# creación dependencias
    -include $(DEPS) $(DEPS_LIB)
Como se ve el makefile enseñado es verdaderamente simple. Pero también es muy completo: hace todo lo necesario, incluyendo la generación de los files de dependencia de los header, y podemos utilizarlo para cualquier proyecto, sin importar el número de file (las directory lib e include pueden estar vacías o contener cientos de file). Podemos agregar y eliminar fuentes y header y re-compilar sin cambiar una sola línea del makefile, porque el se adapta automáticamente a lo que encuentra en las tres directory del proyecto: ¿Qué queremos más?

Algunos pequeños detalles sobre los bloques (comentados) que componen el makefile:

# variables
Aquí se ponen las variables que se utilizan en el resto del makefile. En particular, la variable CC indica que compilador utilizar: en nuestro caso es gcc, pero podría ser, por ejemplo, g++ (para C++). Obviamente, en este caso las fuentes serían .cpp o .cc, por lo que hay que acordarse de modificar las otras variables que hacen referencia a los .c.

# creacion del target file ejecutable
Aquí se pone el comando para linkar los file objeto creados y producir el file ejecutable final. Si utilizamos laguna librería externa la referencia se añade aquí (en el ejemplo se linka la libcurl usando -lcurl).

# creacion de los object files
Aquí se pone el comando para compilar cada fuente y crear el file objeto correspondiente, activando todas las opciones del compilador que necesitamos. Si utilizamos alguna #ifdef especial (como las que hemos visto alli) la activación hay que ponerla aquí (en el ejemplo se activa una define SIMULATION utilizada en las fuentes).

# directivas phony
Aquí se ponen las directivas phony (es un poco largo de explicar: mirar en el link, que está muy claro). 

# limpieza proyecto ($(RM) es de default "rm -f")
Aquí se pone el comando de borrado de los objetos para forzar, eventualmente, una siguiente completa re-compilación.

# creacion dependencias
Aquí se pone el comando para generar los file de dependencia que nos permiten volver a compilar sólo lo que necesita cuando se modifica un header file. 

El makefile enseñado es un ejemplo real, listo para su uso. Obviamente las directivas -lcurl y -D SIMULATION se han añadido como ejemplo para mostrar cómo extender las funcionalidades del makefile: si no las necesitamos las podemos eliminar sin problemas (y añadiremos las que necesitamos utilizando la misma sintaxis).

¿Qué pensáis? El objetivo no era explicar lo que es un makefile y cómo se escribe (uh, hay una enorme documentación en la red sobre el tema). Tampoco era para explicar los secretos de la sintaxis (que permite también soluciones complejas). El objetivo era proporcionar un makefile básico y completo al mismo tiempo, un makefile universal para (casi) cualquier proyecto. Yo diría que el objetivo se ha logrado... luego, si tenemos que hacer proyectos complejos y portátiles, con auto-instaladores, etc. tal vez nos vamos a encontrar más cómodos usando un IDE de buena calidad o usando herramientas manuales como Autotools  o CMake... pero os aseguro que el método rápido y de la vieja escuela que he descrito es utilizable siempre y sin limitaciones. Que alivio...

¡Hasta el próximo post!