Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.org

Librerías estáticas y dinámicas

Según vamos haciendo programas de ordenador, nos damos cuenta que algunas partes del código se utilizan en muchos de ellos. Por ejemplo, podemos tener varios programas que utilizan números complejos y las funciones de suma, resta, etc son comunes. También es posible, por ejemplo, que nos guste hacer juegos, y nos damos cuenta que estamos repitiendo una y otra vez el código para mover una imagen (un marcianito o a Lara Croft) por la pantalla.

Sería estupendo poder meter esas funciones en un directorio separado de los programas concretos y tenerlas ya compiladas, de forma que podamos usarlas siempre que queramos. Las ventajas enormes de esto son:

La forma de hacer esto es hacer librerías. Una librería son una o más funciones que tenemos ya compiladas y preparadas para ser utilizadas en cualquier programa que hagamos.  Hay que tener el suficiente ojo cuando las hacemos como para no meter ninguna dependencia de algo concreto de nuestro programa. Por ejemplo, si hacemos nuestra función de mover la imagen de Lara Croft, tendremos que hacer la función de forma que admita cualquier imagen, ya que no nos pegaría nada Lara Croft dando saltos en un juego estilo "space invaders".

Cómo tenemos que organizar nuestro código

Para poder poner nuestro código en una librería, necesitamos organizarlo de la siguiente manera:

Como siempre, vamos a hacer un ejemplo. Los ficheros serían estos:

libreria1.h
#ifndef _LIBRERIA_1_H
#define _LIBRERIA_1_H
int suma (int a, int b);
int resta (int a, int b);
#endif

libreria1.c
int suma (int a, int b)
{
    return a+b;
}
int resta (int a, int b)
{
   return a-b;
}

Es un fichero con un par de funciones simples de suma() y resta().

Un detalle importante a tener en cuenta, son los #define del fichero de cabecera (.h). Al hacer una librería, no sabemos en qué futuros programas la vamos a utilizar ni cómo estarán organizados. Supongamos en un futuro programa que hay un fichero de cabecera fichero1.h que hace #include del nuestro. Imaginemos que hay también un fichero2.h que también hace #include del nuestro. Finalmente, con un pequeño esfuerzo más, imaginemos que hay un tercer fichero3.c que hace #include de fichero1.h y fichero2.h, es decir, más o menos lo siguiente:

fichero1.h
#include <libreria1.h>
...

fichero2.h
#include <libreria1.h>
...

fichero3.c
#include <fichero1.h>
#include <fichero2.h>
...

Cuando compilemos fichero3.c, dependiendo de lo que haya definido en libreria1.h, obtendremos un error. El problema es que al incluir fichero1.h, se define todo lo que haya en ese fichero, incluido lo de libreria1.h. Cuando se incluye fichero2.h, se vuelve a intentar definir lo contenido en libreria1.h, y se obtiene un error de que esas definiciones están definidas dos veces.

La forma de evitar este problema, es meter todas las definiciones dentro de un bloque #ifndef - #endif, con el nombre (_LIBRERIA_1_H en el ejemplo) que más nos guste y distinto para cada uno de nuestros ficheros de cabecera. Es habitual poner este nombre precedido de _, acabado en _H y que coincida con el nombre del fichero de cabecera, pero en mayúsculas.

Dentro del bloque #ifndef - #endif, hacemos un #define de ese nombre (no hace falta darle ningún valor, basta con que esté definido) y luego definimos todos nuestros tipos y prototipos de funciones.

Cuando incluyamos este fichero por primera vez, _LIBRERIA_1_H no estará definido, así que se entrará dentro del bloque #ifndef - #endif y se definirán todos los tipos y prototipos de funciones, incluido el mismo _LIBRERIA_1_H. Cuando lo incluyamos por segunda vez, _LIBRERIA_1_H ya estará definido (de la inclusión anterior), por lo que no se entrará en el bloque #ifndef - #endif, y no se redefinirá nada por segunda vez.

Es buena costumbre hacer esto con todos nuestros .h, independientemente de que sean o no para librerías. Si te fijas en algún .h del sistema verás que tienes este tipo de cosas hasta aburrir. Por ejemplo, en /usr/include/stdio.h, lo primero que hay después de los comentarios, es un #ifndef _STDIO_H.

Librerias estáticas y dinámicas

En linux podemos hacer dos tipos de librerías: estáticas y dinámicas.

Una librería estática es una librería que "se copia" en nuestro programa cuando lo compilamos. Una vez que tenemos el ejecutable de nuestro programa, la librería no sirve para nada (es un decir, sirve para otros futuros proyectos). Podríamos borrarla y nuestro programa seguiría funcionando, ya que tiene copia de todo lo que necesita. Sólo se copia aquella parte de la librería que se necesite. Por ejemplo, si la librería tiene dos funciones y nuestro programa sólo llama a una, sólo se copia esa función.

Una librería dinámica NO se copia en nuestro programa al compilarlo. Cuando tengamos nuestro ejecutable y lo estemos ejecutando, cada vez que el código necesite algo de la librería, irá a buscarlo a ésta. Si borramos la librería, nuestro programa dará un error de que no la encuentra.

 ¿Cuáles son las ventajas e inconvenientes de cada uno de estos tipos de librerías?

¿Qué tipo de librería uso entonces?

Es como siempre una cuestión de compromiso entre las ventajas y los inconvenientes. Para programas no muy grandes y por simplicidad, yo suelo usar librerías estáticas. Las dinámicas están bien para programas enormes o para librerías del sistema, que como están en todos los ordenadores con linux, no es necesario andar llevándoselas de un lado a otro.

En unix las librerías estáticas suelen llamarse libnombre.a y las dinámicas libnombre.so, donde nombre es el nombre de nuestra librería.

Compilar y enlazar con librerías estáticas

Una vez que tenemos nuestro código, para conseguir una librería estática debemos realizar los siguientes pasos:

Hacer todo este proceso a mano cada vez puede ser un poco pesado. Lo habitual es hacer un fichero de nombre Makefile en el mismo directorio donde estén los fuentes de la librería y utilizar make para compilarla. Si no sabes de qué estoy hablando, échale un ojo a la paginilla de los makefiles. Afortunádamente, las reglas implícitas de make ya saben hacer librerías estáticas. El fichero Makefile quedaría tan sencillo como esto:

Makefile
CFLAGS=-I<path1> -I<path2> ...
libnombre.a: libnombre.a (objeto1.o ojbeto2.o ...)
 

En CLAGS debes poner tantas opciones -I<path> como directorios con ficheros .h tengas que le hagan falta a los fuente de la librería para compilar.

La librería depende de los ficheros objetos que hay dentro de ella. Eso se pone poniendo el nombre de la librería y entre paréntesis los ficheros objeto. Hay algunas verisones de make que sólo admiten un fichero objeto dentro de los paréntesis. Debe ponerse entonces

libnombre.a: libnombre.a(objeto1.o) libnombre.a(objeto2.o) ...

Ya tenemos la librería. Ahora, al compilar nuestro programa con el compilador, debemos decirle dónde están las librerías y cuales son. La orden de compilación quedaría entonces

$ cc -o miprograma miprograma.c -I<path1> -I<path2> ... -L<path1> -L<path2> ... -llibreria1 -llibreria2

Los -I<path> son para indicar dónde están los ficheros de cabecera necesarios para la compilación (tanto propios del programa como los de nuestras librerías).

Los -L<path> son para indicar los directorios en los que se encuentran las librerías.

Los -llibreria son para indicar que se debe coger esa librería. En el comando sólo ponemos "librería". El prefijo lib y la extensión .a ya la pone automáticamente el compilador.

Hay un detalle importante a tener en cuenta. Las librerías deben ponerse de forma que primero esté la de más alto nivel y al final, la de más bajo nivel. Es decir, tal cual lo tenemos en el ejemplo, libreria1 puede usar funciones de libreria2, pero no al revés. El motivo es que al compilar se van leyendo las librerías consecutivamente y cargando de cada una de ellas sólo lo necesario. Vamos a verlo con un ejemplo

Supongamos que miprograma.o llama a la funcion1 de libreria1 y esta funcion1 llama a funcion2 de libreria2. El compilador lee miprograma.o. Como este necesita funcion1, la apunta como "necesaria". Luego lee libreria1. Busca en las funciones necesarias, encuentra funcion1 y la carga. Como funcion1 llama a funcion2, apunta funcion2 como función necesaria. Luego lee libreria2 y como funcion2 es necesaria, la carga. Todo correcto.

Supongamos ahora que le hemos dado la vuelta al orden, que hemos puesto -llibreria2 antes que -llibreria1. El compilador lee miprograma.c. Como este necesita funcion1, se apunta como "necesaria". Luego lee libreria2. Como funcion1 no es de esta libreria y no hay más funciones "necesarias" (hasta ahora), ignora libreria2 y no carga nada de ella. Luego lee libreria1, carga funcion1 y ve que esta necesita funcion2. Apunta funcion2 como necesaria pero ... ya se han acabado las librerias. Se obitiene un error de "linkado" en el que dice que "no encuentro funcion2".

Esto nos dice también que tenemos que tener un cierto orden a la hora de diseñar librerías. Debemos hacerlas teniendo muy claro que unas pueden llamar a otras, pero no las otras a las unas, es decir, organizarlas como en un arbol. Las de arriba pueden llamar a funciones de las de abajo, pero no al revés.

Existe una pequeña trampa, pero no es muy elegante. Consiste en poner la misma librería varias veces en varias posiciones. Si en el supuesto que no funcionaba hubiesemos puesto otra vez al final -llibreria2, habría compilado.

Compilar y "enlazar" con librerías dinámicas

Para compilar los mismos ficheros, pero como librería dinámica, tenemos que seguir los siguientes pasos:

Igual que antes, hacer esto a mano puede ser pesado y se suele hacer un Makefile para compilar con make. Al igual que antes, si no sabes de que estoy hablando, ahí tienes la paginilla de los makes. Desgraciadamente, las reglas implícitas no saben hacer librerías dinámicas (o, al menos, yo no he visto cómo), así que tenemos que trabajar un poco más en el Makefile. Quedaría algo así como:
 
Makefile
liblibreria.so: objeto1.c objeto2.c ...
   cc -c -o objeto1.o objeto1.c
   cc -c -o objeto2.o objeto2.c
   ...
   ld -o liblibreria.so objeto1.o objeto2.o ... -shared
   rm objeto1.o objeto2.o ...

La librería depende de los fuentes. Se compilan para obtener los .o (habría que añadir además las opciones -I<path> que fueran necesarias), se construye la librería con ld y se borran los objetos generados. He hecho depender la librería de los fuentes para que se compile sólo si se cambia un fuente. Si la hago depender de los objetos, como al final los borro, siempre se recompilaría la librería.

El comando ld es más específico que ar, y no he encontrado opciones para modificar o borrar los objetos que hay dentro de la librería. No queda más remedio que construir la librería entera cada vez que se modifique algo.

Una vez generada la librería, para enlazar con ella nuestro programa, hay que poner:

cc -o miprograma miprograma.c -I<path1> -I<path2> ... -L<path1> -L<path2> ...  -Bdynamic -llibreria1 -llibreria2

El comando es igual que el anterior de las librerías estáticas con la excepción del -Bdynamic. Es bastante habitual generar los dos tipos de librería simultáneamente, con lo que es bastante normal encontrar de una misma librería su versión estática y su versión dinámica. Al compilar sin opción -Bdynamic puden pasar varias cosas:

La opción -Bdynamic cambia el primer caso, haciendo que se coja liblibreria.so en vez de liblibreria.a. La opción -Bdynamic afecta a todas las librerías que van detrán en la línea de compilación. Para volver a cambiar, podemos poner -Bstatic en cualquier momento.

Una vez compilado el ejecutable, nos falta un último paso. Hay que decirle al programa, mientras se está ejecutando, dónde están las librerías dinámicas, puesto que las va a ir a buscar cada vez que se llame a una función de ellas. Tenemos que definir la variable de entorno LD_LIBRARY_PATH, en la que ponemos todos los directorios donde haya librerías dinámicas de interés.

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:<path1>:<path2>:<path3>
$ export LD_LIBRARY_PATH

Siendo <path> los directorios en los que están las librerías dinámicas. Se ha puesto el $LD_LIBRARY_PATH pata mantener su valor anterior y añadirle los nuevos directorios.

¿Te acuerdas del ejemplo del principio con la suma?. Aquí están todos los fuentes para que puedas jugar con ellos.

En el Makefile hay algunas cosillas que he añadido respecto a lo explicado y las comento. He puesto la opción de compilación -Wall para obtener todos los warning posibles. También hay un objetivo "clean", que sirve para borrar las librerías y los ejecutables.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: