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:
- No tener que volver a escribir el código (o hacer copy-paste).
- Nos ahorraremos el tiempo de compilar cada vez ese código que ya está compilado. Además, ya sabemos que mientras hacemos un programa, probamos y corregimos, hay que compilar entre muchas y "más muchas" veces.
- El código ya compilado estará probado y será fiable. No las primeras veces, pero sí cuando ya lo hayamos usado en 200 programas distintos y le hayamos ido corrigiendo los errores.
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:
- Uno o más ficheros fuente .c con el código de nuestras funciones.
- Uno o más ficheros de cabecera .h con los tipos (typedefs, structs y enums) y prototipos de las funciones que queramos que se puedan utilizar.
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
#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;
}
{
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>
...
#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?
- Un programa compilado con librerías estáticas es más grande, ya que se hace copia de todo lo que necesita.
- Un programa compilado con librerías estáticas se puede llevar a otro ordenador sin necesidad de llevarse las librerías.
- Un programa compilado con librerías estáticas es, en principio, más rápido en ejecución. Cuando llama a una función de la librería, la tiene en su código y no tiene que ir a leer el fichero de la librería dinámica para encontrar la función y ejecutarla.
- Si cambiamos una librería estática, a los ejecutables no les afecta. Si cambiamos una dinámica, los ejecutables se ven afectados. Esto es una ventaja si hemos cambiado la librería para corregir un error (se corrige automáticamente en todos los ejecutables), pero es un inconveniente si tocar eso nos hace cambiar los ejecutables (por ejemplo, hemos añadido un parámetro más a una función de la librería, los ejecutables ya hechos dejan de funcionar).
¿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:
- Obtener los ficheros objeto (.o) de todos nuestros fuentes (.c). Para ello se compilan con cc -c fuente.c -o fuente.o. La opción -c le dice al compilador que no cree un ejecutable, sino sólo un fichero objeto. Aquí pongo el compilador cc, porque es el que he usado para el ejemplo, pero puede usarse gcc, o el g++ (para C++) o uno de fortran, pascal, etc.
- Crear la librería (.a). Para ello se usa el comando ar con los siguientes parámetros: ar -rv libnombre.a fuente1.o fuente2.o ... La opción -r le dice al comando arque tiene que insertar (o reemplazar si ya están dentro) los ficheros objeto en la librería. La opción -v es "verbose", para que muestre información mientras está haciendo las cosas. A continuación se ponen todos los fichero objeto que deseemos. ar es en realidad un comando mucho más genérico que todo esto y sirve para empaquetar cualquier tipo de fichero (no sólo ficheros objeto). Tiene además opciones para ver qué ficheros hay dentro, borrar algunos de ellos, reemplazarlos, etc.
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 ...)
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:
- Compilar los fuentes, igual que antes, para obtener los objetos.
- Crear la librería con el comando ld. Las opciones para este comando serían ld -o liblibreria.so objeto1.o objeto2.o ... -shared. La opción -o liblibreria.so le indica el nombre que queremos dar a la librería. La opción -shared le indica que debe hacer una librería y no un ejecutable (opción por defecto). objeto1.o, objeto2.o ... son los ficheros objeto que queremos meter en la librería.
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 ...
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:
- Existen liblibreria.a y liblibreria.so. Se coge por defecto liblibreria.a
- Sólo existe una de ellas. Se coge la que existe.
- No existe ninguna de ellas. Error.
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
$ 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.
- suma.c, resta.c y libreria1.h son los fuentes para la librería. Descárgalos y quítales la extensión .txt
- principal.c es el fuente para el programa principal que usa las funciones de la librería. Descárgalo y quítale la extensión .txt
- Makefile es un Makefile para generar todo. Descárgalo y quítale la extensión .txt. Si haces make p1, se generará la librería estática y se compilara principal.c con la librería estática para generar un ejecutable p1. Si haces make p2, se generará la librería dinámica y se compilará principal.c con la librería dinámica para generar un ejecutable p2.
Si ejecutas ./p2 a pelo no funcionará. Acuérdate de poner el directorio actual (en el que se supone está la librería dinámica) en la variable de entorno LD_LIBRARY_PATH.
$ LD_LIBRARY_PATH=.
$ export LD_LIBRARY_PATH
$ ./p2
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.
No hay comentarios:
Publicar un comentario
Gracias por comentar en mi blog. Saludos.