Laden Sie eine Funktion dynamisch aus einer DLL

87

Ich schaue mir DLL-Dateien an, verstehe deren Verwendung und versuche zu verstehen, wie man sie verwendet.

Ich habe eine DLL-Datei erstellt, die eine Funktion enthält, die eine Ganzzahl namens funci () zurückgibt.

Mit diesem Code habe ich (glaube) die DLL-Datei in das Projekt importiert (es gibt keine Beschwerden):

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

Wenn ich jedoch versuche, diese CPP-Datei zu kompilieren, von der ich glaube, dass sie die DLL importiert hat, tritt der folgende Fehler auf:

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

Ich weiß, dass sich eine DLL von einer Header-Datei unterscheidet, daher weiß ich, dass ich eine solche Funktion nicht importieren kann, aber es ist das Beste, was ich mir einfallen lassen kann, um zu zeigen, dass ich es versucht habe.

Meine Frage ist, wie kann ich den hGetProcIDDLLZeiger verwenden, um auf die Funktion innerhalb der DLL zuzugreifen.

Ich hoffe, diese Frage macht Sinn und ich belle nicht noch einmal einen falschen Baum an.

Nachsicht
quelle
statische / dynamische Verknüpfung suchen.
Mitch Wheat
Vielen Dank, ich werde dies untersuchen
Ich rücke meinen Code ein, aber wenn ich ihn hier

Antworten:

150

LoadLibrarytut nicht das, was du denkst. Es lädt die DLL in den Speicher des aktuellen Prozesses, importiert jedoch nicht die darin definierten Funktionen auf magische Weise! Dies wäre nicht möglich, da Funktionsaufrufe vom Linker zur Kompilierungszeit aufgelöst werden, während LoadLibrarysie zur Laufzeit aufgerufen werden (denken Sie daran, dass C ++ eine statisch typisierte Sprache ist).

Sie benötigen eine separate WinAPI-Funktion, um die Adresse der dynamisch geladenen Funktionen abzurufen : GetProcAddress.

Beispiel

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

Außerdem sollten Sie Ihre Funktion korrekt aus der DLL exportieren . Dies kann folgendermaßen geschehen:

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

Wie Lundin bemerkt, ist es empfehlenswert, das Handle für die Bibliothek freizugeben, wenn Sie es nicht länger benötigen. Dies führt dazu, dass es entladen wird, wenn kein anderer Prozess noch ein Handle für dieselbe DLL enthält.

Niklas B.
quelle
Klingt vielleicht nach einer dummen Frage, aber was ist / sollte der Typ von f_funci sein?
8
6
Beachten Sie, dass f_funciin der Tat ist eine Art ( und nicht hat einen Typen). Der Typ f_funcilautet "Zeiger auf eine Funktion, die ein zurückgibt intund keine Argumente akzeptiert". Weitere Informationen zu Funktionszeigern in C finden Sie unter newty.de/fpt/index.html .
Niklas B.
Nochmals vielen Dank für die Antwort, funci nimmt keine Argumente und gibt eine Ganzzahl zurück; Ich habe die Frage bearbeitet, um die kompilierte Funktion anzuzeigen. in die .dll. Als ich versuchte zu laufen, nachdem ich "typedef int ( f_funci) ();" Ich habe folgende Fehlermeldung erhalten: C: \ Dokumente und Einstellungen \ Benutzer \ Desktop \ fgfdg \ onemore.cpp || In der Funktion 'int main ()': | C: \ Dokumente und Einstellungen \ Benutzer \ Desktop \ fgfdg \ onemore.cpp | 18 | Fehler: 'int ( ) ()' kann nicht in 'const CHAR *' für Argument '2' in 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Build beendet: 1 Fehler, 0 Warnungen === |
Nun, ich habe dort eine Besetzung vergessen (bearbeitet in). Der Fehler scheint jedoch ein anderer zu sein. Sind Sie sicher, dass Sie den richtigen Code verwenden? Wenn ja, können Sie bitte Ihren fehlerhaften Code und die vollständige Compilerausgabe auf pastie.org einfügen ? Auch das typedef, das Sie in Ihrem Kommentar geschrieben haben, ist falsch (ein *fehlt, was den Fehler verursacht haben könnte)
Niklas B.
34

Zusätzlich zu der bereits veröffentlichten Antwort dachte ich, ich sollte einen praktischen Trick teilen, mit dem ich alle DLL-Funktionen über Funktionszeiger in das Programm lade, ohne für jede Funktion einen eigenen GetProcAddress-Aufruf zu schreiben. Ich rufe auch gerne die Funktionen direkt auf, wie im OP versucht.

Definieren Sie zunächst einen generischen Funktionszeigertyp:

typedef int (__stdcall* func_ptr_t)();

Welche Typen verwendet werden, ist nicht wirklich wichtig. Erstellen Sie nun ein Array dieses Typs, das der Anzahl der Funktionen in der DLL entspricht:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

In diesem Array können wir die tatsächlichen Funktionszeiger speichern, die in den DLL-Speicherbereich zeigen.

Das nächste Problem ist, GetProcAddressdass die Funktionsnamen als Zeichenfolgen erwartet werden. Erstellen Sie also ein ähnliches Array, das aus den Funktionsnamen in der DLL besteht:

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

Jetzt können wir GetProcAddress () einfach in einer Schleife aufrufen und jede Funktion in diesem Array speichern:

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

Wenn die Schleife erfolgreich war, besteht das einzige Problem, das wir jetzt haben, darin, die Funktionen aufzurufen. Der Funktionszeiger typedef von früher ist nicht hilfreich, da jede Funktion ihre eigene Signatur hat. Dies kann gelöst werden, indem eine Struktur mit allen Funktionstypen erstellt wird:

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;

Um diese von zuvor mit dem Array zu verbinden, erstellen Sie eine Union:

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

Jetzt können Sie alle Funktionen aus der DLL mit der praktischen Schleife laden, sie jedoch über das by_typeGewerkschaftsmitglied aufrufen .

Aber natürlich ist es etwas mühsam, so etwas zu tippen

functions.by_type.dll_add_ptr(1, 1); wann immer Sie eine Funktion aufrufen möchten.

Wie sich herausstellt, habe ich aus diesem Grund den Namen mit dem Postfix "ptr" versehen: Ich wollte sie von den tatsächlichen Funktionsnamen unterscheiden. Wir können jetzt die icky struct-Syntax glätten und die gewünschten Namen mithilfe einiger Makros erhalten:

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

Und voilà, Sie können jetzt die Funktionsnamen mit dem richtigen Typ und den richtigen Parametern verwenden, als wären sie statisch mit Ihrem Projekt verknüpft:

int result = dll_add(1, 1);

Haftungsausschluss: Genau genommen sind Konvertierungen zwischen verschiedenen Funktionszeigern nicht durch den C-Standard definiert und nicht sicher. Was ich hier also formal mache, ist undefiniertes Verhalten. In der Windows-Welt sind Funktionszeiger jedoch unabhängig von ihrem Typ immer gleich groß und die Konvertierungen zwischen ihnen sind auf jeder von mir verwendeten Windows-Version vorhersehbar.

Außerdem könnte theoretisch eine Auffüllung in die Vereinigung / Struktur eingefügt werden, die dazu führen würde, dass alles fehlschlägt. Zeiger haben jedoch zufällig die gleiche Größe wie die Ausrichtungsanforderungen in Windows. A, static_assertum sicherzustellen, dass die Struktur / Union keine Auffüllung hat, ist möglicherweise noch in Ordnung.

Lundin
quelle
1
Dieser Ansatz im C-Stil würde funktionieren. Aber wäre es nicht angebracht, ein C ++ - Konstrukt zu verwenden, um das #defines zu vermeiden ?
Harper
@harper Nun, in C ++ 11 könnten Sie verwenden auto dll_add = ..., aber in C ++ 03 gibt es kein Konstrukt, von dem ich denken könnte, dass es die Aufgabe vereinfachen würde (ich sehe hier auch kein besonderes Problem mit dem #defines)
Niklas B.
Da dies alles WinAPI-spezifisch ist, müssen Sie keine eigenen eingeben func_ptr_t. Stattdessen können Sie verwenden FARPROC, welches der Rückgabetyp von ist GetProcAddress. Auf diese Weise können Sie mit einer höheren Warnstufe kompilieren, ohne dem GetProcAddressAufruf eine Besetzung hinzuzufügen .
Adrian McCarthy
@ NiklasB. Sie können jeweils nur autoeine Funktion verwenden, wodurch die Idee, sie ein für alle Mal in einer Schleife auszuführen, zunichte gemacht wird. aber was ist los mit einem Array std :: function
Francesco Dondi
1
@Francesco Die std :: Funktionstypen unterscheiden sich genauso wie die funcptr-Typen. Ich denke, verschiedene Vorlagen würden helfen
Niklas B.
1

Dies ist nicht gerade ein heißes Thema, aber ich habe eine Factory-Klasse, mit der eine DLL eine Instanz erstellen und als DLL zurückgeben kann. Es ist das, wonach ich gesucht habe, aber nicht genau finden konnte.

Es heißt wie,

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

Dabei ist IHTTP_Server die reine virtuelle Schnittstelle für eine Klasse, die entweder in einer anderen oder derselben DLL erstellt wurde.

DEFINE_INTERFACE wird verwendet, um einer Klassen-ID eine Schnittstelle zu geben. Platzieren Sie innerhalb der Schnittstelle;

Eine Schnittstellenklasse sieht aus wie:

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

Die Header-Datei ist wie folgt

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Die Bibliotheken sind in dieser Makrodefinition aufgeführt. Eine Zeile pro Bibliothek / ausführbarer Datei. Es wäre cool, wenn wir eine andere ausführbare Datei aufrufen könnten.

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

Dann definieren Sie für jede DLL / Exe ein Makro und listen seine Implementierungen auf. Def bedeutet, dass dies die Standardimplementierung für die Schnittstelle ist. Wenn dies nicht die Standardeinstellung ist, geben Sie einen Namen für die Schnittstelle an, mit der sie identifiziert wird. Dh, special, und der Name lautet 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)

Wenn alle Bibliotheken eingerichtet sind, verwendet die Header-Datei die Makrodefinitionen, um die erforderlichen zu definieren.

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

Dadurch wird eine Aufzählung für die Bibliotheken erstellt.

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

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

Dadurch wird eine Aufzählung für Schnittstellenimplementierungen erstellt.

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

    long CallEntryPoint(long id, long interfaceId);

Dies definiert die Factory-Klasse. Hier ist nicht viel dran.

    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

Dann ist der CPP,

#include "sn_factory.h"

#include <windows.h>

Erstellen Sie den externen Einstiegspunkt. Sie können überprüfen, ob es existiert, mit abhängige.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;
        }
    }
}

Die Makros richten alle benötigten Daten ein.

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

Jede Bibliothek enthält diese "cpp" mit einem Stub cpp für jede Bibliothek / ausführbare Datei. Alle spezifischen kompilierten Header-Inhalte.

#include "sn_pch.h"

Richten Sie diese Bibliothek ein.

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

Ein Include für den Haupt-CPP. Ich denke, dieser CPP könnte ein .h sein. Es gibt jedoch verschiedene Möglichkeiten, dies zu tun. Dieser Ansatz hat bei mir funktioniert.

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