2016年新春祝贺帖
转C/C++插件式编程综述1. 概述
在模块化越来越规范的现代软件开发方法中,插件式编程是一个绕不开的话题,它涉及范围太广,很多编程人员在入职的初级阶段,一般都是维护一个小功能,或者维护一堆小功能,或者扩充一堆功能,但基本上不涉及到大的框架搭建,但作为提升之路,自己上了一个台阶之后,能够负责一个项目的整体搭建了,或者从头开始一个项目,就必须考虑构架了。插件式编程属于构架范围,我在这方面研究了几年,也实现了几个项目,想整理一下,当是自己的一个总结。
所以我准备趁小孩回老家了的这些天,写几篇关于这方面的文章,我经验不够多,只有12年的编程经验,且主要是用C/C++语言。在现在的公司,挂了个SE的职务,(我们都笑称为Sex Engineer,哈哈),经常会做些设计,但基本上没有断档写代码,我认为不会写代码的构架师,如果只画UML,基本等于在瞎说,当然,特别牛的构架师,不在此例。本文描述的插件式编程,使用C/C++语言做例子,后面也会给出一个详细的例子,供参考,也请高手们指导。我写代码的水平,应该高于我写文章的水平,文字多有不通之处,请海涵。
现在插件式编程目前的资料非常多了,但很多文章属于滥竽充数,希望我这篇能不被列入这个分类。所有一讲插件式的文章,都会举例以前的winamp,不知道还有人用这个播放器不,比较老了,经典的插件式编程。我这里就不说这个了,发散性,系统性的说这个话题。我这里给的例子全部基于windows平台,linux平台说啥好呢?还是不说了。看这个文章的人,应该90%都是工作于windows平台的。好了,不废话,正式进入这个话题。
2. 接口编程
要说插件式编程,先要说模块化编程,因为插件式编程是用来开发整个项目框架的,把一个项目按模块划分之后,可以并行作业,并减少关注的细节。而模块化编程,又涉及到接口编程模式。这些都是软件开发过程中,抽象与整理的一个过程。刚学习C/C++编程的同学,只会在main函数里面写自己的代码,不会把事情分成几个函数来做,等到会抽取函数,又无法做到公用性,代码像写日记似的,流水账一来就是几百行。这方面很重要的提升就是会提取公用函数,形成函数库。再到面向对象设计,写出自己的类,再到写出DLL。本文假设我说的以上这些,您都已经会了,会抽象公共函数,会自己封装设计自己的类,做成模块DLL,在主程序中加载DLL使用等。如果是C++语言,您一定要对virtual虚函数有完整的理解,对于C语言,则要完全理解回调函数。
接口编程,也不是什么新鲜事情,在C#和JAVA语言中,都提供了标准的接口关键字,C/C++没有这样的关键字,但接口编程是一种理念,不需要这些关键字,用普通的手法就能实现这个过程,我尽量避开讲语言层面的知识,因为这些知识,说多了容易说错,怕言多必失啊。
举一个例子吧,这是后面所有代码的“需求”,把这个需求实现的过程,就是一个接口实现的过程。我们要写一个网络爬虫模块,这个模块就2个函数:开始,结束。不需要其他的函数了,参数设置什么的,都忽略掉。现在要实现这个模块设计,给其他人留出接口,让他们写的程序,调用这个模块的“开始”函数,则实现自动爬网络数据的过程,调用“停止”函数,则停止爬数据,停下来即可。
1 C++接口定义
用C++语言,我们可以这样定义我们的接口:
class ICrawler
{
public:
virtual bool Start() = 0;
virtual bool Stop() = 0;
};
ICrawler* CreateCrawler();
void DestroyCrawler(ICrawler* pCrawler);
上面的virtual 关键字,还有末尾的 =0,是C++的纯虚函数的定义范式,关于这个知识点,就不说了,请百度一下,如果虚函数的作用不清楚,您将看不懂以下的所有内容了,要不您先复习以下吧。这个类定义中,类名称也不是用“C”开头,而是用“I”开头,表示这个是接口的意思,当然,你换成别的字母也行,只是潜规则嘛,大家都这么定义接口,你懂的。定义好这样的接口之后,关健是实现这个接口。
C++的类需要有public这样的定义,我们知道C++的类实际上和struct是一样的,我们把定义修改成这样,这样也许更好看一些:
struct ICrawler
{
virtual bool Start() = 0;
virtual bool Stop() = 0;
};
ICrawler* CreateCrawler();
void DestroyCrawler(ICrawler* pCrawler);
这下好看多了,通过struct关键字之后,就把public去掉了。将以上的内容做成一个头文件,命名为 “icrawler.h”吧。如果想要实现的更加的像C#或者Java,那么可以来这么一行:
typedef struct interface;
很多人都这么定义一下,不过我不喜欢,还是struct直观。
把以上的定义做成头文件,供大家调用,我们在模块内部,实现这个接口。先继承这个接口,然后实现它。代码大概看起来像这样:
class CCrawlerImpl : public ICrawler
{
public:
virtual bool Start()
{
//Start new thread.
_tprintf(_T("Starting ..."));
return true;
}
virtual bool Stop()
{
//Stop thread.
_tprintf(_T("Stoped."));
return true;
}
};
ICrawler* CreateCrawler()
{
return new CCrawlerImpl();
}
void DestroyCrawler(ICrawler* pCrawler)
{
CCrawlerImpl* p = (CCrawlerImpl*)pCrawler;
delete p;
}
将以上的代码放置到一个dll里面,然后在DLL的def文件里面,导出函数CraeteCrawler和DestroyCrawler。这个模块就算是写完了。
外面可以这么调用这个模块:
#include "icrawler.h"
int _tmain(int argc, _TCHAR* argv[])
{
ICrawler* g_pCrawler = CreateCrawler();
g_pCrawler->Start();
g_pCrawler->Stop();
DestroyCrawler(g_pCrawler);
g_pCrawler = 0;
return 0;
}
以上过程就是C++的一个简单接口编程方法。
2 C接口定义
有人喜欢用C语言,C语言没有类的概念,能实现接口吗?答案当然是可以的。接口编程和语言无关,常见的语言都可以实现。C语言的实现过程也很简单,关键地方就在于回调函数,C语言的回调函数能够实现比C++的虚函数表更快的调用,只是实现过程稍微没那么方便而已,请看我下面的实例代码。头文件变更为如下定义方法:
typedef bool (*StartProc)();
typedef bool (*StopProc)();
struct ICrawler
{
StartProc Start;
StopProc Stop;
};
ICrawler* CreateCrawler();
void DestroyCrawler(ICrawler* pCrawler);
是不是很简单,新增加了两个回调函数定义,然后接口还是原来的样子。那么模块的实现呢?这里也给出例子,代码如下:
bool s_StartProc()
{
//Start new thread.
_tprintf(_T("Starting ..."));
return true;
}
bool s_StopProc()
{
//Stop thread.
_tprintf(_T("Stoped."));
return true;
}
ICrawler* CreateCrawler()
{
ICrawler* p = (ICrawler*)malloc(sizeof(ICrawler));
memset(p, 0, sizeof(ICrawler));
p->Start = s_StartProc;
p->Stop = s_StopProc;
return p;
}
void DestroyCrawler(ICrawler* pCrawler)
{
free(pCrawler);
}
将以上代码放到DLL的模块里面,其他和C++一样,同样,导出那两个标准函数。
我们的外面调用,即main函数中的代码不用做任何修改,就可以调用C或者C++写的模块代码了。
C语言的回调函数用到的地方实在太多了,用好了的话,比什么C++虚函数,泛型函数之类都管用。比如openssl库,它所有的加解密函数,都是接口定义方式,底层几乎所有的函数,都可以通过修改定义的方式,实现对openssl的修改,让它不用自己的默认函数,再比如winpcap库,还有其他的库等,都会采用这种方式,屏蔽掉操作系统的差别,实现扩平台的代码兼容。
当然,任何实际的模块都会比这个例子复杂,函数中,也会带各种各样的参数,这样就会和C++有一些不一样的地方了,而C++的函数中,隐含了一个this指针,回调函数没有这个参数,导致回调函数通常最后都要带上一个void*类型数据的参数,用来做数据回传的转换。 如果不这么做,用全局变量的话,又不符合编程规范。这些地方就不展开了,有不清楚的地方,可以给我来信探讨。
我再根据我实际的工程经验,说说这两种接口实现方法的差别:
l C++版本,阅读代码更方便,VisualStdio(以后简称vs)里面可以直接F12查看各种跳转。而C版本就没这么方便了,自己要跟踪各个函数的实现。无法实现Start函数具体到实现的跳转。(当然了,安装了辅助工具的同学可能不这么认为了。)
l C++版本函数定义之后,编译器检验更严格。C语言没这么严格,C语言可以实现更多种实现,C++版本则不断需要继承出新的类来实现。但C语言的各个函数中,需要增加一个void*类型的变量用于回传数据用。
l C++版本接口升级,比如增加了新的函数,如果在现有的函数中插入或者打乱了函数顺序,那么原来的使用者的程序得重新编译,而C语言版本则没有这个问题。
l 两者都高效,不讨论了,但如果用于插件式开发,则一定要先选好模型,虽然两者都可以用于插件式开发,但最好统一成一个,不要两种同时上,当然你要同时上,我也没意见。
标准的接口编程就这样的简单。下面该说说插件式编程了。
3. 插件式编程。
插件式编程看你工程的实际需要,可以做的很大,也可以做的很小,并且在界面程序中实现插件机制,会更复杂,现代的软件,基本上70%以上的投入都是花在界面上,特别是软件升级版本,核心的代码部分,通常都不需要花什么时间,而我们大部分软件还都没有自己的核心,呵呵,自嘲一下,不是指你的软件工程啊。
要系统的讲插件式编程,由易到难,分以下个类型来说吧:
l 简单的单向接口插件构架。
l 双向式接口插件构架。
l 界面式软件的插件构架。
l 进程式插件构架
1 简单的单向接口插件构架
看名字就简单,也就是说,我的框架不向插件开放任何接口,框架决定了怎么加载插件,怎么调用插件,怎么卸载插件,插件完全是被动的,只能干自己的活,不能反过来要求框架。也就是说,框架老大,我说怎么干就怎么干。这种一般用于算法式的框架,比如提供加密算法框架,主程序不做具体的加密,由插件来完成加密,每完成一种合适的加密算法,就加入到插件目录,主程序中就可以多出一种加密算法选择,具体选择哪个,由用户来选择。
举个什么例子好呢?还是以前面的爬虫做例子吧。我现在开发一个主框架,有各种各样的爬虫模块,他们都遵循我定义好的接口,我是框架,它们是插件。接口还按以前的例子,对他们提出以下要求:
1 必须用C/C++标准的DLL方式实现。
2 必须导出CreateCrawler和DestroyCrawler方法。
3 必须放置于同一个目录下,文件名称后缀都为 .dll
好了,以上的要求够了。开始实现我们的框架,我们的主框架很简单,就一个Dialog对话框,界面上列出所有的爬虫模块,用户选择一个或者多个爬虫,然后点击开始按钮,调用插件干活,调用停止按钮,停止干活。OK,就这么多事情。
1 主界面初始化,找到插件目录,开始遍历这个目录下的所有dll。
2 对DLL调用LoadLibrary函数,进行加载,加载不成功的DLL直接continue跳过。
3 调用GetProcess函数,能找到CreateCrawler和DestroyCrawler这两个导出函数的通过,没有这两个函数的DLL,直接卸载后跳过。
4 把符合条件的DLL放到一个数组中,保存各个DLL的函数地址等信息,直到整个遍历过程结束。
5 在界面上,把这个数组显示出来,然后等待用户点击“开始”按钮。
6 收到点击事件之后,把前面例子中main函数中的代码,挨个调用一遍,直到用户选择的所有爬虫都执行完。
7 所有事情干完,卸载所有插件。FreeLibrary。
这个例子太简单,就不放代码了,因为我们后面有更强大的例子,并且会放代码。这个例子应该很简单,基本上都能做出来,如果不能的话,说明基本功不够啊。
以上的简单例子,说明了插件式构架的基本要素,例举如下:
1 整个构架需要有一个好的文件夹规划,哪个目录放置插件,需要预先定义好。
2 整个构架,对插件有一个统一规定,就是后缀名称一样,我们在实际项目过程中,可以不用DLL做后缀,改个别的名字吧,显得拉风一点。
3 插件都是DLL文件,靠导出函数引导,为什么不用COM之类的呢?我后面会有说明。
4 有规定好的必须导出的函数和接口。比如至少应该加入插件初始化,插件反初始化等几个必须的接口,这样才好更容易的检查插件的合法性。
当然我以上几点等于没说,我就看见有人用配置文件的方式,把我得第一条和第二条给忽略了,不过我还是很鄙视他的作法,因为他每新增加一个插件,就需要写一个配置文件,主框架加载DLL的时候,需要先到一个特定目录找到配置文件,然后解析配置文件得到真正的插件文件,然后再加载插件。我的方法比他的简单,写好的DLL文件丢到这个插件目录即可,不需要配置文件,更加简单。
使用C#和Java语言的同志,这些语言在做框架的时候,可以使用类反射机制,实现类的创建过程,比我这DLL里面导出固定函数的作法,要先进不少。这里就不细说了。
2 双向式接口插件构架
单向式插件构架是最简单的一种,插件处于模块化的地位,没有任何的话语权,一般的介绍插件机制的文章,讲的都是这种,比较容易说清楚,但这种情况在真实项目中,一般是很少存在的,充其量是个模块化编程,根本算不上插件式构架。框架调用插件的功能,天经地义,插件反过来调用框架,也是天经地义的。双方只有交互的情况下,才能更智能,更符合用户习惯,这才能算是一个标准的插件式构架。
交互就是一个互相调用过程而已,因此,实现过程也不难,主框架也提供几个头文件,定义好一些接口,插件在运行过程中,可以调用主框架的功能,实现一些环境变量的获取与交互等功能。在实际项目中,插件和框架的头文件通常都放在一个目录即可,双方各负责实现自己定义好的头文件。这点在前面的例子中,已经演示过了,这里说说运行过程中的加载过程。
我们还是用前面的例子做一些扩展,插件的功能不变,但主框架需要向插件提供以下功能:
1 结果保存路径,用于保存结果用。
2 用户设置好的参数,比如过滤好的URL。
3 只是个简单例子,就不要3了吧。就前面两个就够了。
插件在运行过程中,要动态的通过主框架的接口获取信息,做相应的操作,而不是靠配置信息来实时获取。以上列子全部用C++的方式来实现,不提供C语言版本的示例代码了。
我们的头文件修改成以下内容:
struct IFrame;
struct ICrawler
{
virtual LPCTSTR GetName() = 0;
virtual bool Start() = 0;
virtual bool Stop() = 0;
};
void PluginInit(IFrame* pFrame, int mainVer);
void PluginUninit();
struct IFrame
{
virtual void RegisterPlugin(ICrawler* pCrawler) = 0;
virtual LPCTSTR GetResultPath() = 0;
virtual LPCTSTR GetConfigValue(int type) = 0;
};
插件的接口,增加了一个函数,GetName,框架显示插件名称是要用到,插件DLL提供的函数,不再需要前面例子中的那两个函数了,创建和删除,框架都不管了。导出函数改成了以下两个函数:
void PluginInit(IFrame* pFrame, int mainVer);
void PluginUninit();
和函数定义名称一样,插件中,PluginInit的实现,需要修改一下了,以前是主框架主调用插件的创建函数,实现插件的具体实例化,并在最后负责销毁。现在不这样做了,主框架在调用插件这个初始化函数的时候,插件自己需要主动向主框架注册,告诉主框架,有了我这么个接口存在。把新增加的两个函数实现,并加入到def导出函数中,函数体的实现,只需要判断版本是否合适,记录Iframe指针即可。要用指针的时候,直接用。
代码现在是这样子了:
CCrawlerImpl theApp;
void PluginInit(IFrame* pFrame, int mainVer)
{
if (mainVer != 100)
return;
theApp->m_pCore = pFrame;
pFrame->RegisterPlugin(&theApp);
}
void PluginUninit()
{
theApp->m_pCore = 0;
}
主框架中,新增加一个类,CframeImpl,从Iframe继承,实现定义的那两个框架接口函数。
主框架的接口实现代码有点像这个样子:
class CFrameImpl : public IFrame
{
std::array m_plugins;
public:
void RegisterPlugin(ICrawler* pCrawler)
{
m_plugins.push_back(pCrawler);
}
LPCTSTR GetResultPath()
{
return _T("C:\\result");
}
LPCTSTR GetConfigValue(int type)
{
return 0;
}
};
好了,我们现在看看主框架的流程:
1 主界面初始化,找到插件目录,开始遍历这个目录下的所有dll。
2 对DLL调用LoadLibrary函数,进行加载,加载不成功的continue跳过。
3 调用GetProcess函数,能找到PluginInit和PluginUninit这两个导出函数的通过,没有这两个函数的DLL,直接卸载丢弃。直接调用PluginInit一把。调用的时候,需要把自己的指针传入,以便让插件注册用。
4 框架在Register函数中,攒积了各个插件的接口,并且可以通过GetName获取到插件的名称了。在界面上,把这个名称显示出来。等待用户点击“开始”按钮。
5 收到点击事件之后,把前面例子中main函数中的代码,挨个调用一遍,直到用户选择的所有爬虫都执行完。
6 所有事情干完,退出前,挨个调用PluginUninit函数,让插件有机会释放自己的资源。
和前面的例子比前来,少了一个步骤,主框架调用插件的创建函数来创建实例,由插件自己来注册,可以让插件有更多的自由选择的权力,并且一个DLL中可以写多个插件,这种方法在实际项目中用的比较多。
通过这个例子,插件式构架看起来有那么点味道了。终于像一个插件式构架了。
3 界面式软件的插件构架
这个分类和前面说的构架,是一样的,唯独不一样的地方在于,界面。前面说的构架方法,在linux上面是通用的,但界面这块,Linux上面的界面,我没开发过,就不说linux了,我们这里只说windows上面的界面。
我们现在的客户端软件,如果不提供一个好看的,易用的界面,基本上很难扩大使用人群,拿不出手啊。自己都不好意思,而这点也制约了个人共享软件的发展,现在很难再出现以前那样有影响力的个人版本共享软件了,精力有限。而C++界面库的发展,虽然也如火如荼,群雄并起,但最后咋样,还真不好说,但有句古话说得好“自古文人相轻”,在C++库这方面,很多程序员好像也有这个毛病,总认为别人的库写得不好,喜欢自己来一套,所以C++除了几个基础库,其他库则基本上混乱不堪,使用者渺渺。又扯远了。
一个桌面软件框架,肯定有一部分的界面,需要插件来提供,比如提供工具栏,菜单,主客户端区域等等,而主框架也需要开放很多接口,供插件操作各种界面元素。这涉及到了大量的界面交互内容,这么一来,整个框架的复杂度将会提高很多,而框架的接口定义,也将增加很多,导致整个接口阅读和理解的难度增加。
在界面框架这块,事先的预定义很重要,要根据项目的内容进行规划,比如Word这样的,一个或者多个文档,各个插件都可以操作这个文档,还不需要提供客户区窗口。还比如winamp那样的,各个插件互相不干扰,也不使用主界面,都提供自己的界面,还比如QQ界面那样的,多个tab页,每个tab页面算一个插件,点击不同的tab按钮,出来自己的界面。这个界面需求定下来,接口设计也就不一样了。因此,构架需要按照你的界面要求而开发,这好像等于废话,呵呵。
我这里给出一个例子,类似于Tab页面切换机制,每个插件一个Tab页面,有自己的工具栏,有自己的客户区,算了,前面说的很容易绕晕。我自己都晕了,还是看图片吧,看图最容易理解。
框架的样子:
加载了插件1的样子
加载了插件2的样子
这个例子中,主框架的主界面是第一个Tab页面“主页”,后面的“Test1”和“Test2”都是插件实现的,这个例子中一共加载了两个插件。第一个插件是随机显示图形,第二个插件是一个标准的FormView,里面有一些控件。如果有更多的插件的话,将会有更多的工具栏出现。(我把Ribbon叫工具栏,更好理解一些)
以上程序,请直接运行示例代码中的文件,就可以看到贴图一样的效果了。
这个例子的理论还是很简单,从主框架中,导出了UI接口,UI接口中,又提供了Toolbar,DockBar等元素的接口。插件也需要导出UI接口,实现主客户区创建等。代码面前无秘密,请直接参考代码,这里说一些插件式框架要注意的问题:
约束问题
插件式构架,应该尽量对插件的开发过程减少要求和规定,因为这些东西也需要学习成本,比如必须使用MFC,必须使用vs2008+sp1等等,比如微软的COM,甚至做到了不要求语言,不关注线程安全,不关注存在地点等,但COM也有不好的地方,它其他方面的要求太多太高了,导致学习COM的成本很高。
界面资源句柄问题
Windows的资源句柄在多DLL编程的时候,一直是一个麻烦,如果没有MFC,全部基于windowsAPI编程的话,还好一点,因为大部分和资源相关的函数,会要求提供DLL的句柄,而在MFC里面,大部分的资源函数,都不要求提供DLL的句柄,所以如果当前的DLL句柄不对的情况下,就会调用资源函数失败。所以AfxSetResourceHandle这个函数要多多使用,或者使用其他的技巧,比如利用C++类的构造函数和析构函数机制,自动设置当前资源句柄等,或者使用MFC的几个资源宏等。
标准数据类型的问题
C++ STL库很好用,还有其他的模板库等,泛型编程技术,在自己的代码中用用也就算了,能在导出函数中使用吗?比如我们提供这样一个函数:
string GetName();
这个函数在作为接口导出函数时,显然不合适。
l 模板头在dll的导出头文件中使用时,会出来一堆的warning告警。
l 每个vs版本的stl不一定兼容,因为它是源代码版本,不是编译版本。
l 加了string,等于约束插件要必须使用某某stl,我不喜欢这样的约束。
所以这个函数,我通常都这么定义
LPCTSTR GetName();
使用了标准的类型,就没有问题了。但这个只是特例,我们其他地方还是有很多需要返回类的地方,总不能什么都还原到最基本类型吧,所以,一个基本的公共库是不可少的。比如提供一些基本的功能类,这样主框架和插件都使用这些类,不会出现问题,这些类也可以使用在插件的接口定义中。
不管怎么定义,怎么约束,插件和主框架都处于同一个进程,写插件的人水平不高,引入这个带风险的插件,将导致整个框架在运行过程中死掉,还不好查找时谁的问题。这个问题,是同一个进程插件无法解决的问题。解决方案就是多进程插件模式。
4 进程式界面插件构架
最典型的例子,就是早期的某些浏览器,具备多tab页面功能,但如果某个页面卡死,整个浏览器都将无法使用,导致其他已经打开的网页也无法查看了。这促使后来的浏览器全部采用了多进程方式,也就是说,每个tab页面,实际上都是一个新的进程,某一个进程卡死,将不会对其他进程产生影响。
进程式插件构架实际上只是把多个窗口叠加在一起,看起来像一个程序,底层则是各走各的路,互不干扰。最基本的原理,就是基于WINDOWS的窗口HWND可以互相嵌套的原理。
Windows上面的各个进程,进程空间都是互不干扰,本进程要访问其他进程的东西,并在它的进程中加入自己的代码,都需要非常麻烦或者说高深的技术,但有一样除外,窗口。只需要使用FindWindow,就可以找到其他进程的窗口,窗口时windows的资源,所有的进程公用这些资源,当窗口所有资源耗尽的时候,就再也创建不了新的窗口了。这个限制,我得观察是6万多个,不知道对不对。自己的窗口在创建的时候,需要传入一个父窗口句柄,而这个父窗口句柄,是没有进程限制的。
进程式插件在解决了窗口问题之后,另外面临的问题,就是通讯问题,原来的接口指针,直接调用即可,现在这些指针,全部不能使用了。这里,我们可以采用如下图所示方案。
主框架接口
通讯模块
加封/解封装模块
插件模块接口
加封/解封模块
通讯模块
如果把上面的通讯模块,换成http通讯,把加封/解封换成xml封装方式,是不是和“soap”协议一模一样了?没错,标准的SOAP协议就是这么做的。我们可以不用HTTP,简单的TCP或者管道通讯即可,把接口用xml方式封装,然后传递到对端,即可形成插件调用机制。
作为构架,主要是把接口开放给插件,但要屏蔽掉进程通讯相关的细节,因此把以上的通讯机制,封装成一个lib或者dll,在插件中,调用这些函数的时候,通过通讯,会调用到主框架程序中。只要这个封装库做的够好,插件几乎完全不知道自己是进程式插件,还是单进程式插件。这个封装库,我这里暂时无法给出实例,因为我得测试代码还没有写完。
4. 后记:
为了写这篇文章,还有代码实例,花了我将近1一个月的时间,单位长期加班,大部分日子回到家都是晚上9:30以后了,根本就没有自己的时间写代码,偶尔找个时间出来吧,还有很多的应酬与私事要处理,原来计划是完成基本库的开发,把单进程插件和多进程插件的代码全完成,但发现真的没有这个精力,北京大雪,气温剧降,我又病了一场。一边咳嗽一边写文档,现在都还没有好,真不是什么好状态。后面的代码要全部完成,我估计还得持续2~3个月,光基本库的单元测试的代码,就不是个小数字,实在是茫然。就像当年准备写DirectUI库一样,结果断断续续了2年,人家DirectUI库都成立公司,卖了好几个版本,我的代码依然没有着落。虽然忙不是理由,但要实现它,真的太需要时间了。势单力薄,成不了什么事情,要是有个团队就好了,后面看情况,还是继续写点吧。
示例代码采用vs2010编译,界面部分,全部采用最新的ribbon模式。两个插件,分别使用了两种不同的DLL方式,供参考。
[此贴子已经被作者于2016-1-1 10:01编辑过]