Carica dinamicamente una funzione da una DLL


88

Sto dando una piccola occhiata ai file .dll, capisco il loro utilizzo e sto cercando di capire come usarli.

Ho creato un file .dll che contiene una funzione che restituisce un numero intero chiamato funci ()

usando questo codice, (penso) di aver importato il file .dll nel progetto (non ci sono lamentele):

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

Tuttavia, quando provo a compilare questo file .cpp che penso abbia importato il .dll, ho il seguente errore:

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

So che un file .dll è diverso da un file di intestazione, quindi so che non posso importare una funzione come questa, ma è il meglio che potrei trovare per dimostrare che ho provato.

La mia domanda è: come posso utilizzare il hGetProcIDDLLpuntatore per accedere alla funzione all'interno di .dll.

Spero che questa domanda abbia senso e non abbaio ancora una volta su qualche albero sbagliato.


ricerca collegamento statico / dinamico.
Mitch Wheat

Grazie, esaminerò questo

Ho fatto rientrare il mio codice ma quando lo inserisco qui il formato si

Risposte:


153

LoadLibrarynon fa quello che pensi che faccia. Carica la DLL nella memoria del processo corrente, ma non importa magicamente le funzioni definite in essa! Ciò non sarebbe possibile, poiché le chiamate di funzione vengono risolte dal linker in fase di compilazione mentre LoadLibraryviene chiamato in fase di esecuzione (ricorda che C ++ è un linguaggio tipizzato staticamente ).

Avete bisogno di una funzione WinAPI separata per ottenere l'indirizzo delle funzioni caricate dinamicamente: GetProcAddress.

Esempio

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

Inoltre, dovresti esportare correttamente la tua funzione dalla DLL. Questo può essere fatto in questo modo:

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

Come osserva Lundin, è buona norma liberare la maniglia della libreria se non ne hai bisogno più a lungo. Ciò causerà lo scaricamento se nessun altro processo contiene ancora un handle per la stessa DLL.


Potrebbe sembrare una domanda stupida, ma qual è / dovrebbe essere il tipo di f_funci?

8
Oltre a questo, la risposta è eccellente e facilmente comprensibile

6
Nota che f_funciin effetti è un tipo (piuttosto che ha un tipo). Il tipo si f_funcilegge come "puntatore a una funzione che restituisce un inte non accetta argomenti". Maggiori informazioni sui puntatori a funzione in C possono essere trovate su newty.de/fpt/index.html .
Niklas B.

Grazie ancora per la risposta, funci non accetta argomenti e restituisce un numero intero; Ho modificato la domanda per mostrare la funzione che è stata compilata? nel file .dll. Quando ho provato a eseguire dopo aver incluso "typedef int ( f_funci) ();" Ho ricevuto questo errore: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || Nella funzione 'int main ()': | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | errore: impossibile convertire 'int ( ) ()' in 'const CHAR *' per l'argomento '2' in 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Build terminata: 1 errori, 0 avvisi === |

Beh, ho dimenticato un cast lì (modificato in). L'errore però sembra essere un altro, sei sicuro di utilizzare il codice corretto? Se sì, puoi incollare il codice in errore e l'output completo del compilatore su pastie.org ? Inoltre, il typedef che hai scritto nel tuo commento è sbagliato ( *manca un an , che potrebbe aver causato l'errore)
Niklas B.

34

Oltre alla risposta già pubblicata, ho pensato di condividere un pratico trucco che uso per caricare tutte le funzioni DLL nel programma tramite puntatori a funzione, senza scrivere una chiamata GetProcAddress separata per ciascuna funzione. Mi piace anche chiamare le funzioni direttamente come tentato nell'OP.

Inizia definendo un tipo di puntatore a funzione generico:

typedef int (__stdcall* func_ptr_t)();

Quali tipi vengono utilizzati non sono molto importanti. Ora crea un array di quel tipo, che corrisponde alla quantità di funzioni che hai nella DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

In questo array possiamo memorizzare i puntatori a funzioni effettive che puntano nello spazio di memoria della DLL.

Il problema successivo è che GetProcAddressprevede i nomi delle funzioni come stringhe. Quindi crea un array simile composto dai nomi delle funzioni nella DLL:

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

Ora possiamo facilmente chiamare GetProcAddress () in un ciclo e memorizzare ogni funzione all'interno di quell'array:

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 il ciclo ha avuto successo, l'unico problema che abbiamo ora è chiamare le funzioni. Il typedef del puntatore a funzione di prima non è utile, perché ogni funzione avrà la propria firma. Questo può essere risolto creando una struttura con tutti i tipi di funzione:

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 infine, per connetterli all'array di prima, crea un'unione:

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

Ora puoi caricare tutte le funzioni dalla DLL con il comodo ciclo, ma chiamarle tramite il by_typemembro dell'unione.

Ma ovviamente è un po 'gravoso scrivere qualcosa di simile

functions.by_type.dll_add_ptr(1, 1); ogni volta che vuoi chiamare una funzione.

A quanto pare, questo è il motivo per cui ho aggiunto il suffisso "ptr" ai nomi: volevo mantenerli diversi dai nomi delle funzioni effettive. Ora possiamo appianare la sintassi icky della struttura e ottenere i nomi desiderati, utilizzando alcune macro:

#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à, ora puoi usare i nomi delle funzioni, con il tipo ei parametri corretti, come se fossero staticamente collegati al tuo progetto:

int result = dll_add(1, 1);

Dichiarazione di non responsabilità: a rigor di termini, le conversioni tra diversi puntatori a funzione non sono definite dallo standard C e non sono sicure. Quindi formalmente, quello che sto facendo qui è un comportamento indefinito. Tuttavia, nel mondo Windows, i puntatori a funzione sono sempre della stessa dimensione indipendentemente dal tipo e le conversioni tra di loro sono prevedibili su qualsiasi versione di Windows che ho usato.

Inoltre, in teoria potrebbe esserci un riempimento inserito nell'unione / struttura, che farebbe fallire tutto. Tuttavia, i puntatori hanno le stesse dimensioni dei requisiti di allineamento in Windows. A static_assertper assicurarsi che la struttura / unione non abbia imbottitura potrebbe essere ancora in ordine.


1
Questo approccio in stile C funzionerebbe. Ma non sarebbe appropriato usare un costrutto C ++ per evitare la #defines?
Harper

@harper Bene in C ++ 11 potresti usare auto dll_add = ..., ma in C ++ 03 non c'è nessun costrutto a cui potrei pensare che semplifichi l'attività (inoltre non vedo alcun problema particolare con le #defines qui)
Niklas B.

Poiché questo è tutto specifico per WinAPI, non è necessario digitare il proprio func_ptr_t. Invece puoi usare FARPROC, che è il tipo restituito di GetProcAddress. Ciò potrebbe consentire di eseguire la compilazione con un livello di avviso più elevato senza aggiungere un cast alla GetProcAddresschiamata.
Adrian McCarthy

@NiklasB. puoi usare solo autoper una funzione alla volta, il che vanifica l'idea di farlo una volta per tutte in un ciclo. ma cosa c'è di sbagliato in un array std :: function
Francesco Dondi

1
@Francesco i tipi di funzione std :: differiranno proprio come i tipi di funcptr. Immagino che i modelli variadici aiuterebbero
Niklas B.

1

Questo non è esattamente un argomento caldo, ma ho una classe factory che consente a una dll di creare un'istanza e restituirla come DLL. È quello che cercavo ma non sono riuscito a trovare esattamente.

Si chiama come,

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

dove IHTTP_Server è l'interfaccia virtuale pura per una classe creata in un'altra DLL o nella stessa.

DEFINE_INTERFACE viene utilizzato per fornire a un id di classe un'interfaccia. Posiziona all'interno dell'interfaccia;

Una classe di interfaccia sembra,

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

Il file di intestazione è così

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Le librerie sono elencate in questa definizione di macro. Una riga per libreria / eseguibile. Sarebbe bello se potessimo chiamare un altro eseguibile.

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

Quindi per ogni dll / exe si definisce una macro e si elencano le sue implementazioni. Def significa che è l'implementazione predefinita per l'interfaccia. Se non è l'impostazione predefinita, dai un nome all'interfaccia utilizzata per identificarla. Vale a dire, speciale, e il nome sarà 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 le librerie tutte impostate, il file di intestazione utilizza le definizioni delle macro per definire ciò che è necessario.

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

Questo crea un enum per le librerie.

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

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

Questo crea un enum per le implementazioni dell'interfaccia.

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

    long CallEntryPoint(long id, long interfaceId);

Questo definisce la classe factory. Non c'è molto qui.

    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

Quindi il CPP è,

#include "sn_factory.h"

#include <windows.h>

Crea il punto di ingresso esterno. Puoi verificare che esista utilizzando dipende.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;
        }
    }
}

Le macro impostano tutti i dati necessari.

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

Ogni libreria include questo "cpp" con uno stub cpp per ogni libreria / eseguibile. Qualsiasi materiale di intestazione compilato specifico.

#include "sn_pch.h"

Imposta questa libreria.

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

Un include per il cpp principale. Immagino che questo cpp potrebbe essere un .h. Ma ci sono diversi modi per farlo. Questo approccio ha funzionato per me.

#include "../inc/sn_factory.cpp"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.