Динамически загружать функцию из 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.

Надеюсь, этот вопрос имеет смысл, и я снова не лаю на какое-то неправильное дерево.

Снисходительность
источник
поиск статических / динамических ссылок.
Митч Уит
Спасибо, я
Я делаю отступ в своем коде, но когда я засовываю его сюда, формат портится, поэтому я делаю отступ всего на 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, который вы написали в своем комментарии, неправильный ( *отсутствует, что могло вызвать ошибку)
Niklas B.
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»: я хотел, чтобы они отличались от настоящих имен функций. Теперь мы можем сгладить неприглядный синтаксис структуры и получить желаемые имена, используя некоторые макросы:

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

И вуаля, теперь вы можете использовать имена функций с правильным типом и параметрами, как если бы они были статически связаны с вашим проектом:

int result = dll_add(1, 1);

Отказ от ответственности: строго говоря, преобразования между разными указателями функций не определены стандартом C и небезопасны. Итак, формально то, что я здесь делаю, - это неопределенное поведение. Однако в мире Windows указатели на функции всегда имеют один и тот же размер независимо от их типа, и преобразования между ними предсказуемы в любой версии Windows, которую я использовал.

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

Лундин
источник
1
Такой подход в стиле C. подойдет. Но разве не уместно использовать конструкцию C ++, чтобы избежать #defines?
harper
@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>

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

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

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"
Питер Дрисколл
источник