65.9K
CodeProject 正在变化。 阅读更多。
Home

实现插件功能的机制

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (8投票s)

2009 年 1 月 29 日

CPOL

5分钟阅读

viewsIcon

46178

downloadIcon

350

本文讨论了通过使用插件库来支持多种通信协议的能力。

PluginDemo.JPG

引言

我参与的一个项目的一个关键需求是,能够通过使用 **插件库** 来支持多种通信协议。在常规的网站上搜索,并没有太多关于此类系统设计的实用示例。本文将尝试填补这一空白,并解释为满足项目需求所采取的方法。

背景

所涉及的项目是一个通用的实用控制系统,它被划分为两个主要部分:1) 核心应用程序,它提供图形用户界面 (GUI),并访问串行通信、事件、定时器和数据库等多个系统资源;2) 一系列用于支持待控制设备通信协议的库。这些设备可能来自不同的供应商,并且通常“不使用”相同的命令语言。

项目早期决定使用动态链接库 (DLL) 来提供插件功能。

设计问题

需要解决的第一个设计问题是核心应用程序和插件库之间的接口构成。组成此接口的函数需要与待控制设备的具体细节解耦。这将允许接口是通用的,并且能够与使用任何通信协议的设备一起使用。这些函数应该代表将要发送给被控制设备的所有可能命令的“最小公分母”。这些命令的部分列表(以及将用作示例的命令)是

  • plugin_PluginInit
  • plugin_DeviceReset
  • plugin_DeviceWriteData
  • plugin_DeviceReadData

插件命令将实现为标准的 DLL 函数,并且任何符合接口设计的插件都将包含这些函数。

需要解决的下一个设计问题是核心应用程序将提供给插件的资源访问(服务)。核心应用程序提供的服务函数列表需要是完整的,以便插件库中的任何命令函数都可以使用所有服务函数。

为插件提供服务函数访问的最简单方法是使用标准的函数回调;然而,这可能会要求在插件命令函数调用中传递多个函数回调地址。为了简化这一点,在加载和初始化插件时,将一个包含所有必需服务函数指针的结构传递给插件。这些服务函数的示例列表(以及将用作示例的函数)是

  • cbSerialOpen
  • cbSerialClose
  • cbSerialRead
  • cbSerialWrite

此服务函数列表将实现为标准的 DLL 回调函数,但是这些函数的指针将在调用 plugin_PluginInit 时发送给插件 DLL。

请注意,函数指针名称和函数名称不能相同。

实现

附带的项目演示了这种插件方法,用于一个简单的基于 Win32 的应用程序。该解决方案包括核心应用程序和单个插件 DLL 的独立项目。

库头文件

在插件头文件 (plugin.h) 中,您声明了插件 DLL 的接口。此处声明的函数代表核心应用程序将发送给设备的命令。在示例项目中,这些函数很简单,返回 void 并只接受一个 HWND。在实际示例中,这些函数签名将反映需要发送到插件的数据类型,以及要返回的数据类型或状态码。

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);

注意 PluginInit 调用中额外的 tServiceLookup* 参数,这是用于将服务函数指针传递给插件的结构。

同样在插件头文件中,定义了服务函数和命令函数的函数指针类型,以及插件函数指针本身。

// Typedefs for the service functions (function pointers)
// In a more meaningful implementation, the signatures here would
// reflect valid data being passed to the service function as arguments.
typedef void (*svcSerialOpen)();
typedef void (*svcSerialClose)();
typedef void (*svcSerialRead)();
typedef void (*svcSerialWrite)();
 
// Typedefs for the plugin command functions (function pointers)
// In a more meaningful implementation, the signatures here would
// reflect valid data being passed to the command functions as arguments.
typedef void (*pluginInitFunct)( HWND, tServiceLookup* );
typedef void (*pluginFunct)( HWND );
 
// Plugin Function pointers
pluginInitFunct   PluginInit;
pluginFunct       DeviceReset;
pluginFunct       DeviceReadData;
pluginFunct       DeviceWriteData;

服务函数查找表结构在同一个文件中定义。

// Service function lookup structure
typedef struct
{
    svcSerialOpen        serialopenfunc;
    svcSerialClose       serialclosefunc;
    svcSerialRead        serialreadfunc;
    svcSerialWrite       serialwritefunc;
}
tServiceLookup;

库实现文件

在插件实现文件 (plugin.cpp) 中,声明了一个 tServicesLookup* 类型的全局作用域变量。该结构将作为插件可用的服务函数表,由核心应用程序提供。

// The service function pointer structure (file scope)
tServiceLookup* services;

该表在 PluginInit 函数中分配和初始化。由于该结构是全局作用域,因此它在库实现文件中定义的所有命令函数中都可用。

// Create the DLL copy of the services table
services = new tServiceLookup;

if( services )
{
    // Copy the values from the Core App copy to the DLL copy
    services->serialopenfunc = svc->serialopenfunc;
    services->serialclosefunc = svc->serialclosefunc;
    services->serialreadfunc = svc->serialreadfunc;
    services->serialwritefunc = svc->serialwritefunc;
}

读者可以在示例中看到服务函数指针是如何在各个命令函数中被调用的。

核心应用程序文件

在核心应用程序文件 (CoreApp_main.cpp) 中,您定义了服务函数。

在核心应用程序的初始化阶段,您将使用 LoadLibrary 系统调用将插件加载为普通 DLL。如果库成功加载,将使用 GetProcAddress 系统调用设置命令函数指针。对于这两个调用,请应用错误检查以确保库已加载并且所有函数指针都已初始化。插件的最终初始化步骤是创建一个 tServiceLookup 结构并用服务函数的地址对其进行初始化。该结构将用于调用 PluginInit

//*****************************************************************************
//         Set the function pointers of the plugin command functions          *
//                                                                            *
//*****************************************************************************

// Plugin (DLL) Instance variable
HINSTANCE      hLib;

// Load the plug-in DLL
hLib = LoadLibrary( L"plugin.DLL" );
if( hLib == NULL )
{
    MessageBox( hWnd, L"Could Not Load Plugin", L"DLL ERROR", MB_ICONEXCLAMATION );
    break;
}

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))
{
    // Could not load functions
    MessageBox( hWnd, L"Could Not Load Functions", L"DLL ERROR", MB_ICONEXCLAMATION );
    FreeLibrary((HMODULE)hLib);
    break;
}


//*****************************************************************************
//    Set the function pointers of the core application service functions     *
//                                                                            *
//*****************************************************************************

// Initialize the callback system
callbacks.serialopenfunc = (svcSerialOpen)cbSerialOpen;
callbacks.serialclosefunc = (svcSerialClose)cbSerialClose;
callbacks.serialreadfunc = (svcSerialRead)cbSerialRead;
callbacks.serialwritefunc = (svcSerialWrite)cbSerialWrite;

一旦插件加载并初始化,就可以根据需要调用命令函数。在程序退出时,请记住清理任何已分配的内存,并对插件 DLL 实例句柄调用 FreeLibrary。

摘要

前面的讨论和附带的示例只包含了一个插件 DLL。读者可以很容易地实现代码来根据需要“替换”插件 DLL,或者将各个插件包装在可以根据应用程序需求创建和使用的独立对象中。

示例很简单,插件除了调用 MessageBox 来显示正在进行的调用之外,没有做任何事情。实际实现将包含有意义的数据作为命令和服务函数的参数传递。作者希望本文提供了足够的信息供读者完成这项工作。

© . All rights reserved.