Making a Plugin System

Making a Plugin System

Score: 3.7/5 (44 votes)
* * * * *
Now, lets say that you are making a program, maybe a game, and you decide that you want to make it moddable without your intervention. Now, of course, you think of how that might be possible, and without forcing the users to inject code directly into your executable, or modifying the source code directly. How would you do this?

Well, of course, the answer is a plugin system. I'll briefly explain how it works: A plugin system is simply where a specified folder is searched for DLLs (or the like), and if any are found, adds the contents to the program. Of course, because the program doesn't actually know what is  in the DLLs, the normal way is for the DLL's to define a set entry point and calling functions defined by the program itself, which can then use the functionality exposed in those DLLs. The way this can be done is up to you, whether defining functions to implement or maybe getting the DLL to provide an instance of a base class, and then use the functionality from that. In this article, I am briefly going to demonstrate both of those options. First, though, lets look at how to actually  load libraries at all.



Loading a library


Well, lets start with the basics. To load a DLL at runtime, simply call  LoadLibrary, with the parameter being the file path of the DLL to load. However, when you think about this, this isn't much help, is it? We want to load a  variable number of DLLs, whose names  cannot be known at compile time. So, this means that we need to find all the DLLs that are plugins, and then load them.

Now, the easiest way of doing this is using WinAPI's  FindFile functions, using a file mask to collect all the .dll files. Though, this can have the problem that you suddenly try loading the DLLs that your program needs to run! This is the reason why programs normally have a 'plugins' folder: If you tried to load all the DLLs just from the directory of your program, you might start trying to load non-plugin DLLs. The seperation into a specified plugin folder helps prevent this from happening.

Now, enough talking, here is some example code of how to loop through all the files in a directory and load the values for each:
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));


Well, that is fairly complicated. Just thought I'd mention now, you do need a C++11 compiler to be able to compile these examples, otherwise some of the things like raw string literals will not compile. Also, if you use a Unicode compiler, you will need to specify that it is using wide strings.

Now, we have loaded all our plugins, but if we don't free them when we are done, we will cause a memory leak, and that can become a real problem in bigger projects. However, because we have stored all our handles in a vector, freeing them isn't actually that hard:
1
2
for (HINSTANCE hInst : modules)
    FreeLibrary(hInst);




Actually doing something with our library


OK, no we can load the libraries. The thing is, it doesn't actually  do anything yet. Lets change that. For starters, we should define a header file for the DLLs to include: This defines the functions and classes that we want them to export. I've decided to show two things here: How to export a polymorphic class and how to export a function. Once you get the idea, though, most things are fairly easy. Anyway, lets define our header file:
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__ 


Now, for the complicated bit. We need to load these functions from the DLL's that we loaded earlier. The function to do this is called  GetProcAddress(), which returns a pointer to the function in the DLL with the name you specify. Because it doesn't know what type of function it is getting, however, we need to explicitly cast that pointer returned to a function pointer of the appropriate type. Add this code to the earlier example:
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 (...);


And that is it for loading and using a plugin! You probably want to store the objects / names in their own lists, but it doesn't really matter, this is just an example.



Building the plugins


Now, there is one thing left: Actually building the plugins. This is very simple, comparitively. You need to  #include "main.hpp" so as to get the classes, and then simply implement the functions. The  main() function is the only thing you have to watch out for: For one, it isn't actually called main anymore! Here is just a basic main function (you don't normally need more than this):
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;
}




Getting the source code

Here I have posted a link to the source code for your convenience. This was compiled using the MinGW-w64 toolchain, but it should work for most Windows compilers. The examples require C++11 support (mostly for raw string literals and std::unique_ptr), and are developed with ANSI encoding in mind (rather than Unicode). This shouldn't make much of a difference, just go through and change the string literals to be long string literals, and use wide strings instead (std::wstring).

By way of project files, unfortunately I can't provide them all, there is merely a GCC makefile. To find out how to compile the project using your compiler, look at the documentation for that compiler. Probably the bit of information that you are least likely to know is how to generate a DLL, and how to define symbols when compiling. 

Download the source code 


Final Remarks

If you don't understand anything in this article, first check out MSDN (the MicroSoft Developer Network). In fact, I would have to quote MSDN as my main source of information for this article. Here are some links to relevant pages that you may be interested in looking at:

DLL loading functions:
File related functins:
Please let me know if there is anything unclear in this article, an error that you find, or you have any suggestions for updates to this article!



Update - Using the plugin system across different compilers

Due to  this issue that has been encountered, I have decided to update this article slightly. The above has been kept the same, however. Basically, I will put how to fix issues such as using DLLs built by other compilers.

The Problem
If you use different compilers, different versions of the compiler, or even different settings on the same compiler, the DLL will be generated differently and may cause crashes with the application it is linked to. This is because C++ is  notbinary standardized - i.e. There is no requirement for the same source code on different compilers to behave in the same way. This is especially true of things like the C++ standard library, were different compilers can have different implementations, which can cause problems with the program.

Another thing that can change between compilers (and as a general rule, WILL change) is the name mangling of functions. In the example above, the function  getObj was replaced with the following name:  _Z6getObjv. However, this particular name is dependent on the compiler that produces it: This name was from a MinGW compiler, an MSVS compiler will produce something different, and an Intel compiler something else again. This can also cause problems.

Some Solutions
For the above problems, there are a few solutions. The first (non-solution) is just to always use the same compiler. This is useful for if you or your company are the only people providing plugins for this application, so you can ensure that you are using the same compiler settings as when you exported your primary application.

Another solution is to avoid using the standard library. The standard library is very useful, but due to different implementations it can cause problems with the usage of the objects: My compiler's  std::string and another compilers std::string might  look and  behave the same, but in reality can be very different on the inside, so using one instead of the other can cause problems. Possible workarounds are to pass the raw data associated with the object, rather than the object itself.

For example, you can still use  std::string and  std::vector<int> in your programs, but for the exported interface you would pass a  const char* or an  int* instead, and just convert to and from in the process. 

Which brings me to the final problem: The name mangling. C++ compilers will often mangle the names of functions and variables differently if the compiler has different options set (such as level of optimization or debug/release builds). However, C compilers do no name mangling, which means that the functions name will not change based on the compiler options. Here is how to say that we are exporting the functions with 'C' linkage:
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 

Then, when you implement the functions, you need to specify that you are using C linkage for them as well:
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();
}


This is to tell the compiler to use C linkage, which normally guarantees that all compilers will see the functions in the same way, and also has the bonus side effect of getting rid of a lot of the strange symbols that may otherwise appear. Of course, this means that you can only export C-style functions and structures, but that is the price to pay for the compatibility that you receive.
以下是对提供的参考资料的总结,按照要求结构化多个要点分条输出: 4G/5G无线网络优化与网规案例分析: NSA站点下终端掉4G问题:部分用户反馈NSA终端频繁掉4G,主要因终端主动发起SCGfail导致。分析显示,在信号较好的环境下,终端可能因节能、过热保护等原因主动释放连接。解决方案建议终端侧进行分析处理,尝试关闭节电开关等。 RSSI算法识别天馈遮挡:通过计算RSSI平均值及差值识别天馈遮挡,差值大于3dB则认定有遮挡。不同设备分组规则不同,如64T和32T。此方法可有效帮助现场人员识别因环境变化引起的网络问题。 5G 160M组网小区CA不生效:某5G站点开启100M+60M CA功能后,测试发现UE无法正常使用CA功能。问题原因在于CA频点集标识配置错误,修正后测试正常。 5G网络优化与策略: CCE映射方式优化:针对诺基亚站点覆盖农村区域,通过优化CCE资源映射方式(交织、非交织),提升RRC连接建立成功率和无线接通率。非交织方式相比交织方式有显著提升。 5G AAU两扇区组网:与三扇区组网相比,AAU两扇区组网在RSRP、SINR、下载速率和上传速率上表现不同,需根据具体场景选择适合的组网方式。 5G语音解决方案:包括沿用4G语音解决方案、EPS Fallback方案和VoNR方案。不同方案适用于不同的5G组网策略,如NSA和SA,并影响语音连续性和网络覆盖。 4G网络优化与资源利用: 4G室分设备利旧:面对4G网络投资压减与资源需求矛盾,提出利旧多维度调优策略,包括资源整合、统筹调配既有资源,以满足新增需求和提质增效。 宏站RRU设备1托N射灯:针对5G深度覆盖需求,研究使用宏站AAU结合1托N射灯方案,快速便捷地开通5G站点,提升深度覆盖能力。 基站与流程管理: 爱立信LTE基站邻区添加流程:未提供具体内容,但通常涉及邻区规划、参数配置、测试验证等步骤,以确保基站间顺畅切换和覆盖连续性。 网络规划与策略: 新高铁跨海大桥覆盖方案试点:虽未提供详细内容,但可推测涉及高铁跨海大桥区域的4G/5G网络覆盖规划,需考虑信号穿透、移动性管理、网络容量等因素。 总结: 提供的参考资料涵盖了4G/5G无线网络优化、网规案例分析、网络优化策略、资源利用、基站管理等多个方面。 通过具体案例分析,展示了无线网络优化中的常见问题及解决方案,如NSA终端掉4G、RSSI识别天馈遮挡、CA不生效等。 强调了5G网络优化与策略的重要性,包括CCE映射方式优化、5G语音解决方案、AAU扇区组网选择等。 提出了4G网络优化与资源利用的策略,如室分设备利旧、宏站RRU设备1托N射灯等。 基站与流程管理方面,提到了爱立信LTE基站邻区添加流程,但未给出具体细节。 新高铁跨海大桥覆盖方案试点展示了特殊场景下的网络规划需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值