首页 > 编程语言 >Making a Plugin System with c++

Making a Plugin System with c++

时间:2023-02-13 23:55:05浏览次数:64  
标签:std function functions load System c++ DLL Making compiler

    cplusplus.com

  sign up log in
[Legacy version]

C++

Articles

  Published by TwilightSpectre Jan 25, 2014 (last update: Apr 10, 2014)

Making a Plugin System

Score: 4.1/5 (483 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 not binary 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.

Attachments: [plugin-src.zip]

      Home page | Privacy policy
© cplusplus.com, 2000-2022 - All rights reserved - v3.3.3
Spotted an error? contact us

标签:std,function,functions,load,System,c++,DLL,Making,compiler
From: https://www.cnblogs.com/chenyalin/p/17118347.html

相关文章

  • 【C++复习】运算符重载中的特殊运算符
    无法被重载类属关系运算符 .成员指针运算符 .*作用域分辨符 ::三目运算符 ?:只能通过成员函数重载赋值运算符=方括号[]圆括号()指向结构体成员运算符->......
  • 【C++复习】同名函数判断条件(重载,隐藏,覆盖)
    1、重载以下条件要全部满足:函数名相同以下条件满足其1:函数形参数目不同函数形参类型不同注意:不看返回值调用形式要不同//下面两个函数不能重载fun(inta,......
  • C/C++多线程实现龟兔赛跑
    题⽬:⻳兔赛跑跑道距离50⽶乌⻳(⼀个线程)每秒3⽶不睡觉;兔⼦(⼀个线程)每秒5⽶每跑15⽶睡2秒钟。请模拟⽐赛情况:#include<iostream>#include<thread>#include<......
  • [C++] thread未定义
    [C++]无法识别thread在已经包括thread头文件和使用了std命名空间的情况下编译器依旧无法使用C++11的thread类,大概率是因为使用的MinGW的线程模型为win32而非POSIX。解决......
  • C++构造和析构
    category:cpp参考书籍:C++PrimerEssentialC++编译器:gcc/g++C++构造和析构构造函数名字和类名相同没有返回值构造函数是用来构造对象,构造对象时候必定调用构造函数不......
  • 【C++复习】运算符优先级(简)
    不同优先级的运算符混在一起,就根据优先级算相同优先级的运算符混在一起,就看它们的结合性(这里不谈)1、运算符有哪些?单目运算符*++自增,有前置和后置--自减,有前置和后置......
  • C++ 修改防火墙firewall设置(Linux、Ubuntu、CentOS)
    1、简介1.1Ubuntuhttps://ubuntu.com/download/desktopUbuntu是一个以桌面应用为主的Linux操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu"一词,意思是“人性”“......
  • BooksManagementSystem(图书管理系统)
    一、数据库创建数据库--添加数据库createdatabaseifnotexistsbms;创建表--创建user表createtable`user`(`id`int(11)notnullauto_incrementcomme......
  • Calling C++ Code From Go With SWIG
    http://zacg.github.io/blog/2013/06/06/calling-c-plus-plus-code-from-go-with-swig/ RecentlywhileworkingonaGobasedprojectIneededtousesomefunctio......
  • VScode 配置C++环境记录
    gcc/g++是c/c++编译器,Windows需要借助Mingw来使用c++的编译器,下面是安装MinGW的教程:搬运:https://blog.csdn.net/jjxcsdn/article/details/123058745在VScode中配置编译......