自行设计NPAPI开发框架

    经历了一年有余的插件开发,对插件的工作机制也比较熟悉了,在开发插件的过程中使用sdk中的np_entry.cpp、npn_gate.cpp、npp_gate.cpp以及pluginbase.h这几个文件,极大的提高了插件开发的效率,使开发过程变得简单高效,但是在使用的过程中也发现了一些不足之处以及一些细微的bug。在开发过程中我已经对这几个文件进行了不同程度的修改以满足我的开发需求。虽然修改了能满足我的需求,但总有一个重写该框架的想法。前面因为排除一个bug在Firefox源代码中寻找到了一份非常有价值的测试插件实例代码,并进行了研究,结合上述提及的几个sdk中的文件以及为scriptable插件准备的npruntime实例。综合起来写就了一份自己的插件开发框架代码。
    本文以我写这个插件开发框架的过程为基本线索,介绍我是如何来写这个框架的,同时介绍NPAPI插件的工作机制,希望插件开发初学者看完本文之后对NPAPI插件一个清晰的认识;也希望熟悉插件开发的朋友看到此文之后能够对插件有新的感悟,体验到新的东西;更希望所有看到本文之后对插件开发或对本文提出的框架有任何意见和建议的朋友能与我交流。
    本文主要依据windows平台下开发可用于Firefox浏览器的NPAPI插件来阐述,一般来讲对于windows平台其他支持NPAPI插件机制的浏览器也是适用的,并且与其他平台的NPAPI插件开发基本原理也应该是等同的。但本文不保证其他平台或其他浏览器中是完全一样的。下面言归正传。

插件的本质

    插件的本质,就是一个供浏览器调用的动态链接库,在windows平台是一个dll文件,在unix平台是so文件。只不过NPAPI插件规范了这个动态库的接口,规定了接口需要实现哪些最基本的功能。既然是动态库,就从定义接口之处对插件进行寻根探源,不管哪个插件代码中相信都可以找到.def文件中如下描述:
EXPORTS
NP_GetEntryPoints @1
NP_Initialize @2
NP_Shutdown @3
可能有的还有另外一个:
NP_GetMIMEDescription @4
    因此有必要看看这几个接口实现了什么功能, NP_GetEntryPoints的代码如下:
NPError OSCALL NP_GetEntryPoints(NPPluginFuncs* pFuncs)
{
    if (!fillPluginFunctionTable(pFuncs)) {
       return NPERR_INVALID_FUNCTABLE_ERROR;
    }

    return NPERR_NO_ERROR;
}

    很简单,所调用的函数名已经说明了这个接口的功能:填充插件函数表。接下来看看NP_Initialize,代码如下:
NPError OSCALL NP_Initialize(NPNetscapeFuncs* aNPNFuncs)
{
	NPError rv = fillNetscapeFunctionTable(aNPNFuncs);
	if (rv != NPERR_NO_ERROR)
		return rv;

	return NS_PluginInitialize();
}

    同样的,所调用的函数名已经说明了该接口的功能:填充浏览器端函数表。NP_Shutdown这个接口的功能不言自明,结尾的工作由这个接口来做。另外,NP_GetMIMEDescription是浏览器获取该插件所支持的MIME类型描述的接口,可有可无(至少,我是这么认为的)。
     另外在NP_Initialize函数中,宏定义
#if defined(XP_UNIX) && !defined(XP_MACOSX)
#endif
包含的部分直接调用了填充插件函数表的函数,说明在UNIX或MAC上NP_Initialize一个函数实现了windows平台NP_Initialize和NP_GetEntryPoints的功能。
       于是可以推断出,浏览器与插件直接建立关系的一个过程,首先浏览器利用插件端提供的函数填充一个函数指针表(浏览器调用插件端的函数实现我们开发的功能),接着浏览器将浏览器端提供给插件调用的函数填充一个函数指针表,并将这个表告知插件(插件由这个函数指针表来调用浏览器提供的功能)。
        fillPluginFunctionTable函数的参数是一个NPPluginFuncs结构的指针,fillNetscapeFunctionTable的参数是一个NPNetscapeFuncs结构的指针,我们开发插件主要是想让浏览器实现我们指定的功能,因此开发插件的主要工作也就集中在了实现用来填充NPPluginFuncs结构的函数的功能上。用来填充NPNetscapeFuncs结构的函数的功能已经由浏览器实现好了,我们可以在插件中使用。为了方便我们调用浏览器端实现的函数,定义了一堆NPN_开头的全局函数供我们使用,为了方便我们清晰的知道要实现哪些函数接口提供给浏览器,定义了一堆NPP_开头的全局函数。开发插件的工作现在就是实现这堆NPP_开头的函数,并且将这些NPP_和NPN_的函数与前面两个结构NPPluginFuncs和NPNetscapeFuncs联系起来。
        如何进行联系呢,看看代码就明了:fillPluginFunctionTable函数中很多类似如下的语句:
pFuncs->newp = NPP_New;
pFuncs->destroy = NPP_Destroy;

其中pFuncs 就是NPPluginFuncs结构的指针,上面这两条语句就将NPP_New和 NPP_Destroy(这些是需要我们插件开发者来实现的函数)与NPPluginFuncs结构中的newp和destroy联系起来了。浏览器调用NPPluginFuncs结构的newp指针就是在调用我们实现的NPP_New。
          再来看看NPN_函数的实现,以NPN_GetValue为例,代码如下:
NPError NPN_GetValue(NPP instance, NPNVariable variable, void* value)
{
        return sBrowserFuncs->getvalue(instance, variable, value);
}

直接调用了NPNetscapeFuncs结构中的相应函数,因此我们调用NPN_GetValue其实就是在调用NPNetscapeFuncs结构中的getvalue。
        其实将这些NPP_和NPN_的函数结构NPPluginFuncs和NPNetscapeFuncs联系起来的工作都是几乎一样的,于是就有了NPAPI插件开发的各种框架,这些框架的最基本作用就是干这事儿。有了框架我们开发插件就可以集中精力在实现这些NPP开头的函数的功能上。
        以此为指导思想,写出一个插件,只有一个头文件和一个cpp文件(当然还有rc文件和def文件),编译生成dll在Firefox中about:plugins页面可以看到我们的插件。该代码请参考本文提供的附件。我将其命名为aemo,就当是alpha版的demo吧!下载地址: http://download.csdn.net/detail/z6482/4913874

面向对象开发插件

         相信绝大多数插件开发者都是选择C++来开发插件的吧,利用C++面向对象对插件开发进行一定的封装,可以让我们在开发插件的过程中更加专注于插件的实际功能。为了达到这个目的,我们最直接的想法就是将前面提到的NPP_开头的函数封装到一个类中,当我们开发插件的时候就只需要根据我们实际的功能需求来实现类中的相应函数就可以了。来看看sdk中是如何做的:
NPError NPP_NewStream(NPP instance, NPMIMEType type, NPStream* stream, NPBool seekable, uint16_t* stype)
{
	if (!instance)
		return NPERR_INVALID_INSTANCE_ERROR;

	nsPluginInstanceBase * plugin = (nsPluginInstanceBase *)instance->pdata;
	if (!plugin) 
		return NPERR_GENERIC_ERROR;

	return plugin->NewStream(type, stream, seekable, stype);
}

       函数代码比较简单,功能就是将instance的pdata转换为类,然后调用该类的NewStream成员函数。因此我们要实现NPP_NewStream的功能,就只需要实现nsPluginInstanceBase类的NewStream函数的功能即可。简要叙述一下如何将instance的pdata与nsPluginInstanceBase联系起来。请看NPP_New中的代码片段:
	nsPluginInstanceBase * plugin = NS_NewPluginInstance(&ds);
	if (!plugin)
		return NPERR_OUT_OF_MEMORY_ERROR;
	instance->pdata = (void *)plugin;

这里NS_NewPluginInstance创建了一个nsPluginInstanceBase类的对象(准确的说是其子类的对象),然后将这个对象指针转换为void类型的指针,赋值给instance的pdata。这样就建立了instance与我们创建的对象之间的关系。完成了上述操作,就完成了NPP_函数的封装,
        这里还有一点非常美妙的东西需要指出,instance是一个NPP结构,NS_NewPluginInstance中,用这个instance创建了一个插件实例对象,即该对象包含了一个指向instance的指针,而后面instance的pdata又指向了我们创建的这个对象,这样就达到了既可以通过instance访问plugin对象又可以通过plugin对象访问instance的目的。可能你觉得这没什么稀罕的,但是如果在开发插件的过程中需要创建多个类。而这些类又需要与plugin对象共享一些数据,那么就可以为这些类都添加一个NPP的成员,指向这个instance,然后,共享数据就变得非常有意思了。当然你也可以用C++固有的数据共享的方式(如:友元等)来共享数据,但我更喜欢这种方式!
        nsPluginInstanceBase是一个插件实例的基类,该基类定义了几个纯虚函数,我们在创建插件的时候只需要以该类为基类派生一个子类并实现这几个定义为纯虚函数的函数即可生成一个最简单的插件,如果有其他需求则可以根据实际功能对其他虚函数进行覆写即可。如果没有这个基类的其他函数的实现那么我们每次写的时候都要将所有与NPP有关的函数都进行实现,即使没有功能,也至少需要一个空函数体,比如:一般NPP_Print这个打印功能,我们一般是不需要的。如果没有nsPluginInstanceBase,那么每次我们都要写一段这样的代码void NPP_Print(NPPrint* printInfo) { return; },即使是每次都复制,也显得不舒服,而有了nsPluginInstanceBase的封装,我们只需要在派生子类的时候不覆写这个虚函数即可。因此nsPluginInstanceBase可以说完美的封装了NPP_函数,让我们开发插件从如此繁琐的工作转化为仅仅只关注功能的实现。
        添加了类的实现方式,我做了一个与前面类似的插件,是的bemo,beta版的demo。代码请参考本文提供的附件。下载地址: http://download.csdn.net/detail/z6482/4913883

插件接口脚本化

          Scriptable plugin,完整的表述应该叫具有脚本化编程接口的插件(cross-browser NPAPI extensions, commonly called npruntime,跨浏览器NPAPI扩展功能,通常称为npruntime。总之就是为普通的插件增加了可以在JS脚本中访问的接口和属性)。
          简单描述一下整个过程,当浏览器发现JS中在调用插件对象的某些接口或属性的时候,就会调用NPP_GetValue来获取一个NPObject的对象,而这样一个对象我们是通过调用NPN_CreateObject来创建,并调用NPN_RetainObject来获取并返回给浏览器。浏览器根据得到的这个对象,调用该对象的HasProperty、GetProperty、SetProperty等等来进行相关的操作。当然不需要了的时候我们需要调用NPN_ReleaseObject来通知浏览器释放该对象,该操作一遍在实例的析构函数中进行。
          添加脚本化接口也不难,创建一个类似于nsPluginInstanceBase的类nsScriptObjectBase类,该类需要派生自NPObject类,还需要有一个NPP对象来保存其对应的插件实例。仿照npruntime实例代码写出来,在文件中做了一些修改:将所有全局变量全部做完类的static成员变量。并进行一些简单的测试,最终生成一个可用的既能够用于开发一般插件又可以开发具有脚本化接口插件的框架,主要有三个文件:npfrmwk.h、npfrmwkbase.h和npfrmwk_entry.cpp。一切尽在代码中。下一小节结合本小节生成脚本化接口插件简要介绍如何利用本框架来开发NPAPI插件。本小节与下一小节的代码为同一个: demo_frmwk

框架使用说明

        手动创建文件(rc文件),也可以在项目创建好之后在VS中添加新建文件,demo太多了,故将本demo命名为fdemo,生成dll命名为npfdemo.dll,
创建文件npfdemo.rc。内容如下:
#include<winver.h>


/////////////////////////////////////////////////////////////////////////////
//
// Version
//

VS_VERSION_INFO VERSIONINFO
 FILEVERSION    1,0,0,0
 PRODUCTVERSION 1,0,0,0
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS VOS__WINDOWS32
 FILETYPE VFT_DLL
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904e4"
        BEGIN
            VALUE "CompanyName", "JumuFENG.zcf.org"
            VALUE "FileDescription", "demo plugin for our own NPAPI framework"
            VALUE "FileVersion", "1.0.0.1"
            VALUE "InternalName", "npfdemo.dll"
			VALUE "LegalCopyright", "Copyright (C) 2012"
            VALUE "MIMEType", "application/x-frmwk-demo"
            VALUE "OriginalFilename", "npfdemo.dll"
            VALUE "ProductName", "Test Plug-in"
            VALUE "ProductVersion", "1.0.0.1"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1252
    END
END

该文件可以由VS新建,但需要修改相应块描述中的值为上述内容,当然CompanyName等条目需根据实际情况修改为你想设定的值。
         创建好上述文件之后,打开VS(我用的是VS2010)创建一个win32应用程序,设置中选择创建dll。注意勾选上空项目。创建好项目之后。即可向该项目添加现有项,选择添加本文提供的三个框架文件:npfrmwk.h、npfrmwkbase.h、npfrmwk_entry.cpp以及前面创建的文件npfdemo.rc。接着添加新建项,添加def文件。npfdemo.def文件内容如下:
LIBRARY NPFDEMO

EXPORTS
NP_GetEntryPoints @1
NP_Initialize @2
NP_Shutdown @3
NP_GetMIMEDescription @4
        添加完毕之后,为该项目添加一个新的派生自nsPluginInstanceBase的类:CFrmwkPlugin,已经自动将头文件命名为FrmwkPlugin.h实现文件命名为FrmwkPlugin.cpp了,我们要在这里新建两个类,一个是CFrmwkPlugin类另一个是派生自nsScriptObjectBase的 CScriptPluginObject类。设置项目属性,添加npapi的头文件包含目录,在属性->C/C++->附加包含目录中添加,当然你也可以将npapi.h、npruntime.h、npfunctions.h这些文件直接复制到项目中如上述一样添加到项目中,而不添加附加包含目录。
接下来将刚才创建的FrmwkPlugin.h文件和FrmwkPlugin.cpp文件进行修改,主要就是实现两个类中必须实现的函数以及几个相当于全局函数的静态成员函数,代码就不复制到这里来了,请参考本文提供的代码进行实现吧!
        文件中采用预编译指令区分是否编译脚本化接口,如果需要编译支持脚本化接口则需要设置属性->预处理器->预处理定义中添加ENABLE_SCRIPT_OBJECT,当然也可以在源文件中适当位置添加#define ENABLE_SCRIPT_OBJECT达到同样的效果。不需要编译脚本化接口则不需要这些操作,直接编译生成即可。
        为了方便开发插件,减少重复劳动,我用C#开发了一个小工具来完成上述操作,进行必要的设置之后可以自动生成def文件、rc文件以及类FrmwkPlugin.h和FrmwkPlugin.cpp文件,当然这些文件的名称都可以进行指定,如果没有下载我提供的框架也可以用该工具生成并导出该框架的三个文件npfrmwk.h、npfrmwkbase.h、npfrmwk_entry.cpp。更进一步的,如果你连开发插件的最基本头文件npapi.h、npruntime.h之类的文件都没有下载,也可以用这个小工具直接导出。总之我希望这个小工具能让你做尽量少的工作来完成插件的开发,have fun!
        如图是该工具的一个截图:
   自行设计NPAPI开发框架_第1张图片
        需要填写的信息包括文件名,mimetype类型,以及插件实例类名称,其他各项都有预设默认值,或者会根据填写这几项信息时自动填写相关信息。自动填写或预设默认值都可以手动修改。
        最上面一行可以选择生成的文件类型,选择第一项则生成的文件中不包含scriptable的接口,文件非常简洁。选择第二项则生成包含scriptable相关的内容,而且没有添加ENABLE_SCRIPT_OBJECT预编译宏;选择第三项则会生成带 ENABLE_SCRIPT_OBJECT预编译宏的文件,默认是不支持脚本化接口的。要使其支持只需要在项目属性中设置预编译宏ENABLE_SCRIPT_OBJECT即可。
        复选框生成VS工程选项可选择是否生成VS2010版的项目文件,若选择该项则自动选择将框架文件同时生成在项目文件中,生成的项目文件可以直接用VS2010打开,设置好包含目录之后即可生成最简单的插件dll。也可以在VS中将该项目添加到其他解决方案中。若不选择该项,则生成必要的文件,自行创建新的项目,这种方式可以用于其他版本的VS,但需要注意def文件不能直接在新建的项目中添加现有项来完成,否则生成的dll在测试页面中会有问题,正确的做法是用VS添加新建项,添加一个空白的def文件,然后将本工具生成的def文件内容复制到空白def文件中;其他文件则可以直接在VS中添加现有项来进行。建议不要用本工具生成VS项目,因为每个项目文件都有不同的GUID,而本工具没有自动生成GUID的功能,其中使用的GUID是试验的时候创建项目所采用的GUID。

         右下角的附加导出功能可以直接单独导出本框架文件(也有前述三种形式)或NPAPI必须的头文件。本小节与上一节的实例代码为同一个:http://download.csdn.net/detail/z6482/4913893

工具下载地址:下载

闲话

         插件开发刚开始觉得很复杂,满世界找实例,找到的还不一定能编译成功,后来找到的例子都不编译了,直接看关键代码,不管例子是否能编译生成成功,至少例子在作者发布的时候是可以用的,其设计思想和功能实现还是有值得学习的地方,我在学习研究了这些代码之后,也感觉对插件掌握得更透彻了。希望所有同行特别是初学者能够借鉴这种方式。

        为什么要自己写一套NPAPI开发框架,首先是想回答一些朋友的问题,是不是必须要用sdk,我说不是必须,必须要的只有npapi.h、npfunctions.h、nptypes.h等头文件,有了这些个头文件,就能开发NPAPI插件了,本文第一节是一个最简单的NPAPI插件,没有任何多余的东西。写一套如此简单的框架还有一个原因是因为原来的sdk中有bug,可能也是其中唯一的bug吧!当时排查这个bug花费了我三四天的时间。现在我写的这个开发框架可能也会存在未知的bug,因此如果有人使用我这一个开发框架来开发NPAPI插件,遇到任何改进建议或发现任何bug请及时通知我,我将不胜感激。

        建了一个NPAPI插件开发者群,希望对插件开发技术感兴趣的朋友加入,共同探讨学习,交流进步:81424643

你可能感兴趣的:(自行设计NPAPI开发框架)