DLL 的可重用函数加载器






4.10/5 (6投票s)
2002 年 10 月 14 日
7分钟阅读

132849

782
提供了简洁的语法,用于显式加载 DLL 及其导出函数。导出的 DLL 函数在源代码级别上显示为局部 extern "C" 函数或类成员函数。
调用同一 DLL 导出函数的不同方法。
引言
本文描述了一个 DLL 加载器类的设计和实现。该类的实例能够将 DLL 文件显式加载和卸载到内存中,并提供访问 DLL 文件任何导出函数的方法。
此设计的重点在于提供一种简洁、清晰的源代码级别语法来执行驻留在 DLL 中的例程。该类的主要设计目标是提供一种简单的方法,将现有应用程序的各种函数重定位到各种动态链接库 (DLL) 中,以便在运行时加载同一函数的不同实现。
特别是,所描述的类最初是为了解决以下场景而设计的。假设一个应用程序已被开发并编译成一个独立的 exe 文件。该产品取得了巨大的成功,现在一位软件架构师(我的天!)希望将该应用程序的各种通用(可能可扩展)函数分解到一个 DLL 模块中。想法是在运行时从 DLL 加载这些函数,并将(现在驻留在 DLL 中的)函数调用得就像它们在应用程序内部本地定义一样。DLL 文件利用所谓的PE 格式。由于需要维护现有的代码库,程序员显然希望对现有源代码的更改越少越好。
/// /// macro redefinition for your brain /// (code fix by Mr. Anonymous) #ifdef marco #undef marco #define marco macro #endif
根据这种情况,需要完成的任务是
- 将函数重定位到 DLL 并导出它们,
- 在应用程序运行时加载 DLL,
- 提供对导出 DLL 函数的访问,以及
- 在适当的时候卸载 DLL。
如上图(页面顶部)所示,加载导出函数所需的代码非常简单。更重要的是,那里的示例代码演示了我们的 DLL 加载器设计可以适应提供 C 或 C++ 接口来访问导出的 DLL 函数。同时请注意,与其他已知实现相比,宏的使用已降至最低。当前实现比作者在撰写本文时已知的其他现有技术提供了明显更简洁的语法。
我想指出的是,最初的概念是让程序在运行时加载和发现给定 DLL 的所有导出函数。这仍然是无法实现的。
顺便说一句,我一直认为代码的可重用性可以通过实现某个特定模式所需的行数来衡量。所需的行数越少,代码的可重用性就越高。此外,可重用性意味着自然的 C++ 语法。晦涩的基于宏的实现会降低可重用性,因为程序员必须查阅文档才能找出该宏的作用(例如:MFC 宏METHOD_PROLOGUE(xx,yy)
)。
设计问题
第一个想到的想法是重载一个类的operator FunctionPointerType ()
,其中FunctionPointerType
是函数指针,这样在编译时,编译器就会对所有候选函数进行排序,并自动将变量类型转换为函数指针类型。例如,考虑以下代码片段
1 class DllFunction { 2 typedef int(*FuncPtrType)(int); 3 FuncPtrType fp; 4 public: 5 // ctor() 6 DllFunction(FuncPtrType t) { fp =t;} 7 8 // typecast 9 operator FuncPtrType () { 10 return fp; 11 } 12 }; 13 14 int test(int i) { cout << i << endl; } 15 16 int main() { 17 DllFunction a(test); 18 19 int bb = test(10); 20 int cc = a(10); // automatically typecast // a from class DllFunction to FuncPtrType 21 return 0; 22 } 23
这实际上是我实现 DLL 加载器的第一次尝试。不幸的是,这段代码在 MSVC 系列编译器(测试过:MSVC6, VS.NET)上无法编译。在深入研究之后,我意识到operator FuncPtrType ()
从未在编译阶段被视为候选函数。这是 MSVC 系列编译器特有的。确切的错误出现在第 20 行,"error C2064: term does not evaluate to a function"
。上面的代码段使用g++可以完美编译。使用运算符重载有一个明显的优点:它允许在实际调用导出函数之前,将 DLL 的显式加载推迟到内存中。也就是说:在operator FuncPtrType()
中实现一些逻辑来加载给定的 DLL。
第一个变通方法
作者想出了许多不同的方法来绕过 MSVC 编译器的这个限制。目前,有两个具体的实现值得考虑。
编译错误发生是因为变量a
(在上面的第 20 行)无法自动转换为函数指针(由于编译器的限制)。我问自己的一些问题是:有没有办法绕过那个限制?考虑到我们有大量的代码库需要维护,有什么方法最简单(对我来说最省力)来实现a(10)
实际调用函数?
这些问题的答案代表了我的第一个和第二个解决方案,将立即得到解决。对于第一个解决方案,关键是使用在应用程序中本地或全局定义的函数指针变量。换句话说,我们将a
声明为int(* a)(int) = test;
。这消除了显式类型转换的需要,因为实际变量已经是函数指针了。不过缺点是,与使用类封装 DLL 函数相比,延迟加载行为不再能很好地支持。以下代码段显示了如何做到这一点。我想知道为什么代码看起来像标准的 C 代码?
1 2 int _stdcall test(int i) { cout << i << endl; } 3 4 int main() { 5 int (_stdcall *a) (int); // a is now a function pointer 6 a=test; 7 8 int bb = test(10); 9 int cc = a(10); 10 return 0; 11 } 12
第二个变通方法
我不太愿意透露我的第二个实现,因为它与我现有的代码库在源代码级别上不兼容。因此,需要进行大量的搜索和替换才能使我现有的代码与此设计配合使用。正是出于行业不合规和糟糕的编程实践的精神(这要归功于一家臭名昭著的软件公司),我决定展示我的第二个实现。再次重申,我接下来要展示的是一个糟糕的设计,因为它
- 与我现有的代码库不兼容;
- 它未能体现
operator()()
的真正威力; - 如果程序员选择不使用我的设计,则必须手动重新编辑已开发的代码才能再次编译。
说了这么多,你看到的一切都是 C++。
为了理解开发此实现的根本原因,让我们回到之前提出的一个问题。如果变量a
无法自动转换为函数指针(由于编译器的限制),我们是否可以手动转换它,以便a(10)
确实调用了一个函数?也就是说
typedef (int)(_stdcall* FuncPtrType)(int); (FuncPtrType a)(10);
当然,这是可能的。有多种方法可以做到这一点。我选择使用operator()()
返回一个 Function Pointer Type 对象,以便我们可以编写诸如a()(10)
之类的代码。以下代码段说明了如何做到这一点。
1 class DllFunction { 2 typedef int(*FuncPtrType)(int); 3 FuncPtrType fp; 4 public: 5 // ctor() 6 DllFunction(FuncPtrType t) { fp =t;} 7 8 // typecast... now useless bcos compiler // doesnt select it as candidate function 9 operator FuncPtrType () { 10 return fp; 11 } 12 13 // function operator 14 FuncPtrType operator ()() { 15 return fp; 16 } 17 18 }; 19 20 int test(int i) { cout << i << endl; } 21 22 int main() { 23 DllFunction a(test); 24 25 int bb = test(10); 26 int cc = a()(10); // a() returns a function pointer object. 27 // a()(10) invoke the actual function 28 return 0; 29 } 30
由于此设计使用对象来封装 DLL 导出的函数,因此现在可以实现延迟加载行为。这可以通过在operator()()
中引入一些逻辑来轻松实现。有关示例,请参阅 Zip 文件。
其他可能性
- 重载
operator ()(...)
如果使用
a(10);
之类的语法,默认将触发函数调用。要求
- 如何将参数传递给 DLL 函数(已在堆栈上!)。
- 在
operator() (...)
内部使用 ASMjmp
到 DLL 函数地址,但类成员和extern "C"
函数的堆栈组织不同。 - 需要在 Win32 实现中清理堆栈组织。
可下载的 Zip 文件
附加的可下载 zip 文件包含两个示例实现。这些示例仅应视为概念验证实现。它们不是生产就绪的代码!!!
有关 DLL 映射和从 DLL 加载函数的更多信息,请参阅 MSVC 帮助文件。
结束语
所述的 DLL 加载器实现使得在源代码级别上访问 DLL 函数的语法简洁明了。加载和执行 DLL 文件中的导出函数需要四行代码。
作者想重申,最初的概念是让程序在运行时加载和发现给定 DLL 的所有导出函数。这目前仍然无法实现,尽管在这方面已经取得了重大进展。
最后,我想指出的是,存在基于宏的实现,它们对于通用的 DLL 用途效果非常好。例如,由姚志峰 A class to wrap DLL functions(日期:2002 年 6 月 26 日)实现的。