Динамічно завантажувати функцію з DLL


88

Я трохи переглядаю файли .dll, розумію їх використання та намагаюся зрозуміти, як ними користуватися.

Я створив файл .dll, який містить функцію, яка повертає ціле число з ім'ям funci ()

використовуючи цей код, я (думаю) я імпортував файл .dll у проект (скарг немає):

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

Однак при спробі скомпілювати цей файл .cpp, який, на мою думку, імпортував .dll, у мене виникає така помилка:

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

Я знаю, що .dll відрізняється від файлу заголовка, тому знаю, що не можу імпортувати таку функцію, але це найкраще, що я міг придумати, щоб показати, що я пробував.

Моє питання полягає в тому, як я можу використовувати hGetProcIDDLLвказівник для доступу до функції в .dll.

Сподіваюся, це питання має сенс, і я ще раз не гавкаю якесь неправильне дерево.


пошук статичного / динамічного зв'язування.
Mitch Wheat 02

Дякую, я розгляну це

Я роблю свій відступ у своєму коді, але коли я засовую його сюди, формат псується, тому в підсумку я відступаю все на 4 рядки

Відповіді:


152

LoadLibraryне робить того, що, на вашу думку, робить. Він завантажує DLL в пам'ять поточного процесу, але магічним чином не імпортує визначені в ньому функції! Це було б неможливо, оскільки виклики функцій вирішуються компонувальником під час компіляції під час виклику під час LoadLibraryвиконання (пам’ятайте, що C ++ - це статично набрана мова).

Вам потрібно окрему функцію WinAPI , щоб отримати адресу динамічно завантажуваних функцій: GetProcAddress.

Приклад

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

Крім того, вам слід правильно експортувати свою функцію з DLL. Це можна зробити так:

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

Як зазначає Лундін, хороша практика - звільнити ручку до бібліотеки, якщо вона вам довше не потрібна. Це призведе до його вивантаження, якщо жоден інший процес все ще не має дескриптора до тієї ж DLL.


Може здатися дурним запитанням, але який є / повинен бути типом f_funci?

8
Крім цього, відповідь чудова і легко зрозуміла

6
Зверніть увагу, що f_funciнасправді є типом (а не має тип). Тип f_funciчитається як "вказівник на функцію, що повертає intі не бере аргументи". Додаткову інформацію про вказівники на функції в C можна знайти за адресою newty.de/fpt/index.html .
Ніклас Б.

Ще раз дякую за відповідь, funci не бере аргументів і повертає ціле число; Я відредагував запитання, щоб показати компільовану функцію? в .dll. Коли я намагався запустити після включення "typedef int ( f_funci) ();" У мене виникла така помилка: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || У функції 'int main ()': | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | помилка: не вдається перетворити 'int ( ) ()' у 'const CHAR *' для аргументу '2' в 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Збірка закінчена: 1 помилка, 0 попереджень === |

Ну, я забув там акторський склад (відредагував його). Однак помилка, схоже, ще одна, ви впевнені, що використовуєте правильний код? Якщо так, чи можете ви, будь-ласка, вставити свій невдалий код та повний висновок компілятора на pastie.org ? Крім того, typedef, який ви написали у своєму коментарі, помилковий ( *відсутній символ, який міг спричинити помилку)
Ніклас Б.

34

На додаток до вже опублікованої відповіді, я подумав, що маю поділитися зручним трюком, який я використовую для завантаження всіх функцій DLL у програму за допомогою покажчиків на функції, без написання окремого виклику GetProcAddress для кожної функції. Я також люблю викликати функції безпосередньо, як це було зроблено в OP.

Почніть з визначення загального типу покажчика функції:

typedef int (__stdcall* func_ptr_t)();

Які типи використовуються, насправді не важливі. Тепер створіть масив такого типу, який відповідає кількості функцій, які ви маєте в DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

У цьому масиві ми можемо зберігати фактичні покажчики функцій, які вказують на простір пам'яті DLL.

Наступна проблема полягає в тому, GetProcAddressщо імена функцій очікуються як рядки. Тож створіть подібний масив, що складається з імен функцій у DLL:

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

Тепер ми можемо легко викликати GetProcAddress () у циклі і зберігати кожну функцію всередині цього масиву:

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

Якщо цикл був успішним, єдиною проблемою, яку ми маємо зараз, є виклик функцій. Покажчик функції typedef від попереднього не є корисним, оскільки кожна функція матиме власний підпис. Це можна вирішити, створивши структуру з усіма типами функцій:

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;

І нарешті, щоб зв’язати їх із масивом раніше, створіть об’єднання:

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

Тепер ви можете завантажити всі функції з DLL за допомогою зручного циклу, але викликати їх через by_typeчлена об'єднання.

Але звичайно, дещо обтяжливо набирати щось подібне

functions.by_type.dll_add_ptr(1, 1); щоразу, коли ви хочете викликати функцію.

Як виявляється, саме з цієї причини я додав до імен постфікс "ptr": я хотів, щоб вони відрізнялися від власне імен функцій. Тепер ми можемо згладити синтаксис icky struct і отримати бажані імена, використовуючи деякі макроси:

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

І voilà, тепер ви можете використовувати імена функцій із правильним типом та параметрами, як якщо б вони були статично пов’язані з вашим проектом:

int result = dll_add(1, 1);

Застереження: Строго кажучи, перетворення між різними покажчиками функцій не визначаються стандартом С і не є безпечними. Тож формально те, що я тут роблю, - це невизначена поведінка. Однак у світі Windows покажчики функцій завжди однакового розміру, незалежно від їх типу, і перетворення між ними передбачувані в будь-якій версії Windows, яку я використовував.

Крім того, теоретично в об'єднання / структуру може бути вставлено заповнення, що призведе до того, що все вийде з ладу. Однак покажчики мають той самий розмір, що і вимога до вирівнювання в Windows. A, static_assertщоб переконатись, що у struct / union немає відступів, все ще може бути в порядку.


1
Цей підхід у стилі С спрацював би. Але чи не доречно буде використовувати конструкцію C ++, щоб уникнути #defines?
харпер

@harper Ну, в C ++ 11 ви могли б використовувати auto dll_add = ..., але в C ++ 03 немає конструкції, яку я міг би придумати, яка б спростила завдання (я також не бачу тут особливої ​​проблеми з #defines)
Ніклас Б.

Оскільки це все, що стосується WinAPI, вам не потрібно вводити свій власний текст func_ptr_t. Замість цього ви можете використовувати FARPROC, який є типом повернення GetProcAddress. Це може дозволити вам компілювати з вищим рівнем попередження, не додаючи GetProcAddressзаклик до дзвінка.
Адріан Маккарті

@NiklasB. Ви можете використовувати лише autoодну функцію за раз, що перешкоджає ідеї робити це раз і назавжди в циклі. але що не так з масивом std :: function
Франческо Донді

1
@Francesco типи std :: function будуть відрізнятися так само, як типи funcptr. Думаю, варіативні шаблони допомогли б
Ніклас Б.

1

Це не зовсім гаряча тема, але у мене є заводський клас, який дозволяє dll створювати екземпляр і повертати його як DLL. Це те, що я шукав, але не міг точно знайти.

Це називається як,

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

де IHTTP_Server - це чисто віртуальний інтерфейс для класу, створеного або в іншій DLL, або в тій самій.

DEFINE_INTERFACE використовується для надання ідентифікатору класу інтерфейсу. Місце всередині інтерфейсу;

Клас інтерфейсу виглядає так:

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

Файл заголовка такий

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Бібліотеки перелічені у цьому визначенні макросу. Один рядок на бібліотеку / виконуваний файл. Було б круто, якби ми могли зателефонувати до іншого виконуваного файлу.

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

Потім для кожного dll / exe ви визначаєте макрос і перелічуєте його реалізації. Def означає, що це реалізація за замовчуванням для інтерфейсу. Якщо це не за замовчуванням, ви вказуєте назву інтерфейсу, який використовується для його ідентифікації. Тобто, спеціальне, і ім'я буде 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)

З усіма налаштуваннями бібліотек файл заголовка використовує визначення макросів для визначення необхідного.

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

Це створює перелік бібліотек.

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

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

Це створює перелік для реалізації інтерфейсу.

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

    long CallEntryPoint(long id, long interfaceId);

Це визначає заводський клас. Тут мало до цього.

    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

Тоді CPP,

#include "sn_factory.h"

#include <windows.h>

Створіть зовнішню точку входу. Ви можете перевірити, чи існує він, використовуючи 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;
        }
    }
}

Макроси встановлюють усі необхідні дані.

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

Кожна бібліотека включає цей "cpp" із заглушеним cpp для кожної бібліотеки / виконуваного файлу. Будь-які конкретні скомпільовані матеріали заголовка.

#include "sn_pch.h"

Налаштуйте цю бібліотеку.

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

Включення для основного cpp. Я думаю, це cpp може бути .h. Але існують різні способи зробити це. Цей підхід спрацював для мене.

#include "../inc/sn_factory.cpp"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.