现在,假设您正在编写一个程序,可能是一款游戏,并且您希望在不干预的情况下使其具有可修改性。当然,您会思考这如何实现,并且不用强迫用户直接将代码注入您的可执行文件,或者直接修改源代码。您将如何做到这一点?
嗯,答案当然是一个插件系统。我将简要解释它是如何工作的:插件系统只是搜索一个指定的文件夹以查找 DLL(或其他类似文件),如果找到任何文件,则将内容添加到程序中。当然,由于程序实际上不知道 DLL 中
包含什么,所以通常的做法是让 DLL 定义一个由程序本身定义的入口点和调用函数,然后程序可以使用这些 DLL 中暴露的功能。如何做到这一点取决于您,无论是定义要实现的函数,还是让 DLL 提供一个基类的实例,然后使用该实例的功能。在本文中,我将简要演示这两种选项。不过,首先,让我们看看如何实际
加载库。
加载库
好了,让我们从基础开始。要在运行时加载 DLL,只需调用
LoadLibrary
,参数是要加载的 DLL 的文件路径。但是,当您想到这一点时,这并没有多大帮助,对吧?我们想加载
可变数量的 DLL,它们的名称
在编译时无法得知。所以,这意味着我们需要找到所有是插件的 DLL,然后加载它们。
现在,最简单的方法是使用 WinAPI 的
FindFile
函数,使用文件掩码来收集所有 .dll 文件。不过,这可能会遇到一个问题,那就是您可能会尝试加载您的程序需要运行的 DLL!这就是程序通常有一个“plugins”文件夹的原因:如果您尝试从程序的目录加载所有 DLL,您可能会开始尝试加载非插件 DLL。将它们分离到一个指定的插件文件夹有助于防止这种情况发生。
好了,说得够多了,这里有一些示例代码,演示如何遍历目录中的所有文件并加载每个文件的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
// A list to store our DLL handles
std::vector<HINSTANCE> modules;
// The data for each file we find.
WIN32_FIND_DATA fileData;
// Find the first DLL file in out plugins folder,
// and store the data in out fileData structure.
HANDLE fileHandle = FindFirstFile(R"(.\plugins\*.dll)", &fileData);
if (fileHandle == (void*)ERROR_INVALID_HANDLE ||
fileHandle == (void*)ERROR_FILE_NOT_FOUND) {
// We couldn't find any plugins, lets just
// return for now (imagine this is in main)
return 0;
}
// Loop over every plugin in the folder, and store
// the handle in our modules list
do {
// Load the plugin. We need to condense the plugin
// name and the path together to correctly load the
// file (There are other ways, I won't get into it here)
HINSTANCE temp = LoadLibrary((R"(.\plugins\)" +
std::string(fileData.cFileName)) .c_str());
if (!temp) {
// Couldn't load the library, continue on
cerr << "Couldn't load library " << fileData.cFileName << "!\n";
continue;
}
// Add the loaded module to our list of modules
modules.push_back(temp);
// Continue while there are more files to find
} while (FindNextFile(fileHandle, &fileData));
|
好了,这相当复杂。我只是想现在提一下,您需要一个 C++11 编译器才能编译这些示例,否则像原始字符串字面量之类的一些东西将无法编译。另外,如果您使用 Unicode 编译器,您将需要指定它正在使用宽字符串。
现在,我们已经加载了所有插件,但是如果我们完成工作后不释放它们,我们将导致内存泄漏,这在大型项目中可能成为一个真正的问题。但是,因为我们将所有句柄都存储在 vector 中,所以释放它们实际上并不难。
1 2
|
for (HINSTANCE hInst : modules)
FreeLibrary(hInst);
|
实际使用我们的库
好的,现在我们可以加载库了。问题是,它实际上
还没有做任何事情。让我们改变一下。首先,我们应该为 DLL 定义一个头文件,以便它们可以包含:这定义了我们希望它们导出的函数和类。我决定在这里展示两件事:如何导出多态类以及如何导出函数。一旦您掌握了想法,大多数事情就很容易了。总之,让我们定义我们的头文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
#ifndef __MAIN_HPP_INCLUDED__
#define __MAIN_HPP_INCLUDED__
// includes we need
#include <string>
#include <memory>
// Test to see if we are building a DLL.
// If we are, specify that we are exporting
// to the DLL, otherwise don't worry (we
// will manually import the functions).
#ifdef BUILD_DLL
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI
#endif // BUILD_DLL
// This is the base class for the class
// retrieved from the DLL. This is used simply
// so that I can show how various types should
// be retrieved from a DLL. This class is to
// show how derived classes can be taken from
// a DLL.
class Base {
public:
// Make sure we call the derived classes destructors
virtual ~Base() = default;
// Pure virtual print function, effect specific to DLL
virtual void print(void) = 0;
// Pure virtual function to calculate something,
// according to an unknown set of rules.
virtual double calc(double val) = 0;
};
// Get an instance of the derived class
// contained in the DLL.
DLLAPI std::unique_ptr<Base> getObj(void);
// Get the name of the plugin. This can
// be used in various associated messages.
DLLAPI std::string getName(void);
#endif // __MAIN_HPP_INCLUDED__
|
现在,到了复杂的部分。我们需要从我们之前加载的 DLL 中加载这些函数。用于此目的的函数称为
GetProcAddress()
,它返回一个指向 DLL 中具有您指定的名称的函数的指针。但是,由于它不知道它获取的函数类型,我们需要将返回的指针显式转换为适当类型的函数指针。将此代码添加到之前的示例中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
do {
...
modules.push_back(temp);
// Typedefs for the functions. Don't worry about the
// __cdecl, that is for the name mangling (look up
// that if you are interested).
typedef std::unique_ptr<Base> (__cdecl *ObjProc)(void);
typedef std::string (__cdecl *NameProc)(void);
// Load the functions. This may or may not work, based on
// your compiler. If your compiler created a '.def' file
// with your DLL, copy the function names from that to
// these functions. Look up 'name mangling' if you want
// to know why this happens.
ObjProc objFunc = (ObjProc)GetProcAddress(temp, "_Z6getObjv");
NameProc nameFunc = (NameProc)GetProcAddress(temp, "_Z7getNamev");
// use them!
std::cout << "Plugin " << nameFunc() << " loaded!\n";
std::unique_ptr<Base> obj = objFunc();
obj->print();
std::cout << "\t" << obj->calc() << std::endl;
} while (...);
|
加载和使用插件就是这样!您可能希望将对象/名称存储在自己的列表中,但这并不重要,这只是一个示例。
构建插件
现在,还有最后一件事:实际构建插件。相比之下,这非常简单。您需要
#include "main.hpp"
以获取类,然后简单地实现函数。您需要注意的唯一一件事是
main()
函数:首先,它实际上不再称为 main 了!这里只是一个基本的 main 函数(您通常不需要比这更多)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
extern "C" DLLAPI BOOL APIENTRY DllMain(HINSTANCE hInst,
DWORD reason,
LPVOID reserved)
{
switch (reason) {
case DLL_PROCESS_ATTACH:
// attach to process, return FALSE to fail
break;
case DLL_PROCESS_DETACH:
// detaching from process
break;
case DLL_THREAD_ATTACH:
// attach to thread within process
break;
case DLL_THREAD_DETACH:
// detach from thread within process
break;
}
// return success
return TRUE;
}
|
获取源代码
在这里,我提供了一个源代码链接以方便您使用。这是使用 MinGW-w64 工具链编译的,但它应该适用于大多数 Windows 编译器。这些示例需要 C++11 支持(主要是原始字符串字面量和 std::unique_ptr),并且考虑到 ANSI 编码(而不是 Unicode)而开发。这应该不会有太大区别,只需将字符串字面量更改为长字符串字面量,然后使用宽字符串(std::wstring)即可。
关于项目文件,不幸的是我无法提供全部,只有一个 GCC makefile。要了解如何使用您的编译器编译项目,请查看该编译器的文档。可能您最不知道的信息是如何生成 DLL,以及在编译时如何定义符号。
下载源代码
最后的 Remarks
如果您不理解本文中的任何内容,请首先查看 MSDN(Microsoft 开发者网络)。事实上,我将引用 MSDN 作为本文的主要信息来源。这里有一些您可能感兴趣的相关页面的链接。
DLL 加载函数
文件相关函数
如果本文中有任何不清楚的地方、您发现的错误或您对本文更新有任何建议,请告诉我!
更新 - 在不同编译器中使用插件系统
由于遇到了
此问题,我决定稍微更新本文。以上内容保持不变,但是。基本上,我将介绍如何解决使用其他编译器构建的 DLL 的问题。
问题
如果您使用不同的编译器、编译器的不同版本,甚至同一编译器的不同设置,DLL 的生成方式都会不同,并可能导致与其链接的应用程序崩溃。这是因为 C++
不是二进制标准化的 - 即,不同编译器上的相同源代码没有要求以相同的方式运行。特别是 C++ 标准库,不同的编译器可能有不同的实现,这可能导致程序出现问题。
在编译器之间可能更改的另一件事(通常,它
会更改)是函数的名称修饰。在上面的示例中,函数
getObj
被替换为以下名称:
_Z6getObjv
。但是,这个特定的名称取决于生成它的编译器:这个名称来自 MinGW 编译器,MSVS 编译器会生成不同的名称,Intel 编译器会生成另一个名称。这也可能导致问题。
一些解决方案
对于上述问题,有几种解决方案。第一个(非解决方案)是始终使用相同的编译器。如果您或您的公司是此应用程序的唯一插件提供者,那么使用相同的编译器设置很有用,这样您就可以确保与导出主应用程序时使用的编译器设置相同。
另一个解决方案是避免使用标准库。标准库非常有用,但由于实现不同,它可能在使用对象时导致问题:我的编译器
std::string和另一个编译器的
std::string可能
看起来和
行为相同,但实际上内部可能非常不同,所以使用一个而不是另一个可能会导致问题。可能的解决方法是传递与对象关联的原始数据,而不是对象本身。
例如,您仍然可以使用
std::string和
std::vector<int>在您的程序中,但对于导出的接口,您将传递一个
const char*
或一个
int*
,并在过程中进行转换。
这就引出了最后一个问题:名称修饰。C++ 编译器如果设置了不同的选项(例如优化级别或调试/发布版本),通常会以不同的方式修饰函数和变量的名称。但是,C 编译器不进行名称修饰,这意味着函数名称不会根据编译器选项而改变。以下是声明我们正在导出具有“C”链接的函数的方法。
1 2 3 4 5 6 7 8 9 10
|
#ifdef __cplusplus // if we are compiling C++
extern "C" { // export the functions with C linkage
#endif
// ... your DLL exported functions, e.g.
const char* getName(void);
#ifdef __cplusplus
}
#endif
|
然后,在实现函数时,您需要指定您也为它们使用了 C 链接。
1 2 3 4 5 6 7 8 9 10 11
|
// ...
const std::string pluginName = "TestPlugin";
extern "C"
const char* getName(void) {
// just extrapolating on information given above: we can still
// use the C++ Standard Library within functions, just you can't
// pass them as the return value or function arguments
return pluginName.c_str();
}
|
这是告诉编译器使用 C 链接,这通常可以确保所有编译器以相同的方式看到函数,并且还有一个附带的好处是摆脱了许多可能出现的奇怪符号。当然,这意味着您只能导出 C 风格的函数和结构,但这却是为了获得兼容性而付出的代价。
附件:[plugin-src.zip]