One of the key requirements of a project that I am involved with, is the ability to support multiple communication protocols through the use of Plug-In Libraries. A search of the usual web locations turned up little in practical examples of the design of such a system for an application. This article will attempt to fill that void and explain the approach that was taken to meet the project requirement.
The project involved was a generic utility control system, which was divided into two major sections; 1) The core application which provides a GUI, and access to several system resources such as serial communications, events, timers, and a database, and 2) A series of libraries used to support the communication protocols of the devices to be controlled. These devices can be from several different vendors, and typically do not ‘speak’ the same command language.
It was decided early in the project to use Dynamically Linked Libraries (DLLs) to provide the plug-in capability.
The first design issue that needed to be addressed was the composition of the interface between the core application and the plug-in library. The functions that comprise this interface need to be decoupled from the specifics of the devices that will be controlled. This will allow the interface to be generic, and able to be used with devices that use any communication protocol. These functions should represent the “least common denominator” of all of the possible commands that will be made to the controlled devices. A partial list of these commands (and those that will be used for example) would be:
The plug-in commands will be implemented as standard DLL functions, and any plug-in conforming the interface design will contain these functions.
The next design issue that needed to be addressed was the resource access (services) that would be made available to the plug-in, by the core application. The list of service functions provided by the core application needs to be complete in that all service functions must be available to any command function in the plug-in library.
The simplest approach to provide the plug-in access to the service functions would have been to use standard callback functions, however this would have the potential of requiring multiple callback function addresses to be passed in the plug-in command function calls. To simplify this, the plug-in will be passed a structure containing all function pointers of all of the required service functions when the plug-in is loaded and initialized. A partial list of these service functions (and those that will be used for example) would be:
This list of service functions will be implemented as standard DLL callback functions, however the pointers to these functions will be sent to the plug-in DLL in the call to
Note the function pointer names and the function names cannot be the same.
The attached project demonstrates this plug-in method for a simple Win32 based application. The solution includes separate projects for the core application and a single plug-in DLL.
The Library Header File
In the plug-in header file (plugin.h), you declare the interface to the plug-in DLL. The functions declared here represent the commands that the core application will send to the devices. In the example project these functions are trivial, returning void and accepting only a HWND. In a real example, these function signatures will reflect the types of data that need to be sent to the plug-in, and the types of data or status codes to be returned.
extern "C" __declspec(dllexport) void plugin_PluginInit(HWND, tServiceLookup*);
extern "C" __declspec(dllexport) void plugin_DeviceReset(HWND);
extern "C" __declspec(dllexport) void plugin_DeviceWriteData(HWND);
extern "C" __declspec(dllexport) void plugin_DeviceReadData(HWND);
Note the additional
tServiceLookup* parameter for the PluginInit call, this is the structure that is used to pass the service function pointers to the plug-in.
Also in the plug-in header file, the function pointer types to both the service functions and command functions are defined, as well as the plug-in function pointers themselves.
typedef void (*svcSerialOpen)();
typedef void (*svcSerialClose)();
typedef void (*svcSerialRead)();
typedef void (*svcSerialWrite)();
typedef void (*pluginInitFunct)( HWND, tServiceLookup* );
typedef void (*pluginFunct)( HWND );
The service function lookup table structure is defined in the same file.
The Library Implementation File
In the plug-in implementation file (plugin.cpp), a file scope variable of the
tServicesLookup* type is declared. This structure will serve as the table of service functions available to the plug-in, provided by the core application.
This table is allocated and initialized in the
PluginInit function. Since this structure has file scope, it is available to all of the command functions defined in the library implementation file.
services = new tServiceLookup;
if( services )
services->serialopenfunc = svc->serialopenfunc;
services->serialclosefunc = svc->serialclosefunc;
services->serialreadfunc = svc->serialreadfunc;
services->serialwritefunc = svc->serialwritefunc;
The reader can see in the example how the service function pointers are called in the individual command functions.
The Core Application File
In the core application file (CoreApp_main.cpp), you define the service functions.
During the initialization phase of the core application, you will load the plug-in as a normal DLL using the
LoadLibrary system call. If the library is successfully loaded, the command function pointers are set using the
GetProcAddress system calls. With both calls, apply error checking to make sure that the library is loaded and all of the function pointers are initialized. The final initialization step for the plug-in is to create a
tServiceLookup structure and initialize it with the address of the service functions. This structure is to be used in the call to
hLib = LoadLibrary( L"plugin.DLL" );
if( hLib == NULL )
MessageBox( hWnd, L"Could Not Load Plugin", L"DLL ERROR", MB_ICONEXCLAMATION );
PluginInit =(pluginInitFunct)GetProcAddress((HMODULE)hLib, "plugin_PluginInit");
DeviceReset =(pluginFunct)GetProcAddress((HMODULE)hLib, "plugin_DeviceReset");
DeviceReadData =(pluginFunct)GetProcAddress((HMODULE)hLib, "plugin_DeviceReadData");
DeviceWriteData =(pluginFunct)GetProcAddress((HMODULE)hLib, "plugin_DeviceWriteData");
if((PluginInit == NULL) || \
(DeviceReset == NULL) || \
(DeviceReadData == NULL) || \
(DeviceWriteData == NULL))
MessageBox( hWnd, L"Could Not Load Functions", L"DLL ERROR", MB_ICONEXCLAMATION );
callbacks.serialopenfunc = (svcSerialOpen)cbSerialOpen;
callbacks.serialclosefunc = (svcSerialClose)cbSerialClose;
callbacks.serialreadfunc = (svcSerialRead)cbSerialRead;
callbacks.serialwritefunc = (svcSerialWrite)cbSerialWrite;
Once the plug-in is loaded and initialized, the command functions may be called as needed. On program exit, remember to cleanup any allocated memory and call FreeLibrary on the plug-in DLL instance handle.
The preceding discussion, and attached example, included only one plug-in DLL. It would be simple for the reader to implement code to “swap out” plug-in DLLs as needed, or wrap the individual plug-ins in separate objects that could be created and used as needed, depending on the applications requirements.
The example was trivial, in that the plug-in did nothing other than post a MessageBox to show the calls as they were being made. A practical implementation would include meaningful data passed in as arguments to the command and service functions. The author hopes that this article has shown enough information for the reader to accomplish this.