Cargar dinámicamente una función desde una DLL

88

Estoy echando un vistazo a los archivos .dll, entiendo su uso y estoy tratando de entender cómo usarlos.

He creado un archivo .dll que contiene una función que devuelve un número entero llamado funci ()

usando este código, creo que he importado el archivo .dll al proyecto (no hay quejas):

#include <windows.h>
#include <iostream>

int main() {
  HINSTANCE hGetProcIDDLL = LoadLibrary("C:\\Documents and Settings\\User\\Desktop  \\fgfdg\\dgdg\\test.dll");

  if (hGetProcIDDLL == NULL) {
    std::cout << "cannot locate the .dll file" << std::endl;
  } else {
    std::cout << "it has been called" << std::endl;
    return -1;
  }

  int a = funci();

  return a;
}

# funci function 

int funci() {
  return 40;
}

Sin embargo, cuando intento compilar este archivo .cpp que creo que ha importado el .dll, aparece el siguiente error:

C:\Documents and Settings\User\Desktop\fgfdg\onemore.cpp||In function 'int main()':|
C:\Documents and Settings\User\Desktop\fgfdg\onemore.cpp|16|error: 'funci' was not     declared in this scope|
||=== Build finished: 1 errors, 0 warnings ===|

Sé que un .dll es diferente de un archivo de encabezado, así que sé que no puedo importar una función como esta, pero es lo mejor que se me ocurre para demostrar que lo he intentado.

Mi pregunta es, ¿cómo puedo usar el hGetProcIDDLLpuntero para acceder a la función dentro del .dll.

Espero que esta pregunta tenga sentido y no volveré a ladrar a un árbol equivocado.

Lenidad
fuente
búsqueda de enlaces estáticos / dinámicos.
Mitch Wheat
Gracias,
Sangro mi código, pero cuando lo introduzco aquí, el formato se estropea, así que termino sangrando todo en 4 líneas

Respuestas:

152

LoadLibraryno hace lo que cree que hace. Se carga el archivo DLL en la memoria del proceso actual, pero sí no importar mágicamente funciones definidas en ella! Esto no sería posible, ya que el enlazador resuelve las llamadas a funciones en tiempo de compilación mientras LoadLibraryse llama en tiempo de ejecución (recuerde que C ++ es un lenguaje escrito de forma estática ).

Se necesita una función API de Windows por separado para obtener la dirección de las funciones de carga dinámica: GetProcAddress.

Ejemplo

#include <windows.h>
#include <iostream>

/* Define a function pointer for our imported
 * function.
 * This reads as "introduce the new type f_funci as the type: 
 *                pointer to a function returning an int and 
 *                taking no arguments.
 *
 * Make sure to use matching calling convention (__cdecl, __stdcall, ...)
 * with the exported function. __stdcall is the convention used by the WinAPI
 */
typedef int (__stdcall *f_funci)();

int main()
{
  HINSTANCE hGetProcIDDLL = LoadLibrary("C:\\Documents and Settings\\User\\Desktop\\test.dll");

  if (!hGetProcIDDLL) {
    std::cout << "could not load the dynamic library" << std::endl;
    return EXIT_FAILURE;
  }

  // resolve function address here
  f_funci funci = (f_funci)GetProcAddress(hGetProcIDDLL, "funci");
  if (!funci) {
    std::cout << "could not locate the function" << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "funci() returned " << funci() << std::endl;

  return EXIT_SUCCESS;
}

Además, debe exportar su función desde la DLL correctamente. Esto se puede hacer así:

int __declspec(dllexport) __stdcall funci() {
   // ...
}

Como señala Lundin, es una buena práctica liberar el identificador de la biblioteca si ya no lo necesita. Esto hará que se descargue si ningún otro proceso aún tiene un identificador para la misma DLL.

Niklas B.
fuente
Puede parecer una pregunta estúpida, pero ¿cuál es / debería ser el tipo de f_funci?
8
Aparte de eso, la respuesta es excelente y fácilmente comprensible
6
Tenga f_funcien cuenta que, de hecho, es un tipo (en lugar de tener un tipo). El tipo se f_funcilee como "puntero a una función que devuelve inty no acepta argumentos". Puede encontrar más información sobre punteros de función en C en newty.de/fpt/index.html .
Niklas B.
Gracias de nuevo por la respuesta, funci no acepta argumentos y devuelve un número entero; Edité la pregunta para mostrar la función que se compiló. en el .dll. Cuando intenté ejecutar después de incluir "typedef int ( f_funci) ();" Recibí este error: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || En la función 'int main ()': | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | error: no se puede convertir 'int ( ) ()' a 'const CHAR *' para el argumento '2' a 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Compilación finalizada: 1 errores, 0 advertencias === |
Bueno, me olvidé de un elenco allí (lo edité). Sin embargo, el error parece ser otro, ¿está seguro de que utiliza el código correcto? En caso afirmativo, ¿puede pegar su código defectuoso y la salida completa del compilador en pastie.org ? Además, el typedef que escribió en su comentario es incorrecto ( *falta un, lo que podría haber causado el error)
Niklas B.
34

Además de la respuesta ya publicada, pensé que debería compartir un truco útil que uso para cargar todas las funciones DLL en el programa a través de punteros de función, sin escribir una llamada GetProcAddress separada para todas y cada una de las funciones. También me gusta llamar a las funciones directamente como se intentó en el OP.

Empiece por definir un tipo de puntero de función genérico:

typedef int (__stdcall* func_ptr_t)();

Los tipos que se utilizan no son realmente importantes. Ahora cree una matriz de ese tipo, que corresponde a la cantidad de funciones que tiene en la DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

En esta matriz podemos almacenar los punteros de función reales que apuntan al espacio de memoria de la DLL.

El siguiente problema es que GetProcAddressespera los nombres de las funciones como cadenas. Así que cree una matriz similar que consista en los nombres de las funciones en la DLL:

const char* DLL_FUNCTION_NAMES [DLL_FUNCTIONS_N] = 
{
  "dll_add",
  "dll_subtract",
  "dll_do_stuff",
  ...
};

Ahora podemos llamar fácilmente a GetProcAddress () en un bucle y almacenar cada función dentro de esa matriz:

for(int i=0; i<DLL_FUNCTIONS_N; i++)
{
  func_ptr[i] = GetProcAddress(hinst_mydll, DLL_FUNCTION_NAMES[i]);

  if(func_ptr[i] == NULL)
  {
    // error handling, most likely you have to terminate the program here
  }
}

Si el ciclo tuvo éxito, el único problema que tenemos ahora es llamar a las funciones. El puntero de función typedef de antes no es útil, porque cada función tendrá su propia firma. Esto se puede resolver creando una estructura con todos los tipos de funciones:

typedef struct
{
  int  (__stdcall* dll_add_ptr)(int, int);
  int  (__stdcall* dll_subtract_ptr)(int, int);
  void (__stdcall* dll_do_stuff_ptr)(something);
  ...
} functions_struct;

Y finalmente, para conectarlos a la matriz de antes, cree una unión:

typedef union
{
  functions_struct  by_type;
  func_ptr_t        func_ptr [DLL_FUNCTIONS_N];
} functions_union;

Ahora puede cargar todas las funciones de la DLL con el bucle conveniente, pero llamarlas a través del by_typemiembro de la unión.

Pero, por supuesto, es un poco engorroso escribir algo como

functions.by_type.dll_add_ptr(1, 1); siempre que desee llamar a una función.

Resulta que esta es la razón por la que agregué el sufijo "ptr" a los nombres: quería mantenerlos diferentes de los nombres de funciones reales. Ahora podemos suavizar la sintaxis de la estructura icky y obtener los nombres deseados, usando algunas macros:

#define dll_add (functions.by_type.dll_add_ptr)
#define dll_subtract (functions.by_type.dll_subtract_ptr)
#define dll_do_stuff (functions.by_type.dll_do_stuff_ptr)

Y voilà, ahora puede usar los nombres de las funciones, con el tipo y los parámetros correctos, como si estuvieran vinculados estáticamente a su proyecto:

int result = dll_add(1, 1);

Descargo de responsabilidad: Estrictamente hablando, las conversiones entre diferentes punteros de función no están definidas por el estándar C y no son seguras. Así que formalmente, lo que estoy haciendo aquí es un comportamiento indefinido. Sin embargo, en el mundo de Windows, los punteros de función son siempre del mismo tamaño sin importar su tipo y las conversiones entre ellos son predecibles en cualquier versión de Windows que haya usado.

Además, en teoría, podría haber relleno insertado en la unión / estructura, lo que haría que todo fallara. Sin embargo, los punteros tienen el mismo tamaño que el requisito de alineación en Windows. A static_assertpara asegurarse de que la estructura / unión no tenga relleno podría estar todavía en orden.

Lundin
fuente
1
Este enfoque de estilo C funcionaría. ¿Pero no sería apropiado usar una construcción C ++ para evitar la #defines?
harper
@harper Bueno, en C ++ 11 podría usar auto dll_add = ..., pero en C ++ 03 no hay ninguna construcción en la que pueda pensar que simplifique la tarea (tampoco veo ningún problema en particular con los #defines aquí)
Niklas B.
Dado que todo esto es específico de WinAPI, no es necesario que escriba su propia definición func_ptr_t. En su lugar, puede usar FARPROC, que es el tipo de retorno de GetProcAddress. Esto podría permitirle compilar con un nivel de advertencia más alto sin agregar una conversión a la GetProcAddressllamada.
Adrian McCarthy
@NiklasB. solo puede usar autopara una función a la vez, lo que anula la idea de hacerlo de una vez por todas en un ciclo. pero qué hay de malo con una función de matriz std ::
Francesco Dondi
1
@Francesco los tipos de función std :: diferirán al igual que los tipos de funcptr. Supongo que las plantillas variadas ayudarían
Niklas B.
1

Este no es exactamente un tema candente, pero tengo una clase de fábrica que permite que una dll cree una instancia y la devuelva como una DLL. Es lo que vine a buscar pero no pude encontrar exactamente.

Se llama como,

IHTTP_Server *server = SN::SN_Factory<IHTTP_Server>::CreateObject();
IHTTP_Server *server2 =
      SN::SN_Factory<IHTTP_Server>::CreateObject(IHTTP_Server_special_entry);

donde IHTTP_Server es la interfaz virtual pura para una clase creada en otra DLL o en la misma.

DEFINE_INTERFACE se utiliza para proporcionar una interfaz a un ID de clase. Coloque la interfaz interior;

Una clase de interfaz se parece a,

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

    virtual void MyMethod1() = 0;
    ...
};

El archivo de encabezado es así

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Las bibliotecas se enumeran en esta definición de macro. Una línea por biblioteca / ejecutable. Sería genial si pudiéramos llamar a otro ejecutable.

#define SN_APPLY_LIBRARIES(L, A)                          \
    L(A, sn, "sn.dll")                                    \
    L(A, http_server_lib, "http_server_lib.dll")          \
    L(A, http_server, "")

Luego, para cada dll / exe, define una macro y enumera sus implementaciones. Def significa que es la implementación predeterminada de la interfaz. Si no es el predeterminado, asigne un nombre a la interfaz utilizada para identificarlo. Es decir, especial, y el nombre será IHTTP_Server_special_entry.

#define SN_APPLY_ENTRYPOINTS_sn(M)                                     \
    M(IHTTP_Handler, SNI::SNI_HTTP_Handler, sn, def)                   \
    M(IHTTP_Handler, SNI::SNI_HTTP_Handler, sn, special)

#define SN_APPLY_ENTRYPOINTS_http_server_lib(M)                        \
    M(IHTTP_Server, HTTP::server::server, http_server_lib, def)

#define SN_APPLY_ENTRYPOINTS_http_server(M)

Con todas las bibliotecas configuradas, el archivo de encabezado usa las definiciones de macro para definir lo necesario.

#define APPLY_ENTRY(A, N, L) \
    SN_APPLY_ENTRYPOINTS_##N(A)

#define DEFINE_INTERFACE(I) \
    public: \
        static const long Id = SN::I##_def_entry; \
    private:

namespace SN
{
    #define DEFINE_LIBRARY_ENUM(A, N, L) \
        N##_library,

Esto crea una enumeración para las bibliotecas.

    enum LibraryValues
    {
        SN_APPLY_LIBRARIES(DEFINE_LIBRARY_ENUM, "")
        LastLibrary
    };

    #define DEFINE_ENTRY_ENUM(I, C, L, D) \
        I##_##D##_entry,

Esto crea una enumeración para implementaciones de interfaz.

    enum EntryValues
    {
        SN_APPLY_LIBRARIES(APPLY_ENTRY, DEFINE_ENTRY_ENUM)
        LastEntry
    };

    long CallEntryPoint(long id, long interfaceId);

Esto define la clase de fábrica. No hay mucho que hacer aquí.

    template <class I>
    class SN_Factory
    {
    public:
        SN_Factory()
        {
        }

        static I *CreateObject(long id = I::Id )
        {
            return (I *)CallEntryPoint(id, I::Id);
        }
    };
}

#endif //SN_FACTORY_H_INCLUDED

Entonces el CPP es,

#include "sn_factory.h"

#include <windows.h>

Crea el punto de entrada externo. Puede verificar que exista usando depende.exe.

extern "C"
{
    __declspec(dllexport) long entrypoint(long id)
    {
        #define CREATE_OBJECT(I, C, L, D) \
            case SN::I##_##D##_entry: return (int) new C();

        switch (id)
        {
            SN_APPLY_CURRENT_LIBRARY(APPLY_ENTRY, CREATE_OBJECT)
        case -1:
        default:
            return 0;
        }
    }
}

Las macros configuran todos los datos necesarios.

namespace SN
{
    bool loaded = false;

    char * libraryPathArray[SN::LastLibrary];
    #define DEFINE_LIBRARY_PATH(A, N, L) \
        libraryPathArray[N##_library] = L;

    static void LoadLibraryPaths()
    {
        SN_APPLY_LIBRARIES(DEFINE_LIBRARY_PATH, "")
    }

    typedef long(*f_entrypoint)(long id);

    f_entrypoint libraryFunctionArray[LastLibrary - 1];
    void InitlibraryFunctionArray()
    {
        for (long j = 0; j < LastLibrary; j++)
        {
            libraryFunctionArray[j] = 0;
        }

        #define DEFAULT_LIBRARY_ENTRY(A, N, L) \
            libraryFunctionArray[N##_library] = &entrypoint;

        SN_APPLY_CURRENT_LIBRARY(DEFAULT_LIBRARY_ENTRY, "")
    }

    enum SN::LibraryValues libraryForEntryPointArray[SN::LastEntry];
    #define DEFINE_ENTRY_POINT_LIBRARY(I, C, L, D) \
            libraryForEntryPointArray[I##_##D##_entry] = L##_library;
    void LoadLibraryForEntryPointArray()
    {
        SN_APPLY_LIBRARIES(APPLY_ENTRY, DEFINE_ENTRY_POINT_LIBRARY)
    }

    enum SN::EntryValues defaultEntryArray[SN::LastEntry];
        #define DEFINE_ENTRY_DEFAULT(I, C, L, D) \
            defaultEntryArray[I##_##D##_entry] = I##_def_entry;

    void LoadDefaultEntries()
    {
        SN_APPLY_LIBRARIES(APPLY_ENTRY, DEFINE_ENTRY_DEFAULT)
    }

    void Initialize()
    {
        if (!loaded)
        {
            loaded = true;
            LoadLibraryPaths();
            InitlibraryFunctionArray();
            LoadLibraryForEntryPointArray();
            LoadDefaultEntries();
        }
    }

    long CallEntryPoint(long id, long interfaceId)
    {
        Initialize();

        // assert(defaultEntryArray[id] == interfaceId, "Request to create an object for the wrong interface.")
        enum SN::LibraryValues l = libraryForEntryPointArray[id];

        f_entrypoint f = libraryFunctionArray[l];
        if (!f)
        {
            HINSTANCE hGetProcIDDLL = LoadLibraryA(libraryPathArray[l]);

            if (!hGetProcIDDLL) {
                return NULL;
            }

            // resolve function address here
            f = (f_entrypoint)GetProcAddress(hGetProcIDDLL, "entrypoint");
            if (!f) {
                return NULL;
            }
            libraryFunctionArray[l] = f;
        }
        return f(id);
    }
}

Cada biblioteca incluye este "cpp" con un stub cpp para cada biblioteca / ejecutable. Cualquier contenido de encabezado compilado específico.

#include "sn_pch.h"

Configura esta biblioteca.

#define SN_APPLY_CURRENT_LIBRARY(L, A) \
    L(A, sn, "sn.dll")

Una inclusión para el cpp principal. Supongo que este cpp podría ser un .h. Pero hay diferentes formas de hacerlo. Este enfoque funcionó para mí.

#include "../inc/sn_factory.cpp"
Peter Driscoll
fuente