Todo lo que quisiste saber sobre las DLL y no te atreviste a preguntar (II)

Como podéis ver no se me ha olvidado el tema, simplemente es que he estado demasiado gandulete como para escribir sobre esto, pero una pregunta en el foro web de C++ me ha hecho que me ponga a ello. En teoría esta debería ser la tercera parte en lugar de la segunda, aunque la verdad no es que realmente tenga mucha importancia. Por lo tanto, esta entrada que estás leyendo sigue a esta otra.

Aquí voy a contar los tres modos que hay de usar una DLL desde C y C++, aunque solo voy a explicar dos de ellos porque el tercero digamos que viene de las primeras versiones de Windows y ni siquiera sé si los compiladores modernos la soportan.

Pero antes, una introducción.

¿Qué necesito para usar una DLL de terceros?

Cuando vamos a usar una DLL, ya sea nuestra o de terceros, se nos deben suministrar al menos tres archivos. El primero, que no nos haría falta para el segundo método, es un fichero cabecera conteniendo los prototipos de las exportaciones de la DLL, es decir, las firmas de las funciones y la declaración de los tipos contenidos.

En general, el formato de cada prototipo debe seguir de cerca el siguiente formato:

__declspec(dllexport) __stdcall int HazAlgo(int algo);

O

extern “C” __declspec(dllexport) int HazAlgo(int algo);

La primera firma declara una función con formato de llamada estándar (el __stdcall) que pertenece a una DLL y que resulta exportado, es decir, que podemos usar. La segunda declara lo mismo pero con el protocolo de llamada de C, que también podríamos haber puesto como __cdecl. Del protocolo de llamada ya hablé en la entrada anterior, y en general todos los compiladores de C y C++ deberían entender ambas declaraciones, y si no lo hacen seguro que tienen expresiones similares.

El segundo fichero es lo que se conoce como biblioteca de importación, que es un archivo con la extensión .lib, con la diferencia de que, en este caso, en lugar de contener las propias funciones como en una biblioteca normal, lo que contiene son las firmas de las funciones exportadas y cómo están relacionadas con la propia DLL.

En el mundo Windows existen dos formatos de ficheros .lib: el COFF, que es el que usa Microsoft y compiladores compatibles y el OMF, que es el que usan otros compiladores como el C++Builder. Generalmente todos los compiladores suelen venir con una herramienta de conversión de formato.

Y finalmente necesitamos la DLL y todas sus dependencias si las hubiera.

En general muchas veces no es necesario recompilar el código de nuestra aplicación cada vez que se modifique la DLL si no hemos cambiado nada de la parte pública. Creo que incluso si se añaden funciones a la DLL tampoco es necesario recompilar siempre y cuando no se cambie ninguna declaración de las existentes y enlazadas.

Primer método: el fácil

El método más sencillo para usar una DLL en nuestro código en C o C++ es añadir la biblioteca de importación (el fichero .lib) a las demás bibliotecas en nuestro proyecto, lo que se suele hacer en las opciones del enlazador.

Luego incluimos el fichero cabecera en donde nos haga falta y ya está todo hecho: tan sólo debemos llamar a las funciones de la DLL como si fueran funciones normales y corrientes.

Debemos tener en cuenta que si la DLL contiene clases o tipos no estándar, quizás el enlazador sea incapaz de encontrar los nombres porque cada compilador de C++ ofusca el nombre de las funciones de una manera diferente, y en ese caso sólo nos queda la opción de o bien pedir una DLL compatible con nuestro compilador o bien hacer un strip del fichero LIB, ver cómo se llaman en realidad las funciones, y hacernos nosotros mismos el fichero cabecera. No os lo recomiendo.

El último paso es dejar la DLL en una ruta que nuestro programa sea capaz de encontrar, como el directorio donde se está ejecutando o en algún camino disponible en el PATH.

Segundo método: carga dinámica

No lo he dicho antes, pero cuando se usa el primer método, las DLL se cargan de forma automática a la vez que nuestra aplicación, y siguen en memoria hasta que salimos.

Existe una forma de carga manual un poco más laboriosa, pero que nos permite cosas como la carga dinámica de temas, de extensiones, de plugins o de diferentes versiones de una misma DLL.

Supongamos que tenemos una DLL con las cadenas para cada idioma. Cuando arranquemos nuestra aplicación miramos qué idioma está en la configuración y cargamos manualmente la DLL adecuada. Es lo que hace automáticamente el sistema MUI de Windows y la internacionalización en .NET y otros lenguajes. Pues de igual forma que se hace automáticamente, se puede hacer manualmente.

Para este método sólo necesitamos la DLL en cuestión y la lista de prototipos de funciones. Lo primero es hacernos un puntero a función de cada una de las funciones de la DLL:

int (__stdcall *pHazAlgo)(int);

Luego llamamos a la función de Win32 LoadLibrary() pasándole la cadena en donde está la DLL que queramos cargar. La función nos devolverá un HINSTANCE, que si todo ha ido bien será diferente de NULL. Entonces, para cada una de las funciones de la DLL, la cargamos sobre nuestro puntero mediante GetProcAddress() pasándole el HINSTANCE y la cadena con el nombre de la función en la DLL:

pHazAlgo=GetProcAddress(hLib,”HazAlgo”);

Ahora tendremos un puntero a función (que realmente es un doble puntero, porque ese puntero apunta a una tabla dentro de la DLL que a su vez apunta a la función real). Con ese puntero podemos llamar a la función sin problemas:

int respuesta=pHazAlgo(3);

Quizás tengas que hacer algún tipo de moldeo en la llamada a GetProcAddress().

Cuando queramos descargar de memoria esta DLL, llamamos a FreeLibrary() y será descargada. Nuestros punteros seguirán asignados, pero apuntarán a ningún sitio, por lo que si volvemos a necesitar la DLL (u otra versión de la misma) deberemos repetir el proceso.

Tercer método: ordinales y ficheros DEF

Este es el que no voy a explicar porque es muy antiguo y ni siquiera sé si sigue siendo válido. Antiguamente las funciones dentro de las DLL se guardaban numeradas. Es decir, en lugar de guardar una cadena con el nombre y un puntero a la relación relativa dentro del fichero en los metadatos de la DLL, se guardaba un array de punteros que apuntaba a cada una de las funciones.

Luego se tenía un fichero DEF que ponía el nombre de la función y el ordinal que tenía dentro de la DLL (la posición dentro de ese array), de forma que cuando se compilaba un programa para una DLL de este tipo (usando el fichero DEF en lugar del LIB), se dejaba un array de punteros a funciones que el cargador pareaba con el de la DLL…

Ya os podéis imaginar la que se podía armar si por cualquier motivo ese orden era cambiado por el compilador. El que faltara un ordinal simplemente lanzaba un error, pero el que se cambiara uno por otro podía terminar tumbando no solo nuestra aplicación, sino Windows al completo, ya que en aquella época las DLL sólo se cargaban una vez y eran usadas y compartidas por todas las aplicaciones que la requerían, sistema operativo incluido.

Esto se hacía así porque con la velocidad de los procesadores de aquellos años, el tiempo para buscar la cadena con la función en los metadatos y parearla con el puntero correspondiente consumía una gran cantidad de tiempo, sobre todo si había muchas funciones dentro de una DLL, y en la forma de ordinales tan sólo había que copiar punteros en bucle.

3 comentarios en “Todo lo que quisiste saber sobre las DLL y no te atreviste a preguntar (II)”

  1. me gusta mucho tu blog lo sigo siempre q hay actualizaciones pero sabes?¡ el negro con blanco para leer cansa mucho, no podrias poner un color mas amigable para leer gracias!!

    seria bueno q comentarias tus experiencias de trbajo usando c y derivados, bueno si se puede y no tinenes un contrato de confidencialidad
    thks

  2. @Ángel, casi siempre lo he tenido con fondo blanco, lo que pasa es que estuve trasteando con el formato porque no me salen los textos tachados y al final lo dejé con fondo negro y se me pasó cambiarlo…

    Ahora está con fondo blanco.

    Y no, ni puedo ni debo hablar de mi trabajo, aunque la mayoría de tips que pongo se deben a él. Son cosas que tengo que hacer y las busco o las pienso y las pongo aquí para que me sirvan de recordatorio y de bloc de notas.

    A poco que leas el histórico, verás en qué lenguajes/tecnologías estoy trabajando. Quitando las entradas generales sobre cómo compilar esto o aquello, los anuncios, las entradas son fiel reflejo. En el blog se pueden ver los coletazos de mi etapa linuxera, mi paso por Windows CE como creador de plataformas, mi paso por .NET y C# y finalmente mi vuelta a los orígenes: C++ y MFC, con un tiento al C++ Builder que, la verdad, es peor que nunca.

    Estoy pensando poner una entrada con las herramientas que uso habitualmente, pero tampoco las puedo poner todas o más bién poner para qué las uso…

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *