分享

Article: A simple plug-in architecture patter...

 weicat 2010-07-08
A simple plug-in architecture pattern for C++ applications on Win32
Author: George Mihaescu
Published: March 10, 2006
Category: Architecture / C++
Notes:
Description: This article presents a very simple yet robust architecture for dynamically plug-able components (plug-ins) for C++ applications. This architecture has been implemented in several real-life projects.
View count: 5,311
Comments: 3 Read comments or post your own

  Print viewOpens in new window

A simple plug-in architecture pattern for C++ applications on Win32

 

By George Mihaescu

 

Summary: This article presents a simple and elegant solution to creating components that can be dynamically deployed and loaded by a C++ Win32 application without the need of any framework / infrastructure or technology (such as COM). It relies on basic C++ mechanisms and two commonly used Win32 API calls.

 

Download the code here (VC++ projects + source).

 

The Problem

You have a C++ Win32 application to which you want to be able to “attach” components dynamically, developed either by yourself or by other parties. In the context of this document I will call those components plug-ins, by analogy with the well-known web browser add-on components. In the same manner with the browsers, you want your application to be aware of such plug-ins that were developed and deployed possibly long after the application itself was developed and deployed. Ideally, you don’t want the application to even need to restarted – what you’d like is to drop the plug-in at a know location (e.g. somewhere under the application installation directory) even as the application is running, and the application all of a sudden has enhanced functionality. Can this be done in a simple and reliable way?

The Solution

Many readers will immediately dismiss the problem and this article by saying “of course, that’s what COM is all about”. But I’m not in favor of using COM unless there is a very compelling reason for it. Generally, if it can be done without COM (without making the code overly complex – I don’t want to re-implement COM), then why bother with COM? Say COM and you say code complexity, registration issues (such as registration requiring certain user security privileges), dependency on registry, application usually needing to be re-started, etc. I argue below that I can meet the requirements of this problem without COM, in a much simpler and reliable way.

 

High-level design

My solution is based on marrying the C++ polymorphic mechanism with the Win32 APIs LoadLibrary and GetProcAddress.

 

The principle is that the main application publishes a contract with the plug-ins in the form of an interface that the plug-ins are expected to implement in order to meet the contract. Then the main application will scan a known location on disk (e.g. a “plugins” directory relative to its installation directory) and attempt to load all plug-ins (using LoadLibrary API) that export implementations of this interface (determined by using the GetProcAddress API). Each plug-in is packaged in its own DLL (or multiple DLLs) that must be deployed at the location the application expects them in ordered to be detected and loaded by the application.

Low-level design

As C++ does not offer a language construct to model the concept of an interface, we will use the next best thing available: an abstract base class.

Also, because the Win32 API GetProcAddress uses a function name as a parameter, while our plug-ins are C++ (because they need to provide a concrete derivative of the abstract base class) and because C++ does function name mangling, our plug-ins will need to export as a minimum one C-style function to act as the class factory. To keep things balanced and because the application has (in theory) no way of knowing what allocation strategy each plug-in factory function uses, it only makes sense to ask plug-ins to also export the counter-part of the factory function, another C-style function to act as the plug-in clean-up / tear-down procedure.

 

So, to sum it up:

·         The main program has an abstract class through which it will use all dynamically loaded plug-ins. It also has a few lines of code to scan a known location and look for DLLs that export two known C-style functions: the plug-in factory and the plug-in clean-up.

·         Each plug-in has a class implementing the abstract class in the main program, and is packaged as a Win32 DLL exporting two C-style functions: the plug-in factory and the plug-in clean-up.

 

This is illustrated in the diagram below. As you can see, there is no registration required, no need for the user to have special privileges on the machine, no framework / runtime dependency other than what you already have: C++ and Win32.

 

The code

Below is a sample “contract” (abstract class) in the main program that plug-ins will need to implement. Of course, the methods of this class are going to be specific to what your plug-ins need to do:

 

//////////////////////////////////////////////////////////////////////////

// Abstract base class ("interface") for the concrete plugin implementations

 

class IPlugin

{

public:

       //Add whatever functions each plugin needs to implement

       //Those below are just dummy examples to illustrate the principle

 

       //returns the name of the concrete plugin

       virtual const char* Get_Name () const = 0;

 

       //does the actual data processing

       virtual void Process_Data () = 0;

};

 

/////////////////////////////////////////////////////////////////////////

//Extern "C" functions that each plugin must implement in order to be

//recognized as a plugin by us.

 

// Plugin factory function

//extern "C" IPlugin* Create_Plugin ();

 

// Plugin cleanup function

//extern "C" void Release_Plugin (IPlugin* p_plugin);

 

Below is the code in the main program that scans for plug-ins, determines that they are indeed exporting the two C-style functions it expects from a plug-in, then loads and executes each plug-in found:

 

#include "IPlugin.h"       //for the IPlugin abstract base

 

 

//convenience typedef for the pointers to the 2 functions we

//expect to find in the plugins

typedef IPlugin* (*PLUGIN_FACTORY)();

typedef void (*PLUGIN_CLEANUP)(IPlugin*);

 

int main(int argc, char* argv[])

{

       //get the program's directory

       char dir [MAX_PATH];

       ::GetModuleFileName (NULL, dir, MAX_PATH);

 

       //eliminate the file name (to get just the directory)

       char* p = ::strrchr (dir, '\\');

       *(p + 1) = 0;

 

       //find all DLLs in the plugins subdirectory

       char search_parms [MAX_PATH];

       ::strcpy_s (search_parms, MAX_PATH, dir);

       ::strcat_s (search_parms, MAX_PATH, "plugins\\*.dll");

 

       WIN32_FIND_DATA find_data;

       HANDLE h_find = ::FindFirstFile (search_parms, &find_data);

       BOOL f_ok = TRUE;

       while (h_find != INVALID_HANDLE_VALUE && f_ok)

       {

              //load each DLL and determine whether it is exporting

//the functions we care about

              char plugin_full_name [MAX_PATH];

              ::strcpy_s (plugin_full_name, MAX_PATH, dir);

              ::strcat_s (plugin_full_name, MAX_PATH, "plugins\\");

              ::strcat_s (plugin_full_name, MAX_PATH, find_data.cFileName);

 

              HMODULE h_mod = ::LoadLibrary (plugin_full_name);

              if (h_mod != NULL)

              {

                     PLUGIN_FACTORY p_factory_function =

(PLUGIN_FACTORY) ::GetProcAddress (h_mod, "Create_Plugin");

                     PLUGIN_CLEANUP p_cleanup_function =

(PLUGIN_CLEANUP) ::GetProcAddress (h_mod, "Release_Plugin");

 

                     if (p_factory_function != NULL &&

    p_cleanup_function != NULL)

                     {

                           //yes, this DLL exposes the 2 functions we need,

//it is a plugin we can use!

 

                           //invoke the factory to create the plugin object

                           IPlugin* p_plugin = (*p_factory_function) ();

 

                           //show which plugin it is, and let the plugin

//do the processing

                           printf ("Now working with plugin: %s\n",

p_plugin ->Get_Name ());

                           p_plugin ->Process_Data ();

                          

                           //done, cleanup the plugin by invoking its

//cleanup function

                           (*p_cleanup_function) (p_plugin);

                     }

 

                     ::FreeLibrary (h_mod);

              }

 

              //go for the next DLL

              f_ok = ::FindNextFile (h_find, &find_data);

       }

 

       return 0;

}

 

And finally, here is the code for one such plug-in:

 

#include "stdio.h"

 

#include "..//MainProgram//IPlugin.h"

 

////////////////////////////////////////////////////////////////////////

// A concrete plugin implementation

////////////////////////////////////////////////////////////////////////

 

// Plugin class

class Plugin1 : public IPlugin   

{

public:

       //returns the name of the concrete plugin

       const char* Get_Name () const

       {

              return "Plugin1";

       }

 

       //does the actual data processing

       virtual void Process_Data ()

       {

              for (int i = 0; i < 3; i++)

              {

                     printf ("Plugin 1 is processing....\n");

              }

              printf ("Plugin 1 processing done!\n");

       }

 

};

 

 

extern "C"

{

       // Plugin factory function

       __declspec(dllexport) IPlugin* Create_Plugin ()

       {

              //allocate a new object and return it

              return new Plugin1 ();

       }

 

       // Plugin cleanup function

       __declspec(dllexport) void Release_Plugin (IPlugin* p_plugin)

       {

              //we allocated in the factory with new, delete the passed object

              delete p_plugin;

       }

 

}

 

But wait: what about the promise that the user won’t even have to re-start the application after deploying a new plug-in? For that, just throw in a couple more Win32 APIs: as the application starts, create a low-priority thread that calls FindFirstChangeNotification / FindNextChangeNotification / WaitForSingleObject or WaitForMultipleObjects and this thread can notify every time a valid plug-in DLL is deployed at the known location – so that the application can act (start using the plug-in / ask user whether to enable the plug-in, etc.).

Other enhancements

  • As mentioned above, in most cases if you want the application to dynamically sense when plug-ins are deployed and run them without having to re-start, you will need a disk monitoring thread like the one described above.
  • You will probably want to implement versioning on your program’s contract with the plug-ins. After all, it’s very likely that your plug-ins interface will evolve over time, and you want the program to be able to ignore / reject plug-ins that were not written for its version of the contract (e.g. user deploys plug-ins written for a more recent version of the program on an older version of the program and vice versa). Such a versioning protocol can be implemented in the abstract class that represents your program’s contract with the plug-ins, so that the program can decline using plug-ins that don’t conform to its versioning requirements.
  • Sometimes the plug-ins may need to add to the application’s help (CHM) files. I will not get into the details, but it can be done quite easily if the main program’s help file is properly written for file merging. If the plug-in is deployed together with its help file, the Windows help engine can automatically merge the main program’s CHM help file with each of the plug-ins CHM help files, resulting in a seamless user experience. Maybe I will address this in another article – until then, Google “Merging Help Files at Run Time”. I have done this and I know it works fine without any pain.

 

I have implemented this pattern since 1998 with great results. One of the free programs available from this site (daVinci) uses this architecture to implement parsers for different file formats. As a user needs a parses for another file format, we implement it and make the parser available for download on our site. The user downloads the parser and deploys it under the “parsers” subdirectory of the application (without even closing the application), and all of a sudden the application can handle the new file format.

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多