Dynamicznie ładuj funkcję z biblioteki DLL

88

Przyglądam się plikom .dll, rozumiem ich użycie i próbuję zrozumieć, jak ich używać.

Utworzyłem plik .dll, który zawiera funkcję zwracającą liczbę całkowitą o nazwie funci ()

używając tego kodu, (chyba) zaimportowałem plik .dll do projektu (nie ma żadnych skarg):

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

Jednak kiedy próbuję skompilować ten plik .cpp, który moim zdaniem zaimportował .dll, pojawia się następujący błąd:

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

Wiem, że plik .dll różni się od pliku nagłówkowego, więc wiem, że nie mogę zaimportować takiej funkcji, ale jest to najlepsze, co mogłem wymyślić, aby pokazać, że próbowałem.

Moje pytanie brzmi: w jaki sposób mogę użyć hGetProcIDDLLwskaźnika, aby uzyskać dostęp do funkcji w pliku .dll.

Mam nadzieję, że to pytanie ma sens i nie szczekam po raz kolejny niewłaściwego drzewa.

Wyrozumiałość
źródło
wyszukiwanie statyczne / dynamiczne łączenie.
Mitch Wheat
Dziękuję, przyjrzę się temu
Wciskam swój kod, ale kiedy go

Odpowiedzi:

152

LoadLibrarynie robi tego, co myślisz, że robi. Ładuje bibliotekę DLL do pamięci bieżącego procesu, ale nie importuje w magiczny sposób zdefiniowanych w niej funkcji! Nie byłoby to możliwe, ponieważ wywołania funkcji są rozwiązywane przez konsolidator w czasie kompilacji, podczas gdy LoadLibraryjest wywoływane w czasie wykonywania (pamiętaj, że C ++ jest językiem z typowaniem statycznym ).

Trzeba osobnej funkcji WinAPI, aby uzyskać adres dynamicznie obciążonych funkcji: GetProcAddress.

Przykład

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

Powinieneś także poprawnie wyeksportować swoją funkcję z biblioteki DLL. Można to zrobić w następujący sposób:

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

Jak zauważa Lundin, dobrą praktyką jest zwolnienie uchwytu do biblioteki, jeśli nie potrzebujesz go dłużej. Spowoduje to zwolnienie go, jeśli żaden inny proces nie ma jeszcze uchwytu do tej samej biblioteki DLL.

Niklas B.
źródło
Może brzmieć jak głupie pytanie, ale jaki jest / powinien być typ f_funci?
8
Poza tym odpowiedź jest doskonała i łatwa do zrozumienia
6
Zauważ, że f_funciw rzeczywistości jest typem (a nie ma typ). Typ jest f_funciodczytywany jako „wskaźnik do funkcji zwracającej inti nie pobierającej argumentów”. Więcej informacji o wskaźnikach funkcji w C można znaleźć na stronie newty.de/fpt/index.html .
Niklas B.
Jeszcze raz dziękuję za odpowiedź, funci nie przyjmuje argumentów i zwraca liczbę całkowitą; Zredagowałem pytanie, aby pokazać skompilowaną funkcję? do .dll. Kiedy próbowałem uruchomić po uwzględnieniu "typedef int ( f_funci) ();" Otrzymałem ten błąd: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || W funkcji „int main ()”: | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | błąd: nie można przekonwertować 'int ( ) ()' na 'const CHAR *' dla argumentu '2' na 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Kompilacja zakończona: 1 błąd, 0 ostrzeżeń === |
Cóż, zapomniałem o obsadzie (zredagowałem ją). Wydaje się jednak, że błąd jest inny, czy na pewno używasz poprawnego kodu? Jeśli tak, czy możesz wkleić kod, który uległ awarii i pełne dane wyjściowe kompilatora na pastie.org ? Ponadto, wpisany w komentarzu typ tekstu jest nieprawidłowy ( *brak znaku , który mógł spowodować błąd)
Niklas B.
34

Oprócz już opublikowanej odpowiedzi, pomyślałem, że powinienem podzielić się poręczną sztuczką, której używam do ładowania wszystkich funkcji DLL do programu za pomocą wskaźników funkcji, bez pisania oddzielnego wywołania GetProcAddress dla każdej funkcji. Lubię również wywoływać funkcje bezpośrednio, jak próbowano w PO.

Zacznij od zdefiniowania ogólnego typu wskaźnika funkcji:

typedef int (__stdcall* func_ptr_t)();

Jakie typy są używane, nie są naprawdę ważne. Teraz utwórz tablicę tego typu, która odpowiada liczbie funkcji, które masz w bibliotece DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

W tej tablicy możemy przechowywać rzeczywiste wskaźniki funkcji, które wskazują na przestrzeń pamięci DLL.

Następnym problemem jest to, że GetProcAddressoczekuje się , że nazwy funkcji są łańcuchami. Stwórz więc podobną tablicę składającą się z nazw funkcji w bibliotece DLL:

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

Teraz możemy łatwo wywołać GetProcAddress () w pętli i przechowywać każdą funkcję w tej tablicy:

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

Jeśli pętla się powiedzie, jedynym problemem, jaki mamy teraz, jest wywoływanie funkcji. Wcześniejszy wskaźnik funkcji typedef nie jest pomocny, ponieważ każda funkcja będzie miała swój własny podpis. Można to rozwiązać, tworząc strukturę ze wszystkimi typami funkcji:

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;

I na koniec, aby połączyć je z tablicą sprzed wcześniej, utwórz sumę:

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

Teraz możesz załadować wszystkie funkcje z biblioteki DLL za pomocą wygodnej pętli, ale wywołać je za pośrednictwem elementu by_typeczłonkowskiego unii.

Ale oczywiście napisanie czegoś takiego jest trochę uciążliwe

functions.by_type.dll_add_ptr(1, 1); kiedykolwiek chcesz wywołać funkcję.

Jak się okazuje, jest to powód, dla którego dodałem do nazw przedrostek „ptr”: chciałem, aby różniły się one od rzeczywistych nazw funkcji. Możemy teraz wygładzić składnię icky struct i uzyskać żądane nazwy, używając kilku makr:

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

I voilà, możesz teraz używać nazw funkcji z odpowiednim typem i parametrami, tak jakby były statycznie połączone z Twoim projektem:

int result = dll_add(1, 1);

Zastrzeżenie: Ściśle mówiąc, konwersje między różnymi wskaźnikami funkcji nie są zdefiniowane w standardzie C i nie są bezpieczne. Więc formalnie to, co tutaj robię, to niezdefiniowane zachowanie. Jednak w świecie Windows wskaźniki funkcji są zawsze tego samego rozmiaru bez względu na ich typ, a konwersje między nimi są przewidywalne w każdej wersji systemu Windows, z której korzystałem.

Teoretycznie może istnieć wypełnienie wstawione do union / struct, co spowoduje, że wszystko zawiedzie. Jednak wskaźniki mają taki sam rozmiar, jak wymóg wyrównania w systemie Windows. static_assertAby upewnić się, że struct / union nie ma wyściółka może być w porządku nadal.

Lundin
źródło
1
Takie podejście w stylu C zadziałałoby. Ale czy nie byłoby właściwe użycie konstrukcji C ++, aby uniknąć #defines?
harper
@harper Cóż, w C ++ 11 można by użyć auto dll_add = ..., ale w C ++ 03 nie ma konstrukcji, o której mógłbym pomyśleć, który uprościłby zadanie (nie widzę tutaj również żadnego problemu z #defines)
Niklas B.
Ponieważ to wszystko jest specyficzne dla WinAPI, nie musisz wpisywać własnego func_ptr_t. Zamiast tego możesz użyć FARPROC, który jest zwracanym typem GetProcAddress. Może to pozwolić na kompilację z wyższym poziomem ostrzeżenia bez dodawania rzutowania do GetProcAddresswywołania.
Adrian McCarthy
@NiklasB. można używać tylko autodla jednej funkcji naraz, co niweczy pomysł zrobienia tego raz na zawsze w pętli. ale co jest nie tak z tablicą std :: function
Francesco Dondi
1
@Francesco typy funkcji std :: będą się różnić tak samo jak typy funcptr. Myślę, że pomogłyby szablony wariadyczne
Niklas B.
1

Nie jest to gorący temat, ale mam klasę fabryczną, która umożliwia dll utworzenie instancji i zwrócenie jej jako biblioteki DLL. To jest to, czego szukałem, ale nie mogłem dokładnie znaleźć.

Nazywa się to

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

gdzie IHTTP_Server jest czystym interfejsem wirtualnym dla klasy utworzonej w innej lub tej samej bibliotece DLL.

DEFINE_INTERFACE służy do nadania identyfikatorowi klasy interfejsu. Umieść interfejs wewnątrz;

Klasa interfejsu wygląda następująco:

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

Plik nagłówkowy jest taki

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Biblioteki są wymienione w tej definicji makra. Jedna linia na bibliotekę / plik wykonywalny. Byłoby fajnie, gdybyśmy mogli wywołać inny plik wykonywalny.

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

Następnie dla każdego dll / exe definiujesz makro i wypisujesz jego implementacje. Def oznacza, że ​​jest to domyślna implementacja interfejsu. Jeśli nie jest to ustawienie domyślne, podajesz nazwę interfejsu używanego do jego identyfikacji. To znaczy specjalne, a nazwa będzie miała postać 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)

W przypadku wszystkich konfiguracji bibliotek plik nagłówkowy używa definicji makr do zdefiniowania potrzebnych.

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

Spowoduje to utworzenie wyliczenia dla bibliotek.

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

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

Spowoduje to utworzenie wyliczenia dla implementacji interfejsu.

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

    long CallEntryPoint(long id, long interfaceId);

Definiuje klasę fabryczną. Niewiele do tego tutaj.

    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

Następnie CPP jest,

#include "sn_factory.h"

#include <windows.h>

Utwórz zewnętrzny punkt wejścia. Możesz sprawdzić, czy istnieje, używając depend.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;
        }
    }
}

Makra konfigurują wszystkie potrzebne dane.

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

Każda biblioteka zawiera ten „cpp” ze skrótowym cpp dla każdej biblioteki / pliku wykonywalnego. Wszelkie konkretne skompilowane elementy nagłówka.

#include "sn_pch.h"

Skonfiguruj tę bibliotekę.

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

Dołącz do głównego cpp. Myślę, że ten cpp może być .h. Ale można to zrobić na różne sposoby. To podejście zadziałało dla mnie.

#include "../inc/sn_factory.cpp"
Peter Driscoll
źródło