現在,假設您正在編寫一個程式,可能是一款遊戲,並且您希望在不干預的情況下使其具有可修改性。當然,您會思考這如何實現,並且不用強迫使用者直接將程式碼注入您的可執行檔案,或者直接修改原始碼。您將如何做到這一點?
嗯,答案當然是一個外掛系統。我將簡要解釋它是如何工作的:外掛系統只是搜尋一個指定的資料夾以查詢 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]