纯 C 语言的简单插件架构
如何使用支持插件的架构编写应用程序。
引言
本文介绍如何使用支持插件的架构编写应用程序。我们将使用纯 C 语言来实现。文中解释的概念可以用于任何语言的实现。读者应透彻理解函数指针和 DLL 的动态加载。了解 GTK 工具包将是加分项,但并非理解这些概念的必要条件。您可以从 http://downloads.sourceforge.net/gladewin32/gtk-dev-2.12.9-win32-2.exe 下载 GTK 开发环境安装程序。
背景
插件是可以在运行时或启动时由宿主应用程序加载的软件组件。插件为应用程序提供了附加功能。然而,插件也需要能够访问宿主应用程序,以便通过插件自定义应用程序的功能。我们将使用 DLL 的动态加载机制来实现插件架构。整个架构、应用程序和插件都将用纯 C 语言实现。GUI 使用 GTK+ 实现,使其跨平台。
架构
任何支持动态加载软件组件的应用程序,其目的都是为了在不重新编译或访问其源代码的情况下,能够定制和/或增强现有应用程序。要使应用程序支持此功能,它应该至少能够:
1) 在运行时加载插件模块(动态加载 DLL)。
2) 识别属于其预定类型的模块。
3) 查询模块的最小函数和对象。
4) 更新其界面,使能让用户通过菜单或图标使用添加的功能。
下图展示了典型的基于插件的软件架构……
加载插件
通常,应用程序使用预定的文件夹路径,并期望插件开发者将插件放在该路径下,以便在运行时或启动时从中加载它们。一些应用程序也可能提供交互式对话框(插件管理器)来添加插件路径并将其保存到应用程序配置文件中。应用程序使用标准的操作系统 API 调用动态加载库,例如 Windows 上的 LoadLibrary
和 UNIX/Linux 上的 dlopen
。插件的有效性通过调用某些预期由插件开发者实现的函数来验证。
识别插件类型
应用程序可能支持各种操作的插件模块。这些操作可以分为不同的类型。应用程序还会查询模块的类型。基于此查询,应用程序将决定通过相应的工具箱、工具栏或菜单类别向用户公开它。
API
为了这一切正常工作,插件开发者在编写插件时必须遵循一定的协议。这通过让开发者实现某些函数并让它们返回某些预定义值来强制执行。然而,这只是游戏的一半。通过让开发者实现这些函数,应用程序才获得了对插件模块的控制权。但插件仍然无法控制应用程序,没有这个,插件就没有意义去做任何有用的事情来操作应用程序。这也必须由应用程序提供。但是,如果应用程序被构建为一个整体代码,插件开发者如何链接到应用程序中的代码呢?为了让开发者能够调用函数来控制应用程序,这些函数必须被编写在一个应用程序和插件都可以共享的独立模块中。所以,本质上,应用程序希望与插件共享的大部分功能必须从宿主应用程序中分离出来。这一套应用程序功能和插件协议,再加上一个可共享的模块和接口,就构成了应用程序的应用程序编程接口(API)或软件开发工具包(SDK)。
实现
现在让我们看看如何实现这样的 API、其宿主应用程序和插件模块。为了方便实现这个简单的演示,应用程序的大部分功能,包括 GUI,都保留在 API 中,并提供导出的函数来访问和修改它。下图是实现的示意图。
该应用程序是一个简单的 OpenGL 应用程序,在屏幕上绘制一个圆环。API 提供了函数来修改 OpenGL 查看器以及其中显示的圆环的属性。宿主应用程序(主可执行文件)从 API 调用主 GUI 并使其保持运行。GUI 在应用程序的整个生命周期内都通过一个静态句柄可用。这使得 GUI 可以通过插件进行自定义。通过插件添加菜单来编辑圆环以更改其属性。插件中实现的对话框提供了控件来设置属性值,这些值通过访问 API 的相应函数应用于圆环。OpenGL 查看器也通过一个静态句柄由 API 公开,允许插件开发者获取它来修改查看器属性,如背景颜色或其中显示的实体。
我们必须记住,动态加载插件模块并通过菜单使其在应用程序中可用仅仅是故事的一半。另一半是,插件模块中的代码能够控制应用程序二进制文件中的对象,应用程序本身必须在 API 中实现,以便插件开发者可以链接存在应用程序组件的模块。将核心应用程序对象通过 API 中的静态句柄公开的原因是,插件必须能够访问这些对象的运行实例并通过它们访问应用程序的功能。这是可能的,因为当正在运行的应用程序将插件加载到其自己的进程空间时,API 引用的静态对象、插件模块和宿主应用程序将是相同的。
为了使事情尽可能简单直观,我们将使用简单的 C 结构和操作这些结构的函数来表示插件接口、应用程序功能和宿主应用程序本身。当然,使用 C++ 和面向对象范式可以使大型应用程序的开发更有组织、更模块化、更易于维护,但要掌握其核心概念,在 C 中实现会更容易。
让我们看一下插件头接口。我们的插件接口由 _PluginStruct
结构表示,该结构只包含几个有用的函数指针。
struct _PluginStruct
{
NAMEPROC nameProc;
PROVIDERPROC providerProc;
MENUPROC menuProc;
MENUCATPROC menuCatProc;
RUNPROC runProc;
DESTROYPROC destProc;
};
typedef struct _PluginStruct PluginStruct, *LPPLUGINSTRUCT;
函数指针指向预期由插件开发者实现的函数。
typedef LPPLUGINSTRUCT (*CREATEPROC) (void);
typedef void (*DESTROYPROC) (LPPLUGINSTRUCT);
typedef const gchar* (*NAMEPROC) (void);
typedef const gchar* (*PROVIDERPROC)(void);
typedef const gchar* (*MENUPROC) (void);
typedef const gchar* (*MENUCATPROC) (void);
typedef void (*RUNPROC) (void);
这些函数用于创建、销毁、获取插件类别信息、菜单信息和执行插件功能。
PLUGINAPP_API LPPLUGINSTRUCT plugin_app_create_plugin(void); PLUGINAPP_API void plugin_app_destroy_plugin(LPPLUGINSTRUCT); PLUGINAPP_API const gchar* plugin_app_get_plugin_name(void); PLUGINAPP_API const gchar* plugin_app_get_plugin_provider(void); PLUGINAPP_API const gchar* plugin_app_get_menu_name(void); PLUGINAPP_API const gchar* plugin_app_get_menu_category(void); PLUGINAPP_API void plugin_app_run_proc(void);
然后,我们为应用程序框架(主窗口)和 OpenGL 查看器定义了静态句柄……
static GtkWidget* _mainwindow = NULL;
static LPGLVIEW _glview = NULL;
……以及相关的查询函数……
PLUGINAPP_API GtkWidget* plugin_app_get_mainwindow();
PLUGINAPP_API LPGLVIEW plugin_app_get_glview();
PLUGINAPP_API void plugin_app_get_torus_data(LPGLVIEW iView, double* majorRadius, double* minorRadius, int* r, int* g, int* b, BOOL* shaded);
PLUGINAPP_API void plugin_app_set_torus_data(LPGLVIEW iView, double majorRadius, double minorRadius, int r, int g, int b, BOOL shaded);
这些函数提供了访问应用程序对象(如主窗口、查看器、显示的 3D 对象等)的方法。它们还促进了对这些对象的操作。
这就是简单的插件接口头的结尾。
现在让我们看一下核心 API 中插件加载过程的实现。提供加载、卸载和维护插件功能的头文件包含以下函数……
PLUGINAPP_API void plugin_helper_add_plugin_directory (const gchar *directory);
PLUGINAPP_API void plugin_helper_find_plugins_in_directory();
PLUGINAPP_API GList* plugin_helper_get_plugin_list();
/* Use these functions to load/unload plugins. */
PLUGINAPP_API void* plugin_helper_load_plugin (const gchar *filename);
PLUGINAPP_API void plugin_helper_unload_plugin(void* handle);
让我们看看加载插件的函数……
void* plugin_helper_load_plugin(const gchar *filename)
{
gchar *pathname = NULL;
void *plugin = NULL;
if (!filename || !filename[0])
return NULL;
pathname = plugin_helper_find_plugin_file (filename);
if (!pathname)
{
g_warning (_("Couldn't find plugin file: %s"), filename);
return NULL;
}
plugin = (void*)LoadLibrary(pathname);
g_free (pathname);
return plugin;
}
Windows API 函数 LoadLibrary
将插件 DLL 加载到内存中,并返回一个句柄给上面显示的函数。
当应用程序的主 GUI 显示时,Show 事件会加载预定义文件夹中所有可用的插件。下图显示了加载和公开插件的代码……
void load_all_plugins(GtkWidget *widget, gpointer user_data)
{
LPPLUGINSTRUCT pls = NULL;
CREATEPROC create = NULL;
MENUPROC menuproc = NULL;
MENUCATPROC menucatproc = NULL;
RUNPROC runproc = NULL;
…
…
void* handle = NULL;
GtkWidget* mw = plugin_app_get_mainwindow();
editmenuitem = lookup_widget(mw, "editmenuitem");
insertmenuitem = lookup_widget(mw, "insertmenuitem");
elem = plugin_helper_get_plugin_list();
while (elem)
{
filename = (gchar*)elem->data;
handle = plugin_helper_load_plugin(filename);
if(handle)
{
create = (CREATEPROC) GetProcAddress(handle, "plugin_app_create_plugin");
if ((error = GetLastError()) != 0)
{
…
…
}
else
{
/* store the handle for later use */
plugin_handles = g_list_prepend(plugin_handles, handle);
/* Create an instance of the plugin struct */
pls = create();
if(pls && pls->menuProc && pls->runProc)
{
/* store the plugin struct for deletion at exit */
plugin_structs = g_list_prepend(plugin_structs, pls);
menuproc = (MENUPROC) pls->menuProc;
menuName = (gchar*)menuproc();
menucatproc = (MENUCATPROC) pls->menuCatProc;
menuCategory = (gchar*)menucatproc();
g_message (_("Creating menu item: %s"), menuName);
menu = gtk_image_menu_item_new_with_label (menuName);
gtk_widget_set_name (menu, menuName);
gtk_widget_show (menu);
g_object_set_data_full (G_OBJECT (mw), menuName,
gtk_widget_ref (menu), (GDestroyNotify) gtk_widget_unref);
if(g_strcmp0(menuCategory, "edit") == 0)
{
gtk_container_add (GTK_CONTAINER (editmenuitem), menu);
}
else
{
gtk_container_add (GTK_CONTAINER (insertmenuitem), menu);
}
runproc = (RUNPROC)pls->runProc;
g_signal_connect(G_OBJECT(menu), "activate", G_CALLBACK(runproc), NULL);
}
else
{
g_printerr("Invalid Plugin Structure!");
}
}
}
elem = elem->next;
}
gtk_widget_show_all(widget);
}
上面的函数加载所有有效的插件,创建带有适当名称的菜单,将插件的执行函数指针附加到菜单处理程序,并在相关类别中公开它。这时插件结构中的函数指针就派上用场了。插件结构中的函数指针要么用于查询,要么作为回调附加到菜单。它们也可以直接调用以执行插件。
当应用程序退出时,插件会被优雅地卸载和销毁。
请记住,我们所讨论的都是 API 的核心部分。我们还没有看到实际的可执行应用程序,它将实例化应用程序主窗口并使其运行。让我们看一下……
int main (int argc, char *argv[])
{
GtkWidget *MainWindow;
char* exeName = getexepath();
gchar* exePath = g_path_get_dirname(exeName);
gchar* pluginPath = g_strdup_printf("%s%s", exePath, "/../plugins");
…
…
…
gtk_init (&argc, &argv);
add_pixmap_directory (PACKAGE_DATA_DIR "/" PACKAGE "/pixmaps");
plugin_helper_add_plugin_directory (pluginPath);
// Find plugins in the plugin directory
plugin_helper_find_plugins_in_directory();
// This is where the main window gets instanciated
// The function returns the static MainWindow object
// after creating it if necessary.
MainWindow = plugin_app_get_mainwindow ();
gtk_widget_show (MainWindow);
g_signal_connect(G_OBJECT(MainWindow), "destroy", _CALLBACK(on_quit1_activate), NULL);
gtk_main ();
return 0;
}
可执行文件只是设置插件文件夹路径,检查可用插件,并维护一个有效插件路径列表。然后它实例化应用程序主窗口并进行设置使其运行,进而加载和公开插件。
现在让我们看一个简单的插件,它控制显示圆环的图形属性……
Torusdlg/pluginimpl.c
#include <pluginappsdk.h>
#include "interface.h"
...
...
static GtkWidget* pDlg1 = NULL;
...
LPPLUGINSTRUCT plugin_app_create_plugin()
{
LPPLUGINSTRUCT PLS = (LPPLUGINSTRUCT)malloc(sizeof(PluginStruct));
g_debug("TorusDlg::plugin_app_create_plugin");
if(!PLS)
{
return NULL;
}
PLS->nameProc = plugin_app_get_plugin_name;
PLS->providerProc = plugin_app_get_plugin_provider;
PLS->menuProc = plugin_app_get_menu_name;
PLS->menuCatProc = plugin_app_get_menu_category;
PLS->runProc = plugin_app_run_proc;
PLS->destProc = plugin_app_destroy_plugin;
return PLS;
}
void plugin_app_run_proc()
{
pDlg1 = create_TorusDlg();
gtk_window_set_transient_for(GTK_WINDOW(pDlg1), GTK_WINDOW(plugin_app_get_mainwindow()));
gtk_widget_show(pDlg1);
}
我们首先实现 plugin_app_create_plugin()
函数,在该函数中我们创建一个插件结构实例,并将指向我们实现的其他函数的指针分配给结构中的函数指针成员。
plugin_app_run_proc()
函数只创建圆环编辑对话框并显示它。圆环属性的修改在“应用”按钮的点击事件中完成。
void on_applybutton1_clicked(GtkButton *button, gpointer user_data)
{
GtkWidget* wid;
GdkColor color;
gdouble min, maj;
gboolean shaded;
...
LPGLVIEW view = plugin_app_get_glview();
...
wid = lookup_widget(GTK_WIDGET(button), "colorbutton1");
gtk_color_button_get_color(GTK_COLOR_BUTTON(wid), &color);
...
wid = lookup_widget(GTK_WIDGET(button), "spinbutton1");
maj = gtk_spin_button_get_value(GTK_SPIN_BUTTON(wid));
wid = lookup_widget(GTK_WIDGET(button), "spinbutton2");
min = gtk_spin_button_get_value(GTK_SPIN_BUTTON(wid));
wid = lookup_widget(GTK_WIDGET(button), "checkbutton1");
shaded = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(wid));
...
if(view)
{
plugin_app_set_torus_data(view, maj, min, color.red/257,color.green/257,color.blue/257, shaded);
}
}
我们使用以下代码行获取 OpenGL 视图的静态句柄……
LPGLVIEW view = plugin_app_get_glview();
……并用它来操作圆环属性。魔术在于开发者能够将此单元编译为独立的模块并链接到 API,并且在模块代码被加载到应用程序的进程空间后,能够访问应用程序对象。
已经实现了几个插件来演示在不同类别中加载多个插件。解释所有内容会使本文太长。我希望我已经提供了足够的洞察力。附带的源代码肯定会提供更多见解。鼓励读者通过调试代码来查看函数调用的序列。
一个插件管理器对话框显示所有已加载的插件,并允许用户重新加载插件,从而实现运行时将软件模块插入应用程序。
解决方案中构建的插件会自动复制到名为 plugins 的文件夹中。如果您第一次只构建和运行 pluginapp 项目,您将看不到列表框中的任何插件。一旦您构建了解决方案中的所有其他项目,插件将被生成并复制到 plugins 文件夹。您无需关闭应用程序即可加载插件。只需单击插件管理器对话框上的“重新加载”按钮,即可加载 plugins 文件夹中所有可用的插件,并填充相应的菜单。
我希望本文能为理解基于插件的软件架构提供一个良好的起点。
关注点
编写基于插件的软件的基本原则在于,让应用程序能够动态加载 DLL/共享对象,并将应用程序功能构建到 API 而不是可执行文件中。可执行文件通常轻量级,只需从 API 调用静态应用程序对象(从而首次实例化它)并将其暴露给用户。
提示
要构建附带的源代码,您需要安装本文开头提供的链接中的 GTK,并在 Visual Studio 中设置以下包含和库文件夹路径:
包含:
C:\GTK\include; C:\GTK\include\atk-1.0; C:\GTK\include\glib-2.0;
C:\GTK\include\gtk-2.0; C:\GTK\include\cairo; C:\GTK\include\pango-1.0;
C:\GTK\include\gtkglext-1.0; C:\GTK\lib\glib-2.0\include;
C:\GTK\lib\gtk-2.0\include; C:\GTK\lib\gtkglext-1.0\include;
库
C:\GTK\lib
历史
初版 2012 年 5 月 22 日
添加了 GTK 开发环境安装程序链接 2012 年 5 月 23 日
添加了在 Visual Studio 中设置 GTK 的提示。 2012 年 5 月 24 日