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]