从DLL动态加载函数

113

我在研究.dll文件,我理解它们的用法并试图了解如何使用它们。

我创建了一个包含返回整数的函数funci()的.dll文件。

使用这段代码,我(认为)已将.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;
}

但是当我尝试编译这个我认为已经导入了.dll文件的.cpp文件时,出现了以下错误:

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中的函数。
我希望这个问题是有意义的,而且我没有再次走错路。
3个回答

181
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() {
   // ...
}

正如Lundin所指出的那样,如果您不再需要库句柄,最好进行释放。这将导致它在没有其他进程仍然持有同一DLL的句柄时被卸载。


8
除此之外,答案非常优秀且易于理解。 - user969416
6
请注意,f_funci实际上是一种类型(而不是拥有类型)。类型f_funci表示“指向返回int且不带参数的函数的指针”。有关C语言中函数指针的更多信息,请访问http://www.newty.de/fpt/index.html。 - Niklas B.
我想再补充一件事:当从Windows本地C ++(即来自user32.dll的函数,但也包括任何其他使用__stdcall调用约定的函数)进行调用时,typedef应该如下所示: typedef int (_stdcall*f_funci)(); - baderman
1
请注意,在程序结束时需要调用 FreeLibrary() - Lundin
1
为什么要使用__stdcall?在Windows 10上,使用VS2013会导致名称混淆,从而防止GetProcAddress()找到函数。对我来说,__cdecl可以工作。此外,在加载dll时链接所有dll函数并不是不可能的(但很可能是不可取的)。我不知道Windows上的等效方法,但在Linux上,可以通过向dlopen()传递RTLD_NOW来完成此操作。这是一种不寻常的动态加载方式,但基本上就是动态链接所发生的事情。 - Praxeolitic
显示剩余18条评论

41

除了已经发布的答案,我认为我应该分享一个方便的技巧,即通过函数指针将所有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中指针恰好与对齐要求具有相同的大小。建议使用static_assert来确保结构体/联合体没有填充位。

1
这种 C 风格的方法是可行的。但是使用 C++ 构造来避免 #define 不是更合适吗? - harper
@harper 在C++11中,您可以使用auto dll_add = ...,但在C++03中,我想不到任何可以简化任务的结构(我也没有看到这里的#define存在任何特定问题)。 - Niklas B.
1
既然这都是WinAPI特定的,你就不需要自己typedef func_ptr_t。相反,你可以使用GetProcAddress的返回类型FARPROC。这可以让你在不添加到GetProcAddress调用的强制转换的情况下使用更高的警告级别进行编译。 - Adrian McCarthy
1
@Francesco,std::function类型将与funcptr类型不同。我猜可变模板会有所帮助。 - Niklas B.
@Lundin,您可否通过将“functions_struct”作为匿名结构体嵌入到“functions_union”中,并通过“functions_union”访问函数,来消除需要通过宏或者使用“by_type”访问函数的必要性。示例如下:typedef union { struct { int (__stdcall* add)(int, int); }; func_ptr_t func_ptr [DLL_FUNCTIONS_N]; } functions_union; - Bja
显示剩余2条评论

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或同一DLL中创建。

DEFINE_INTERFACE用于为类ID指定一个接口。将其放置在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表示它是接口的默认实现。如果不是默认值,则为其提供用于标识的接口名称。例如,special,名称将为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"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接