Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


1.4.4b2a   Construir una DLL

§1  Sinopsis

En este epígrafe tratamos los detalles necesarios para construir una librería dinámica; un tipo especial de código ejecutable cuyos recursos pueden ser utilizados desde otros ejecutables.  Entre los usuarios pueden incluirse quizás el propio Sistema Operativo (caso de Windows por ejemplo) u otras librerías.  Aunque hemos señalado que tales librerías pueden tener cualquier terminación ( 1.4.4b), aquí las denominaremos DLLs, apelativo por el que son más conocidas.

Nota: recordar que el Estándar C++ no indica nada respecto a estas librerías, ya que las particularidades del enlazado dinámico dependen del SO (de la plataforma) y poco que ver con el lenguaje en que hayan sido programadas. En particular, las DLLs son específicas de los SOs de Microsoft, aunque por supuesto, otros SOs utilizan también librerías dinámicas.

§2  Construcción de una DLL

Cuando se construye un fuente que será compilado para producir una DLL, los objetos (funciones y clases) que deban ser accesibles desde otros ejecutables, se denominan exportables, también callbacks si son funciones, en atención a una denominación muy usual en la literatura inglesa ("callback functions" [3]).  Esta circunstancia debe ser conocida por el compilador, por lo que es necesario especificar qué recursos se declaran "exportables"; además debe instruirse al "linker" para que genere una librería dinámica en vez de un ejecutable normal.  En el caso de BC++, esto último se consigue con las opciones de compilación -WD/-tWD-WR/-tWR.  Ver ejemplos .

Nota: al tratar de la utilización de librerías dinámicas, veremos que de forma simétrica a lo señalado, desde la óptica del ejecutable que utiliza la librería, estos recursos se denominan importables, y como tales deben también ser señalados al compilador.

El enlazador MinGW dispone de dos opciones: --no-undefined y --enable-runtime-pseudo-reloc, que permiten despreocuparse de la necesidad de declarar exportables o importables los atributos de los objetos cuando se crean o utilizan librerías dinámicas.  La razón es que su presencia hace que todas las funciones sean automáticamente exportadas/importadas por defecto.

En los compiladores para la plataforma Windows existen varias formas para declarar una función o recurso como exportable, pero aquí nos ceñiremos a dos de las más simples y directas, los especificadores _export y dllexport .


Además de los fuentes de la librería, en determinados casos, la creación de una DLL exige la existencia de dos ficheros auxiliares: una librería de importación ( 1.4.4b2c) y un fichero de definición .def ("definition file") al que ya nos hemos referido ( 1.4.4a).  La primera es una librería estática clásica (.lib o .a) que sirve como índice o diccionario de la dinámica. El segundo es un fichero ASCII [6].  En caso de ser necesarios, la creación de estos ficheros auxiliares se realiza generalmente en el mismo momento en que se crea la librería. Sin embargo, en determinadas circunstancias, especialmente cuando se dispone de una DLL construida de la que no se tienen los fuentes, la creación exige de herramientas auxiliares.

La necesidad de tales ficheros depende del compilador y de las circunstancias. La documentación de Microsoft señala que generalmente, la librería de importación es necesaria para usar la librería con enlazado estático, pero no para enlazado dinámico (explícito). En cambio, la documentación de MinGW señala: "la librería de importación es necesaria si (y solo si) la DLL debe ser utilizada por un compilador distinto de la colección de herramientas MinGW, ya que estas son perfectamente capaces de enlazar con sus DLLs sin necesidad de ningún recurso auxiliar".

Nota: en ocasiones, cuando el objeto descrito no es una DLL normal, sino un control ActiveX, un servidor OLE, o un servidor COM, la librería no se denomina "de importación". En su lugar se utiliza el término librería de tipos ( 1.4.4b2d).

§2.1  Tabla de entradas

Cualquiera que sea la forma utilizada, los recursos declarados exportables son incluidos por el enlazador en una tabla especial contenida en la DLL, que se denomina tabla de exportación ("export table") o tabla de entrada ("entry table").  Esta tabla juega aquí un papel similar al diccionario ( 1.4.4b) de las librerías estáticas (ojo , no confundirla con la librería de importación citada en el epígrafe anterior).

La tabla de exportación tiene dos tipos de datos importantes (en realidad son dos tablas): los nombres con que aparecen los recursos [2] y un número de orden. Cuando una aplicación (.exe o librería dinámica) invoca una función situada en una librería, el módulo que realiza la invocación puede referirse al recurso por nombre o por número de orden.  Por supuesto la segunda forma es ligeramente más rápida, ya que no se necesitan comparaciones de cadenas para localizar la entrada.

Cuando un recurso es exportado por número, la parte de nombres de la tabla no necesita ser residente en la memoria del ejecutable que la utilizará.  En cambio, si es exportada por nombres, dicha tabla sí necesita ser residente, y será cargada en memoria cada vez que el módulo sea cargado [1].

Nota: el compilador Borland C++ dispone de la utilidad IMPDEF, que permite conocer las entradas de la tabla de exportación de una DLL ( 1.4.4b2c). Como ocurre con muchos otros aspectos de los ejecutables, es posible indicar al enlazador ciertos detalles sobre los nombres, orden con que queramos que aparezcan estos recursos en la mencionada tabla y permanencia de la misma en memoria ( 1.4.4a).

 A su vez, MS dispone de una utilidad gratuita denominada Dependency Walker, que permite analizar cualquier módulo Windows de 32 o 64 bits (.exe, .dll, .sys, .ocx, etc) y construir un organigrama jerárquico con las dependencias existentes entre sus diversos módulos.  Para cada módulo encontrado, se muestran las funciones exportadas y cuales de ellas son utilizadas por otros módulos.  Otro esquema muestra el mínimo conjunto de ficheros requeridos incluyendo información detallada sobre cada uno de ellos.  Es una herramienta insustituible para situaciones en que se obtienen errores relacionados con la carga y ejecución de los distintos módulos de una aplicación.    http://dependencywalker.com/

Es importante recordar, sobre todo si vamos a construir DLLs que serán utilizadas por terceros, que los nombres de los recursos exportados no pueden colisionar con ningún otro nombre utilizado por el programa o por otra DLL del sistema, de forma que debemos asegurarnos que estos nombres serán únicos.

§2.2  Especificador  _export

Tanto si se usa el copilador C++ Borland como Visual de MS, los recursos "exportables" pueden ser declarados con los especificadores _export o __export (son equivalentes).

Nota: recordemos que C++ dispone de una palabra clave específica: export ( 4.12.1-2) cuyo significado se asemeja al que utilizamos aquí:  "indica al compilador que la declaración será accesible desde otras unidades de compilación".  Sin embargo, tener en cuenta que _export y export no tienen nada que ver entre sí. La primera es una particularidad de ciertos compiladores; la segunda es una palabra clave del C++ Estándar.

Sintaxis

Son posibles tres formas, según que el recurso a exportar sea una clase, función o variable normal:

__export valor-devuelto nombre-funcion (argumentos);  // Ilegal !!

valor-devuelto __export nombre-funcion (argumentos);

valor-devuelto _export nombre-funcion (argumentos);   §2.2.1a

class _export nombre-de-clase;                        §2.2.1b

tipo-de-dato _export nombre-de-variable;              §2.2.1c

Ejemplos:

extern "C" _export double MayorValor(double, double);

class _export miClase;

double _export db;

Nota: si se utiliza el especificador _export, para exportar una función, dicha función debe ser exportada por nombre en vez de por número de orden (§2.1 ).

§2.3  Especificador  dllexport

Los recursos "exportables" pueden ser también declarados mediante el especificador  __declspec(dllexport)  ( 4.4.1b).

Sintaxis

Existen dos formas:

__declspec(dllexport) valor-devuelto funcion (argumentos);   §2.3.1a
class __declspec(dllexport) nombre-de-clase;                 §2.3.1b

__declspec(dllexport) tipo-de-dato nombre-de-variable;       §2.3.1c

Ejemplos:

extern "C" __declspec(dllexport) double MayorValor(double, double);
class __declspec(dllexport) A { /* ... */ };

__declspec(dllexport) int x;

§3  Ejemplo de construcción de Librería Dinámica

Para ilustrar el proceso de creación de una librería dinámica con un ejemplo concreto, utilizaremos una modificación de los ficheros utilizados anteriormente para construir una librería estática ( 1.4.4.b1).  Suponemos que los fuentes y la cabecera que siguen están situados en el directorio D:\LearnC\planets\dlibs (observe que no existe una función main o WinMain en ninguno de los ficheros).

// planets.cpp

#include <windows.h>

bool WINAPI DllMain (HINSTANCE hInst, DWORD dwd, LPVOID lpv ) {
   return true;
}

 

// planet1.cpp

#include <iostream>

extern "C" __declspec(dllexport) void showMercury () {
   std::cout << "Primer planeta: Mercurio" << std::endl;
}

 

// planet2.cpp

#include <iostream>

extern "C" __declspec(dllexport) void showVenus () {
   std::cout << "Segundo planeta: Venus" << std::endl;
}

 

// planet3.cpp

#include <iostream>

extern "C" __declspec(dllexport) void showEarth () {
   std::cout << "Tercer planeta: Tierra" << std::endl;
}

.

// planets.h
#ifndef _PLANETS_H
#define _PLANETS_H

extern "C" __declspec(dllimport) void showMercury ();
extern "C" __declspec(dllimport) void showVenus ();
extern "C" __declspec(dllimport) void showEarth ();

#endif        // _PLANETS_H

La primera observación y más importante, es que, contra lo que ocurre con las librerías estáticas ( 1.4.4b1), en las que no es generalmente necesario atender a consideraciones relativas al planchado de nombres o convención de llamada de las funciones ( 4.4.6a), en las dinámicas sí deben tenerse en cuenta estas cuestiones en los recursos declarados como exportables.  La razón es que frecuentemente los distintos módulos, ejecutables y DLLs que componen la aplicación, pueden estar incluso escritos en lenguajes diferentes, por lo que la interoperabilidad exige de un acuerdo respecto a la convención adoptada.  En las aplicaciones para Windows la convención es no utilizar planchado para los nombres de funciones exportables, y la convención de llamada __pascal para las funciones que serán invocadas por el Sistema ("callbacks").

Respecto a los módulos planet1.cpp, planet2.cpp y planet3.cpp, observar que las funciones showMercury, showVenus y showEarth se declaran exportables mediante el especificador __declspec(dllimport), al tiempo que se les añade el especificador extern "C" para garantizar que sus identificadores no serán planchados ("mangled").

El módulo planets.cpp es singular y un tanto extraño a la luz de lo expuesto hasta aquí.  La razón de su existencia es que los entornos Windows de MS, exigen que en cada DLL exista una función de nombre DllMain o DllEntryPoint. Esta función es invocada por los mecanismos del Sistema encargados  de la carga y descarga de la librería. En cada ocasión le pasan ciertos argumentos, y la función debe devolver true, de lo contrario se genera un error.  Para garantizar que pueda ser tratada como una callback, su convención de llamada es WINAPI, un typedef de windows.h que se traduce en __pascal. Los tipos de sus argumentos, HINSTANCE, DWORD y LPVOID son igualmente typedefs de windows.h que se traducen en tipos básicos ( 3.2.1a1).

Nota: diseñando convenientemente esta función inicial, es posible adoptar distintas acciones según el evento (motivo por el que se ha producido la invocación).  En su forma más general, suele tener el siguiente aspecto:

BOOL WINAPI DllMain (HINSTANCE hIn, DWORD dwd, LPVOID lpv) {
   switch (dwd)  {
   case DLL_PROCESS_ATTACH:

     //  ...
     break;
   case DLL_PROCESS_DETACH:

     // ...
     break;
   case DLL_THREAD_ATTACH:

     // ...
     break;
   case DLL_THREAD_DETACH:

     // ...
     break;
   }
   return true;    // o false en caso de error
}

Por su parte, el módulo de cabecera planets.h, que está pensado para ser incorporado en los módulos del ejecutable que utilizará la DLL, contiene las declaraciones y macros necesarios para utilizar los recursos exportables, pero observe que desde la óptica del módulo que los utilizará, estos recursos son importables, de forma que se utiliza el especificador __declspec(dllimport).

  Es frecuente que el fichero de cabecera sea utilizado también en los ficheros de la propia librería, con lo que pueden presentarse situaciones contradictorias. La razón es que al ser utilizado en la construcción de la librería, ciertos identificadores deben ser declarados dllexport, mientras que al ser utilizado en la construcción de los ejecutables, los identificadores deben ser dllimport.  Para resolver el conflicto se recurre a utilizar directivas de preproceso, de forma que el fichero de cabecera puede tener el siguiente aspecto:

// planets.h

#ifndef _PLANETS_H
#define _PLANETS_H

#ifdef BUILD_DLL    // en la construcción de la librería
   #define EXPORT __declspec(dllexport)
#else               // en la construcción del ejecutable
   #define EXPORT __declspec(dllimport)
#endif

extern "C" EXPORT void showMercury ();
extern "C" EXPORT void showVenus ();
extern "C" EXPORT void showEarth ();

#endif        // _PLANETS_H

Para que el mecanismo funcione, es necesario que en la construcción de la librería esté definida la macro BUILD_DLL, y que no lo esté durante la construcción de los ejecutables que utilizarán la librería. Esto puede hacerse en el comando que invoca al compilador.  Por ejemplo, utilizando GNU gcc, la compilación del fichero planets1.cpp puede ser un comando del siguiente aspecto [4]:

gcc -c -DBUILD_DLL planet1.cpp

En cambio, la compilación de un fichero application.cpp de la aplicación que utilizara la DLL anterior, tendría este otro:

gcc -c application.cpp

§3.1 Construcción con GNU Make

La construcción de una librería dinámica con el compilador GNU puede efectuarse de varias formas. Aquí analizaremos la más simple y canónica. Para ello utilizaremos un makefile, denominado makefil.gnu, situado en el mismo directorio que los fuentes de la librería D:\LearnC\planets\dlibs.

# Makefil.GNU para GNU make
# crear librería dinámica planets.dll

CXXFLAGS = -I"C:/DEV-CPP/lib/gcc/mingw32/3.4.2/include" \
           -I"C:/DEV-CPP/include/c++/3.4.2/backward" \
           -I"C:/DEV-CPP/include/c++/3.4.2/mingw32" \
           -I"C:/DEV-CPP/include/c++/3.4.2" \
           -I"C:/DEV-CPP/include" 

all: planetsG.dll

planetsG.dll: planets.o planet1.o planet2.o planet3.o
     g++ -shared -o planetsG.dll planets.o planet1.o planet2.o planet3.o

planets.o: planets.cpp
     g++.exe -c planets.cpp -o planets.o $(CXXFLAGS)

planet1.o: planet1.cpp
     g++.exe -c planet1.cpp -o planet1.o $(CXXFLAGS)

planet2.o: planet2.cpp
     g++.exe -c planet2.cpp -o planet2.o $(CXXFLAGS)

planet3.o: planet3.cpp
     g++.exe -c planet3.cpp -o planet3.o $(CXXFLAGS)

Las cuatro últimas reglas sirven para preparar los ficheros-objeto que serán después enlazados juntos a través del compilador GNU g++.exe para formar la librería. La opción -shared indica que se genere una librería compartida, que es el nombre que reciben estas librerías en Unix/Linux [5].

 La invocación del makefile se realiza de la forma usual; nos situamos en el directorio correspondiente, incluimos el directorio con los binarios de Dev-Cpp en nuestra variable de entorno PATH, e invocamos la utilidad para que utilice nuestro makefile:

D:\>cd LearnC\planets\dlibs
D:\LearnC\planets\dlibs>set PATH=C:\Dev-Cpp\bin;%path%
D:\LearnC\planets\dlibs>make -f makefil.gnu

Como resultado, se crean en el directorio actual los correspondientes ficheros-objeto (que pueden ser borrados) y la librería propiamente dicha planetsG.dll.  A continuación ya estamos en disposición de utilizar nuestra flamante librería en cualquier aplicación ( 1.4.4b2b).

Si fuese necesario obtener una librería de importación para nuestra DLL, podemos modificar la primera regla, que pasaría a tener el siguiente diseño:

planets.dll: planets.o planet1.o planet2.o planet3.o

    g++ -shared -o planetsG.dll planets.o planet1.o planet2.o planet3.o \
    -Wl,--out-implib,planets.a

El parámetro -Wl, señala al compilador (g++) que debe pasar al enlazador (ld) los argumentos que siguen. En nuestro caso, los argumentos --out-implib,planets.a  indican al enlazador que debe crear la librería de importación planets.a.

§3.2 Construcción de una librería dinámica con Borland C++ 5.5 Make

Asumiendo que utilizamos Borland C++ 5.5, la compilación de los ficheros del ejemplo para construir una librería dinámica requiere utilizar la opción de compilación -WDR. Utilizaremos un makefile, al que llamaremos makefil.bor, situado en el mismo directorio que los fuentes de la librería D:\LearnC\planets\dlibs, con el siguiente diseño:

# Makefil.bor para Make de Borland C++ 5.5.1
# crear librería dinámica planetsB.dll y de importación planetsB.lib
.autodepend
LIBS      = /LE:\BorlandCPP\Lib

all: planetsB.dll planetsB.lib

planetsB.dll: planets.cpp planet1.cpp planet2.cpp planet3.cpp
   bcc32 -eplanetsB -P -Q $(LIBS) -WDR planets.cpp planet1.cpp planet2.cpp planet3.cpp

# -WDR Generate a .DLL executable. 
# -P Perform C++ compile regardless of source extension
# -Q Extended compiler error information (Default = OFF)

planetsB.lib: planetsB.dll
   implib -c planetsB.lib planetsB.dll

La invocación al compilador bcc32 con los argumentos indicados, genera la librería dinámica planetsB.dll (no olvide incluir la directiva .autodepend). A continuación, la invocación a implib genera la librería de importación planetsB.lib a partir de la información contenida en la anterior.  Como veremos en la página siguiente ( 1.4.4b2b) esta última deberá ser enlazada estáticamente con las aplicaciones que utilicen nuestra librería.

La invocación de Make sigue la forma usual:

C:\Windows>D:
D:\>cd LearnC\planets\dlibs
D:\LearnC\planets\dlibs>set PATH=E:\BORLAN~1\BIN;%path%
D:\LearnC\planets\dlibs>make -f makefil.bor

Si las cosas funcionan como es de suponer, junto con algunos ficheros auxiliares (que pueden ser borrados), se obtienen la librería dinámica planetsB.dll y la de importación planetsB.lib. Recuerde que junto a estos últimos, la cabecera planets.h también debe ser distribuida a los usuarios potenciales de la librería.

Nota:  ver un fichero de definición ( 1.4.4a), obtenido a partir del la librería planetsB.dll aquí construida ( 1.4.4b2c).

  Inicio.


[1]  Aunque para ahorrar memoria y mejorar el acceso puede utilizarse una exportación/acceso por número, Microsoft recomienda que las librerías dinámicas se exporten por nombre; de lo contrario no se garantiza que nuestras librerías sean utilizables por todas las plataformas y versiones de Windows.

[2]  Los nombres asignados a los recursos en la tabla de exportación (que se verán desde el exterior), no tienen porqué coincidir con los nombres asignados a tales recursos en la aplicación (nombres interiores),  que dependerán del nombre utilizado en el fuente y de cualquier posible "planchado" o alteración realizada posteriormente por el compilador ( 4.4.6a)

[3]  Traducido literalmente "Callback" es retrollamada.  Una expresión muy frecuente en comunicaciones para señalar la comunicación que establece el proveedor del servicio después que el usuario ha manifestado su deseo de hacerlo.  Como ya hemos señalado, en ingeniería de software "callback" indica una función que puede ser accesible desde otros ejecutables, pero en realidad el término no se aplica a todas, sino a las que cumplen ciertas misiones específicas (que recuerdan la acepción en comunicaciones).  Hay circunstancias en que determinadas rutinas de una aplicación son llamadas a desde el cuerpo de una función.  A su vez, esta función es definida como exportable (sería un "callback"), de forma que esperamos que sea invocada por el Sistema cuando ocurran determinados eventos.

En la programación para Windows, la más conocida y famosa es el denominado procedimiento Windows  ("Windows procedure"),  que en realidad si programamos en C++, no es un "procedure" sino una función que mediante un proceso especial, es declarada "exportable"; en este caso, accesible desde el Sistema.  En un programa Windows este "callback" representa la vía de comunicación entre el Sistema y la aplicación.

Si establecemos una analogía con las retrollamadas.  Aquí el proveedor de servicio es el Sistema Operativo y el usuario es la aplicación.  La aplicación informa al Sistema su deseo de comunicación invocando una función de la API ( 1.7.1) del sistema ( RegisterClass ) en la que pasa un puntero a su "Windows procedure", de este modo el sistema puede invocar dicha función mediante este puntero y pasar a la aplicación información sobre los eventos ocurridos.

  Como verdadera curiosidad para los que no han programado nunca este tipo de aplicaciones, señalar que esta función exportable pertenece al programa pero no es invocada por él. De hecho, es frecuente que no exista en todo el programa ningún código que la invoque directamente.  En este "callback" se define qué hace el programa con cada mensaje que recibe del Sistema; normalmente mediante una larga sentencia switch ( 4.10.2).  Digamos finalmente que en muchas librerías utilizadas para programar en entornos gráficos como Windows o GTK, los eventos enviados desde el Sistema a la aplicación suelen denominarse señales ("signals"), y las "callbacks" asociadas con ellas ranuras o huecos ("slots").  Una señal puede tener múltiples slots y un slot puede responder a diferentes señales (por supuesto en la programación C++ los slots son funciones).

Puede encontrar una magnífica introducción al tema en "CALLBACKS IN C++ USING TEMPLATE FUNCTORS" de Rich Hickey   www.tutok.sk 

[4]  El parámetro -D establece la macro BUILD_DLL.  Aquí se obtendrá un objeto planets1.o que supuestamente se utilizará para construir la librería, aunque en nuestro ejemplo, el fichero planets1.cpp no utiliza la cabecera planets.h y por tanto, la precaución es innecesaria.

[5]  GNU también dispone de la utilidad dllwrap (dllwrap.exe en el entorno Windows) que permite construir una librería dinámica. Sin embargo, actualmente (2006) se considera una utilidad a extinguir ("deprecated") en favor de la opción -shared del compilador que utilizamos aquí.

[6]  Puede encontrarse información adicional al respecto en  www.redhat.com.