Carregar dinamicamente uma função de uma DLL

88

Estou dando uma olhada nos arquivos .dll, entendo seu uso e estou tentando entender como usá-los.

Eu criei um arquivo .dll que contém uma função que retorna um inteiro chamado funci ()

usando este código, eu (acho) importei o arquivo .dll para o projeto (não há reclamações):

#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;
}

No entanto, quando tento compilar este arquivo .cpp que acho que importou o .dll, recebo o seguinte erro:

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 ===|

Eu sei que um .dll é diferente de um arquivo de cabeçalho, então sei que não posso importar uma função como essa, mas é o melhor que pude sugerir para mostrar que tentei.

Minha pergunta é: como posso usar o hGetProcIDDLLponteiro para acessar a função dentro do .dll.

Espero que essa pergunta faça sentido e eu não esteja latindo em uma árvore errada de novo.

Clemência
fonte
pesquisa de links estáticos / dinâmicos.
Mitch Wheat
Obrigado, vou olhar para isso
Eu recuo meu código, mas quando o coloco aqui, o formato bagunça, então acabo recuando tudo em 4 linhas

Respostas:

152

LoadLibrarynão faz o que você pensa que faz. Ele carrega a DLL na memória do processo atual, mas não importa magicamente as funções definidas nele! Isso não seria possível, já que as chamadas de função são resolvidas pelo vinculador em tempo de compilação enquanto LoadLibrarysão chamadas em tempo de execução (lembre-se de que C ++ é uma linguagem de tipo estático ).

Você precisa de uma função WinAPI separado para obter o endereço de funções carregadas dinamicamente: GetProcAddress.

Exemplo

#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;
}

Além disso, você deve exportar sua função da DLL corretamente. Isso pode ser feito assim:

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

Como observa Lundin, é uma boa prática liberar o identificador para a biblioteca se você não precisar mais dele. Isso fará com que ele seja descarregado se nenhum outro processo ainda tiver um identificador para a mesma DLL.

Niklas B.
fonte
Pode soar como uma pergunta estúpida, mas qual é / deveria ser o tipo de f_funci?
8
Fora isso, a resposta é excelente e facilmente compreensível
6
Observe que, f_funcina verdade, é um tipo (em vez de ter um tipo). O tipo é f_funcilido como "ponteiro para uma função que retorna um inte não recebe argumentos". Mais informações sobre ponteiros de função em C podem ser encontradas em newty.de/fpt/index.html .
Niklas B.
Obrigado novamente pela resposta, funci não aceita argumentos e retorna um inteiro; Eu editei a pergunta para mostrar a função que foi compilada? para o .dll. Quando tentei executar após incluir "typedef int ( f_funci) ();" Recebi este erro: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || Na função 'int main ()': | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | erro: não é possível converter 'int ( ) ()' em 'const CHAR *' para o argumento '2' em 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Compilação concluída: 1 erros, 0 avisos === |
Bem, eu esqueci um elenco lá (editei em). O erro, entretanto, parece ser outro, tem certeza de que está usando o código correto? Em caso afirmativo, você pode colar o código com falha e a saída completa do compilador em pastie.org ? Além disso, o typedef que você escreveu em seu comentário está errado ( *falta um, o que pode ter causado o erro)
Niklas B.
34

Além da resposta já postada, pensei que deveria compartilhar um truque útil que uso para carregar todas as funções DLL no programa por meio de ponteiros de função, sem escrever uma chamada GetProcAddress separada para cada função. Também gosto de chamar as funções diretamente como tentado no OP.

Comece definindo um tipo de ponteiro de função genérico:

typedef int (__stdcall* func_ptr_t)();

Os tipos usados ​​não são realmente importantes. Agora crie uma matriz desse tipo, que corresponde à quantidade de funções que você tem na DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

Nessa matriz, podemos armazenar os ponteiros de função reais que apontam para o espaço de memória DLL.

O próximo problema é que GetProcAddressespera os nomes das funções como strings. Portanto, crie uma matriz semelhante consistindo nos nomes das funções na DLL:

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

Agora podemos facilmente chamar GetProcAddress () em um loop e armazenar cada função dentro dessa 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
  }
}

Se o loop foi bem sucedido, o único problema que temos agora é chamar as funções. O ponteiro de função typedef anterior não é útil, porque cada função terá sua própria assinatura. Isso pode ser resolvido criando uma estrutura com todos os tipos de função:

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;

E, finalmente, para conectá-los ao array anterior, crie uma união:

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

Agora você pode carregar todas as funções da DLL com o loop conveniente, mas chamá-las por meio do by_typemembro do sindicato.

Mas, claro, é um pouco pesado digitar algo como

functions.by_type.dll_add_ptr(1, 1); sempre que você quiser chamar uma função.

Acontece que esta é a razão pela qual adicionei o postfix "ptr" aos nomes: eu queria mantê-los diferentes dos nomes reais das funções. Agora podemos suavizar a sintaxe da estrutura icky e obter os nomes desejados, usando algumas 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)

E voilà, agora você pode usar os nomes das funções, com o tipo e os parâmetros corretos, como se estivessem estaticamente vinculados ao seu projeto:

int result = dll_add(1, 1);

Isenção de responsabilidade: falando estritamente, as conversões entre ponteiros de função diferentes não são definidas pelo padrão C e não são seguras. Então, formalmente, o que estou fazendo aqui é um comportamento indefinido. No entanto, no mundo do Windows, os ponteiros de função são sempre do mesmo tamanho, independentemente do tipo, e as conversões entre eles são previsíveis em qualquer versão do Windows que usei.

Além disso, em teoria pode haver preenchimento inserido na união / estrutura, o que faria com que tudo falhasse. No entanto, os ponteiros têm o mesmo tamanho que o requisito de alinhamento do Windows. Um static_assertpara garantir que a estrutura / união não tem preenchimento pode estar em ordem ainda.

Lundin
fonte
1
Essa abordagem de estilo C funcionaria. Mas não seria apropriado usar uma construção C ++ para evitar os #defines?
Harper de
@harper Bem, em C ++ 11 você poderia usar auto dll_add = ..., mas em C ++ 03 não há nenhuma construção que eu pudesse pensar que simplificaria a tarefa (também não vejo nenhum problema particular com os #defineaqui)
Niklas B.
Uma vez que tudo isso é específico do WinAPI, você não precisa digitar o seu próprio func_ptr_t. Em vez disso, você pode usar FARPROC, que é o tipo de retorno de GetProcAddress. Isso pode permitir que você compile com um nível de aviso mais alto sem adicionar um elenco à GetProcAddresschamada.
Adrian McCarthy
@NiklasB. você só pode usar autopara uma função por vez, o que anula a ideia de fazer isso de uma vez por todas em um loop. mas o que há de errado com uma matriz std :: function
Francesco Dondi
1
@Francesco os tipos std :: function serão diferentes assim como os tipos funcptr. Acho que modelos variados ajudariam
Niklas B.
1

Este não é exatamente um tópico quente, mas tenho uma classe de fábrica que permite que uma dll crie uma instância e a retorne como uma DLL. É o que vim procurar mas não consegui encontrar exatamente.

É chamado como,

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

onde IHTTP_Server é a interface virtual pura para uma classe criada em outra DLL ou na mesma.

DEFINE_INTERFACE é usado para fornecer uma interface a um id de classe. Coloque dentro da interface;

Uma classe de interface parece,

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

O arquivo de cabeçalho é assim

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

As bibliotecas estão listadas nesta definição de macro. Uma linha por biblioteca / executável. Seria legal se pudéssemos chamar outro executável.

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

Então, para cada dll / exe você define uma macro e lista suas implementações. Def significa que é a implementação padrão da interface. Se não for o padrão, você dá um nome para a interface usada para identificá-lo. Ou seja, especial, e o nome 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)

Com todas as bibliotecas configuradas, o arquivo de cabeçalho usa as definições de macro para definir o necessário.

#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,

Isso cria um enum para as bibliotecas.

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

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

Isso cria um enum para implementações de interface.

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

    long CallEntryPoint(long id, long interfaceId);

Isso define a classe de fábrica. Não muito aqui.

    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

Então o CPP é,

#include "sn_factory.h"

#include <windows.h>

Crie o ponto de entrada externo. Você pode verificar se ele existe usando o depends.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;
        }
    }
}

As macros configuram todos os dados necessários.

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 inclui este "cpp" com um stub cpp para cada biblioteca / executável. Qualquer coisa de cabeçalho compilada específica.

#include "sn_pch.h"

Configure esta biblioteca.

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

Uma inclusão para o cpp principal. Acho que esse cpp pode ser um .h. Mas existem diferentes maneiras de fazer isso. Essa abordagem funcionou para mim.

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