1.4.4b2b Usar una DLL
§1 Sinopsis
En contra de lo que ocurre en su construcción, que sigue un
proceso único con independencia de como vaya a ser utilizada más tarde (
1.4.4b2a), existen varias formas de utilizar
los recursos de una librería dinámica desde otro ejecutable. En el
presente capítulo se exponen los detalles de su utilización, incluyendo ejemplos de construcción de una
aplicación que utiliza los recursos de las DLLs
de la página anterior.
§2 Generalidades
El acceso desde un ejecutable a un identificador definido en una DLL, como puede ser la invocación de una función o la utilización de una clase de librería, se denomina importar y el objeto "importable". La razón de este apelativo es obvia: el programa debe disponer de un mecanismo (cargador) que permita cargar en memoria la DLL, para a continuación, acceder a dicho recurso que se encuentra fuera del ejecutable (recuerde todos los ejecutables deben ser previamente cargados en memoria). De forma recíproca, el hecho de hacer accesibles desde el exterior determinados recursos de una librería se denomina exportar, y dichos objetos se declaran exportables.
Evidentemente, además de ser cargadas, tales librerías deben enlazarse de alguna
forma con los ejecutables que utilicen sus recursos. Veremos que existen
dos modalidades para este enlazado .
De otro lado, también resulta evidente que el programa usuario debe tener
algún modo de declarar al compilador que determinadas funciones o recursos
son "importados", es decir, que están en librerías dinámicas y por
tanto, su acceso es un tanto especial.
Nota: existen herramientas que permiten conocer las librerías dinámicas utilizadas por un ejecutable. Por ejemplo, la utilidad tdump del compilador BC++.
§3 Declarar un recurso importado
En el caso de las plataformas BC++, MSVC y GNU MinGW para Windows, la declaración de que un recurso se encuentra en una librería
dinámica, y por tanto es "importado", puede hacerse de dos formas, que como se verá, son simétricas de
las utilizadas en la construcción (
1.4.4b2a) para declarar que
un recurso es exportable.
a.- Utilizar el declarador _import
b.- Utilizar el declarador _declspec(dllimport)
§3.1 Especificador _import
Los recursos "importados" pueden ser declarados con los especificadores _import o __import (son equivalentes).
Sintaxis
Son posibles tres formas, según que el recurso a importar sea una función, una clase o una variable normal:
valor-devuelto _import nombre-funcion (argumentos); §3.1.1a
class _import nombre-de-clase; §3.1.1b
tipo-de-dato _import nombre-de-variable; §3.1.1c
Ejemplos:
extern "C" _import double MayorValor(double, double);
class _import miClase;
double _import db;
§3.2 Especificador dllimport
Los recursos importados pueden ser también declarados mediante el especificador
__declspec(dllimport) (
4.4.1b).
Sintaxis
Existen dos formas:
__declspec(dllimport) valor-devuelto funcion (argumentos); §3.2.1a
__declspec(dllimport) tipo-de-dato nombre-de-variable; §3.2.1b
Ejemplos:
extern "C" __declspec(dllimport) double MayorValor(double, double);
__declspec(dllimport) int x;
§3 Tipos de enlazado
Atendiendo a la forma en que se relaciona la DLL con el
ejecutable que deba utilizar sus recursos, existen dos formas de enlazado: estático y dinámico,
ya citados en el capítulo anterior (
1.4.4), pero cuyo significado cuando se refiere a
librerías dinámicas tiene algunos matices que repasaremos aquí.
§3.1 DLL
enlazada estáticamente. Significa que la
DLL estará siempre presente (cargada) cuando corra el ejecutable. Para
ello, algunos compiladores exigen la utilización de una librería
estática auxiliar (.LIB), denominada librería
de importación [1], que es enlazada
estáticamente con el ejecutable. Esta librería auxiliar contiene
referencias a la DLL (
1.4.4b2c). En otros casos, estas referencias son
incluidas por el enlazador en el ejecutable en base a la información extraída
directamente de la librería.
En cualquier caso, el fichero ejecutable contiene las referencias que momentáneamente no están resueltas, pero en cuanto comienza la ejecución, la DLL es cargada en memoria y las referencias pueden ser resueltas. Las DLLs enlazadas estáticamente a un ejecutable .EXE son cargadas e inicializadas por el módulo de inicio como cualquier otro módulo del programa (sin que el programador tenga que hacer nada especial al respecto). Es decir, que serán inicializadas antes que comience la ejecución de main.
Nota: no confundir lo anterior
"librería dinámica enlazada estáticamente" con
"librería de enlazado estático". Esto último significa que se
ha enlazado una librería estática (
1.4.4b1), generalmente con
extensiones .a o .lib, que contiene recursos (.OBJ) que se han incluido en el
ejecutable durante el proceso de enlazado.
§3.2 DLL
enlazada dinámicamente significa que su carga se realiza solo en el momento en que es necesitada por el
ejecutable. Este tipo de librería es inicializada solo en el momento de la
carga, siendo el programador responsable de decidir el momento de carga y
eventualmente el de descarga.
En el caso de Windows, para la carga pueden utilizarse dos funciones de la API del sistema: LoadLibrary() y LoadLibraryEx(). La segunda permite establecer algunas particularidades sobre la forma en que se realizará la carga. Para la descarga puede utilizarse FreeLibrary(); tienen el siguiente aspecto:
HINSTANCE LoadLibrary(LPCTSTR);
HINSTANCE LoadLibraryEx(LPCTSTR, HANDLE, DWORD);
BOOL FreeLibrary(HMODULE);
§4 Formas de uso
Según lo anterior, son posibles dos posibilidades de uso de una librería dinámica:
a.- Librería dinámica (DLL) enlazada estáticamente. En ocasiones, mediante una librería de importación .LIB que referencia la DLL. Para esto solo hay que enlazar la mencionada librería de importación con el resto de las que componen el ejecutable.
b.- Librería dinámica (DLL) enlazada dinámicamente. Esto puede hacerse de dos formas:
b1.- Enlazando la librería estáticamente mediante la librería de importación (como en el caso anterior), pero utilizando la opción de carga retrasada (
1.4.4b2e).
b2.- Utilizando las funciones ya citadas de la API de windows: LoadLibrary() y LoadLibraryEx(). A continuación, se utiliza GetProcAddress() para obtener punteros individuales a los recursos que deban utilizarse.
Para ilustrar el proceso con ejemplos concretos, construiremos sendos
ejecutables que utiliza las librerías dinámicas creadas en la página anterior
( 1.4.4.b2a).
Ambas utilizan sendos fuentes situados en el directorio
D:\LearnC\planets. La primera versión utiliza carga estática, es
el fuente mainE.cpp; la otra, correspondiente al fuente mainD.cpp
utiliza carga dinámica. Ambas versiones construirán de dos formas;
utilizando el compilador C++ GNU y el de Borland.
§4.1 Utilizar una librería dinámica con carga estática
La aplicación consta de un solo fichero mainE.cpp con el siguiente diseño:
// mainE.cpp
#include <windows.h>
#include <stdlib.h>
#include "dlibs/planets.h"
int main(int argc, char *argv[]) {
showMercury();
showVenus();
showEarth();
system("PAUSE");
return EXIT_SUCCESS;
}
Observe que el fuente es exactamente análogo al utilizado para el uso de
librerías estáticas (
1.4.4b1) y que, aparte de la
utilización del fichero de cabecera correspondiente, nada hace presagiar que
utilizaremos una librería dinámica. Recuerde que el fichero planets.h, junto con el resto de
los utilizados para crear la librería, están en D:\LearnC\planets\dlibs.
§4.1.1 Con GNU Make
Para construir un ejecutable que utilice la referida librería dinámica con carga estática, utilizamos un makefile makefilE.gnu situado en el directorio del fuente anterior:
# MakefilE.gnu Uso de planetsG.dll
# crear aplicación planetsE.exe usando librería dinámica con enlazado estático
LIBS = -L"C:/DEV-CPP/lib"
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"
planetsE.exe: mainE.o
g++ mainE.o -o "planetsE.exe" $(LIBS) dlibs/planetsG.dll
mainE.o: mainE.cpp
g++ -c mainE.cpp -o mainE.o $(CXXFLAGS)
La última regla simplemente obtiene el fichero objeto mainE.o, que será enlazado para obtener el ejecutable. La construcción se realiza en el comando de la primera regla; en ella, a través de compilador, le indicamos al enlazador que incluya nuestra librería junto con el objeto de la aplicación.
La invocación de make para la construcción del ejecutable se realiza de forma análoga a la utilizada para la construcción de la librería:
D:\LearnC\planets>make -f makefilE.gnu
Como resultado se obtiene el objeto mainE.o y el ejecutable deseado, planetsE.exe, cuya ejecución proporciona desde luego las mismas salidas que cuando se utilizó una librería estática.
Primer planeta: Mercurio
Segundo planeta: Venus
Tercer planeta: Tierra
Presione cualquier tecla para continuar . . .
Como se ha señalado, si todo el proceso se ha realizado con las herramientas
MinGW, no es necesaria la librería de importación, pero si no fuese este
el caso. Por ejemplo, porque la DLL hubiese sido construida con otro compilador,
podríamos compilar con la librería de importación planets.a, en lugar
de con la DLL (la suponemos en el directorio dlibs). Para esto
bastaría modificar ligeramente la primera regla del makefile anterior que
quedaría como sigue:
planetsE.exe: mainE.o
g++ mainE.o -o "planetsE.exe" $(LIBS) dlibs/planets.a
Nota: en la página dedicada a las librerías de
importación, se indica cómo obtener una de estas librerías a partir de una
DLL existente (
1.4.4b2c).
§4.1.2 Con Borland 5.5 Make
Para la construcción de nuestro ejecutable, que usará la librería
dinámica creada en la página anterior (
1.4.4b2a) mediante
un enlazado implícito (estático), utilizamos un makefile, makefilE.bor, situado en
el mismo directorio que el fuente mainE.cpp.
# MakefilE.bor para Borland C++ 5.5.1
# crear aplicación planetsE.exe usando librería dinámica con enlazado estático
LIBS = -LE:\BorlandCPP\Lib
all: planetsE.exe
planetsE.exe: mainE.cpp dlibs\planetsB.lib
bcc32 -eplanetsE.exe $(LIBS) -WCR -P -Q mainE.cpp dlibs\planetsB.lib
# -P Perform C++ compile regardless of source extension
# -Q Extended compiler error information (Default = OFF)
# -WCR Console aplication
Observe que, aparte de la indicación de incluir la librería planetsB.lib en el ejecutable, en la compilación no existe ninguna mención a la librería dinámica planetsB.dll que se utilizará en runtime (el nombre de esta DLL está incluida en la información aportada por la librería de importación).
La invocación de make es como siempre:
C:\Windows>D:
D:\>cd LearnC\planets\
D:\LearnC\planets\>set PATH=E:\BORLAN~1\BIN;%path%
D:\LearnC\planets\>make -f makefilE.bor
Una vez construido el ejecutable planetsE.exe, su ejecución solo exige de la presencia del propio ejecutable y de la DLL correspondiente planetsB.dll (la librería de importación planetsB.lib no es necesaria).
§4.2 Utilizar una librería dinámica con carga dinámica
La utilización de una librería dinámica con carga
dinámica (explícita) en nuestra aplicación, exige ciertas modificaciones en el fuente respecto al diseño
utilizado para la carga estática .
El fichero del nuevo fuente será mainD.cpp situado en el mismo
directorio que el anterior: D:\LearnC\planets.
// mainD.cpp
// usar librería dinámica con enlazado dinámico
#include <windows.h>
#include <stdlib.h>
#include <iostream>
#include "dlibs/planets.h"
typedef void __stdcall (* FPTR)();
int main(int argc, char *argv[]) {
HMODULE dllHandle = LoadLibrary("planetsX.dll"); //
cargar librería [2]
if (!dllHandle) {
std::cout << "Error en la carga de
planets.dll\n";
} else {
FPTR mercurio = (FPTR) GetProcAddress(dllHandle,
"showMercury");
FPTR venus = (FPTR) GetProcAddress(dllHandle,
"showVenus");
FPTR tierra = (FPTR) GetProcAddress(dllHandle,
"showEarth");
if (!mercurio)
std::cout << "Error
al obtener direccion de showMercury()\n";
else mercurio();
if (!venus)
std::cout << "Error
al obtener direccion de showVenus()\n";
else venus ();
if (!tierra)
std::cout << "Error
al obtener direccion de showEarth()\n";
else tierra();
FreeLibrary(dllHandle); //
descargar librería
}
system("PAUSE");
return EXIT_SUCCESS;
}
Como puede verse, el diseño es muy distinto de utilizado cuando se usa la DLL con carga implícita (estática). El typedef FPTR, definido como "puntero-a-función que no acepta argumentos y devuelve void", se ha incluido para simplificar la sintaxis de las expresiones con las que obtenemos los punteros mercurio, venus, tierra a las funciones de la librería. Estas expresiones utilizan invocaciones a la función GetProcAddres de la API de Windows. Observe que antes hemos cargado explícitamente la librería, mediante la función LoadLibrary de la API.
La invocación de las funciones de la librería no se realizan directamente mediante sus nombres, sino a través de los punteros obtenidos con GetProcAddres. Finalmente, antes de salir de la aplicación, descargamos la librería mediante FreeLibrary. Esta descarga puede efectuarse en cualquier momento a partir del instante en que los recursos de la librería no sean necesarios.
§4.2.1 Con GNU Make
Para la construcción de una aplicación que utilice la librería dinámica planets.dll
creada en la página anterior (
1.4.4b2a) con carga explícita
(dinámica) con el compilador GNU gcc, utilizaremos el makefile makefilD.gnu:
# MakefilD.gnu
# crear aplicación planetsD.exe usando librería dinámica con enlazado dinámico
LIBS = -L"C:/DEV-CPP/lib"
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"
planetsD.exe: mainD.o
g++ mainD.o -o "planetsD.exe" $(LIBS) dlibs/planetsG.dll
mainD.o: mainD.cpp
g++ -c mainD.cpp -o mainD.o $(CXXFLAGS)
La invocación de make se realiza en la forma acostumbrada:
D:\LearnC\planets>make -f makefilD.gnu
La construcción no exige que exista ningún fichero especial en el
directorio D:\LearnC\planets de trabajo; solo que el fichero de cabecera planets.h
y la librería planets.dll, se encuentren en el directorio D:\LearnC\planets\dlibs.
Sin embargo, para la ejecución del ejecutable planetsD.exe obtenido, es
necesario que la librería se encuentre en alguno de los sitios estándar donde
busca el cargador del Sistema (
1.4.4b2).
La compilación anterior también puede hacerse contra la librería de importación planets.a en lugar de contra la librería planets.dll propiamente dicha. Esto puede ser necesario en alguno de los supuestos ya comentados. Para ello, basta modificar la primera regla del makefile:
planetsD.exe: mainD.o
g++ mainD.o -o "planetsD.exe" $(LIBS) dlibs/planets.a
§4.2.2 Con Borland 5.5 Make
El fichero makefilD.bor para construir el ejecutable que utiliza enlazado dinámico con la librería es el siguiente:
# MakefilD.bor para Borland C++ 5.5.1
# crear aplicación planetsD.exe usando librería dinámica con enlazado dinámico
LIBS = -LE:\BorlandCPP\Lib
all: planetsD.exe
planetsD.exe: mainD.cpp dlibs/planetsB.dll
bcc32 -eplanetsD.exe $(LIBS) -WCR -P -Q mainD.cpp
# -P Perform C++ compile regardless of source extension
# -Q Extended compiler error information (Default = OFF)
# -WCR Console aplication (enlazado dinámico)
Una vez invocado el makefile de la forma usual, el ejecutable obtenido planetsD.exe
produce la misma salida que en los casos anteriores, pero recuerde que su ejecución exige que la librería planetsB.dll se encuentre en alguno de los sitios estándar donde
busca el cargador del Sistema (
1.4.4b2).
[1] Existe un tipo de
librerías muy similar a las que aquí comentamos. Son las denominadas librerías
de tipos. Un tipo particular de librería de
importación que no contiene referencias a una DLL, sino a un control ActiveX; un objeto OLE, o un objeto COM (
1.4.4b2d).
[2] Sustituir en cada caso, la X por la letra adecuada a la DLL utilizada. Es decir, planetsG.dll para la compilación con GNU y planetsB.dll para la compilación con Borland.