Desarrollo Portable con GNU Autotools

Historial de revisiones
Revisión 1.021/01/2006Diego
Bravo
Primera versión

Tabla de contenidos

1. Introducción
2. GNU Build System
2.1. Noción de Portabilidad
2.2. Objetivos del GNU Build System
3. Herramientas del GNU Build System
3.1. Especificación de configuración del paquete
3.2. Aclocal
3.3. Autoconf
3.4. Autoheader
3.5. Automake
3.6. Configure
4. Ejemplo: aplicación trivial
4.1. Versión sin Herramientas GNU
4.2. Versión con Herramientas de Construcción GNU
4.3. Creando un paquete para distribución
5. Mejorando la Portabilidad del Código Fuente
5.1. Recomendaciones para elevar la portabilidad
5.2. Análisis del Código Fuente de la Aplicación Trivial
5.3. Reorganización del código fuente
6. Librerías y Desarrollo de Aplicaciones Medianas
6.1. Librería SAL (acceso a DBMS), 1era Versión
6.2. Librería SAL, 2da Versión
6.3. Librería SAL, 3ra Versión
6.4. Conclusión
7. Introducción a make
7.1. Programas de varios archivos de código fuente
7.2. Dependencias y el Makefile
7.3. Procesamiento de dependencias
7.4. Reglas implícitas
7.5. Variables
8. Archivos informativos de proyectos GNU
9. Crear y Utilizar Librerías en Linux
9.1. Un ejemplo: mejoras para el procesamiento de strings
9.2. Construcción de archivos de librería
9.3. Ejecución dinámica con dlopen
9.4. Problemas Frecuentes

Resumen

Esta es una guía para desarrolladores que pretenden crear aplicaciones portables en sistemas Linux y Unix. Se proporciona una teoría breve y algunos ejemplos, lo que debe ser complementado con la documentación oficial de las herramientas correspondientes.

Las herramientas GNU cuyo uso se describe aquí son Aclocal, Autoconf, Automake, Autoheader y Libtool. Asimismo, se proporciona un apéndice que explica la conocida herramienta Make, y otro referido a la creación de librerías desde lenguaje C.

1. Introducción

El "GNU Build System" (traducido literalmente como "Sistema de Construcción GNU") es una colección de herramientas orientadas a desarrollar aplicaciones siguiendo las mejores prácticas. En particular, en este texto nos interesan las facilidades que aquéllas brindan a fin de alcanzar un alto grado de portabilidad entre un amplio conjunto de plataformas.

En este texto describo y ejemplifico el uso de las herramientas Aclocal, Autoconf, Automake, Autoheader y Libtool. Se asume que el lector tiene cierta experiencia con el lenguaje C y ha compilado programas en dicho lenguaje en plataformas Linux o Unix. Asimismo, se asume que el lector conoce la popular herramienta make; sin embargo, se proporciona un apéndice para quien aún no la ha utilizado (de ser necesario, es allí donde debería empezarse la lectura.)

La estructura del texto es como sigue: Se inicia con una explicación informal acerca de la razón de ser de la portabilidad y el por qué nos hacemos tantos problemas por la misma. Se prosigue con una explicación breve acerca de las herramientas del "GNU Build System", la cual de seguro dejará aturdido a más de uno, y que deberá ser clarificada en las siguientes secciones, en las que se proporcionan diversos ejemplos muy sencillos. Finalmente, se desarrollan ejemplos más sofisticados, los cuales proporcionan algunas ideas acerca de los requerimientos y las soluciones que se aplican para desarrollos medianos y grandes.

Los conceptos presentados en este texto son aplicables a cualquier lenguaje de programación; sin embargo, las herramientas del GNU Build System están especialmente dirigidas a las aplicaciones que se desarrollan en lenguaje C, C++, y Fortran.

Lima, enero de 2006

Este documento se distribuye bajo la licencia Creative Commons Attribution-ShareAlike 3.0 Unported License http://creativecommons.org/licenses/by-sa/3.0/

2. GNU Build System

A continuación una serie de conceptos y objetivos del GNU Build System [1].

2.1. Noción de Portabilidad

Se dice que un programa o aplicación que inicialmente se desarrolla y ejecuta en una plataforma [2] P1 es portable con respecto a una plataforma P2, cuando es (relativamente) sencillo adaptarla para que se ejecute en P2.

De forma inversa, aquella no será portable si resulta muy difícil su adaptación, al punto de ser más sencillo re-escribirla totalmente (desde cero) en la plataforma P2.

A continuación, diversos factores que promueven la portabilidad:

  1. Las plataformas P1 y P2 son muy similares
  2. La aplicación está escrita mediante lenguajes de programación fuertemente estandarizados y disponibles para ambas plataformas
  3. La aplicación sólo hace uso de facilidades disponibles en cualquier plataforma (como por ejemplo, operaciones aritméticas)
  4. La aplicación no hace asunciones con respecto a la plataforma en la que se ha desarrollado y/o se está ejecutando; es decir, no asume facilides que sólo están presentes en P1

En términos generales, es conveniente que las aplicaciones sean portables a muchas plataformas en la medida que esto permite que sean empleadas por más personas sin requerirse ninguna adaptación (o en el extremo, una reescritura total [3].) Asimismo, una aplicación diseñada desde el inicio para ser muy portable, tiene más posibilidades de adaptarse y sobrevivir en plataformas del futuro.

Hacer un programa portable tiene el (¿grave?) inconveniente de significar trabajo extra para los desarrolladores, así como demandar más conocimientos por parte de los mismos. La portabilidad probablemente no está indicada para aplicaciones en las que se tiene la certeza que se emplearán por poco tiempo y en una única plataforma; pero, ¿quién puede tener esta certeza?

2.2. Objetivos del GNU Build System

Podrían resumirse en:

"Proporcionar un conjunto de herramientas y convenciones para permitir y facilitar la creación y el uso de aplicaciones portables entre diversas plataformas similares a Unix (como Linux.)"

Téngase en cuenta que esto descarta (pero no limita) la portabilidad con respecto a otras clases de plataformas [4].

Más concretamente, el GNU Build System ataca los siguientes problemas:

Facilidades del sistema operativo

La mayoría de programas hace uso de algunas facilidades proporcionadas por el sistema operativo en su kernel o sus librerías. Por ejemplo, muchos sistemas derivados de BSD proporcionan la rutina usleep() que permite hacer pausas con precisión de microsegundos. El estándar POSIX, sin embargo, define la función nanosleep() que permite hacer una operación similar. Dado que es muy probable que las futuras plataformas se adscriban al estándar POSIX, concluimos que resulta preferible el empleo del moderno nanosleep().

Sin embargo, la solución óptima consiste en desarrollar el programa de modo tal que se pueda "configurar" para que utilice preferentemente el moderno nanosleep(), y de no ser esto posible, el obsoleto usleep(). Conseguir esta flexibilidad no es tarea sencilla.

El GNU Build System cuenta con el programa autoconf entre sus herramientas. Como veremos, este programa genera un script (llamado configure) que se encarga de analizar las facilidades disponibles (como usleep() vs nanosleep()) en la plataforma en la que se pretende compilar la aplicación, lo cual es una gran ayuda a fin de hacer más portable a nuestro programa.

Especificación de la "construcción" de la aplicación

Cualquier aplicación no trivial consta de un número considerable de archivos de diversos tipos, repartidos en varios directorios. Todos estos componentes deben integrarse adecuadamente para conformar las librerías, los ejecutables, los archivos auxiliares (datos, imágenes, etc.) así como la documentación.

En los sistemas tipo Unix se recurre a la herramienta make (que se describe en el apéndice A) para automatizar todo este proceso. Esta herramienta requiere que el desarrollador escriba un archivo de comandos con una sintaxis especial (el Makefile) la cual especifica con cierto detalle qué es lo que hay que hacer para construir la aplicación.

Lamentablemente, la escritura de un Makefile "completo" [5] es un proceso relativamente largo y propenso a errores (salvo en programas triviales.) A tal efecto, el GNU Build System proporciona una herramienta llamada automake que permite al desarrollador escribir una especificación más sencilla acerca de lo que quiere construir. A diferencia del Makefile, la especificación de automake (que se denomina Makefile.am) sólo requiere especificar qué se quiere construir, y no cómo se debe construir.

En conclusión, el GNU Build System no sólo mejora la portabilidad, sino que simplifica la especificación de la construcción de las aplicaciones no triviales.

Uniformizar la "construcción" de la aplicación

Las dos secciones anteriores discutieron facilidades que atañen únicamente a los desarrolladores que crean aplicaciones portables. En esta sección nos referimos a lo que tiene que hacer el usuario/instalador/administrador que obtiene el paquete del desarrollador, y pretende utilizarlo en su plataforma (que posiblemente es distinta a la del desarrollador.)

El GNU Build System promueve la distribución de paquetes de código fuente, los cuales deben ser "configurados", "construidos" e "instalados" por los usuarios finales instaladores. Esto involucra normalmente tres pasos (obviando el desempacado del paquete):

./configure [opciones]
make
make install

El último paso (make install) probablemente debe realizarse con el usuario administrador (root) si se instalarán archivos en directorios del sistema (por omisión, todo el software generado con el GNU Build System se instalará a partir de /usr/local.)

Este proceso es el mismo para todo paquete generado con el GNU Build System, y al estar ampliamente difundido facilita el trabajo a los usuarios/instaladores.

Siempre es posible especificar un conjunto de opciones a configure, las cuales se pueden obtener mediante:

./configure --help

Ciertas opciones están presentes en casi todos los paquetes; por ejemplo, una opción muy popular corresponde al directorio en el cual se instalará el software tras ser compilado, mediante make install (como indicamos, por omisión es /usr/local):

./configure --prefix=$HOME/pruebas

Con esto, al ejecutar make install, el software se instalará en un subdirectorio de nuestro propio "Home Directory", lo cual tiene la ventaja de no requerir permisos de escritura adicionales.

3. Herramientas del GNU Build System

Los problemas que pretende resolver el GNU Build System no son triviales. A tal efecto son diversas las herramientas que coordinadamente trabajan hacia este fin. Sugiero al lector no desanimarse si se desconcierta en esta sección.

En lo que sigue, es imprescindible que el lector consulte paralelamente la documentación oficial de las herramientas que se mencionan, la cual se suele obtener al ser éstas instaladas.

3.1. Especificación de configuración del paquete

Todos los paquetes que se construyen usando las herramientas del GNU Build System deben tener un único archivo de especificación llamado configure.ac [6], el cual detalla ciertos aspectos de la aplicación, así como ciertas facilidades de la plataforma de las que aquella hace uso. Este archivo se compone de macros escritas en lenguaje M4 (lo cual es normalmente transparente para el desarrollador) y de comandos de Shell.

En resumen, configure.ac es un archivo de texto de configuración del proyecto, el cual normalmente es escrito a mano por el desarrollador. No es un comando.

3.2. Aclocal

Si deseamos que nuestro proyecto utilice la herramienta automake (como casi siempre ocurrirá) y/o si se definieran macros M4 complementarias (que sólo aplican a proyectos muy sofisticados), entonces es necesario ejecutar aclocal. Esta herramienta recolecta la definición de diversas macros requeridas en configure.ac colocándolas en el archivo aclocal.m4. Esto permitirá más adelante la ejecución adecuada de autoconf.

En conclusión, salvo casos excepcionales, siempre debemos ejecutar aclocal, que a su vez generará el archivo aclocal.m4.

3.3. Autoconf

A partir de las macros M4 especificadas en el archivo configure.ac y las definiciones de aclocal.m4, la herramienta autoconf generará un script de shell llamado configure, el cual se emplea por los usuarios finales (que reciben el paquete) para construir la aplicación en distintas plataformas.

La ejecución de autoconf requiere que configure.ac contenga al menos las macros AC_INIT y AC_OUTPUT (esto se verá en los ejemplos.)

3.4. Autoheader

Durante el proceso de compilación de los programas, el GNU Build System genera y utiliza una serie de macros del preprocesador con diversos fines.

Con este fin se debe invocar a la herramienta autoheader, la cual genera un archivo llamado config.h.in, el cual posteriormente se usará para generar un archivo cabecera auxiliar llamado config.h [7].

Para que esto funcione, es necesario agregar la siguiente macro al archivo configure.ac:

AM_CONFIG_HEADER([config.h])

3.5. Automake

En los sistemas tipo Unix se proporciona el comando make para "construir aplicaciones" a partir de un archivo de especificación de comandos con una sintaxis particular (el Makefile). Sin embargo, para proyectos no triviales su escritura es un proceso relativamente largo y propenso a errores.

Debido a esto, el GNU Build System recomienda el uso de la herramienta automake la que permite al desarrollador escribir una especificación más sencilla acerca de lo que quiere construir. Esta especificación normalmente se almacena en un archivo llamado Makefile.am.

A partir de Makefile.am y configure.ac, automake crea un archivo llamado Makefile.in que corresponde a una especificación más detallada (pero no definitiva) del proceso de construcción de la aplicación. Este archivo es utilizado posteriormente por el ya mencionado script configure para crear un tradicional Makefile para el también tradicional make.

El uso de automake requiere de la presencia de la macro AM_INIT_AUTOMAKE en configure.ac.

3.6. Configure

Esta "herramienta" es en realidad el producto de autoconf. Se trata de un script de shell que es usado por los usuarios finales (que compilan e instalan la aplicación) y tiene como propósito:

  • Analizar la plataforma en uso de acuerdo a los requerimientos de la aplicación
  • Generar un archivo config.h acorde con el análisis realizado y a partir de config.h.in (la salida de autoheader)
  • Generar un Makefile acorde con el análisis realizado y a partir del archivo Makefile.in (la salida de automake)

Los análisis de plataforma que realiza configure están en función de las macros de test que se incluyeron por el desarrollador en configure.ac.

El script configure está constituido por comandos de shell "sh" (tipo Bourne) empleando sólo las sentencias más portables, debido a que hay muchas incompatibiliades entre plataformas en la implementación de los shell’s tipo Bourne.

La siguiente figura esquematiza la interacción de las herramientas del GNU Build System.

Herramientas del GNU Build System

4. Ejemplo: aplicación trivial

En esta sección introduciremos las herramientas del GNU Build System para una aplicación trivial de un único archivo de código fuente. El objetivo aquí es ilustrar el uso de las herramientas, y NO intentar hacer esta aplicación portable (todavía.)

4.1. Versión sin Herramientas GNU

El siguiente programita muestra en la pantalla un asterisco que se desplaza horizontalmente. Luego aparece un mensaje, espera a que el usuario presione una tecla, y termina. Este programa hace uso de la popular librería Curses, o su variante Open Source, Ncurses.

Como en todo programa, es imprescindible consultar en el manual del sistema todas las funciones utilizadas para averiguar los archivos cabecera a incluir; en nuestro caso son sólo curses.h y unistd.h [8].

#include <ncurses.h>
#include <unistd.h>

#define RETARDO 100000 /* 1E5 */

int main(int argc,char **argv)
{
int z;

initscr();
for(z=10;z<70;z++)
        {
        mvaddch(10,z,'*');
        refresh();
        usleep(RETARDO);
        mvaddch(10,z,' ');
        }
mvaddstr(20,10,"Eso fue todo. Presiona una tecla para continuar.");
getch();
endwin();
return 0;
}

El Makefile empleado para compilarlo es bastante simple, gracias a los atajos conocidos:

LDFLAGS = -lncurses
CFLAGS = -Wall

all: ejemplo1

clean:
        rm -f ejemplo1

Si Ud. no conoce todavía la sintaxis de un Makefile, por favor consulte el apéndice A antes de proseguir.

Este programa trivial tiene problemas de portabilidad y posiblemente el lector no podrá compilarlo. Por ejemplo, podría ser necesario intercambiar el <ncurses.h> por <curses.h>, <curses/curses.h> o <ncurses/ncurses.h>, o modificar el Makefile para enlazar con -lcurses y no -lncurses [9].

Mucho más adelante veremos cómo resolver esto, pero por ahora sólo nos familiarizaremos con las herramientas del Sistema de Construcción GNU.

4.2. Versión con Herramientas de Construcción GNU

Modificar las fuentes

Partimos de un nuevo directorio en blanco en el que depositaremos sólo el código fuente (descartamos nuestro Makefile):

$ ls
ejemplo1.c

A cada archivo fuente de lenguaje C le agregaremos las siguientes tres líneas:

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

Con esto, el programa queda convertido en:

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <ncurses.h>
#include <unistd.h>

#define RETARDO 100000 /* 1E5 */

int main(int argc,char **argv)
{
int z;

initscr();
for(z=10;z<70;z++)
        {
        mvaddch(10,z,'*');
        refresh();
        usleep(RETARDO);
        mvaddch(10,z,' ');
        }
mvaddstr(20,10,"Eso fue todo. Presiona una tecla para continuar.");
getch();
endwin();
return 0;
}

Archivo configure.ac

Este archivo se emplea para especificar las características generales del paquete así como los tests de portabilidad entre plataformas. Lo que sigue se puede considerar una plantilla básica para muchos proyectos:

AC_INIT([Ejemplo Uno],[1.0],[diegobravo@x.com])
AM_INIT_AUTOMAKE([ejemplo1],[1.0])
AM_CONFIG_HEADER(config.h)
AC_PROG_CC
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

Un poco más abajo, al hablar de autoconf, explicaremos su contenido.

Archivo Makefile.am

El archivo Makefile.am corresponde a una especificación acerca de lo que se pretende construir. En nuestro caso se trata de un programa llamado ejemplo1, el cual consta de un archivo fuente ejemplo1.c, y se enlaza con la librería ncurses:

bin_PROGRAMS = ejemplo1
ejemplo1_SOURCES = ejemplo1.c
ejemplo1_LDADD = -lncurses

En este punto tenemos sólo los siguientes archivos:

$ ls
Makefile.am  configure.ac  ejemplo1.c

Eso es todo lo que necesitamos escribir. A continuación las herramientas GNU harán el resto del trabajo, para lo cual ejecutaremos aclocal, autoconf, autoheader y automake (de preferencia en ese orden.)

Ejecutar aclocal

$ aclocal
$ ls
Makefile.am  aclocal.m4  configure.ac  ejemplo1.c

El programa aclocal examina el archivo configure.ac y analiza si se requieren macros no incorporadas en autoconf (por ejemplo, las macros relacionadas con automake que tienen la forma "AM*") las cuales son agregadas en el archivo auxiliar aclocal.m4. También se emplea en ciertos proyectos para proporcionar macros personalizas del desarrollador.

Ejecutar autoconf

El programa autoconf generará el script "configure" y quizá otros archivos auxiliares:

$ autoconf
$ ls -F
Makefile.am  aclocal.m4    autom4te.cache/
configure*   configure.ac  ejemplo1.c

Tal como indicamos anteriormente, autoconf requiere que el archivo configure.ac contenga al menos de las macros AC_INIT y AC_OUTPUT. La macro AC_INIT (que debe estar al principio de configure.ac) recibe los parámetros:

1- Nombre del paquete completo

2- Versión del paquete [10]

3- Dirección email para reporte de bugs

Nótese que se ha rodeado estos argumentos con los símbolos [ y ] correspondientes al "entrecomillado" de M4. Estrictamente, éstos no se requieren cuando el texto es simple, pero son obligatorios cuando el argumento consiste en una invocación a otra macro.

Para los conocedores de M4: Normalmente, M4 emplea los caracteres "‘" y "’" para "entrecomillar". En las herramientas que estamos viendo, éstos se han cambiado por [ y ], lo cual se explica en la documentación oficial de autoconf:

..."but in the context of shell programming (and actually of most programming
languages), it's about the worst choice one can make: because of
strings and back quoted expression in shell (such as `'this'' and
``that`'), because of literal characters in usual programming language
(as in `'0''), there are many unbalanced ``' and `''.  Proper M4
quotation then becomes a nightmare, if not impossible.  In order to
make M4 useful in such a context, its designers have equipped it with
`changequote', which makes it possible to chose another pair of quotes.
M4sugar, M4sh, Autoconf, and Autotest all have chosen to use `\[' and
`\]'.  Not especially because they are unlikely characters, but because
they are characters unlikely to be unbalanced."

Los parámetros de AC_INIT se podrán apreciar por el usuario instalador cuando ejecute:

./configure --version

Todo archivo configure.ac debe terminar con AC_OUTPUT.

La macro AC_PROC_CC instruye a autoconf para programar un test de existencia del compilador de lenguaje C y siempre se debe incluir si el proyecto contiene al menos una fuente de lenguaje C. Finalmente, la macro AC_CONFIG_FILES permite especificar qué archivos se generarán al ejecutarse configure, que en nuestro caso será sólo el Makefile. Un poco más adelante veremos las macros aún no explicadas.

Ejecutar autoheader

$ autoheader
$ ls
Makefile.am  autom4te.cache  configure     ejemplo1.c
aclocal.m4   config.h.in     configure.ac

Como sabemos, este programa genera el archivo de definiciones de preprocesador config.h.in, el cual se usa más adelante para generar a config.h.

Crear archivos obligatorios de documentación

Son imprescindibles. Si Ud. olvida crearlos la ejecución de automake (el siguiente paso) fallará. Su contenido por ahora no nos interesa [11], así que los creamos en blanco:

$ touch README AUTHORS NEWS ChangeLog
$ ls
AUTHORS     Makefile.am  README      autom4te.cache  configure
ChangeLog   NEWS         aclocal.m4  config.h.in     configure.ac
ejemplo1.c

Ejecutar automake

$ automake -a
automake: configure.ac: installing `./install-sh'
automake: configure.ac: installing `./mkinstalldirs'
automake: configure.ac: installing `./missing'
automake: configure.ac: installing `./config.guess'
automake: configure.ac: installing `./config.sub'
automake: Makefile.am: installing `./INSTALL'
automake: Makefile.am: installing `./COPYING'

La opción -a solicita a automake que genere algunos archivos complementarios que son necesarios para etapas posteriores del proceso de generación del paquete.

Tal como se aprecia, hemos llenado nuestro humilde directorio de fuentes con una cantidad increible de archivos autogenerados:

$ ls -F
AUTHORS    Makefile.am  aclocal.m4       config.sub@   install-sh@
COPYING@   Makefile.in  autom4te.cache/  configure*    missing@
ChangeLog  NEWS         config.guess@    configure.ac  mkinstalldirs@
INSTALL@   README       config.h.in      ejemplo1.c    stamp-h.in

En este punto el proyecto está listo para que se ejecute ./configure, el cual generará un Makefile específico para nuestro sistema (y otros archivos complementarios.)

Como se indicó, automake requiere que configure.ac contenga la macro AM_INIT_AUTOMAKE. Esta macro recibe dos parámetros que corresponden al nombre del paquete y la versión del paquete a ser distribuido. En nuestro caso, los parámetros indicados implican que el paquete a generarse (lo veremos más adelante) se llamará ejemplo1-1.0.tar.gz (nombre-version.tar.gz.)

Lanzar configure

Es muy recomendable (especialmente para los desarrolladores que utilizan herramientas GNU) lanzar configure desde un nuevo directorio al que llamaremos "de construcción", en vez de emplear el mismo directorio "de fuentes". Esto ayuda a detectar ciertos errores en las rutas y de paso no se contamina más nuestro directorio de fuentes con los subproductos del proceso de construcción [12]:

$ pwd
/home/diego/escritos/autoconf/ejemplo1/portable1
$ cd ..
$ mkdir build
$ cd build/
$ ../portable1/configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking whether make sets $(MAKE)... yes
...
config.status: creating Makefile
config.status: creating config.h
config.status: executing default-1 commands
$ ls
Makefile  config.h  config.log  config.status  stamp-h

Nótese que se ha generado (al fin') un Makefile. Este Makefile hace referencia al directorio que contiene las fuentes (que no están aquí.) Sin embargo, todos los archivos que se generen como parte de proceso de compilación se almacenarán en este "directorio de compilación". También podemos apreciar la creación del antes mencionado config.h.

Si Ud. revisa el Makefile notará que está muy lejos de aquel que hicieramos manualmente en la versión "no portable".

Ejecutar make

$ make
gcc -DHAVE_CONFIG_H -I. -I../portable -I. -g -O2 \
        -c ../portable1/ejemplo1.c
gcc  -g -O2  -o ejemplo1  ejemplo1.o -lncurses
$ ls -F
Makefile   config.h    config.log  config.status*
ejemplo1*  ejemplo1.o  stamp-h

Apreciamos que se ha generado el ejecutable ejemplo1.

Instalar el programa

Por omisión los programas se instalarán bajo /usr/local, lo cual normalmente requiere ser administrador para tener permiso de escritura:

$ su
Password:
# make install
make[1]: Entering directory `/home/diego/escritos/autoconf/ejemplo1/build'
/bin/sh ../portable1/mkinstalldirs /usr/local/bin
  /usr/bin/install -c  ejemplo1 /usr/local/bin/ejemplo1
make[1]: Nothing to be done for `install-data-am'.
make[1]: Leaving directory `/home/diego/escritos/autoconf/ejemplo1/build'
# exit
exit
$ ls -l /usr/local/bin/ejemplo1
-rwxr-xr-x  1 root staff 25345 Oct  9 13:26 /usr/local/bin/ejemplo1
$ /usr/local/bin/ejemplo1

Como se aprecia, el ejectuable se ha copiado a /usr/local/bin, el cual debería estar en el PATH de los usuarios para facilitar su ejecución.

4.3. Creando un paquete para distribución

El Makefile anteriormente generado tiene la capacidad de generar un paquete en formato tar.gz para ser distribuido a otros usuarios a fin de que reconfiguren, construyan, e instalen el programa. Para esto se puede emplear el "target dist":

$ make dist
rm -rf ejemplo1-1.0
mkdir ejemplo1-1.0
chmod 777 ejemplo1-1.0
here=`cd . && pwd`; \
top_distdir=`cd ejemplo1-1.0 && pwd`; \
distdir=`cd ejemplo1-1.0 && pwd`; \
cd ../portable1 \
  && automake-1.4 --include-deps --build-dir=$here \
  --srcdir-name=../portable1 --output-dir=$top_distdir --gnu Makefile
chmod -R a+r ejemplo1-1.0
GZIP=--best tar chozf ejemplo1-1.0.tar.gz ejemplo1-1.0
rm -rf ejemplo1-1.0
$ ls
Makefile       config.h             config.log
config.status  ejemplo1-1.0.tar.gz  stamp-h

Téngase en cuenta que esto no requiere que se haya compilado previamente el paquete, pues no estamos distribuyendo ningún ejecutable. Como se aprecia, el resultado es el archivo ejemplo1-1.0.tar.gz que corresponde a la combinación de "nombrepaquete-version" de la macro AM_INIT_AUTOMAKE.

Una observación importante: el Makefile que hemos generado en el directorio de compilación tiene la capacidad de determinar las dependencias entre los archivos de código fuente del proyecto. En cambio, el archivo Makefile.in (que generará el Makefile definitivo) del paquete recién creado, ya incluye la información de las dependencias. Para el primer caso, es conveniente que el Makefile del desarrollador recalcule las dependencias en cada compilación, dado que es muy probable que éstas se alteren conforme se modifica el código fuente. Por el contrario, para el segundo caso (el usuario instalador) es conveniente que su Makefile generado ya no recalcule las dependencias debido a que esta operación requiere de herramientas del GNU Build System que posiblemente no tenga disponibles (por no ser un desarrollador) y puesto que es poco probable que efectúe modificaciones notables en el código [13] . El detalle de toda esta explicación en realidad no es muy relevante; lo único que se debe recordar es siempre distribuir el paquete generado por make dist y no empacar cualquier directorio de fuentes.

Durante el desarrollo es conveniente verificar de cuando en cuando el correcto funcionamiento de make dist en tanto ayuda a descubrir ciertos errores (muy fáciles de cometer) con las rutas y con las especificaciones de automake. Aún mejor es emplear make distcheck, el cual además de crear un paquete para su distribución, verifica que está correctamente construido.

5. Mejorando la Portabilidad del Código Fuente

El GNU Build System de por sí no brinda portabilidad al código fuente. Por el contrario, proporciona facilidades a los programadores para que ellos creen código fuente portable, lo que significa que se requiere de programadores con los conocimientos y criterios adecuados para esta tarea.

5.1. Recomendaciones para elevar la portabilidad

A continuación se describen algunas acciones y decisiones que conllevan a conseguir un código fuente portable.

Disponibilidad del Lenguaje y de las Librerías

Es evidente que si empleamos un lenguaje de programación disponible en una única plataforma P1, la portabilidad se restringe tremendamente; el llevar la aplicación a la plataforma P2 típicamente involucra una reescritura total, quizá en otro lenguaje de programación.

Un ejemplo conocido lo constituye la multitud de lenguajes inventados y reinventados por Micros*ft, los que obligan a sus programadores y usuarios a emplear exclusivamente su plataforma Wind*ws.

De forma similar, es muy probable que una aplicación no trivial utilice las facilidades de diversas librerías proporcionadas por el Sistema Operativo o por terceros. En términos generales, siempre se debe tratar de utilizar librerías a fin de no volver a escribir y depurar código que ya ha pasado por este proceso; sin embargo, es conveniente elegir librerías que están disponibles en múltiples plataformas a fin de que nuestra aplicación no requiera modificaciones por este concepto al ser trasladada a dichas plataformas. En particular, es extremadamente recomendable que las librerías utilizadas sean de código abierto, las cuales en caso de necesidad pueden ser portadas por nosotros mismos a cualquier otra plataforma.

Estandarización del Lenguaje

Ciertos lenguajes de programación están implementados en múltiples plataformas, pero lamentablemente tienen diversas incompatibilidades entre sí. En otras palabras, existen diversos "dialectos" más o menos parecidos, pero que convierten la adaptación de aplicaciones en una pesadilla. Un ejemplo clásico es el lenguaje Basic que está disponible en decenas de plataformas, y prácticamente ninguna es igual a la otra. Esto quiere decir que cualquier programa no trivial en Basic requiere de una fuerte dosis de reescritura para llevarse de P1 a P2.

Afortunadamente algunos lenguajes han sido formalmente estandarizados a fin de proporcionar un marco seguro de portabilidad. Uno de los ejemplos más conocidos corresponde al lenguaje C, el cual actualmente es un estándar ANSI/ISO (lo mismo que el C++.) Las organizaciones de estandarización proporcionan referencias que los implementadores deben seguir y así garantizan un comportamiento uniforme entre plataformas.

En particular, los estándares de C y C++ permiten desarrollar muchas aplicaciones (pero no todas) de manera totalmente portable para cualquier plataforma que disponga de un compilador "estandarizado". El lenguaje Java permite hacer programas aún más portables en tanto sus estándares son más estrictos y cubren muchos aspectos que van más allá del lenguaje, lo que deja muy poco a criterio del implementador (como si ocurre con C y C++.)

Evitar extensiones propietarias

Esto es una consecuencia lógica de lo anterior. Muchas veces estamos empleando un lenguaje bien estandarizado, pero la documentación nos señala la posibilidad de emplear "extensiones" para ahorrarnos cierto trabajo. Muchas veces no está bien documentado que se trata precísamente de "extensiones" que minan la portabilidad, o simplemente la gente no le da la debida importancia a este hecho.

El caso es que todas estas extensiones deben ser bien identificadas y su uso debe evitarse en lo posible. Si por algún motivo se decidiera el empleo de éstas, deberíamos al menos tratar de asegurarnos de que será posible removerlas en el futuro. En particular, el código que depende de aquellas debe ailsarse a fin de facilitar su futura reescritura.

Aislar el código no portable

Como indicamos antes, muchas aplicaciones no pueden escribirse empleando exclusivamente los aspectos estandarizados de un lenguaje de programación particular. Típicamente se requiere de librerías de terceros, así como de llamadas específicas del sistema operativo que se está utilizando.

En estos casos, el código fuente en cuestión no es portable; sin embargo, una manera muy efectiva para que la aplicación en su conjunto sea altamente portable consiste en aislar aquel código que necesariamente se tendrá que reescribir en el futuro al momento de portar. Este código bien aislado se suele almacenar en una librería, la cual se intenta mantener lo más reducida posible a fin de reducir lo que potencialmente habrá que reescribir.

Adaptar el código fuente a la plataforma destino

Incluso si nuestro programa ha sido codificado respetando los estándares de portabilidad, es frecuente que se requiera de ciertos cambios relativamente simples (aunque numerosos) cuando se intente portar a otra plataforma. Esto ocurre por diversos motivos: las diversas plataformas respetan o ignoran los diversos estándares, o los cumplen sólo parcialmente; los estándares evolucionan con el tiempo, y no especifican absolutamente todo; los vendedores de las plataformas introducen cambios (y errores) a fin de mejorar sus ofertas, etc.

Uso de Autoconf

Con frecuencia estos cambios en el código fuente requieren de un conocimiento relativamente profundo del ambiente de desarrollo de la plataforma hacia donde se desea portar. Obviamente esto no puede dejarse en las manos de un usuario final instalador que sólo pretende utilizar la aplicación.

Es hacia estas adaptaciones del código fuente donde apunta la herramienta autoconf del GNU Build System. Como indicamos anteriormente, esta herramienta construye un script (llamado configure) que se encarga de analizar el sistema en el que se pretende construir y/o ejecutar la aplicación. A partir de este análisis el código fuente es efectivamente "adaptado" por medio de macros condicionales, lo que finalmente permite que la aplicación sea construida.

La herramienta autoconf permite al desarrollador especificar con facilidad una serie de tests para verificar diversos aspectos de la plataforma de destino (el desarrollador muchas veces ignora cuál es ésta.) Aunque el desarrollador puede programar sus propios tests, autoconf ya cuenta con un gran número de éstos, producto de la experiencia de muchos programadores en sus intentos de portar aplicaciones entre plataformas tipo Unix.

5.2. Análisis del Código Fuente de la Aplicación Trivial

Recapitulemos y analicemos el programa ejemplo1.c:

     1  #ifdef HAVE_CONFIG_H
     2  #include <config.h>
     3  #endif

     4  #include <ncurses.h>
     5  #include <unistd.h>

     6  #define RETARDO 100000 /* 1E5 */

     7  int main(int argc,char **argv)
     8  {
     9  int z;

    10  initscr();
    11  for(z=10;z<70;z++)
    12          {
    13          mvaddch(10,z,'*');
    14          refresh();
    15          usleep(RETARDO);
    16          mvaddch(10,z,' ');
    17          }
    18  mvaddstr(20,10,"Eso fue todo. Presiona una tecla para continuar.");
    19  getch();
    20  endwin();
    21  return 0;
    22  }

Inclusión condicional de config.h

Las tres primeras líneas incluyen condicionalmente el archivo config.h. Como se indicó anteriormente, éstas deben incluirse al inicio de todos los archivos de código fuente C/C++ de nuestra aplicación.

El archivo config.h contiene una serie de macros producto del análisis que realiza configure de la plataforma donde se desea construir la aplicación. De no utilizarse las herramientas GNU, el archivo config.h sencillamente no existiría y su inclusión en el código fuente generaría un error de precompilación; por esto, su inclusión es condicional mediante la macro HAVE_CONFIG_H, la cual es definida por el Makefile generado con herramientas GNU. Si Ud. no comprende el significado de estas directivas, es imprescindible que consulte la documentación del precompilador del lenguaje C/C++ (en los libros de C/C++.)

Headers "Curses/Ncurses"

Posteriormente (línea 4) tenemos la inclusión del header ncurses.h. Para quien no está informado, Curses es una librería disponible en prácticamente todos los sistemas Unix y tiene entre sus objetivos facilitar el desarrollo de aplicaciones que hacen uso extensivo de la pantalla completa de un terminal (por ejemplo, hacer saltar el cursor, cambiar los colores del texto, etc.)

Existen diversas implementaciones comerciales de la librería Curses disponibles en los sistemas Unix comerciales. En los sistemas de código fuente abierto (como Linux) se suele emplear una implementación denominada "New Curses" o "ncurses" que tiene prácticamente la misma funcionalidad.

Nuestro programa ha asumido (correctamente para el sistema que estoy utilizando en este momento) que está disponible el header "ncurses.h" en el directorio estándar de headers (normalmente /usr/include.) Sin embargo, la observación y experiencia con otros sistemas indica que esto no es muy portable. En particular, los sistemas Unix comerciales suelen disponer de un header equivalente llamado "curses.h". Asimismo, es posible encontrar distribuciones de Linux antiguas que incluyen a "ncurses.h" en un directorio no estándar (frecuentemente /usr/include/ncurses.) Y siguiendo esta última idea, podríamos conjeturar que hay sistemas Unix comerciales que incluyen a "curses.h" en su propio directorio no estándar /usr/include/curses. Por lo tanto, solicitaremos a autoconf (mediante configure.ac) que programe cuatro tests de existencia para el archivo cabecera "Curses/Ncurses".

Incluso si la plataforma destino no dispone de ninguno de los headers posibles para "Curses/Ncurses" y el programa no se puede construir, es mucho mejor que configure proporcione un mensaje tal como:

$ ./configure
...
configure: error: No encuentro el archivo ncurses.h ni curses.h.
Por favor, instale la librería Curses/Ncurses.

A que el usuario final instalador obtenga lo siguiente (quizá tras haber perdido mucho tiempo compilando otros módulos):

$ make
gcc -DHAVE_CONFIG_H -I. -I. -I.     -g -O2 -c ejemplo1.c
ejemplo1.c:5:21: ncurses.h: No such file or directory
make: *** [ejemplo1.o] Error 1

¿Qué puede hacer un instalador que no es desarrollador con este mensaje? …de seguro quejarse con el autor del programa.

A fin de programar los tests mencionados, podemos agregar lo siguiente a configure.ac:

AC_CHECK_HEADERS([ncurses.h curses.h ncurses/ncurses.h curses/curses.h])

Esta directiva verifica la existencia de los headers especificados (en el mismo orden) y define una macro para cada uno de aquellos cuya existencia se comprueba. Suponiendo -por ejemplo- que en cierta plataforma se encuentran los headers /usr/include/curses.h y /usr/include/ncurses/ncurses.h (es decir, el segundo y tercer headers de la lista), entonces en config.h se definirían las macros:

#define HAVE_CURSES_H 1
#define HAVE_NCURSES_NCURSES_H 1

Y no se definirán macros correspondientes a los otros headers inexistentes.

Sin embargo, dado que nos basta con uno solo de estos headers, podríamos acelerar un poco la ejecución de configure si le solicitamos que termine este test tan pronto encuentre uno de los headers de la lista:

AC_CHECK_HEADERS([ncurses.h curses.h ncurses/ncurses.h curses/curses.h],
                break)

break es un comando del shell que interrumpe loops, y se emplea para finalizar el loop de configure que itera entre los headers de la lista.

Con el fin de utilizar los descubrimentos de configure plasmados en macros de config.h, debemos añadir a nuestro programa lo siguiente:

#ifdef HAVE_NCURSES_H
#include <ncurses.h>
#else
#ifdef HAVE_CURSES_H
#include <curses.h>
#else
#ifdef HAVE_NCURSES_NCURSES_H
#include <ncurses/ncurses.h>
#else
#ifdef HAVE_CURSES_CURSES_H
#include <curses/curses.h>
#else
#error "No se encuentra header curses/ncurses"
#endif
#endif
#endif
#endif

Por favor asegúrese de comprender esto antes de proseguir; como indicamos, esto son únicamente directivas del preprocesador de lenguaje C/C++.

Header unistd.h

Este header es requerido para la declaración de usleep(). Podemos agregar en configure.ac:

AC_CHECK_HEADERS([unistd.h])

Sólo a modo de ilustración, podemos instruir a configure para emitir un mensaje si se encuentra dicho header, y otro mensaje si no se encuentra:

AC_CHECK_HEADERS([unistd.h],[AC_MSG_RESULT([Si hay unistd!])],
        [AC_MSG_RESULT([No hay unistd])])

Si consideramos imprescindible la presencia de este header, podemos forzar a que configure se detenga al no hallarlo. Para esto, empleamos AC_MSG_ERROR:

AC_CHECK_HEADERS([unistd.h],[AC_MSG_RESULT([Si hay unistd!])],
        [AC_MSG_ERROR([Error Fatal: No hay unistd])])

Recordar que en tanto estamos utilizando macros como argumentos de otra macro, es imprescindible "entrecomillar" los argumentos de éstos con [ y ].

Invocación a usleep()

Como primera aproximación, presentemos un test para verificar la disponibilidad de esta función en las librerías estándares:

AC_CHECK_FUNCS([usleep])

Si configure verifica la disponibilidad de dicha función, se generará (en config.h) la macro:

#define HAVE_USLEEP

Que puede ser convenientemente utilizada en nuestro código fuente.

Sin embargo, anteriormente se comentó que usleep() no era la función más adecuada para nuestro propósito (de hecho, se considera obsoleta.) Por el contrario, el estándar POSIX recomienda en su lugar el empleo de nanosleep(), la cual tiene mayores posibilidades de estar implementada en las plataformas del futuro. Por tanto, podemos modificar nuestro test del siguiente modo:

AC_CHECK_FUNCS([nanosleep usleep],break)

Esto verificará la disponibilidad de nanosleep() y de hallarse, definirá la macro correspondiente (HAVE_NANOSLEEP) y continuará con otros tests. Sólo de no encontrarse disponible se procederá al test de usleep(). Nótese que gracias a break a lo más una de éstas estará disponible.

Esto trae dos consecuencias. En primer lugar, tenemos que considerar ahora el header que declara a nanosleep(); en tanto la documentación indica que se trata de time.h, debemos proceder a programar un test para validar su existencia:

AC_CHECK_HEADERS([time.h])

La segunda consecuencia corresponde a las modificaciones necesarias en el código fuente a fin de poder emplear nanosleep() o usleep(). La primera emplea una estructura especial de tipo timespec para especificar el tiempo de retardo, mientras que la segunda sólo requiere un argumento unsigned long correspondiente a los microsegundos de retardo. A continuación una primera versión modificada del código fuente que hace uso de todo lo anterior:

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_NCURSES_H
#include <ncurses.h>
#else
#ifdef HAVE_CURSES_H
#include <curses.h>
#else
#ifdef HAVE_NCURSES_NCURSES_H
#include <ncurses/ncurses.h>
#else
#ifdef HAVE_CURSES_CURSES_H
#include <curses/curses.h>
#else
#error "No se encuentra header curses/ncurses"
#endif
#endif
#endif
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef HAVE_TIME_H
#include <time.h>
#endif

#define RETARDO 100000 /* 1E5 */

int main(int argc,char **argv)
{
int z;
#ifdef HAVE_NANOSLEEP
struct timespec req;
#endif

initscr();
for(z=10;z<70;z++)
        {
        mvaddch(10,z,'*');
        refresh();
#ifdef HAVE_NANOSLEEP
        req.tv_sec=0;
        req.tv_nsec=1000*RETARDO;
        nanosleep(&req,NULL);
#endif
#ifdef HAVE_USLEEP
        usleep(RETARDO);
#endif
        mvaddch(10,z,' ');
        }
mvaddstr(20,10,"Eso fue todo. Presiona una tecla para continuar.");
getch();
endwin();
return 0;
}

Nótese que no hay riesgo de invocar un doble retardo (un nanosleep() seguido de un usleep()) pues como indicamos, a lo más una de éstas funciones estará definida. Por otro lado, no es riesgoso incluir ambos headers time.h y unistd.h aun si sólo uno de ellos será aprovechado.

El enlace con Curses/Ncurses

Así como hay diversas posibilidades para el header de Curses/Ncurses, también se requiere especificar la librería adecuada para el momento del enlace. Esto se especificó manualmente en el archivo Makefile.am mediante:

ejemplo1_LDADD = -lncurses

Esto significa que el compilador al momento de generar el ejecutable intentará enlazar con la librería "libncurses.*".

Como es de esperarse, en sistemas Unix comerciales el enlace deberá hacerse con la versión comercial de curses: "libcurses.*". Para soportar ambas posibilidades, en primer lugar eliminemos la directiva ejemplo1+LADD de Makefile.am que asumía la versión Ncurses:

bin_PROGRAMS = ejemplo1
ejemplo1_SOURCES = ejemplo1.c
#ejemplo1_LDADD = -lncurses

Y añadiremos el siguiente test a configure.ac:

AC_SEARCH_LIBS([initscr],[ncurses curses],,
        [AC_MSG_ERROR([No se encuentra la libreria Curses/Ncurses])])

Esta macro requiere como primer argumento el nombre de una función que debe estar en la librería que se está buscando. En nuestro caso, Curses/Ncurses proporciona muchas funciones, de las cuales he elegido a initscr() (se puede elegir cualquiera [14].) Con esta información, configure verifica si es posible crear un programa que invoque a dicha función conforme se enlaza con cada una de las librerías proporcionadas en el segundo argumento. Tan pronto se encuentra ésta librería, se agrega al contenido de la variable especial LIBS, la cual en el Makefile especifica las librerías a enlazar con todos los programas.

El tercer argumento corresponde a una acción a realizar cuando se encuentra una librería válida (en nuestro caso, no tenemos nada que hacer), mientras que el cuarto argumento corresponde a qué hacer si NINGUNA de las librerías resulta ser útil (o no se encuentran.) En este caso hemos especificado un mensaje de error (que detiene a configure) lo cual es necesario puesto que nuestro programa sencillamente no se puede usar si no se dispone de tal librería.

Nótese que las librerías se especifican sin el prefijo -l.

Probando la nueva versión

El archivo configure.ac actualmente luce así:

AC_INIT([Ejemplo Uno],[1.0],[diegobravo@x.com])
AM_INIT_AUTOMAKE([ejemplo1],[1.0])
AM_CONFIG_HEADER(config.h)
AC_PROG_CPP
AC_PROG_CC
AC_CHECK_HEADERS([ncurses.h curses.h ncurses/ncurses.h curses/curses.h],
                break)
AC_CHECK_HEADERS([unistd.h])
AC_CHECK_HEADERS([time.h])
AC_CHECK_FUNCS([nanosleep usleep],break)
AC_SEARCH_LIBS([initscr],[ncurses curses],,
        [AC_MSG_ERROR([No se encuentra la libreria Curses/Ncurses])])
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

Siempre que se emplea AC_CHECK_HEADERS es recomendable que esté precedida de AC_PROC_CPP y AC_PROC_CC para evitar ciertos problemas sutiles que ocurren en situaciones más complicadas [15].

Dado que en mi sistema están disponibles tanto nanosleep() como también usleep(), tras ejecutar configure obtengo la siguiente información de config.h:

$ grep SLEEP config.h
#define HAVE_NANOSLEEP 1
/* #undef HAVE_USLEEP */

Es decir, solo una se reporta como disponible.

Tras repetir todo el proceso (desde aclocal…), el nuevo ejecutable ejemplo1 contiene sólo una llamada a nanosleep() y ninguna a usleep():

$ nm ejemplo1|grep sleep
         U nanosleep@@GLIBC_2.0

Asimismo, puesto que mi sistema emplea la versión Ncurses (no Curses), los tests de selección de header y de librería generaron las definiciones correspondientes:

$ grep CURSES config.h
/* #undef HAVE_CURSES_CURSES_H */
/* #undef HAVE_CURSES_H */
#define HAVE_NCURSES_H 1
/* #undef HAVE_NCURSES_NCURSES_H */
$ grep LIBS Makefile
LIBS = -lncurses
...

5.3. Reorganización del código fuente

En esta sección intentaremos resolver algunas limitaciones de las que adolece el código fuente de nuestra aplicación.

Archivo cabecera general de la aplicación

Como se puede apreciar en la última versión de nuestro ejemplo1.c, los tests relacionados a los archivos cabecera que se ubican en su parte inicial constituyen un número considerable de líneas (que pueden crecer con el tiempo.) Dado que esto se debe hacer para cada archivo de código fuente (*.c), es preferible especificarlo una única vez en un archivo cabecera general de la aplicación, el cual será incluido por todos los archivos de lenguaje C.

Esto por lo general ocurre en forma natural en las aplicaciones medianas y grandes, y lo haremos en este momento con fines didácticos. Nuestro archivo cabecera general se llamará ejemplo1.h y (por ahora) cuenta con el siguiente contenido:

#ifndef EJEMPLO1_H
#define EJEMPLO1_H 1

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_NCURSES_H
#include <ncurses.h>
#else
#ifdef HAVE_CURSES_H
#include <curses.h>
#else
#ifdef HAVE_NCURSES_NCURSES_H
#include <ncurses/ncurses.h>
#else
#ifdef HAVE_CURSES_CURSES_H
#include <curses/curses.h>
#else
#error "No se encuentra header curses/ncurses"
#endif
#endif
#endif
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef HAVE_TIME_H
#include <time.h>
#endif

#endif /* DORMIR_H */

Nótese que todo el contenido de este archivo cabecera se ha rodeado de directivas condicionales (relacionadas a EJEMPLO1_H) que se emplean para evitar su reprocesamiento en el caso de que el archivo fuera #incluido más de una vez; como de seguro ya sabe el lector, esto se debe hacer absolutamente con todo archivo cabecera se crea.

Gracias a éste, nuestro archivo de código fuente ejemplo1.c se reduce a:

#include "ejemplo1.h"

#define RETARDO 100000 /* 1E5 */

int main(int argc,char **argv)
{
int z;
#ifdef HAVE_NANOSLEEP
struct timespec req;
#endif

initscr();
for(z=10;z<70;z++)
        {
        mvaddch(10,z,'*');
        refresh();
#ifdef HAVE_NANOSLEEP
        req.tv_sec=0;
        req.tv_nsec=1000*RETARDO;
        nanosleep(&req,NULL);
#endif
#ifdef HAVE_USLEEP
        usleep(RETARDO);
#endif
        mvaddch(10,z,' ');
        }
mvaddstr(20,10,"Eso fue todo. Presiona una tecla para continuar.");
getch();
endwin();
return 0;
}

Asimismo, automake requiere saber que el programa ahora depende también de un archivo cabecera. Así queda el nuevo Makefile.am:

bin_PROGRAMS = ejemplo1
ejemplo1_SOURCES = ejemplo1.c ejemplo1.h
#ejemplo1_LDADD = -lncurses

Aislar el código fuente no portable

Con motivo de soportar simultáneamente las funciones nanosleep() y usleep(), la función main() creció en diez líneas. Si nuestro programa requiriese a futuro invocar en otros puntos a nanosleep()/usleep(), el código se tornará rápidamente engorroso y difícil de readaptar a nuevos cambios.

A tal efecto es muy recomendable aislar este código no portable en una función aparte, y de preferencia en un archivo aparte. Por ejemplo, definiremos una nueva función llamada dormir() que reciba un entero long correspondiente a los "nanosegundos" que se pretende retardar [16]. Asimismo, crearemos el archivo domir.c con el siguiente contenido:

#include "ejemplo1.h"

void dormir(long retardo)
{
#ifdef HAVE_NANOSLEEP
struct timespec req;
req.tv_sec=0;
req.tv_nsec=retardo;
nanosleep(&req,NULL);
#endif

#ifdef HAVE_USLEEP
usleep(retardo/1000);
#endif
}

Agregaremos la declaración correspondiente al interior de ejemplo1.h:

void dormir(long);

Y gracias a esto, el ejemplo1.c se reduce a:

#include "ejemplo1.h"

#define RETARDO 100000 /* 1E5 */

int main(int argc,char **argv)
{
int z;

initscr();
for(z=10;z<70;z++)
        {
        mvaddch(10,z,'*');
        refresh();
        dormir(RETARDO*1000L);
        mvaddch(10,z,' ');
        }
mvaddstr(20,10,"Eso fue todo. Presiona una tecla para continuar.");
getch();
endwin();
return 0;
}

Y como era de esperarse, Makefile.am debe incluir la nueva fuente:

bin_PROGRAMS = ejemplo1
ejemplo1_SOURCES = ejemplo1.c dormir.c ejemplo1.h

A partir de este momento, los programadores deben estar avisados de emplear exclusivamente "dormir()" en donde utilizarían nanosleep() o usleep().

Imaginemos que en un futuro lejano los nuevos estándares promueven una nueva manera de generar retardos (por ejemplo, una función llamada picosleep()), entonces nuestro programa sólo requeriría la modificación de la función dormir(); el resto permanecería incólume.

A modo de ejercicio para el lector se solicita adaptar este proyecto para que soporte ciertos sistemas derivados de Xenix (muy antiguos) que no cuentan ni con usleep() ni con nanosleep(), y por el contrario utilizan una llamada al sistema llamada nap(), la cual recibe un long que representa el retardo en milisegundos.

Usar el shell para mejorar los tests

Si bien el archivo configure.ac es preprocesado por el macroprocesador M4, el resultado del mismo es ejecutado posteriormente por el shell. Esto significa que es posible introducir comandos de shell en este mismo archivo, teniendo cuidado de que no sean dañados durante la etapa de M4.

A fin de ilustrar esto, corregiremos una sutil deficiencia de nuestro script configure: si lo ejecutamos en un sistema que no cuenta con ninguna clase de header Curses/Ncurses, el script lo descubre pero sigue avanzando como si no ocurriese nada malo. A continuación se muestra la ejecución de configure en un sistema con estas características; nótese que configure sólo se detiene al descubrir que no puede hallar una librería Curses/Ncurses para enlazar:

systemx$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
...
checking for stdint.h... yes
checking for unistd.h... yes
checking ncurses.h usability... no
checking ncurses.h presence... no
checking for ncurses.h... no
checking curses.h usability... no
checking curses.h presence... no
checking for curses.h... no
checking ncurses/ncurses.h usability... no
checking ncurses/ncurses.h presence... no
checking for ncurses/ncurses.h... no
checking curses/curses.h usability... no
checking curses/curses.h presence... no
checking for curses/curses.h... no
checking for unistd.h... (cached) yes
checking time.h usability... yes
checking time.h presence... yes
checking for time.h... yes
checking for nanosleep... yes
checking for library containing initscr... no
configure: error: No se encuentra la libreria Curses/Ncurses

Téngase presente que hay sistemas que sí tienen la librería (y pasarían el último test del enlace), pero aun así no disponen de los headers que necesitamos, los cuales se suelen instalar como "software opcional de desarrollo". En ese caso, el usuario instalador recién descubriría el problema en el momento de compilar (gracias a la directiva #error que colocamos en ejemplo1.h):

$ ./configure
...
$ make
gcc -DHAVE_CONFIG_H -I. -I. -I.     -g -O2 -c ejemplo1.c
In file included from ejemplo1.c:1:
ejemplo1.h:20:2: #error "No se encuentra header curses/ncurses"
make: *** [ejemplo1.o] Error 1

Esto ocurre debido a que la macro AC_CHECK_HEADERS no tiene la capacidad de detener la ejecución del script cuando no se encuentra ninguno de los headers especificados [17]. Sin embargo, con una pequeña modificación esto se puede conseguir:

curses_headers=no
AC_CHECK_HEADERS([ncurses.h curses.h ncurses/ncurses.h curses/curses.h],
                [curses_headers=si ; break])
if test $curses_headers = no; then
AC_MSG_ERROR([No encuentro el archivo ncurses.h ni curses.h.
Por favor, instale la libreria Curses/Ncurses.])
fi

Como indicamos, aquellas líneas de configure.ac que no corresponden a macros M4 serán pasadas casi intactas al shell. En nuestro caso, hemos definido una variable de shell llamada curses-headers (el nombre es irrelevante) inicialmente igual a "no". Si la macro AC-CHECK-HEADERS encuentra al menos uno de los headers requeridos, entonces esta variable se modifica a "si" justo antes del "break". Posteriormente hacemos un test de shell (if…then…fi) y generamos un error en el caso de que la variable hubiera permanecido en "no" al haber fallado todos los casos de AC-CHECK-HEADERS.

Con esto, un sistema que carece de los headers (incluso si tiene las librerías) obtiene el siguiente mensaje durante configure:

$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
...
checking for stdint.h... yes
checking for unistd.h... yes
checking ncurses.h usability... no
checking ncurses.h presence... no
checking for ncurses.h... no
checking curses.h usability... no
checking curses.h presence... no
checking for curses.h... no
checking ncurses/ncurses.h usability... no
checking ncurses/ncurses.h presence... no
checking for ncurses/ncurses.h... no
checking curses/curses.h usability... no
checking curses/curses.h presence... no
checking for curses/curses.h... no
configure: error: No encuentro el archivo ncurses.h ni curses.h.
Por favor, instale la librería Curses/Ncurses.

Ya que hemos empezado a emplear el shell, es de rigor hacer un comentario con respecto a su portabilidad.

El script configure es la pieza clave en el descubrimiento y adaptación del software a las plataformas extrañas y sólo requiere de un shell estándar tipo Bourne para su ejecución. Sin embargo, históricamente los divesos sistemas Unix han tenido diversas divergencias en la implementación de su shell Bourne (/bin/sh.) Esto significa que el script configure debe ser construido con sumo cuidado a fin de utilizar sólo características probadamente portables entre todas las implementaciones del shell Bourne conocidas.

Como hemos visto, autoconf es la herramienta encargada de crear a configure, y está diseñada para utilizar estrictamente aquellas características del shell que son ampliamente portables. Sin embargo, en cualquier proyecto no trivial es menester que el desarrollador (nosotros) escriba algunas líneas complementarias utilizando el lenguaje del shell, tal como hicimos en la última versión de configure.ac. Como quiera que estas líneas serán introducidas en configure, debemos asegurarnos de que no dañen su portabilidad.

Esto significa que todo aquello que escribamos con el lenguaje del shell en el archivo configure.ac debe emplear una sintaxis ampliamente portable, lo cual no es tarea trivial.

Afortunadamente, los desarrolladores del GNU Build System ya han documentado con cierta amplitud este asunto, para lo cual remito al lector a consultar la documentación oficial de autoconf (sección "Portable Shell Programming".)

Asimismo, el hecho de que el script sea preprocesado por M4, obliga a evitar el uso de los caracteres que éste considera especiales, tales como los de "entrecomillado" ([ y ].) Por tal motivo, en vez de utilizar

if [ $curses_headers = no ]; then

hemos optado por el comando equivalente "test".

6. Librerías y Desarrollo de Aplicaciones Medianas

Mediante los ejemplos de esta sección, explicaremos diversos aspectos del GNU Build System que facilitan la elaboración de aplicaciones de mediana y gran envergadura. No se desanime el lector si encuentra confuso el asunto de las bases de datos, pues aquí tan solo sirve como pretexto para desarrollar una librería con fines didácticos.

6.1. Librería SAL (acceso a DBMS), 1era Versión

Explicación de los requerimientos

El acceso a los sistemas de base de datos (DBMS) desde las aplicaciones constituye un requisito muy frecuente en la mayoría de ambientes comerciales. Existen muchas maneras de efectuar esto dependiendo de la plataforma, del lenguaje empleado, y de la base de datos instalada. En particular, nosotros intentaremos crear una aplicación en lenguaje C, independiente de la plataforma, y lo que es más importante, independiente del DBMS en uso.

Los creadores de productos DBMS normalmente proporcionan una interfaz de acceso (una librería) para el lenguaje C, y con frecuencia para otros lenguajes. Estas librerías son usadas por los programadores para conectarse e interactuar con la DBMS.

Lamentablemente esto "amarra" a los programas a una DBMS específica (pues cada una tiene su propia interfaz de programación o API), lo que hace que la migración a una nueva DBMS sea una tarea muy ardua. En otras palabras, los programas que hacen uso del API del creador de una base de datos, son poco portables a otras bases de datos.

Debido a esto, existen diversas librerías que "aislan" a las aplicaciones de las API’s específicas de cada DBMS, incrementando su portabilidad.

En esta sección, a modo de ejercicio, desarrollaremos una pequeña y muy incompleta librería para acceder a distintas bases de datos SQL, así como un programa (también muy pequeño) que hace uso de la misma, consiguiendo en efecto ser totalmente portable a cualquier base de datos [18]. Nuestra librería será denominada "SAL" (SQL Abstraction Layer.)

Asumiremos también que nuestra aplicación consta de un número significativo de archivos (cosa que no es cierta) por lo que crearemos una estructura de directorios más acorde un un paquete grande.

Por último, nuestro paquete proporcionará algunos archivos de documentación técnica, cuyo contenido aquí no es relevante.

Creación de la Estructura de Directorios

A partir de un nuevo directorio en blanco, crearemos la siguiente estructura:

--+
  +--src--+
  |       +--lib
  |       +--exe
  +--doc

Téngase presente que el GNU Build System NO obliga a ninguna estructura de directorios en particular; sin embargo, existen diversas "costumbres" que es conveniente tener en consideración a fin de no desconcertar a los usuarios de nuestro paquete.

El directorio doc/ contendrá la documentación técnica del paquete. Sin embargo, de las secciones anteriores sabemos que automake crea automáticamente ciertos archivos de texto (y obliga a crear otros.) Todos estos archivos corresponden a información general y legal acerca del paquete, pero no corresponden a la documentación técnica. Los archivos informativos/legales siempre deben permanecer en el directorio inicial (o raíz) del paquete, que es desde donde se ejecuta automake, en tanto que la documentación técnica se suele almacenar en su propio directorio.

Todo el código fuente ahora se ubicará bajo src/, el cual a su vez contendrá los subdirectorios: lib/ para contener el código fuente de las librerías generadas con nuestro paquete, y exe/ para contener el código fuente de los ejecutables que genera nuestro paquete.

$ mkdir doc
$ mkdir src
$ mkdir src/lib
$ mkdir src/exe

Una primera versión de configure.ac

AC_INIT([SAL Demo],[1.03],[diegobravo@x.com])
AM_INIT_AUTOMAKE([sal],[1.03])
AM_CONFIG_HEADER(config.h)
AC_PROG_CPP
AC_PROG_CC
AC_PROG_RANLIB
AC_PROG_MAKE_SET
AC_CHECK_HEADERS([mysql.h mysql/mysql.h])
AC_SEARCH_LIBS([mysql_real_connect],[mysqlclient],,
        AC_MSG_ERROR([No se puede enlazar con libmysqlclient]))
AC_CONFIG_FILES([Makefile
                src/Makefile
                src/lib/Makefile
                src/exe/Makefile])
AC_OUTPUT

La directiva AC_PROG_RANLIB debe agregarse a configure.ac siempre que el proyecto va a generar alguna librería (como es nuestro caso [19].) En segundo lugar, cada vez que se crean estructuras de directorios en los que el software será construído, es conveniente emplear la directiva AC_PROG_MAKE_SET a fin de resolver ciertos problemas inherentes a ciertas versiones extrañas de make. Asimismo, en AC_CONFIG_FILES es menester especificar todos los Makefiles que se van a generar. Estos Makefiles se invocarán recursivamente cuando el usuario instalador ejecute make desde el directorio principal.

Por último, la experiencia indica que los archivos cabecera de MySQL pueden ubicarse en la ruta estándar o bajo su propio subdirectorio, por lo que usamos AC_CHECK_HEADERS (análogo a lo que se hizo anteriormente para Curses/Ncurses); igualmente, verificamos la disponibilidad de la librería de MySQL probando la "enlazabilidad" de la función (elegida al azar en el API) mysql_real_connect() mediante nuestra conocida AC_SEARCH_LIBS.

Finalmente, recordemos que sólo es necesario un único archivo configure.ac para todo el proyecto, el cual siempre estará ubicado en el directorio principal del mismo.

Archivo cabecera de la librería libsal

El archivo cabecera (sal.h) se ubica en src/lib/:

#ifndef SAL_H
#define SAL_H

struct SQL_RSET_STRUCT {
        void *resultSet;
        int index;
        char **data;
        int nfields;
        };
typedef struct SQL_RSET_STRUCT* SQL_RSET;
typedef char **SQL_ROW;

// SAL Public API
int SQL_connect(const char *dbhost,
                const char *dbuser,
                const char *dbpassword,
                const char *dbname);
SQL_RSET SQL_query(const char *queryText);
int SQL_free(SQL_RSET resultSet);
SQL_ROW SQL_fetch_row(SQL_RSET resultSet);
#endif

En las últimas líneas de este archivo se encuentran las cuatro funciones de que consta nuestra librería las cuales se emplean, respectivamente, para conectar con la DBMS, ejecutar un query, liberar la memoria asociada a un "cursor/result set", e iterar obteniendo filas a través de un "cursor/result set".

Quizá el lector tenga curiosidad por comprender los tipos de datos declarados en este archivo (aunque no afectan a nuestra exposición):

El tipo SQL_ROW corresponde a una fila retornada por la base de datos. Asumiremos que estas filas están siempre constituidas de arreglos de cadenas de caracteres "char *". De este modo, si más adelante se definiera:

SQL_ROW fila;
... /* obtener la fila de la DBMS */

Los campos retornados se podrían apreciar mediante:

printf("%s %s %s...\n",fila[0],fila[1],fila[2]);

La estructura SQL_RSET_STRUCT se emplea como "contenedora" de un "result set" (el resultado de un query.) Dependiendo de la DBMS y las opciones empleadas, esto a veces corresponde a un área de memoria con toda la información generada por la consulta, o en otras ocasiones, a un "cursor" que sirve para traer fila a fila la información desde el servidor.

Con el fin de hacerla más general, nuestra librería asume que el "result set" tiene el comportamiento de un "cursor" y proporciona funciones para iterar sobre el mismo. En las bases de datos que retornan toda la información en un solo paso, nuestro "result set" simula un comportamiento de "cursor" entregando fila a fila la información a la aplicación [20].

En cualquier caso, el "cursor" o "result set" real que devuelve la DBMS, se referencia internamente mediante el puntero resultSet que tiene tipo "desconocido" void* puesto que la interfaz debe de ser independiente de la DBMS en uso.

Código fuente de la librería libsal

El código que se presenta a continuación (sal.c) implementa la interfaz de programación definida en sal.h. En esta primera versión el código fuente está asociado a la DBMS MySQL:

#if HAVE_CONFIG_H
#include "config.h"
#endif

#include "sal.h"
#include <stdio.h>
#include <stdlib.h>

#if HAVE_MYSQL_H
#include <mysql.h>
#else
#if HAVE_MYSQL_MYSQL_H
#include <mysql/mysql.h>
#else
#error "No se encuentra header mysql.h"
#endif
#endif

// Flag estado conexion
static int connected=0;

// Result set comodin para query's sin filas que retornar
static SQL_RSET rsetComodin=NULL;

static MYSQL nhp_sal_conn;
extern MYSQL my;

int SQL_connect(const char *nhp_dbhost,
                const char *nhp_dbuser,
                const char *nhp_dbpassword,
                const char *nhp_dbname)
{
// No se puede reconectar
if(connected==1)
        return 0;

// Inicializar (por unica vez) el result set comodin
if(rsetComodin==NULL)
        rsetComodin=(SQL_RSET)malloc(sizeof(struct SQL_RSET_STRUCT));

// Inicializar libreria mysqlclient
mysql_init(&nhp_sal_conn);

// Conectar a servidor
if(mysql_real_connect(&nhp_sal_conn,nhp_dbhost,
        nhp_dbuser,nhp_dbpassword,nhp_dbname,0,NULL,0))
        {
        connected=1;
        return 1;
        }
connected=0;
return 0;
}

SQL_RSET SQL_query(const char *query)
{
if(!connected)
        return NULL;
if(mysql_query(&nhp_sal_conn,query)==0)
        {
        MYSQL_RES *mysqlResultSet;
        if((mysqlResultSet=mysql_store_result(&nhp_sal_conn)))
                {
                SQL_RSET res=
                        (SQL_RSET)malloc(sizeof(struct SQL_RSET_STRUCT));
                res->resultSet=mysqlResultSet;
                res->index=0;
                res->nfields=mysql_num_fields(mysqlResultSet);
                res->data=(char **)malloc(sizeof(char *)*res->nfields);
                return res;
                }
        else
                return rsetComodin;
        }
else
        return NULL;
}

int SQL_free(SQL_RSET res)
{
if(res==NULL)
        return 1;
if(res==rsetComodin)
        return 1;
if((MYSQL_RES*)res->resultSet == NULL)
        return 0;
mysql_free_result((MYSQL_RES*)res->resultSet);
free(res->data);
free(res);
return 1;
}

SQL_ROW SQL_fetch_row(SQL_RSET rs)
{
if(!rs || rs==rsetComodin)
        return NULL;
SQL_ROW r=mysql_fetch_row((MYSQL_RES*)rs->resultSet);
if(r==NULL)
        return NULL;
int z;
for(z=0;z<rs->nfields;z++)
        rs->data[z]=r[z];
return rs->data;
}

La explicación de este archivo involucaría explicar el API de la librería de clientes de MySQL, cosa que está más allá de mi objetivo. El lector interesado puede consultar fácilmente la documentación respectiva en el .URL http://www.mysql.com/ Sitio Web de MySQL .

El único aspecto a tener claro aquí es que el código de este archivo es completamente específico para la base de datos MySQL y requiere una reescritura completa para acceder a otras bases de datos.

Código fuente de aplicación que usa Libsal

Este programa de ejemplo emplea la librería libsal y es totalmente independiente de la base de datos utilizada:

#if HAVE_CONFIG_H
#include "config.h"
#endif

#include "sal.h"
#include <stdio.h>

int main(int argc,char **argv)
{
char txt[1024];

if(argc!=5)
        {
        fprintf(stderr,"Especificar parametros de conexion:\n"
                "DBMS-host usuario password DATABASE-NAME\n");
        return 1;
        }
fprintf(stderr,"Conectando...\n");
if(SQL_connect(argv[1],argv[2],argv[3],argv[4])==0)
        {
        printf("La conexion fallo\n");
        return 1;
        }
fprintf(stderr,"Conectado!\n");
while(printf("SQL> "),fflush(stdout),fgets(txt,1024,stdin))
        {
        SQL_RSET r=SQL_query(txt);
        SQL_ROW f;
        if(r==NULL)
                {
                printf("El query fallo\n");
                return 1;
                }
        while((f=SQL_fetch_row(r)))
                printf("Primer campo: %s\n",f[0]);
        SQL_free(r);
        }
return 0;
}

Nuestro programa consola.c tiene por finalidad recibir un texto del usuario desde la consola, el cual se asume como una sentencia SQL a ser lanzada al DBMS. A continuación el programa recoge todas las filas resultantes y muestra en la pantalla tan sólo la primera columna de cada una de éstas [21]. Para esto, invoca a SQL-query() con el texto proporcionado, obteniendo un "result set" en la variable r. Con este "result set" a modo de cursor, se trae todas las filas una a una (con SQL-fetch-row()) en la variable f. Finalmente, sólo imprimiremos el primer campo "f[0]".

Documentación

La escritura de documentación técnica de un proyecto de software está más allá del alcance del presente texto, sin embargo, haré algunos comentarios con respecto a las alternativas disponibles:

Texinfo

Los desarrolladores del GNU Build System promueven fervientemente el uso de su sistema de documentación Texinfo. (se pronuncia "tecinfo"). Este sistema tiene la ventaja de requerir un único archivo fuente de parte del documentador a partir del cual es posible generar diversos formatos tales como:

  • HTML, para publicar en sitios Web
  • PDF, para generar documentación impresa
  • INFO, para generar documentación on-line navegable con info
  • TEX, para generar en el formato TeX
  • TXT, texto simple
  • XML, para postproceso con otras aplicaciones

El archivo fuente se crea con cualquier editor de texto aunque como de costumbre, los desarrolladores de GNU recomiendan Emacs.

DocBook

Otra popular alternativa corresponde al sistema de documentación DocBook. Nuevamente, se parte de un único documento fuente creado con cualquier editor de texto, el cual permite generar prácticamente los mismos formatos que Texinfo. La diferencia esencial reside en que los archivos de DocBook ya están en formato estandarizado XML (y no en un formato particular como en Texinfo) lo que potencialmente les proporciona más posibilidades hacia el futuro. A mi modo de ver, tanto Texinfo como DocBook proporcionan alternativas viables para documentar proyectos de software; Texinfo es relativamente más maduro mientras que DocBook va más acorde con los tiempos modernos [22], [23].

TeX/LaTeX

En ambientes científicos es muy popular el sistema de documentación TeX/LaTeX, utilizado preferentemente debido a su gran capacidad de expresar la notación matemática y suelen estar disponibles en prácticamente cualquier plataforma. Lamentablemente este sistema está muy orientado a generar exclusivamente documentación impresa (y quizá sea la mejor opción si sólo estamos interesados en ésta.)

Troff

Asimismo, los sistemas Unix cuentan nativamente con el sistema de documentación Troff (usado entre otras cosas para confeccionar los manuales on-line accesibles mediante el comando man.) Este sistema es el más antiguo de los que mencionamos aquí y tiende a ser cada vez menos empleado en nuevos proyectos.

En adelante, asumiremos que los archivos de documentación técnica (en el formato que se elija) han sido depositados en el subdirectorio doc/, y se incluirán en el paquete sin modificaciones adicionales. No presento aquí el posible contenido de éstos por no ser relevantes para nuestra discusión.

Archivos de automake

Esta es la parte más novedosa del proyecto. Tenemos que recordar que para cada directorio donde se generará un Makefile (y que fueron especificados en configure.ac), debemos escribir un nuevo Makefile.am. Asimismo, automake sólo se debe ejecutar desde el directorio principal, nunca desde otro.

Veamos esto caso por caso:

Directorio principal o raíz del proyecto

En el directorio principal del proyecto no se va a "construir" nada, por tanto lo único que debemos informar a automake es que existen otros directorios a donde debe "descender". Así, nuestro primer Makefile.am (para el directorio principal) sólo consta de:

SUBDIRS = src
EXTRA_DIST = doc

La directiva SUBDIRS indica en qué directorios automake debe continuar su análisis (la búsqueda de nuevos Makefile.am's.) La directiva EXTRA_DIST se emplea para especificar qué archivos y directorios adicionales deben incluirse en el paquete (make dist), aunque éstos no participan del proceso de construcción.

No olvidemos los archivos imprescindibles de documentación para automake:

$ touch README AUTHORS NEWS ChangeLog

Recalco: automake sólo se deberá lanzar desde este directorio, y automáticamente descenderá a los demás (de acuerdo a la directiva SUBDIRS.) Por eso, antes de ejecutarlo, debemos preparar todos los Makefile.ams de 'cada directorio especificado. Nótese también que doc/ no requiere un Makefile.am puesto que automake no ingresará a éste; por el contrario, EXTRA_DIST le indica que sólo deberá empaquetarlo "tal como es", en la distribución.

Directorio src/

Este directorio también es muy simple puesto que tampoco se construye nada aquí. Lo único que debe hacer automake es seguir descendiendo a los subdirectorios lib/ y exe/.

Es importante especificar a lib antes que a exe debido a que normalmente las librerías son prerequisitos para la creación de los ejecutables (como ocurre en nuestro caso.) De este modo su Makefile.am será:

SUBDIRS = lib exe
Directorio lib/

Todas las librerías a construirse se especifican con la directiva lib_LIBRARIES, (análoga a nuestra conocida bin_PROGRAMS.) Luego, para cada librería se especifican los archivos fuente que la constituyen mediante la sintaxis "nombre-librería_a_SOURCES":

lib_LIBRARIES = libsal.a
libsal_a_SOURCES = sal.c sal.h
Directorio exe/
INCLUDES = -I$(top_srcdir)/src/lib
bin_PROGRAMS = consola
consola_SOURCES = consola.c
consola_LDADD = -lsal
consola_LDFLAGS = -L../lib

Nuestro programa hace uso del archivo cabecera "sal.h" el cual es parte de la librería libsal y se ubica en el directorio src/lib. Debido a que es un directorio distinto al actual, debemos informar al compilador dónde debe buscar el archivo cabecera y con este fin utilizamos la variable INCLUDES. A fin de especificar dicho directorio hemos empleado la variable $(top_srcdir) que siempre contiene la ruta del directorio raíz de las fuentes del proyecto. Parece posible utilizar en su lugar algo más simple como "../lib", sin embargo, esto NO funciona si efectuamos la "construcción" del software en un directorio distinto al de las fuentes, dado que en el "directorio de construcción" las fuentes (como ../lib/sal.h) no están presentes.

Muy anteriormente se explicó la directiva LDADD. Recapitulando, ésta permite especificar qué librerías requiere la el programa que se está construyendo. En nuestro caso, aparte de nuestra libsal.a (especificada con -lsal), también se requiere de la librería de clientes de MySQL; sin embargo, ésta ya no se debe especificar puesto que será automáticamente añadida gracias al test AC_SEARCH_LIBS de configure.ac anteriormente explicado.

Finalmente, la directiva consola_LDFLAGS se usa para especificar opciones adicionales para el momento del enlace de este programa. En particular, se usa para especificar (mediante la opción -L del compilador [24]) las rutas no estándares en las cuales se buscarán las librerías solicitadas. Dado que enlazaremos con nuestra librería libsal.a, la cual se ubica en uno de nuestros directorios (es decir, una ruta no estándar), tenemos que especificar dicha ruta (../lib.) Nótese que aquí NO debemos emplear la variable $(top_srcdir) como hicieramos antes, puesto que la librería mencionada se crea como parte del proceso de construcción y por tanto se ubica en el "directorio de construcción", mas no en el "directorio de las fuentes". Quizá esto se comprenda mejor mostrando una forma alternativa para consola_LDFLAGS que también es correcta:

consola_LDFLAGS = -L$(top_builddir)/src/lib

En esta versión se hace uso de la variable $(top_builddir) que hace referencia al "directorio de construcción".

Directorio doc/

Como se explicó anteriormente, este directorio no requiere Makefile.am puesto que no construiremos nada.

Construcción de la aplicación

Teniendo todo esto listo, generamos los archivos de construcción desde el directorio principal del proyecto (no hay necesidad de descender a ningún otro):

$ aclocal ; autoheader ; automake -a ; autoconf

Como se comentó, siempre es preferible (mas no imprescindible) "configurar" y "construir" desde otro directorio totalmente ajeno al de nuestras fuentes:

$ pwd
/home/diego/escritos/gnutools/ejemplo2/sal1
$ cd ..
$ mkdir build
$ cd build
$ ../sal1/configure
...
$ make

Pruebas con la aplicación

El lector comprederá que este no es el lugar adecuado para explicar cómo instalar MySQL y configurar una base de datos (lo cual para esta DBMS es increíblemente sencillo.) Asumiremos por tanto que se dispone de una base de datos llamada "testsal", accesible vía "localhost" con el usuario "diego" y el password "password". El ejemplo que se muestra a continuación asume también la existencia de una tabla llamada tabla1:

$ cd src/exe/
$ ./consola localhost diego password testsal
Conectando...
Conectado!
SQL> select * from tabla1
Primer campo: 10
Primer campo: 11
Primer campo: 12
SQL> desc tabla1
Primer campo: codigo
Primer campo: descripcion
SQL> insert into tabla1 (codigo,descripcion) values (999,'xxx')
SQL> select * from tabla1
Primer campo: 10
Primer campo: 11
Primer campo: 12
Primer campo: 999
SQL>
$

6.2. Librería SAL, 2da Versión

En la sección anterior culminamos con una aplicación que se conectaba a una base de datos sin necesidad de saber cuál era dicha base de datos; sin embargo, la librería desarrollada para tal fin (libsal) sólo tiene capacidad de conectarse a MySQL.

En la presente sección realizaremos los añadidos necesarios para que nuestra librería soporte también la base de datos PostgreSQL, y por ende, también lo soportará nuestro micro programa consola.c.

Especificando opciones para el paquete

El usuario instalador de nuestro paquete deberá elegir qué tipo de base de datos emplear, lo cual ocasionará que la librería libsal compile diferente código fuente.

A fin de que el usuario instalador especifique este tipo de opciones, el script configure proporciona un mecanismo estándar mediante las opciones --enable-FEATURE y --disable-FEATURE, las cuales sirven, respectivamente, para activar y desactivar ciertas opciones en el paquete que se va a compilar.

En nuestro caso, obligaremos a que el usuario instalador especifique una de las opciones --enable-mysql o --enable-postgresql para indicar con qué clase de base de datos se conectará la librería libsal. Para tal fin, presentamos una nueva versión de configure.ac que hace uso de la macro AC_ARG_ENABLE. Esta macro debe ser empleada con cada "opción" o "FEATURE" que el usuario puede especificar a configure. Su primer argumento corresponde al texto de la "opción" (que debe ser una palabra simple), seguida de un texto de ayuda el cual aparece cuando el usuario instalador utiliza la opción --help de configure.

Es recomendable (pero no obligatorio) crear este texto de ayuda mediante la macro AC_HELP_STRING, la cual recibe a su vez dos argumentos correspondientes a dos textos que aparecerán adecuadamente formateados para la salida de configure --help: el primero debería contener la opción tal como el usuario instalador debe escribirla, y el segundo, una explicación de la misma.

El tercer argumento (opcional) de AC_ARG_ENABLE corresponde una acción a ser ejecutada cuando el usuario selecciona la opción o FEATURE. En nuestro caso, hemos invocado a AC_DEFINE a fin de definir una nueva macro a ser incluida en config.h, la cual permite seleccionar el código correspondiente a cada base de datos que debe ser compilado (ver código fuente en sal.c.)

Asimismo, para cada opción o "FEATURE" que el usuario seleccione al ejecutar configure, AC_ARG_ENABLE definirá una variable de shell de nombre "enable+FEATURE" con valor yes, la cual se emplea más adelante para determinar qué funcionalidad debe analizarse en la plataforma. Para nuestro caso, las variables correspondientes serán enable_mysql y enable_postgresql; y mediante comandos de Shell obligaremos al usuario instalador a definir exactamente una de ellas, es decir, a seleccionar exactamente una base de datos.

A continuación la nueva versión de configure.ac:

#
# Libreria libsal, y aplicacion independiente de DBMS
#
AC_INIT([SAL Demo],[1.03],[diegobravo@x.com])
AM_INIT_AUTOMAKE([sal],[1.03])
AM_CONFIG_HEADER(config.h)

# Tests comunes
AC_PROG_CPP
AC_PROG_CC
AC_PROG_RANLIB
AC_PROG_MAKE_SET

# Verificacion de opciones
AC_ARG_ENABLE([mysql],
        AC_HELP_STRING([--enable-mysql],
                [Para conectar a MySQL]),
        AC_DEFINE([DBMS_MYSQL],[1],[Configurada DBMS MySQL]))
AC_ARG_ENABLE([postgresql],
        AC_HELP_STRING([--enable-postgresql],
                [Para conectar a PostgreSQL]),
        AC_DEFINE([DBMS_POSTGRESQL],[1],[Configurada DBMS Postgresql]))

# Verificar que no haya mas de una base de datos seleccionada
if test "x$enable_mysql" = "xyes" &&
        test "x$enable_postgresql" = "xyes"; then
AC_MSG_ERROR([Solo es posible usar una Base de datos a la vez])
fi

# Verificar que haya al menos una base de datos seleccionada
if test "x$enable_mysql" != "xyes" &&
        test "x$enable_postgresql" != "xyes"; then
AC_MSG_ERROR([Debe seleccionar una Base de datos con
--enable-mysql o --enable-postgresql])
fi

# Tests para MySQL
if test "x$enable_mysql" = "xyes"; then
AC_CHECK_HEADERS([mysql.h mysql/mysql.h])
AC_SEARCH_LIBS([mysql_real_connect],[mysqlclient],,
        AC_MSG_ERROR([No se puede enlazar con libmysqlclient]))
fi

# Tests para PostgreSQL
if test "x$enable_postgresql" = "xyes"; then
AC_CHECK_HEADERS([libpq-fe.h postgresql/libpq-fe.h])
AC_SEARCH_LIBS([PQconnectdb],[pq],,
        AC_MSG_ERROR([No se puede enlazar con libpq]))
fi

# Generacion de la salida
AC_CONFIG_FILES([Makefile
                src/Makefile
                src/lib/Makefile
                src/exe/Makefile])
AC_OUTPUT

La nueva librería

En esta nueva versión el código fuente de sal.c contiene código condicional tanto para MySQL como para PostgreSQL (nótese que sal.h no se altera):

#if HAVE_CONFIG_H
#include "config.h"
#endif

#include "sal.h"
#include <stdio.h>
#include <stdlib.h>

#if DBMS_MYSQL
#if HAVE_MYSQL_H
#include <mysql.h>
#else
#if HAVE_MYSQL_MYSQL_H
#include <mysql/mysql.h>
#else
#error "No se encuentra header mysql.h"
#endif
#endif
#endif

#if DBMS_POSTGRESQL
#if HAVE_LIBPQ_FE_H
#include <libpq-fe.h>
#else
#if HAVE_POSTGRESQL_LIBPQ_FE_H
#include <postgresql/libpq-fe.h>
#else
#error "No se encuentra header PostgreSQL libpq-fe.h"
#endif
#endif
#endif


// Flag estado conexion
static int connected=0;

// Result set comodin para query's sin filas que retornar
static SQL_RSET rsetComodin=NULL;

#if DBMS_MYSQL
static MYSQL nhp_sal_conn;
extern MYSQL my;
#endif
#if DBMS_POSTGRESQL
static PGconn *nhp_sal_conn;
#endif

int SQL_connect(const char *nhp_dbhost,
                const char *nhp_dbuser,
                const char *nhp_dbpassword,
                const char *nhp_dbname)
{
// No se puede reconectar
if(connected==1)
        return 0;

// Inicializar (por unica vez) el result set comodin
if(rsetComodin==NULL)
        rsetComodin=(SQL_RSET)malloc(sizeof(struct SQL_RSET_STRUCT));

#if DBMS_MYSQL
// Inicializar libreria mysqlclient
mysql_init(&nhp_sal_conn);

// Conectar a servidor
if(mysql_real_connect(&nhp_sal_conn,nhp_dbhost,
        nhp_dbuser,nhp_dbpassword,nhp_dbname,0,NULL,0))
        {
        connected=1;
        return 1;
        }
#endif
#if DBMS_POSTGRESQL
char cs[1024];
sprintf(cs,"host=%s dbname=%s user=%s password=%s",
        nhp_dbhost,nhp_dbname,nhp_dbuser,nhp_dbpassword);
nhp_sal_conn=PQconnectdb(cs);
if(nhp_sal_conn)
        {
        ConnStatusType cst=PQstatus(nhp_sal_conn);
        if(cst==CONNECTION_OK)
                {
                connected=1;
                return 1;
                }
        nhp_sal_conn=NULL;
        }
#endif
connected=0;
return 0;
}

SQL_RSET SQL_query(const char *query)
{
if(!connected)
        return NULL;
#if DBMS_MYSQL
if(mysql_query(&nhp_sal_conn,query)==0)
        {
        MYSQL_RES *mysqlResultSet;
        if((mysqlResultSet=mysql_store_result(&nhp_sal_conn)))
                {
                SQL_RSET res=
                        (SQL_RSET)malloc(sizeof(struct SQL_RSET_STRUCT));
                res->resultSet=mysqlResultSet;
                res->index=0;
                res->nfields=mysql_num_fields(mysqlResultSet);
                res->data=(char **)malloc(sizeof(char *)*res->nfields);
                return res;
                }
        else
                return rsetComodin;
        }
else
        return NULL;
#endif
#if DBMS_POSTGRESQL
PGresult *res=PQexec(nhp_sal_conn,query);
if(res==NULL)
        return NULL;

ExecStatusType est=PQresultStatus(res);
if(est==PGRES_TUPLES_OK)
        {
        SQL_RSET sres=(SQL_RSET)malloc(sizeof(struct SQL_RSET_STRUCT));
        sres->resultSet=res;
        sres->index=0;
        sres->nfields=PQnfields(res);
        sres->data=(char **)malloc(sizeof(char *)*sres->nfields);
        return sres;
        }

// "Result set" para un query que no retorna data, lo eliminamos
// y retornamos el comodin
if(est==PGRES_COMMAND_OK)
        {
        PQclear(res);
        return rsetComodin;
        }
return NULL;
#endif
}

int SQL_free(SQL_RSET res)
{
if(res==NULL)
        return 1;
if(res==rsetComodin)
        return 1;
#if DBMS_MYSQL
if((MYSQL_RES*)res->resultSet == NULL)
        return 0;
mysql_free_result((MYSQL_RES*)res->resultSet);
#endif
#if DBMS_POSTGRESQL
if((PGresult *)res->resultSet == NULL)
        return 0;
PQclear((PGresult *)res->resultSet);
#endif
free(res->data);
free(res);
return 1;
}

SQL_ROW SQL_fetch_row(SQL_RSET rs)
{
if(!rs || rs==rsetComodin)
        return NULL;
#if DBMS_MYSQL
SQL_ROW r=mysql_fetch_row((MYSQL_RES*)rs->resultSet);
if(r==NULL)
        return NULL;
int z;
for(z=0;z<rs->nfields;z++)
        rs->data[z]=r[z];
return rs->data;
#endif
#if DBMS_POSTGRESQL
PGresult *prs=(PGresult *)rs->resultSet;
if(rs->index==PQntuples(prs))
        return NULL;
int nfields=rs->nfields;
int z=0;
for(z=0;z<nfields;z++)
        rs->data[z]=PQgetvalue(prs,rs->index,z);
rs->index++;
return rs->data;
#endif
}

Nuevamente, entender el detalle de éste código involucra conocer el API de ambas bases de datos, cosa que está fuera de nuestros objetivos.

Pruebas con PostgreSQL

A continuación preparamos el script configure y lo ejecutamos (erróneamente) sin argumentos:

$ aclocal ; autoheader ; automake ; autoconf
$ cd .. ; mkdir build ; cd build
$ ../sal2/configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
...
checking for ranlib... ranlib
checking whether make sets $(MAKE)... (cached) yes
configure: error: Debe seleccionar una Base de datos con
--enable-mysql o --enable-postgresql
$

Especifiquemos esta vez la base de datos PostgreSQL:

$ ../sal2/configure --enable-postgresql
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
...
checking for ranlib... ranlib
checking whether make sets $(MAKE)... (cached) yes
checking libpq-fe.h usability... no
checking libpq-fe.h presence... no
checking for libpq-fe.h... no
checking postgresql/libpq-fe.h usability... yes
checking postgresql/libpq-fe.h presence... yes
checking for postgresql/libpq-fe.h... yes
checking for PQconnectdb in -lpq... yes
...
$ make
...
gcc -DHAVE_CONFIG_H -I. -I../../../sal2/src/lib \
        -I../..     -g -O2 -c ../../../sal2/src/lib/sal.c
rm -f libsal.a
ar cru libsal.a sal.o
ranlib libsal.a
...
Making all in exe
...
gcc -DHAVE_CONFIG_H -I. -I../../../sal2/src/exe \
        -I../.. -I../../../sal2/src/lib    -g -O2 \
        -c ../../../sal2/src/exe/consola.c
gcc  -g -O2  -o consola -L../lib consola.o \
        -lsal -lpq
...
$

Tras preparar un ambiente PostgreSQL idéntico al que se utilizó en MySQL, obtenemos:

$ src/exe/consola localhost diego password testsal
Conectando...
Conectado!
SQL> select * from tabla1
Primer campo: 10
Primer campo: 11
Primer campo: 12
SQL> insert into tabla1 values (666,'Lima Limon')
SQL> select * from tabla1
Primer campo: 10
Primer campo: 11
Primer campo: 12
Primer campo: 666
SQL> $

En rigor, el programa se debería utilizar sólo después de haberse instalado en forma definitiva:

$ su
Password:
# make install
...
/bin/sh ../../../sal2/mkinstalldirs /usr/local/lib
 /usr/bin/install -c -m 644 libsal.a /usr/local/lib/libsal.a
 ranlib /usr/local/lib/libsal.a
...
/bin/sh ../../../sal2/mkinstalldirs /usr/local/bin
  /usr/bin/install -c  consola /usr/local/bin/consola
...
# exit
$ /usr/local/bin/consola localhost diego password testsal
Conectando...
Conectado!
SQL>

Tal como se aprecia, los únicos archivos que se han instalado son /usr/local/bin/consola y /usr/local/lib/libsal.a; es decir, sólo se han instalado aquellos "objetivos" que se espeficó construir.

A modo de ejercicio para esta sección, se deja al lector investigar y corregir por qué lo siguiente…

$ ../sal2/configure --enable-mysql --disable-postgresql

…genera definiciones incorrectas en config.h:

/* Configurada DBMS MySQL */
#define DBMS_MYSQL 1

/* Configurada DBMS Postgresql */
#define DBMS_POSTGRESQL 1

6.3. Librería SAL, 3ra Versión

Ahora nuestro objetivo primordial será transformar a libsal.a en una "librería compartida" (shared library.) No discutiremos aquí las ventajas de las librerías compartidas, remitiendo al lector interesado a consultar el apéndice C en el que se explica con cierto detalle este asunto.

Un segundo aspecto que revisaremos corresponde a la instalación (por el usuario instalador) del archivo cabecera sal.h. Como se sabe, los programadores requieren éstos archivos cuando desarrollan programas referidos a las librerías correspondientes, como es el caso de nuestra libsal [25]. El archivo sal.h (como todo archivo cabecera instalado mediante el GNU Build System) se instalará por omisión en el directorio /usr/local/include.

Introducción a libtool

Asumiendo que Ud. ya conoce el concepto y la utilidad de las librerías compartidas (sino, vea el apéndice C), es menester indicar ahora algunos inconvenientes que acarrea su utilización:

  • Los comandos requeridos para construir las librerías compartidas difieren mucho entre distintas plataformas
  • Los paquetes deberían poder ser construidos y utilizados aún en sistemas que no soportan librerías compartidas
  • No es obvio con qué extensión o sufijo se deben instalar las librerías compartidas, lo que dificulta las reglas del Makefile
  • Las librerías compartidas requieren un esquema de versionamiento adecuado a fin de que sus actualizaciones no dejen inoperativas a las aplicaciones y éstas puedan ser utilizadas con aquéllas sin necesidad de recompilarlas
  • No es sencillo dar al usuario instalador la posibilidad de elegir entre la versión estática o compartida para que seleccione la que le es más conveniente

A fin de contrarrestar estos problemas, el GNU Build System proporciona la herramienta libtool, la cual se suele utilizar en conjunto con las anteriormente explicadas; de hecho, nosotros no la invocaremos directamente, sino que será automáticamente utilizada como parte del proceso normal de construcción [26].

Normalmente, libtool intentará utilizar librerías compartidas para todas las solicitudes de compilación y enlace; pero si la plataforma no soporta aquellas, (o el usuario lo requiere expresamente), empleará la versión estática.

Preparar configure.ac

Al emplearse libtool es necesario agregar la macro AC_PROG_LIBTOOL. Asimismo, AC_PROG_RANLIB se torna redundante:

#
# Libreria libsal, y aplicacion independiente de DBMS
#
AC_INIT([SAL Demo],[1.03],[diegobravo@x.com])
AM_INIT_AUTOMAKE([sal],[1.03])
AM_CONFIG_HEADER(config.h)

# Test libtool
AC_PROG_LIBTOOL

# Tests comunes
AC_PROG_CPP
AC_PROG_CC
#AC_PROG_RANLIB
AC_PROG_MAKE_SET

... El resto permanece igual

Nuevo archivo Makefile.am en src/lib

Aquí especificaremos la construcción de la librería utilizando libtool. Como se aprecia, las librerías creadas con Libtool se especifican con lib_LTLIBRARIES y tienen una pseudo-extensión "la". Para cada librería, como es de rigor, se deben especificar sus fuentes y finalmente, dado que pretendemos crear librerías compartidas debemos especificar la "versión" de la librería (que NO es igual a la de nuestro paquete) mediante LDFLAGS empleando la directiva de Libtool "-version-info" [27].

include_HEADERS = sal.h
lib_LTLIBRARIES = libsal.la
libsal_la_SOURCES = sal.c sal.h
libsal_la_LDFLAGS = -version-info 0:0:0

Asimismo, puesto que hemos decidido facilitar a otros desarrolladores el acceso a nuestra librería, se hace necesario instalar en un lugar público el archivo cabecera sal.h. Con este fin hemos agregado la directiva include_HEADERS que especifica qué archivos cabecera (del directorio actual) se deberán instalar (al ejecutar make install.) Como indicamos, los archivos cabecera se instalan por omisión en /usr/local/incude.

Resultados de la compilación e instalación

Como es usual, ejecutamos la secuencia aclocal ; autoheader ; automake ; autoconf y a continuación (de preferencia desde un nuevo directorio), el script configure eligiendo cualquiera de las bases de datos como se hizo anteriormente. Finalmente, se ejecuta make.

Veamos los archivos generados en lib/:

$ ls src/lib/
Makefile  libsal.la  sal.lo  sal.o

Como se aprecia, libtool genera la "librería" con extensión ".la" así como los archivos "objeto" sal.o y sal.lo, el último de los cuales corresponde a una versión de sal.o compilada con la opción de "código independiente de la posición" usada especialmente para librerías compartidas. Si se revisa el archivo libsal.la, se comprobará que en realidad se trata de un archivo de texto con información relevante para libtool, el cual hace referencia a los verdaderos archivos de librería que están bajo el directorio oculto .libs. En este último se aprecia que libtool ha generado tanto la versión estática (libsal.a) como la dinámica de libsal (libsal.so.0.0.0):

$ ls src/lib/.libs/
libsal.a     libsal.la        libsal.lai  libsal.so
libsal.so.0  libsal.so.0.0.0  sal.o

La intención es que todo esto permanezca oculto y que el desarrollador sólo tome en cuenta a "libsal.la".

Pasemos ahora a verificar el ejecutable:

$ src/exe/consola localhost diego password testsal
Conectando...
Conectado!
SQL>

Todo luce normal. Sin embargo:

$ file src/exe/consola
src/exe/consola: Bourne shell script text executable

Nuevamente, libtool está "ocultando" al verdadero ejecutable (que esta vez está bajo src/exe/.libs):

$ ls src/exe/.libs/
consola
$ file src/exe/.libs/consola
src/exe/.libs/consola: ELF 32-bit LSB executable, Intel
80386, version 1 (SYSV), for GNU/Linux 2.2.0, dynamically
linked (uses shared libs), not stripped
$ ldd src/exe/.libs/consola
                libsal.so.0 => not found
        libpq.so.3 => /usr/lib/libpq.so.3 (0x40029000)
        libc.so.6 => /lib/tls/libc.so.6 (0x40049000)
        ...

El lector observador habrá descubierto que el verdadero ejecutable no puede en realidad funcionar:

$ src/exe/.libs/consola localhost diego password testsal
src/exe/.libs/consola: error while loading shared
libraries: libsal.so.0: cannot open shared object
file: No such file or directory

El motivo es evidentemente que el ejecutable requiere enlazar con libsal.so.0 (vea la salida de ldd más arriba) y ésta no puede ser hallada debido a que no se encuentra en ningún directorio estándar, ni se ha especificado su ubicación. Precisamente esto es lo que resuelve el script "ficticio" src/exe/consola, creado por libtool.

Pasemos a la instalación:

$ su
Password:
# make install
...
/bin/sh ../../../sal3/mkinstalldirs /usr/local/lib
/bin/sh ../../libtool  --mode=install /usr/bin/install \
        -c libsal.la /usr/local/lib/libsal.la
/usr/bin/install -c .libs/libsal.so.0.0.0 /usr/local/lib/libsal.so.0.0.0
(cd /usr/local/lib && rm -f libsal.so.0 && ln -s libsal.so.0.0.0 \
        libsal.so.0)
(cd /usr/local/lib && rm -f libsal.so && ln -s libsal.so.0.0.0 libsal.so)
/usr/bin/install -c .libs/libsal.lai /usr/local/lib/libsal.la
/usr/bin/install -c .libs/libsal.a /usr/local/lib/libsal.a
ranlib /usr/local/lib/libsal.a
chmod 644 /usr/local/lib/libsal.a
PATH="$PATH:/sbin" ldconfig -n /usr/local/lib

...
/bin/sh ../../../sal4/mkinstalldirs /usr/local/include
/usr/bin/install -c -m 644 ../../../sal4/src/lib/sal.h \
        /usr/local/include/sal.h
...
 /bin/sh ../../libtool  --mode=install /usr/bin/install \
        -c  consola /usr/local/bin/consola
/usr/bin/install -c .libs/consola /usr/local/bin/consola
...
# exit

Como de costumbre, las librerías se instalan por omisión en /usr/local/lib y el ejecutable se instala como /usr/local/bin/consola; nótese que ya no se crean directorios ocultos auxiliares como ocurría en el "directorio de construcción". Si ahora ejecutamos el programa, obtenemos el mismo error que explicamos más arriba.

$ /usr/local/bin/consola localhost diego password testsal
/usr/local/bin/consola: error while loading shared
libraries: libsal.so.0: cannot open shared object
file: No such file or directory

Téngase en cuenta que este problema no tiene ninguna relación con Libtool, sino exclusivamente con la configuración de las rutas de las librerías compartidas.

Este problema se puede corregir de diversas maneras. En muchas plataformas, la más sencilla consiste en emplear la variable de entorno LD_LIBRARY_PATH, que instruye al enlazador de tiempo de ejecución a buscar las librerías en rutas adicionales:

$ LD_LIBRARY_PATH=/usr/local/lib /usr/local/bin/consola \
        localhost diego password testsal
Conectando...
Conectado!
SQL>

Una segunda solución consiste en instalar la librería en un directorio estándar de librerías del sistema operativo, para lo cual sólo se requiere repetir el proceso agregando lo siguiente:

../sal3/configure --enable-postgresql --libdir=/usr/lib

Este comando asume que el directorio /usr/lib es uno de los directorios estándares de librerías compartidas.

Una tercera solución corresponde a agregar nuestro directorio problemático /usr/local/lib a la lista de librerías de búsqueda del sistema operativo [28]; sin embargo, esta solución me parece demasiado intrusiva y muy poco portable.

Una última solución que presentamos consiste en generar un ejecutable preparado para buscar siempre en /usr/local/lib aquellas librerías que le faltan. Esto se consigue con diversas opciones del enlazador que varían en distintas plataformas; sin embargo, libtool, nos proporciona una sintaxis estándar para lo cual sólo tendremos que modificar el archivo src/exe/Makefile.am agregando la opción -rpath en las opciones de enlace:

INCLUDES = -I$(top_srcdir)/src/lib
bin_PROGRAMS = consola
consola_SOURCES = consola.c
# Libreria creada con libtool
consola_LDADD = -lsal
consola_LDFLAGS = -L../lib -rpath $(libdir)

6.4. Conclusión

Aquí culmina nuestra exposición del GNU Build System, la cual está muy lejos de ser completa. Mediante estos ejemplos espero haber podido demostrar la gran utilidad que tienen dichas herramientas para el desarrollo de proyectos de mediana y gran envergadura que pretenden ser portables. Como indiqué anteriormente, es imprescindible que el lector consulte las guías oficiales de las herramientas mencionadas a fin de complementar esta introducción.

7. Introducción a make

7.1. Programas de varios archivos de código fuente

En los programas grandes es frecuente separar el código en diversos archivos para que todo sea más manejable. El siguiente archivo (modulo1.c) efectua un cálculo matemático a partir de dos números enteros:

/*
 * modulo1.c
 */
int mcd(int a,int b)
{
int r;
while(b)
        {
        r=a%b;
        a=b;
        b=r;
        }
return a;
}

Espero que el lector haya descubierto que esta función simplemente calcula el máximo común divisor con el algoritmo de Euclides. Ahora viene la segunda parte (modulo2.c):

/*
 * modulo2.c
 */
#include <stdio.h>

int mcd(int,int);

int main(void)
{
int x=510, y=340;

printf("MCD de %d y %d es %d\n",x,y,mcd(x,y));
return 0;
}

Método 1

Para compilar todo en un solo paso (sin generar "archivos objeto"), podríamos hacer lo siguiente:

$ gcc -o mcd modulo1.c modulo2.c

Esto compila ambos archivos fuente y genera un único ejecutable (que he llamado mcd.)

Método 2

La otra manera de compilar esto (y que es más importante para lo que veremos) es hacerlo todo paso a paso:

$ gcc -c modulo1.c
$ gcc -c modulo2.c
$ gcc -o mcd modulo1.o modulo2.o

La diferencia es más trascendente de lo que puede parecer en principio. Obsérvese que las dos primeras operaciones son "compilaciones" y la última sólo es un "enlace". ¿Qué ocurriría si hacemos una modificación en - por ejemplo - modulo1.c? Si usamos el primer método, el compilador estaría recompilando inutilmente a modulo2.c. El segundo método permite evitar esto:

$ gcc -c modulo1.c
$ gcc -o mcd modulo1.o modulo2.o

Por tanto es más eficiente que el primero.

Esta eficiencia es importantísima en proyectos grandes; por ejemplo, con 100 o más archivos de código fuente. La automatización de todas estas cuestiones se las podemos dejar a make.

Make es una herramienta diseñada para compilar proyectos relativamente grandes y con "dependencias" complicadas.

7.2. Dependencias y el Makefile

Un concepto fundamental en "make" [29] son las relaciones de dependencia. En el ejemplo anterior observamos la siguiente secuencia de operaciones:

   modulo1.c  --> modulo1.o ---+
                                +--> mcd
   modulo2.c  --> modulo2.o ---+

Si - por ejemplo - modulo1.c es modificado por el programador, entonces es evidente que debemos generar un nuevo archivo objeto modulo1.o para dar lugar a un nuevo ejecutable mcd. En otras palabras, mcd depende de modulo1.o y de modulo2.o, mientras que modulo1.o depende de modulo1.c, y por último modulo2.o depende de modulo2.c.

Nótese que todo archivo (excepto los de código fuente) debe ser construido a partir de sus "dependencias". Todo esto lo especificaremos en un archivo llamado convencionalmente "Makefile" que es usado por el comando "make" para describir lo que tiene que hacer. Para nuestro ejemplo sería algo así:

# Primera version de Makefile

mcd: modulo1.o modulo2.o
        gcc -o mcd modulo1.o modulo2.o

modulo1.o: modulo1.c
        gcc -c modulo1.c

modulo2.o: modulo2.c
        gcc -c modulo2.c

Por lo que podemos apreciar existen tres "reglas", que son las que indican qué archivos dependen de cuales, y cómo generarlos cuando hace falta. En la primera regla, el "target" u "objetivo" a construir es el ejecutable "mcd". Éste se debe reconstruir cuando está desactualizado con respecto a sus dependencias modulo1.o o modulo2.o. Luego de esta especificación de dependencias se debe colocar los comandos a ejecutarse para satisfacer tales dependencias. Como sabemos, gcc permite generar el ejecutable a partir de los archivos objeto.

Análogamente, tanto modulo1.o como modulo2.o tienen dependencias respecto a los archivos fuente modulo1.c y modulo2.c respectivamente, cosa que se especifica mediante dos reglas adicionales.

Muy Importante: Estos comandos especificados en cada regla deben iniciarse siempre con un tabulador al principio de la línea. De no ser así, make asume que se trata de una nueva regla. No se puede usar espacios; necesariamente es un caracter de tabulación.

Invocación de make

El "Makefile" anteriormente presentado puede guardarse en cualquier archivo de texto, aunque por comodidad se suele utilizar el nombre "Makefile" o "makefile".

Pruebe a compilar el programa a partir de los módulos. Empiece borrando el ejecutable y los módulos objeto para ver el proceso completo:

$ rm -f mcd modulo1.o modulo2.o
$ ls
Makefile  modulo1.c  modulo2.c

Ahora invoque a "make". A continuación muestro lo que obtuve en mi computador [30]:

$ make
gcc -c modulo1.c
gcc -c modulo2.c
gcc -o mcd modulo1.o modulo2.o

Si volvemos a invocar a make Ud. apreciará que éste se niega a hacer algo debido a que todo está ahora actualizado.

Supogamos ahora que modificamos a modulo1.c. Podríamos hacer un cambio editando el archivo, pero para engañar a make simplemente basta con que actualicemos su fecha de modificación (a fin de que sea más reciente que los archivos que dependen de éste.) El comando touch puede actualizar esa fecha:

$ make
make: `mcd' está actualizado.
$ touch modulo1.c
$ make
gcc -c modulo1.c
gcc -o mcd modulo1.o modulo2.o

Observe con atención que make sólo ha compilado lo necesario (no ha recompilado modulo2.c.) Cómo logra hacerlo es lo que veremos a continuación.

7.3. Procesamiento de dependencias

¿En qué casos se considera que un archivo "target" está "desactualizado" con respecto a sus dependencias? Antes de responder a esto, make hace un análisis de cada una de las "dependencias" y verifica si éstas son "target’s" en otras reglas. Todas estas reglas deben ser resueltas antes de proseguir. En nuestro caso, las dependencias modulo1.o y modulo2.o son los "target" de la segunda y tercera regla respectivamente. Analicemos la segunda regla (la tercera es análoga.)

La segunda regla (con target modulo1.o) presenta como dependencia única a modulo1.c. Como ya no hay ninguna otra regla que tenga a modulo1.c como target, entonces Make analiza las fechas de modificación del target modulo1.o y su dependencia modulo1.c. Si modulo1.c es más reciente que modulo1.o, entonces significa que el programador ha hecho cambios en el código fuente; por lo tanto, make ejecuta el comando indicado en la regla, el cual regenera a modulo1.o. Esto trae como consecuencia que éste último ahora sea más reciente que modulo1.c.

Tras analizar a reglas segunda y tercera, make retorna a analizar la primera. Ahora verificará las fechas de modificación para conocer si las dependencias (modulo1.o y modulo2.o) son más recientes que el target mcd. De ser así se ejecutará los comandos asociados a dicha regla; es decir, invocará a gcc para volver a hacer el enlace de los archivos objeto generando un nuevo ejecutable.

Es importante advertir que en el momento en que make regresa a la primera regla, las fechas de modificación de los archivos involucrados pueden haber cambiado (debido a los comandos de las otras reglas.) De este modo, un simple cambio en un archivo de código fuente se propaga adecuadamente al archivo objeto correspondiente y da lugar a un nuevo ejecutable.

También debemos anotar que la primera regla normalmente es la que inicia la cadena recurrente de dependencias. Si Ud. hubiera empezado el archivo con la siguiente regla:

modulo1.o: modulo1.c
        gcc -c modulo1.c
...

Entonces make sólo se preocuparía de actualizar el archivo objeto "modulo1.o" cuando sea necesario (cuando cambie "modulo1.c") pero no hará absolutamente nada más debido a que "modulo1.c" no es target en ninguna otra regla.

A riesgo de sonar repetitivo… recuerde una vez más que los comandos asociados a las reglas deben iniciarse con un tabulador como primer caracter de la línea.

7.4. Reglas implícitas

El Makefile que presentamos arriba se puede abreviar considerablemente. Por ejemplo, la siguiente versión será totalmente equivalente y más poderosa:

# Segunda version de Makefile

mcd: modulo1.o modulo2.o
        gcc -o mcd modulo1.o modulo2.o  # usar tabulador

clean:
        rm -f *.o mcd  # usar tabulador

Empecemos por el final. Hemos añadido una regla con target "clean", la cual no participa normalmente del proceso de compilación (clean no aparece como dependencia de nada.) Simplemente nos sirve para "limpiar" el directorio del programa de todos los archivos generados (objetos y el ejecutable) a fin de forzar una compilación total.

Como mencioné, esto no ocurrirá a no ser que lo solicitemos explícitamente con el siguiente comando:

$ make clean

Si bien no es obligatorio tener una regla con target "clean", muchos proyectos la tienen, y mucha gente espera encontrarla.

Dejando de lado el target "clean", y volviendo a lo anterior, ¿qué ocurrió con las otras dos reglas que estaban en la versión anterior del Makefile? La respuesta es que son redundantes.

Make normalmente viene preconfigurado con lo que se conoce como "reglas implícitas", que simplemente son acciones a tomarse con ciertas relaciones de dependencia cuando no se proporciona una acción explícitamente.

Como vemos, en la primera regla la acción para construir el ejecutable está completamente determinada de manera explícita. Como sabemos, make además buscará otras reglas donde modulo[12].o sean "targets". Al no haber tales reglas, entonces intentará buscar una regla implícita que sea adecuada.

Una regla implícita "adecuada" (que no necesitamos añadir al Makefile pues ya está pre-configurada) tendría aproximadamente el siguiente aspecto:

%.o:%.c
        gcc -c $<

Esta regla es aplicada por make a los archivos modulo[12].c para generar los target’s modulo[12].o. [31]. De este modo, esta nueva versión del Makefile es completamente equivalente a la anterior.

Recuerde que ésto último no requiere añadirse al Makefile; sólo se presenta para que la explicación sea más completa.

7.5. Variables

Continuando con nuestra reducción del Makefile, presentamos una tercera versión:

# Tercera version de Makefile

OBJETOS= modulo1.o modulo2.o

mcd: $(OBJETOS)
        gcc -o $@ $(OBJETOS)

clean:
        rm -f *.o mcd

Como se aprecia, las variables se pueden definir con la notación NOMBRE=VALOR. Éstas se expanden con la notación $(NOMBRE) y pueden proporcionan un gran poder de abstracción en Makefiles complicados.

Algunas variables tienen un significado particular dependiendo de su contexto. Por ejemplo, la variable "@" (que se expande con "$@") significa el "target" de una regla y su valor (en nuestro caso, reemplaza a "mcd".) A estas variables se les denomina a veces "automáticas".

Tabla 1. Algunas variables automáticas en GNU Make

Variable Significado

@

El target de la regla

<

El nombre de la primera dependencia

?

El nombre de todas las dependencias más nuevas que el target

^

El nombre de todas las dependencias sin repeticiones

+

El nombre de todas las dependencias, posiblemente repetidas


Variables en reglas implícitas

Hace un momento presenté una probable regla implícita para construir un archivo objeto (.o) a partir de un archivo fuente C (.c). A continuación presento una versión más acorde con la realidad:

%.o:%.c
        $(CC) -c $(CFLAGS) $<

Esto quiere decir que es posible usar un compilador distinto a gcc con facilidad (al menos desde el punto de vista de make) simplemente definiendo la variable CC al inicio del Makefile. Asimismo, la variable CFLAGS permite introducir algunas opciones al proceso de compilación (ver más adelante.)

Tabla 2. Algunas variables de GNU Make que invocan programas con opciones

Nombre del programa Opciones Significado Valor por omisión

AR

ARFLAGS

Programa de "archivamiento"

ar

AS

ASFLAGS

Programa ensamblador

as

CC

CFLAGS

Compilador de C

cc

CXX

CXXFLAGS

Compilador de C++

g++

FC

FFLAGS

Compilador fortran

f77

LEX

LFLAGS

Analizador lexicográfico

lex

PC

PFLAGS

Compilador de pascal

pc

YACC

YFLAGS

Analizador sintáctico

yacc

RM

Programa que elimina archivos

rm -f


Comentarios finales sobre Make

Make es un programa tremendamente sofisticado. Aquí sólo hemos presentado los aspectos más elementales, aunque ya estamos en capacidad de compilar proyectos pequeños. En Linux la versión de Make distribuida es GNU Make, la cual presenta numerosas extensiones y algunas incopatibilidades con respecto a las versiones "oficiales" de los Unix’es. Para más información sobre GNU Make, consúltese la documentación "info" (info make.)

8. Archivos informativos de proyectos GNU

Estos archivos contienen información relevante a los instaladores/administradores que utilizan un paquete generado con herramientas GNU, y se encuentran en el directorio principal del mismo. Esta información NO corresponde a la documentación técnica (los manuales o guías) del proyecto.

A continuación un listado de éstos archivos junto con algunos comentarios.

README
Este es el archivo que todos los instaladores deberían leer antes de iniciar la construcción del paquete. Por lo general se brinda una introducción a lo que hace el paquete, así como cualquier información importante que el instalador debe saber. El "cómo instalar" se explica normalmente en INSTALL.
INSTALL
Información que suele ser útil para quien por primera vez va a instalar un paquete GNU. Se explica el proceso de ejecutar ./configure, make y make install. En tanto este proceso es siempre similar, el archivo INSTALL suele ser autogenerado por las herramientas GNU.
AUTHORS
Un listado de los autores y sus contribuciones al proyecto, así como cualquier término legal establecido con ellos. Se mantiene por motivos legales.
THANKS
Un listado informal de todos los contribuyentes al proyecto: codificadores, testeadores, etc. Se acostumbra colocar su nombre, su dirección email y a veces una descripción de su contribución al proyecto.
NEWS
Las características principales de la versión actual, y posiblemente, de anteriores versiones.
ChangeLog
Explicación de todos los cambios realizados al código fuente. Tiene un formato estandarizado.
COPYING
Se indican los derehos de autor y copyright del paquete (por ejemplo, la licencia GPL.)

9. Crear y Utilizar Librerías en Linux

Nota

Los ejemplos de esta sección están referidos a Linux y al compilador GNU gcc, sin embargo, con ligeras modificaciones son aplicables a cualquier Unix moderno.

Las librerías son rutinas (en general, cualquier código) usado para proporcionar funcionalidad a los programas. Por lo general, las librerías contienen una colección de funciones que llevan a cabo un conjunto de tareas orientadas a cierto propósito. Por ejemplo, la librería "msqlclient" proporciona un conjunto de rutinas para interactuar con la base de datos MySQL, la librería "zlib" proporciona un conjunto de rutinas para comprimir y descomprimir cualquier conjunto de datos, la librería SDL permite programar aplicaciones multimedia/juegos, etc.

9.1. Un ejemplo: mejoras para el procesamiento de strings

A fin de ilustrar el proceso completo de creación de una librería, utilizaremos (inventaremos) un ejemplo práctico (stringplus.) En realidad, no es muy relevante qué es lo que hace esta librería, sino la forma como está siendo construida.

Diseño

Aunque la librería estándar ya proporciona un conjunto de funciones para procesamiento de cadenas de caracteres (strcpy, strcat, strchr, etc.) es recurrente la necesidad de diversas funciones adicionales tales como:

Función Descripción

replace

Reemplazar las ocurrencias de un texto por otro dentro de un string

trim

Eliminar espacios en blanco al inicio y al final de un string

substr

Proporcionar una sección de una cadena en un nuevo string

A esta pequeña librería le denominaremos "stringplus".

Implementación

A fin de hacer la explicación más interesante y general, implementaremos cada función en un archivo aparte:

Reemplazo de textos

Dada una cadena de texto, buscar todas las ocurrencias de cierto patrón (otra cadena) y reemplazarlo por otra cadena de texto. La cadena original no se altera, y los reemplazos dan lugar a una nueva, asignada dinámicamente (se debe eliminar posteriormente con free(3).) Nótese que esta implementación asume que los strings a crearse no pasan de 1024 bytes. Esto es un fallo que Ud. debería corregir a modo de ejercicio:

/*
 * replace.c: remplazar en cadena todas las ocurrencias
 * de "tal" por "cual", retornando un nuevo string
 */
#include "stringplus.h"

char *replace(const char *cadena,const char *tal,const char *cual)
{
if(cadena==NULL || tal==NULL || cual==NULL || strlen(tal)==0)
        return NULL;

/* Implementacion poco inteligente: reservamos 1K
 * y no verificamos si no alcanza */
char *res=(char *)calloc(1024,sizeof(char));
const char *p1=cadena;
const char *p=cadena;

for(;;)
        {
        p=strstr(p,tal);
        while(*p1 && p1!=p)
                {
                int j=strlen(res);
                res[j]=*p1;
                res[j+1]=0;
                p1++;
                }
        if(p1==p)
                {
                strcat(res,cual);
                p1+=strlen(tal);
                p+=strlen(tal);
                }
        if(!*p1)
                break;
        }
return res;
}
Sub-Cadenas de Texto

La función substring recibe como argumento una cadena (que no se altera), un entero no negativo que indica la posición a partir de la cual se extraerá una nueva cadena, y otro entero (no negativo) que indica cuántos caracteres se extraerán como máximo. Si los enteros proporcionados son negativos, se retorna NULL. Si los enteros intentan obtener caracteres más allá de los que contiene la cadena, se retorna un string vacío.

/*
 * substr.c: Retornar un nuevo string a partir
 * de una seccion de un string inicial
 */
#include "stringplus.h"

char *substr(const char *cadena,int n,int m)
{
if(cadena==NULL || n<0 || m<0)
        return NULL;
if(n>=strlen(cadena))
        return strdup("");
if(n+m <= strlen(cadena))
        {
        char *p=(char*)malloc(m+1);
        p[m]=0;
        strncpy(p,cadena+n,m);
        return p;
        }
return strdup(cadena+n);
}
Recorte de blanks

Es frecuente la necesidad de eliminar espacios en blanco (blanks) en cadenas de texto. Consideramos "blanks" a los espacios, tabuladores y saltos de línea. La función trim retorna un nuevo string recortado a partir de una cadena pasada como argumento (que no se modifica.)

/*
 * trim.c: eliminar espacios en blanco
 * al inicio y al final de un string
 */
#include "stringplus.h"

char *trim(const char *cadena)
{
if(cadena==NULL)
        return NULL;
if(*cadena == 0)
        return strdup("");
char *p=strdup(cadena);
char *ptr=p;
while(*p)
        {
        if(*p !=' ' && *p!='\t' && *p!='\n')
                break;
        p++;
        }
char *t=p+strlen(p)-1;
while(*t)
        {
        if(t==p)
                break;
        if(*t !=' ' && *t!='\t' && *t!='\n')
                break;
        t--;
        }
*(t+1)=0;
char *tmp=strdup(p);
free(ptr);
return tmp;
}
Archivo cabecera de la librería

Es típico que las librerías tengan uno o más headers (archivos cabecera.) Para nuestro caso bastará con uno:

/*
 * stringplus.h: Header de libreria de strings
 */
#ifndef STRINGPLUS_H
#define STRINGPLUS_H
#include <stdio.h>
#include <string.h>

char *replace(const char *cadena,const char *tal,const char *cual);
char *substr(const char *cadena,int n,int m);
char *trim(const char *cadena);

#endif

Probando el código de la librería

Ahora crearemos un pequeño programa para probar todo el código anterior (prueba.c):

#include "stringplus.h"

int main(int argc,char **argv)
{
char *to_replace="Erre con erre guitarra";
char *from_replace=replace(to_replace,"rr","abc");
if(from_replace==NULL)
        fprintf(stderr,"Error en replace\n"),exit(1);
printf("to_replace:[%s]\n",to_replace);
printf("from_replace(rr,abc):[%s]\n\n",from_replace);
free(from_replace);

char *to_substr="La tierra es guera";
char *from_substr=substr(to_substr,3,6);
if(from_substr==NULL)
        fprintf(stderr,"Error en substr\n"),exit(1);
printf("to_substr:[%s]\n",to_substr);
printf("from_substr(3,6):[%s]\n\n",from_substr);
free(from_substr);


char *to_trim="   There goes the Challenger    ";
char *from_trim=trim(to_trim);
if(from_trim==NULL)
        fprintf(stderr,"Error en trim\n"),exit(1);
printf("to_trim:[%s]\n",to_trim);
printf("from_trim:[%s]\n\n",from_trim);
free(from_trim);


return 0;
}
Compilación y ejecución

El siguiente Makefile permite compilar todos nuestros módulos, generando un ejecutable llamado prueba:

OBJECTS = prueba.o replace.o trim.o substr.o

prueba: $(OBJECTS)
        gcc -o $@ $(OBJECTS)

clean:
        rm -f prueba *.o

La salida es la siguiente:

$ ./prueba
to_replace:[Erre con erre guitarra]
from_replace(rr,abc):[Eabce con eabce guitaabca]

to_substr:[La tierra es guera]
from_substr(3,6):[tierra]

to_trim:[   There goes the Challenger    ]
from_trim:[There goes the Challenger]
Recapitulación

Revisemos en lo que hemos conseguido hasta este momento. Gracias al código de los archivos:

  • replace.c
  • substr.c
  • trim.c
  • stringplus.h

El programa prueba.c ha podido ejecutarse haciendo uso de nuestras nuevas funciones extendidas de strings, lo cual era el objetivo de todo esto. Eso significa que los cuatro archivos listados arriba constituyen para todo propósito lo que se conoce conceptualmente por "librería".

De otro lado, otro programador que hace uso de nuestra librería no necesita estrictamente tener acceso al código fuente de la misma. Por tal motivo nuestra librería podría constar sólamente de los archivos compilados:

  • replace.o
  • substr.o
  • trim.o

y del header +stringplus.h:.

El distribuír sólo código compilado es una práctica comercial común, pero que lamentablemente limita las posibilidades de introducir mejoras y correcciones [32].

Ahora bien, desde el punto de vista del sistema operativo Unix/Linux todavía no disponemos de una "librería" como tal, puesto que éstas se suelen distribuír en un solo archivo de formato especial (y no como un conjunto de archivos objeto *.o) tal como veremos a continuación.

9.2. Construcción de archivos de librería

Librerías estáticas

Las librerías estáticas consisten simplemente en empaquetar los archivos de código objeto (*.o) en un solo gran archivo (con extensión "*.a") usando el clásico comando "ar" [33]. Este archivo se puede enlazar con el compilador como si fuese cualquier otro archivo objeto.

Dados los archivos del ejemplo anterior, el siguiente Makefile genera el archivo de librería estática "stringplus.a", y lo enlaza con el código de prueba (prueba.c):

# Crear un archivo de libreria estatica
#
OBJECTS = replace.o trim.o substr.o

prueba: prueba.c stringplus.a
        gcc -o $@ prueba.c stringplus.a

stringplus.a: $(OBJECTS)
        ar rcs $@ $(OBJECTS)

clean:
        rm -f prueba *.o stringplus.a

La ejecución de make se muestra a continuación [34]:

$ make
cc    -c -o replace.o replace.c
cc    -c -o trim.o trim.c
cc    -c -o substr.o substr.c
ar rcs stringplus.a replace.o trim.o substr.o
gcc -o prueba prueba.c stringplus.a

Con esto, la distribución de la librería se limita a los archivos:

  • stringplus.h
  • stringplus.a

    1. Así como la documentación de cómo se usa.

A esta clase de librería se le conoce como "librería estática". Los ejecutables enlazados con librerías estáticas pueden ser llevados a otros computadores (de la misma plataforma) sin necesidad de llevar también la librería, pues ésta ya está contenida en aquellos.

Librerías compartidas (shared libraries)

El esquema de enlace con librerías estáticas funciona bien, pero tiene algunas características que pueden ser muy indeseables:

  1. Si dos o más programas se enlazan a la librería estática, cada ejecutable tendrá añadido el mismo código repetido (desperdicio de espacio de disco)
  2. Como consecuencia de lo anterior, cuando estos ejecutables se cargan en memoria, ésta se llena ineficientemente con código repetido
  3. Si descubrimos un problema en nuestra librería y logramos corregirlo, entonces debemos volver a enlazar todos los programas que utilizaban la versión defectuosa. En la práctica, esto significa generar y reinstalar todos los ejecutables.

Las librerías compartidas (conocidas como shared libraries o dynamic libraries) resuelven todos estos problemas de un modo bastante completo, a costa de cierta complejidad añadida.

Lamentablemente el procedimiento necesario para hacer esto no es totalmente estandarizado entre Unix’es, y lo que haremos aquí (Linux/gcc) puede requerir adaptaciones para otros ambientes.

El siguiente Makefile permite obtener la librería "libstringplus.so" y enlazarla con "prueba.c":

# Crear un archivo de libreria dinamica
#
OBJECTS = replace.o trim.o substr.o
CFLAGS = -fpic

prueba: prueba.c libstringplus.so
        gcc -o $@ prueba.c libstringplus.so
#       gcc -L. -o $@ prueba.c -lstringplus

libstringplus.so: $(OBJECTS)
        gcc -shared -o $@ $(OBJECTS)

clean:
        rm -f prueba *.o libstringplus.so

Convencionalmente, las librerías dinámicas tienen un nombre de archivo de la forma "libXXX.so.v.m.r" [35], donde la extensión "*.so" indica que se trata de un "shared object"; "v", "m" y "r" son enteros positivos: "v" corresponde a la "versión" de la librería, "m" es el "número menor", y "r" el "release". Diferentes valores para el número "v" significan diversas versiones incompatibles de la librería [36]; por el contrario, distintos valores de "m" corresponden a versiones compatibles de la misma [37], y finalmente "r" (que es opcional) complementa a "m" permitiendo indicarle mayor granularidad. En conjunto, estos números permiten la coexistencia de distintas versiones de una librería.

Por simplicidad, en lo que sigue no emplearemos los números "v", "m" y "r", aunque cumplen una función fundamental para el correcto manejo de "dependencias" de las aplicaciones, por lo que se recomienda leer el .URL http://www.dwheeler.com/program-library Programming Library HOWTO .

Usando gcc, la opción -shared genera librerías compartidas mientras que la opción -fpic [38] genera código máquina con "direcciones independientes de la posición", lo cual es indicado para todos los módulos constituyentes de las librerías compartidas (mas no es necesario para los programas que las invocan. [39])

Ahora bien, esta "shared library" por definición NO está contenida en el archivo ejecutable con quien está enlazada. Por ejemplo, nuestro ejecutable (prueba) sólo contiene una referencia a "libstringplus.so" [40], la que deberá ser cargada a memoria recién en el momento de la ejecución del programa.

Esto significa que si queremos llevar nuestro ejecutable "prueba" a otro computador, también tendremos que llevar por separado "libstringplus.so".

Para apreciar a qué shared library’s hace referencia nuestro ejecutable prueba, podemos usar el comando ldd:

$ ldd prueba
libstringplus.so => not found
libc.so.6 => /lib/libc.so.6 (0x40029000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
$

Como se aprecia, nuestra librería está referenciada aunque no está siendo encontrada (ver mensaje "not found".) Esto se debe a que las shared library’s tienen que colocarse en ciertos directorios especiales en el sistema previamente a su uso.

Si tratamos de ejecutar el programa, éste fallará:

$ ./prueba
./prueba: error while loading shared libraries: libstringplus.so:
cannot open shared object file: No such file or directory

Una manera temporal de solucionar esto consiste en especificar la variable de entorno "LD_LIBRARY_PATH" con el directorio en el que se encuentra "libstringplus.so". En nuestro caso, es el directorio actual ("."):

$ LD_LIBRARY_PATH=. ./prueba
to_replace:[Erre con erre guitarra]
from_replace(rr,abc):[Eabce con eabce guitaabca]

to_substr:[La tierra es guera]
from_substr(3,6):[tierra]

to_trim:[   There goes the Challenger    ]
from_trim:[There goes the Challenger]
Enlace en tiempo de ejecución

La solución de largo plazo consiste en "instalar" nuestra librería en el sistema. Para esto podemos hacer lo siguiente:

$ su
Password:
# cp libstringplus.so /usr/lib
# exit
$ ldd prueba
libstringplus.so => /usr/lib/libstringplus.so (0x40029000)
libc.so.6 => /lib/libc.so.6 (0x4002b000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
$ ./prueba
to_replace:[Erre con erre guitarra]
...

Parece que todo se resuelve mágicamente llevando la librería al directorio /usr/lib. Lo que ocurre es que éste directorio (al igual que /lib y otros más) es uno de los lugares preconfigurados donde el "enlazador de tiempo de ejecución" busca las shared libraries. Por ejemplo, en el sistema (Linux) que estoy utilizando, las rutas son [41]:

$ cat /etc/ld.so.conf
/usr/kerberos/lib
/usr/X11R6/lib
/usr/lib/qt-3.0.5/lib
/usr/lib/sane
/usr/lib/mysql
/usr/lib/qt2/lib
/usr/lib/wine

A éstas se les añade automáticamente /lib y /usr/lib. Esta información se puede hallar en el man page de "ld.so".

Para resumir esta sección, cuando distribuimos (hacia usuarios finales) un programa enlazado dinámicamente con la librería "stringplus" (como por ejemplo, prueba), entonces también debemos proporcionarle la librería libstringplus.so, o al menos indicarle que ésta es requisito para que el programa funcione.

Por otro lado, cuando querramos distribuir nuestra librería a otro desarrollador, proporcionaremos los archivos [42]:

  • stringplus.h
  • libstringplus.so

(y la documentación correspondiente.)

La opción "-l" del compilador

Si se observa con atención el último Makefile, observará que para la compilación del programa prueba (tras haber generado la shared library) hemos indicado el enlace a la libstringplus.so simplemente especificando el nombre completo de tal archivo:

        gcc -o $@ prueba.c libstringplus.so

Esto funciona bien puesto que la librería está en nuestro directorio actual.

Sin embargo, muchas veces enlazamos nuestros programas con librerías que no hemos construido nosotros y que están instaladas en diversos directorios del sistema. En ese caso, el comando hipotético podría ser algo como:

        gcc -o $@ prueba.c /usr/lib/libstringplus.so

Sin embargo, el directorio donde se ubican las librerías es variable, y sería engorroso tener que buscarlas para cada caso.

A tal efecto, la opción "-l" del compilador [43] permite que especifiquemos tan solo el nombre de la librería (con -lXXX) y automáticamente se buscará en todos los directorios adecuados un archivo con un nombre que tenga la forma "libXXX.a" o la forma "libXXX.so".

Lamentablemente aquí surge un nuevo inconveniente: el enlazador en el momento de la compilación normalmente no considera al directorio actual (.) como posible lugar de búsqueda de librerías compartidas. A fin de añadirlo a la lista de directorios, empleamos la opción "-L" del compilador [44]. Finalmente, el nuevo comando para generar el ejecutable (en el Makefile) es:

        gcc -L. -o $@ prueba.c -lstringplus

9.3. Ejecución dinámica con dlopen

La interfaz dlopen proporciona un mecanismo para ejecutar código compilado en demanda, lo que proporciona un grado de flexibilidad extremo. Proporcionaremos un nuevo ejemplo para hacer la respectiva demostración [45].

Ejemplo: Factoriales Reimplementados

A continuación tres implementaciones que nos permiten obtener el factorial de un número entero (retornando un entero largo.)

Con loop simple
/*
 * Calculo de un factorial con un loop repetido
 */

long factorial(int n)
{
long f=1;

do
        f*=n;
while(--n);
return f;
}
Con recursividad
/*
 * Calculo de un factorial usando recursividad
 */

long factorial(int n)
{
if(n>1) return n*factorial(n-1);
else return 1L;
}
Con fórmula aproximada
/*
 * Calculo de un factorial usando formula
 * de Stirling
 */

#include <math.h>

#define PI 3.14159265358979323846

long factorial(int n)
{
double f;

return exp(-n)*pow(n,n+0.5)*sqrt(2.0*PI)*
        (1.0+1.0/(12.0*n)+1.0/(288.0*n*n));
}

Aunque no era necesario, me parece más didáctico haber creado las tres implementaciones mediante una función que tiene el mismo nombre (factorial.) Como se sabe, sería imposible crear un programa que acceda a estas tres funciones a la vez (¿cómo las podría distinguir?.)

Pero con dlopen (y compañía) sí podemos. El siguiente programa hace uso de éstas implementaciones de factorial dependiendo de lo que el usuario escoge:

#include <stdio.h>
#include <dlfcn.h>
#include <errno.h>

/* long factorial(int); */

main(int argc,char **argv)
{
int n,m;
void *obj;
long (*fact)(int);

printf("Escriba el numero > ");
scanf("%d",&n);
printf("Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > ");
scanf("%d",&m);

switch(m)
        {
        case 1: obj=dlopen("./fact1.so",RTLD_NOW); break;
        case 2: obj=dlopen("./fact2.so",RTLD_NOW); break;
        case 3: obj=dlopen("./fact3.so",RTLD_NOW); break;
        default:
        fprintf(stderr,"Metodo invalido!\n");exit(1);
        }
if(obj==NULL)
        { fprintf(stderr,"dlopen: %s\n",dlerror()); exit(1); }

fact=dlsym(obj,"factorial");
if(fact==NULL)
        { fprintf(stderr,"dlsym: %s\n",dlerror()); exit(1); }

printf("%ld\n",(*fact)(n));
}

Nótese que nuestro programa accede a shared libraries (.so) aunque -al menos en Linux- también puede acceder a archivos objeto (.o). A tal efecto, el siguiente Makefile permite compilar todo:

LIBS = fact1.so fact2.so fact3.so
CFLAGS = -fpic

all: $(LIBS) prueba

%.so:%.o
        gcc -shared -o $@ $<

prueba: prueba.c
        gcc -o prueba prueba.c -ldl

clean:
        rm -f *.o *.so prueba

Obsérvese que "prueba" no se enlaza con ninguna de las implementaciones de factoriales, pero sí debe hacerlo con la librería "libdl.so" (opción -ldl) que proporciona estas capacidades. Note también la inclusión del header "dlfcn.h".

A continuación una muestra de su ejecución:

$ ./prueba
Escriba el numero > 5
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 1
120
$ ./prueba
Escriba el numero > 5
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 2
120
$ ./prueba
Escriba el numero > 5
Que metodo usamos? (1-loop 2-recursividad 3-Stirling) > 3
dlopen: ./fact3.so: undefined symbol: sqrt

Casi listo, pero en el caso de "fact3" se requería enlazar con la librería matemática (que define la función sqrt().) Esto es exclusivo de fact3, por lo que lo enlazamos en el momento de su creación:

LIBS = fact1.so fact2.so fact3.so
CFLAGS = -fpic

all: $(LIBS) prueba

%.so:%.o
        gcc -shared -o $@ $<

fact3.so: fact3.o
        gcc -shared -o $@ $< -lm

prueba: prueba.c
        gcc -o prueba prueba.c -ldl

clean:
        rm -f *.o *.so prueba

Como se ve, hemos añadido una regla explícita para el caso de fact3.so que incluye a -lm [46]. Esta vez, la ejecución no falla.

Handlers y símbolos

Como se aprecia, nuestro programa central accede a las "shared libraries" mediante la función dlopen(). Ésta retorna un puntero tipo (void *) que sirve para hacer referencia al contenido de la misma (o NULL en caso de error.) Obsérve que requiere especificar la ruta completa (en este caso con "./") pues en caso contrario buscará la librería sólo en los lugares preconfigurados del sistema (ver discusión sobre esto más arriba en la sección de creación de shared libraries [47].) Pruebe a usar "fact1.so" en lugar de "./fact1.so" para que lo compruebe.

Teniéndose el "handler" a la shared library, es posible obtener un puntero a cualquier objeto de la misma (una función o una variable.) Para esto se emplea la función dlsym() que recibe como argumento el nombre de este objeto (lo que en las librerías se conoce como un símbolo.) Estos punteros deben ser del tipo adecuado al objeto al que se está referenciando (en nuestro caso, un puntero a función que recibe "int" y retorna "long".)

Cuando el programa ya no requiere acceder a estos objetos, el "handler" obtenido con dlopen() debe cerrarse con dlclose() (nuestro programa de prueba finaliza de inmediato, por lo que no vale la pena la molestia.)

9.4. Problemas Frecuentes

Referencias no resueltas/circulares

A continuación proporcionamos los archivos constituyentes de dos librerías triviales, y un programa que hace uso de éstas. Esto nos permitirá mostrar un error que aparece con relativa frecuencia, generado por referencias no resueltas:

Código fuente de la librería "a.a"
/*
 * a.c
 */

void fa()
{
}
Código fuente de la librería "b.a"
/*
 * b.c: Llama a fa()
 */
extern void fa(void);

void fb()
{
fa();
}
Código fuente de programa de prueba
/*
 * prueba.c
 */
extern void fb(void);

int main(void)
{
fb();
return 0;
}
Compilación y explicación

A continuación el Makefile originalmente diseñado para compilar esto:

all: prueba

prueba: prueba.c a.a b.a
        gcc -o $@ prueba.c a.a b.a
a.a: a.o
        ar rcs $@ a.o
b.a: b.o
        ar rcs $@ b.o
clean:
        rm -f a.o b.o a.a b.a prueba

Y la salida cuando se utiliza:

cc    -c -o a.o a.c
ar rcs a.a a.o
cc    -c -o b.o b.c
ar rcs b.a b.o
gcc -o prueba prueba.c a.a b.a
b.a(b.o)(.text+0x7): In function `fb':
: undefined reference to `fa'
collect2: ld returned 1 exit status
make: *** [prueba] Error 1

Para comprender la falla, debemos entender cómo opera el enlazador sobre los archivos que se le proporcionan. Si vemos los comandos ejecutados, apreciaremos que la última invocación al compilador gcc compila el archivo "prueba.c" (lo que genera el módulo objeto prueba.o) e invoca al enlazador con los siguientes argumentos (en el orden indicado):

  • prueba.o
  • a.a
  • b.a

El enlazador analiza el primer archivo. En tanto prueba.o sólo hace referencia a la función fb(), el enlazador toma nota de que ésta debe estar definida en algún módulo subsiguiente. Es decir, hasta este momento lo único que "hace falta" es la implementación de fb().

Luego se procesa a.a. En tanto esta librería no contiene fb(), el enlazador asume que su contenido no se necesita, y se ignora completamente. Esto parece un descuido, pero es la manera normal en que se comporta el enlazador [48].

Finalmente se procesa b.a. Dado que contiene a fb(), su código se toma en consideración para el proceso. Sin embargo, fb() hace referencia a una cierta función fa()' El enlazador toma nota de esto, y asume que fa() está implementada en un módulo todavía no analizado. Como b.a es el último módulo, el enlazador no puede resolver la referencia pendiente fa() y falla.

Una manera rápida (y correcta) de resolver esto, consiste simplemente en intercambiar el orden en que se proporcionan los archivos al enlazador:

gcc -o $@ prueba.c b.a a.a

Sin embargo, existen casos más sofisticados (con más archivos participantes) en los cuales no es suficiente con jugar con el orden de los mismos. En tales casos se puede hablar de referencias circulares. Para resolver esto, a veces basta con repetir los archivos a enlazar (por ejemplo, gcc -o $@ prueba.c a.a b.a a.a) o cambiar el comportamiento del enlazador para que "regrese" a buscar referencias no resueltas en los módulos especificados con anterioridad. Para obtener este comportamiento, se puede utilizar la siguiente (extraña) sintaxis [49]:

gcc -o $@ prueba.c -Wl,--start-group -Wl,a.a -Wl,b.a -Wl,--end-group

La opción -Wl, de gcc, permite pasar opciones directamente al enlazador ld. En el enlazador, las opciones --start-group y --end-group permiten encerrar un conjunto de librerías sobre las cuales ld tendrá el comportamiento señalado arriba.

Forzar exportación de símbolos (dlopen)

En el ejemplo que mostramos a continuación el programa principal (prueba.c) invoca mediante dlopen() a la librería "checksum" que contiene la función del mismo nombre. Esta función requiere tener acceso al string llamado "compartido", el cual está definido en el programa prueba.c:

Archivos de ejemplo:
/*
 * prueba.c: Invocar a checksum()
 */
#include <dlfcn.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

char *compartido="Habia una vez, un andinista";

int main(int argc,char **argv)
{
void *obj;
void (*cksum)(void);

obj=dlopen("./checksum.so",RTLD_NOW);
if(obj==NULL)
        { fprintf(stderr,"dlopen: %s\n",dlerror()); exit(1); }

cksum=dlsym(obj,"checksum");
if(cksum==NULL)
        { fprintf(stderr,"dlsym: %s\n",dlerror()); exit(1); }

(*cksum)();
return 0;
}
/*
 * checksum.c: Hallar checksum a string "compartido"
 */
#include <stdio.h>
#include <string.h>

extern char *compartido;

void checksum(void)
{
int z;
unsigned short int s=0;
for(z=0;z<strlen(compartido);z++)
        s+=(unsigned char)compartido[z];
printf("Checksum=%hd\n",s);
}
#
# Makefile para mostrar fallo de export-dynamic
#
LIBS = checksum.so
CFLAGS = -fpic -Wall

all: $(LIBS) prueba

%.so:%.o
        gcc -shared -o $@ $<

prueba: prueba.c
        gcc -Wall -o prueba prueba.c -ldl

clean:
        rm -f *.o *.so prueba
Ejecución y explicación
$ ./prueba
dlopen: ./checksum.so: undefined symbol: compartido

El programa falla cuando se intenta cargar la librería compartida debido a que no se ha podido resolver el símbolo "compartido". Los símbolos accesibles dinámicamente tienen que estar registrados en la llamada "tabla de símbolos dinámicos", la cual normalmente sólo incluye aquellos que son referenciados por otro módulo en el momento del enlace.

En el caso de prueba.c, el enlazador no tiene forma de saber que "compartido" está referenciado por checksum.c (pues se enlazan separadamente) y por tanto no se incluye en la tabla.

La forma más simple de evitar este problema es forzar la exportación de todos los símbolos hacia la tabla mencionada, para lo cual bastará con añadir la opción --export-dynamic del enlazador; por tanto, desde el compilador, usaremos -Wl,--export-dynamic [50]:

$ gcc -Wall -Wl,--export-dynamic -o prueba prueba.c -ldl
$ ./prueba
Checksum=2488


[1] Se puede traducir a "Sistema de Construcción (de software) GNU"; sin embargo, he preferido mantener la frase en inglés pues la traducción me parece que puede traer más confusión que comprensión. En la jerga de software inglesa, la palabra "build" se emplea para hacer referencia al proceso que consiste en "construir" los productos finales de software, partiendo del código fuente (precompilación, compilación, enlace, y a veces instalación.) Lamentablemente no conozco un equivalente en español ampliamente aceptado.

[2] Aquí plataforma hace referencia a la combinación cierto hardware con su sistema operativo, y quizá cierto software adicional (especialmente librerías) que complementan el entorno.

[3] El caso de Java es el más conspicuo al respecto.

[4] Para el conspícuo caso de los sistemas DOS/Wind*ws, la documentación de GNU sugiere el uso del paquete de emulación Unix Cygwin, lo que consigue que dicha plataforma disponga de muchas facilidades similares a las de Unix.

[5] Completo en el sentido de los estándares GNU: un Makefile no debe limitarse a compilar los programas; también debería ser capaz de instalar el software en las rutas adecuadas, desinstalar el mismo, recrear una distribución con las modificaciones deseadas, ejecutar los tests de integridad incluidos con el paquete, eliminar los archivos creados con la compilación, etc.

[6] En versiones anteriores de Autoconf, este archivo se denominaba configure.in. Este nombre es obsoleto.

[7] En vez de utilizar autoheader/config.h, es posible definir estas macros desde la línea de comandos con la opción -D. Sin embargo, en proyectos sofisticados muchas opciones -D pueden generar una línea de comandos demasiado larga para ciertas plataformas, lo que reduce la portabilidad. Asimismo, complican el Makefile y hacen la depuración más tediosa.

[8] Es decir, ejecutar man usleep, man addch, etc.

[9] Evidentemente, el sistema operativo deberá tener instalados los componentes correspondientes (curses o ncurses.) Estos paquetes son estándares en todos los Unix y Linux, aunque no siempre se instalan por omisión. El administrador debería ser capaz de ayudarle en este asunto.

[10] La mayoría de paquetes definen su versión mediante dos enteros: el número mayor y el número menor. El número menor debería ser incrementado cada vez que el paquete proporciona nuevas características y mejoras, mientas que el número mayor sólo debe ser cambiado si los cambios introducidos corresponden a un nuevo nivel de madurez y estabilidad. Esto significa que los nuevos proyectos deberían empezar con un número mayor igual a cero; un "uno" significa que ya es utilizable para el público en general; un "dos" significa que el software ha madurado sustancialmente gracias a un significativo período de uso y feedback de los usuarios.

[11] El apéndice B explica brevemente el contenido que deben tener estos archivos.

[12] Esta técnica se denomina a veces "VPATH" build. Nótese que no está recomendada (aunque no hace daño) a los usuarios finales/instaladores debido a que puede requerir algunas características específicas de GNU Make.

[13] El cálculo de dependencias requiere de opciones exclusivas del compilador GNU/GCC. Si el instalador decidiera efectuar modificaciones considerables al código, deberá regenerar el Makefile.in para lo cual también requerirá de automake. El GNU Build System asume que el usuario final o instalador NO dispone de todas las herramientas de aquél. Por el contrario, se asume que sólo tiene algún compilador del lenguaje requerido, y algunas herramientas estándares como make, el shell, rm, cp, etc.

[14] Hay que tener cuidado de elegir funciones verdaderas y no confundirnos con las macros proporcionadas por las librerías, las cuales a veces tienen aspecto de funciones. La documentación de las librerías deberían explicar esto, aunque no siempre lo hacen con claridad.

[15] AC_PROC_CPP analiza la existencia del preprocesador de lenguaje C.

[16] Podríamos haber definido que reciba un entero que representa "microsegundos" y no "nanosegundos"; sin embargo, esto desperdicia el poder de resolución de nanosleep().

[17] Su tercer argumento es ejecutado cada vez que un header no es encontrado, cosa que aquí no necesitamos.

[18] Todo esto es nuevamente consecuencia del principio: "Aislar el código no portable (en un una librería)". En este caso, el código no portable es el que interactúa con cada base de datos específica.

[19] Esta directiva permite analizar la existencia y necesidad de la herramienta ranlib en la plataforma de destino. ranlib se encarga de realizar (en algunas plataformas) ciertas optmizaciones en el archivo de librería para acelerar el acceso a la misma. En la documentación de libtool se menciona que sirve para darle mejor karma

[20] Sería bastante más complicado y potencialmente ineficiente hacer esto de forma inversa: iterar sobre el cursor completamente para obtener el resultado completo y recién allí brindarlo a la aplicación como un (gran) bloque de memoria.

[21] Nuestra pequeña librería no tiene ninguna función para conocer el número de columnas del "result set". Tampoco tiene ninguna facilidad para manejo y reporte de errores, ni para "desconectarnos" de la DBMS.

[22] Texinfo puede producir buenos documentos DocBook con facilidad. En teoría también es posible generar Texinfo a partir de DocBook, pero en la práctica he encontrado problemas con esto.

[23] No puedo dejar de mencionar mi utilitario Qdk (con el que se ha confeccionado la "fuente" de este texto.) Se trata de un preprocesador para hacer más fácil la escritura de documentos DocBook que puede conseguir en: http://qdk.sourceforge.net/.

[24] Como se sabe, la opción -L instruye al compilador a alterar el comportamiento del enlazador (link editor), el cual es normalmente invocado desde aquél. Es por esto que la opción -L, aunque es procesada por el compilador, está en realidad referida al momento del enlace. Lo mismo se puede decir de las librerías especificadas con -l***.

[25] Sin embargo, no son necesarios para ejecutar un programa ya compilado.

[26] Sin embargo, libtool se ha diseñado también para ser invocada manualmente de ser necesario. Consulte su documentación oficial para más información.

[27] Consulte el apéndice C y la documentación oficial de Libtool para más información sobre las versiones de las librerías. Esto es de lectura obligatoria si Ud. pretende distribuir librerías compartidas a otras personas.

[28] Que en Linux normalmente se halla en el archivo /etc/ld.so.conf; se requiere la ejecución de ldconfig tras su modificación.

[29] Algunos detalles sólo aplican a la versión GNU Make (se indica en cada caso), aunque los conceptos son aplicables a todas las versiones de Make.

[30] Si Ud. hubiera usado un nombre distinto a "Makefile" o "makefile", tendría que invocar a make usando algo como make -f archivo.

[31] Realmente se aplica esta regla porque make se da cuenta de que existen los archivos fuente modulo1/2.c para aplicarla. Si éstos no existieran entonces no se aplicaría dicha regla implícita. Por el contrario, si existieran archivos fuente llamados modulo1/2.f (lenguaje Fortran) entonces la regla implícita correspondiente invocaría a un compilador Fortran.

[32] Existen diversos argumentos para no distribuir el código fuente: secreto de fábrica, control del uso, seguridad, etc; todos son cuestionables en ciertas circunstancias.

[33] En el Makefile empleamos "ar" con las opciones "rcs". Considero que no se gana mucho explicándolas por lo que remito a leer el man page de ar si tiene curiosidad.

[34] En algunos sistemas Unix es necesario ejecutar el comando ranlib tras el ar a fin de actualizar un componente especial llamado +__.SYMDEF+ que contiene un índice de todos los símbolos externos definidos en los otros componentes. Este índice permite ganar velocidad en el momento del enlace. La opción "s" de GNU ar (que usa Linux) implica automáticamente a ranlib por lo que no se requiere de este último.

[35] A este nombre completo se le denomina a veces "nombre real" o "real name".

[36] Por ejemplo, las funciones han cambiado su interfaz, o han desaparecido algunas que otrora se proporcionaban, o han cambiado en su comportamiento, etc. El nombre de la librería junto con el número "v" (libXXX.so.v) se conoce como el "soname" de la librería.

[37] Por ejemplo, se han añadido nuevas funciones; se han corregido bugs; se ha optimizado la ejecución, etc.

[38] En ciertas arquitecturas y en ciertos programas, se puede generar un "desbordamiento" de la tabla de relocalización de direcciones. Para evitar esto se indica utilizar -fPIC (en lugar de -fpic), lo cual hace crecer una pizca a los archivos objeto.

[39] La documentación de gcc menciona la posibilidad de añadir código no "independiente de la posición" (opción -mimpure-text) pero esto ocasiona que el código se copie en el momento de la ejecución (no se comparte como debería.) Evite esto compilando siempre con -fpic.

[40] Un detalle muy importante radica en que se está enlazando con el nombre "base" de la librería, sin considerar los valores "v", "m", "r" antes señalados. Esto normalmente obliga a que el nombre "base" sea un enlace simbólico hacia un "real name".

[41] Si se copia la librería hacia cualquiera de estos directorios (a diferencia de /lib y /usr/lib), es necesario ejecutar además el comando ldconfig para refrescar el "caché" de búsqueda de librerías.

[42] Típicamente se proporciona también la versión estática.

[43] En realidad, este es trabajo del enlazador (ld), el cual es invocado por el el compilador.

[44] Esto no significa que en la ejecución se buscará la librería en el directorio actual. Para nuestro caso, seguiremos empleando LD_LIBRARY_PATH.

[45] Es importante mencionar que la interfaz dlopen no es estándar entre Unix’es. Su origen es SunOS y está incluida en POSIX 1003.1-2003, pero algunas versiones de Unix tienen sus equivalentes propietarios.

[46] Se podría añadir el enlace -lm al ejecutable prueba (en lugar de fact3.so.) Esto también funcionaría, pero no es muy correcto en la medida que crea una dependencia innecesaria en "prueba". Supongase que (como usuarios finales) no dispusiéramos de la librería matemática libm.so, entonces con el método recomendado al menos podríamos usar los dos métodos fact1 y fact2. Por el contrario, si el ejecutable se enlaza de antemano, entonces éste fallaría en el momento de su sóla invocación.

[47] Por ejemplo, tendríamos que usar LD_LIBRARY_PATH.

[48] Si se analiza con detenimiento es bastante lógico: en una librería que contiene muchos módulos y muchas funciones, por lo general son sólo unas pocas las que realmente se necesitan. Sería muy costoso considerar a todas en el análisis.

[49] La documentación del enlazador menciona las opciones -( y -) como equivalentes a start-group y end-group. Como los paréntesis requieren quoting por el shell, he preferido exponer la forma "larga".

[50] En Linux se menciona una opción equivalente -rdynamic. Es menos portable.