En este tutorial aprenderás a importar una librería dll a tu proyecto en Visual Studio de forma dinámica.

¿Qué necesitas tener?

  1. Visual Studio. Preferiblemente la última versión.
  2. Visual C++. A menudo incluido en Visual Studio. Instalar desde este IDE si no se encuentra.
  3. Cliente Git, para poder descargar mis proyectos de GitHub. Descarga: GitHub desktop
  4. Proyecto DummyTest_dynamic: DummyTest_dynamic

¿Qué necesitas saber?

  1. Conocimientos básicos de C++. Un buen tutorial: cplusplus
  2. Creación de un proyecto de tipo “aplicación de consola Win32”. Tuto oficial: microsoft
  3. Conocimientos de control de versiones, en este caso Git. Tutorial oficial: Git
  4. Lee mi post anterior: Importación estática de DLL

También estaría bien saber…

  1. Cómo saber qué funciones y clases contiene una librería DLL si no tenemos los archivos de inclusión (los archivos .h): herramienta DUMPBIN
  2. Decoraciones en los símbolos incluidos en una DLL: Name mangling

Al grano

Paso 1: Creación de proyecto

Crea un proyecto de aplicación de consola Win32 en Visual Studio. Descárgate mi proyecto de GitHub, el cual contiene la librería en un archivo .dll. Ubica este archivo en la raíz del proyecto que acabas de crear.

Código

En nuestro proyecto de prueba, creamos un documento .cpp e incluimos el siguiente código:

 //Header para tener acceso a las funciones de carga de librerías DLL
#include <Windows.h>

//Header para llamar a la función de imprimir en consola "cout"
#include <iostream>

int main()
{
	//proceso de carga de dll en tiempo de ejecución
	//##################################################################

	/*Cargamos la librería .dll (que debe encontrarse en la raíz de este proyecto).
	Al cargarse, la librería devuelve un manejador, que es algo así como un 
	representante de la librería en este código. La función solicita el nombre 
	del archivo .dll a cargar, pero pide una cadena de tipo wchar_t*, por lo que
	antes de iniciar la cadena con la doble comilla, ponemos una L mayúscula. */
	HMODULE handler = LoadLibrary(L"DummyTest.dll");

	/*Comprobamos que la carga no ha fallado. Si ha fallado, ignoramos la DLL y 
	seguimos con la ejecución normal de la aplicación.*/
	if(handler != NULL){

		/*Una vez cargada la librería, debemos acceder a sus funciones y calses. 
		Esto se hace mediante la solicitud de punteros a las funciones de la librería.
		Cuando se solicita un puntero a función, se nos devuelve una dirección de 
		memoria donde se encuentra la función que queremos llamar desde este ejecutable.
		Esto se hace de esta forma porque el espacio de memoria de la librería dinámica
		no es el mismo que el espacio de este ejecutable. Los pasos son los sigueintes:*/

		/*Paso 1: Crear nuestro puntero a la función importada. El puntero debe tener la misma 
		estructura que la función que va a representar, es decir, el mismo número de parámetros, 
		todos del mismo tipo que la función a importar y el mismo valor de retorno. En nuestro
		ejemplo, la función DummyTest_imprime() no requiere ningún parámetro de entrada y devuelve
		void a la salida. Así que el puntero sería: */
		typedef void(*DUMMY_TEST_IMPRIME)(void);

		/*Instanciamos una variable del tipo puntero a función que hemos definido en el paso 
		anterior y llamamos a la función de Windows "GetProcAddress". Esta función requiere el 
		manejador de la librería de la que extraeremos la función. También requiere el nombre 
		de la función a extraer. Lo más llamativo es que el nombre de la función va precedido de 
		una barra baja y seguido de una arroba (@) y un número. A esto se le llama "Name mangling", 
		y es una técnica que se usa para evitar la repetición de nombres de funciones (llamados 
		símbolos). El número tras la arroba indica el número de parámetros que admite la función. 
		ATENCIÓN: Esta nomenclatura solo se da en el caso de que la función definida en la librería 
		sea declarada de la siguiente forma:
		
			extern "C" __declspec(dllexport) void _stdcall DummyTest_imprime(void);

			extern "C" evitará un Name mangling más complejo.
			__declspec(dllexport) se usa para exportar funciones de la DLL al exterior.
			_stdcall es un "calling convention", en este caso es el responsable de que aparezca 
			la arroba seguida del número de parámetros de la función.

		*/
		DUMMY_TEST_IMPRIME DummyTest_imprime = (DUMMY_TEST_IMPRIME) GetProcAddress(handler, "_DummyTest_imprime@0");

		//comporbamos que el puntero a función es correcto
		if (DummyTest_imprime != nullptr) {

			/*Si no hay errores podemos llamar a la función de la DLL. 
			Imprime en consola: "funcion cargada correctamente"*/
			DummyTest_imprime();
		} else {
			//si el puntero a función no es correcto, imprimimos un error en consola
			std::cout << "Error en el puntero" << std::endl;
		}

	} else{

		//si la librería no se cargó correctamente, imprimimos un error en consola
		std::cout << "Error al cargar la librería" << std::endl;
	}

	/*Esta cadena se imprimirá independientemente de que la librería se cargue o no. Esto demuestra
	que la aplicación puede funcionar con o sin ella.*/
	std::cout << "Esto se imprime siempre" << std::endl;

	/*escribimos esto para evitar que la ventana de consola se cierre al instante en cuanto se 
	ejecute*/
	getchar();

	//Devolvemos 0 para indicar que se ha salido de la aplicación sin problemas.
	return 0;
} 

Teoría

En el post anterior se configuró un proyecto Visual Studio para que nuestro ejecutable pudiera cargar librerías dinámicas. A pesar de que esta carga se hacía en tiempo de ejecución, al enlazar el archivo .lib asociado a la DLL, estábamos forzando a nuestra aplicación a depender del archivo .dll, y su ausencia provocaría el típico error de DLLs. Este mecanismo es el estándar por su sencillez, pero solo si realmente nuestra aplicación depende completamente de la librería, es decir, que sin la DLL, nuestra aplicación no estaría completa.

En este post propongo otro escenario. Mi aplicación está completa, es decir, no depende de nada externo, pero su funcionalidad puede ser ampliada opcionalmente usando DLLs externas. Esto implica que nuestra aplicación usará ciertas DLL, pero solo si éstas están presentes. De lo contrario, el ejecutable seguirá funcionando con normalidad (obviamente, sin la funcionalidad adicional). Si queremos hacer esto, necesitamos un mecanismo dinámico de reconocimiento de librerías, que compruebe si una DLL se encuentra presente en el lugar indicado y cargarla si es el caso.

Podemos observar en la configuración de nuestro proyecto que en esta ocasión no ha sido necesario incluir el archivo .lib que habitualmente acompaña a las librerías dinámicas. Tampoco se ha añadido al proyecto los archivos de inclusión .h, aunque contar con estos archivos puede ser muy útil para saber cual es el nombre de las funciones que incluye una DLL, de lo contrario es necesario usar una herramienta llamada DUMPBIN.

Prueba a eliminar del proyecto la librería .dll. Comprobarás que tu ejecutable sigue funcionando sin problemas, salvo por la ausencia de funcionalidad de la misma DLL.

Deja un comentario

Tu email no será publicado. Campos obligatorios marcados con *