Creating a plugin interface

2019-03-16 16:35发布

问题:

I'm working on an application that will need to support a plugin architecture. This is the first time I've done this, so I'm not entirely sure how I need to go about it.

How to create some class from dll(constructor in dll)?(с++) suggests I just need to create a class consisting of entirely virtual functions and let the DLL implement that in a custom class and return that custom object via a GetPluginObject() method or the like. However, C++ DLL plugin interface says that won't be enough, and that a proper (compatible across multiple compilers) approach will require the following:

  • Only basic datatypes are usable
  • Something like COM's QueryInterface must be exposed so the plugin DLL can properly identify which interface(s) it implements
  • Some form of reference counting is required
  • All methods are preferably to be marked as stdcall
  • Any structs must be given fixed alignment

What I need a plugin to do is fairly simple: I just need one array of structs returned from one function.

struct InternalCommand
{
    int commandValue;
    std::wstring commandName;
    std::wstring commandHandlerFunctionName; //I'm planning on using GetProcAddress with the provided function name to get the individual command handler
}

std::vector<InternalCommand> GetEmergeInternalCommands();

Given the restrictions and requirements in the list above, and using another interface from this project as a template, it seems I need to define this in the following way:

#define MAX_LINE_LENGTH 4096

#ifdef __GNUC__
#define ALIGNOF(type) __alignof__(type)
#else
#define ALIGNOF(type) __alignof(type)
#endif

#ifdef __GNUC__
#define ALIGNED(size) __attribute__((aligned (size)))
#else
#define ALIGNED(size) __declspec(align(size))
#endif

#include <windows.h>

// {b78285af-c62f-4cff-9e15-f790a4a219ee}
const IID IID_IEmergeInternalCommand = {0xB78285AF, 0xC62F, 0x4CFF, {0x9E, 0x15, 0xF7, 0x90, 0xA4, 0xA2, 0x19, 0xEE}};

#ifdef __cplusplus
extern "C"
{
#endif

struct ALIGNED((ALIGNOF(int) + ALIGNOF(wchar_t) + ALIGNOF(wchar_t))) EmergeInternalCommandInformation
{
  int commandValue;
  wchar_t commandName[MAX_LINE_LENGTH];
  wchar_t commandHandlerFunctionName[MAX_LINE_LENGTH];
};

#undef INTERFACE
#define INTERFACE IEmergeInternalCommandProvider
DECLARE_INTERFACE_(IEmergeInternalCommandProvider, IUnknown)
{
  STDMETHOD(QueryInterface)(THIS_ REFIID, LPVOID*) PURE;
  STDMETHOD_(ULONG, AddRef)(THIS) PURE;
  STDMETHOD_(ULONG, Release)(THIS) PURE;

  STDMETHOD_(int, GetEmergeInternalCommandCount)(THIS) PURE;
  STDMETHOD_(EmergeInternalCommandInformation, GetEmergeInternalCommandInformation)(THIS_ int) PURE;
};
#undef INTERFACE
typedef IEmergeInternalCommandProvider* LPEMERGEINTERNALCOMMANDPROVIDER;

#ifdef __cplusplus
}
#endif

And then, on the host side, I'd use GetProcAddress on the plugin DLL to call the DLL's QueryInterface, then use the pointer QueryInterface returns to work with the plugin.

This seems like a lot of overkill and a lot of ugly, though. For example, I don't think I can properly pass a std::vector in or out, so I'm stuck using a single-item return for GetEmergeInternalCommandInformation() and a total-count function GetEmergeInternalCommandCount() so I can loop through the plugin's commands one by one. Is there a different way I can safely get a struct array as a return value without breaking the rules?

Also, I'm not at all sure I defined the struct correctly, both in terms of having the wchar_t arrays (am I restricted to single wchar_ts?) and in terms of the alignment value.

I'm also not entirely certain how the plugin DLL is supposed to implement this. I think it just needs to #include the interface-definition header and then create a class inheriting from the interface, right?

#include "EmergeInternalCommandInterface.h"
class EmergeInternalCommands : public IEmergeInternalCommandProvider
//class definition goes here

I'm also not certain if I need to register this interface with COM or if I can just use it. The interface I used as a template is a full-fledged COM interface and is registered as such, but I don't know if I need anything that advanced for a basic plugin system.

And last but definitely not least - am I making this way more complicated than it needs to be?

回答1:

Take a look at my project cppcomponents at https://github.com/jbandela/cppcomponents. I created this library specifically for scenarios such as yours as I found the currently available solutions lacking.

It is a header-only c++11 library that works on Windows and Linux.

It requires a fairly compliant C++11 compiler like MSVC 2013, Gcc 4.7.2, or Clang 3.2

  • It will automatically handle implementation of QueryInterface, AddRef, and Release.
  • When you use it, reference counting is handled automatically
  • It allows you to return std::string, vector, tuple, as well as other standard types
  • It can handle exceptions
  • You can use multiple compilers, for example you can write your program in Visual C++ and write your plugins in GCC

Here is the easiest way to write what you want

First define the interface and plugin in CommandProvider.h

#include <cppcomponents/cppcomponents.hpp>
#include <tuple>
#include <vector>

typedef std::tuple<int, std::wstring, std::wstring> Command;

struct ICommandProvider:cppcomponents::define_interface<cppcomponents::uuid<0xf4b4056d, 0x37a8, 0x4f32, 0x9eea, 0x03a31ed55dfa>>
{
    std::vector<Command>GetEmergeInternalCommands();

    CPPCOMPONENTS_CONSTRUCT(ICommandProvider, GetEmergeInternalCommands)
};

inline std::string CommandProviderId(){ return "CommandProvider"; }
typedef cppcomponents::runtime_class<CommandProviderId, cppcomponents::object_interfaces<ICommandProvider>> CommandProvider_t;
typedef cppcomponents::use_runtime_class<CommandProvider_t> CommandProvider;

Then in ImplementCommandProvider.cpp that will be compiled into CommandProviderDll.dll

#include "CommandProvider.h"

struct ImplementCommandProvider :cppcomponents::implement_runtime_class<ImplementCommandProvider, CommandProvider_t>
{
ImplementCommandProvider(){}


std::vector<Command>GetEmergeInternalCommands(){
    std::vector<Command> vec;
    vec.push_back(std::make_tuple(1, L"Test", L"TestFunction"));
    vec.push_back(std::make_tuple(2, L"Test2", L"TestFunction2"));
    vec.push_back(std::make_tuple(3, L"Test3", L"TestFunction3"));

    return vec;
}


};

CPPCOMPONENTS_REGISTER(ImplementCommandProvider)
CPPCOMPONENTS_DEFINE_FACTORY()

Here is how you would use it

#include "CommandProvider.h"
#include <iostream>


int main(){

    std::string dllName;

    std::cout << "Enter dll name without the .dll extension\n";
    std::cin >> dllName;

    auto p = CommandProvider::dynamic_creator(dllName, "CommandProvider")();

    for (auto& c : p.GetEmergeInternalCommands()){
        std::wcout << L"Value " << std::get<0>(c) << L" Name " << std::get<1>(c) << L" Function " << std::get<2>(c) << L"\n";

    }


}

Here is how you would build it from the command line I am assuming you are in the directory with the 3 files and that the MSVC compiler is in your path

This is how to build the main program

cl MainProgram.cpp /I c:\Users\jrb\Source\Repos\cppcomponents /EHsc

This is how to build the Dll

cl ImplementCommandProvider.cpp  /I c:\Users\jrb\Source\Repos\cppcomponents /EHsc /link /dll /OUT:CommandProviderDll.dll

Then when you run the program, Enter in CommandProviderDll for your dllname

If you want to define a custom struct it is possible, and I can help you with it.

The library is lacking documentation currently (working on it :( ), but I can help you with any questions you have about the library. The library is released under the Boost License so you can use it for commercial applications if you want.