项目需要支持chrome浏览器的使用,需求要支持ActiveX控件或COM组件能在chrome下运行,也就是说要进行使用NPAPI的技术进行对控件封装, 使用动态库都是同理。于是研究了NPAPI的开发过程和原理,现做出以下一些记录。
具备条件: NPAPI SDK. 开发环境。(SDK可以官网下载或其他途径获取);
一个基本的结构式如下所示:
插件的本质,就是一个供浏览器调用的动态链接库,在windows平台是一个dll文件,在unix平台是so文件。只不过NPAPI插件规范了这个动态库的接口,规定了接口需要实现哪些最基本的功能。
开发环境:win7 X64, vs2013,chrome版本 28.0.1500.44.
1. 创建工程
创建一个win32 工程,下一步选择dll和空工程。如下图:
Name项一定要以np开头,为了将来适应不同操作系统,最好全小写,不要太长,尽量控制在8字符内。本例定义为nptestdemo ,工程路径自定义即可;
NPAPI的SDK 本例定义为F:\Propram Code Lib\nptestdemo\ffplugin, 如下图;
2. 添加文件, 将NPAPI的文件添加到工程中(NPAPI的有几个文件必须添加到工程)
我的NPAPI SDK所在位置如下图所示:
将NPAPI SDK中Common的文件添加到工程;
路径为:…\ffplugin\sdk\samples\common\
3. 添加def文件
新建一个def文件,命名最好与项目一致。如下图;
编辑nptestdemo.def文件:
文件中加入以下内容:
LIBRARY"nptestdemo"
EXPORTS
NP_GetEntryPoints@1
NP_Initialize@2
NP_Shutdown @3
4. 添加资源
新建一个资源文件:选择Version, 选择新建,会自动生成一个资源文件。
资源文件右键选择“查看代码”,打开资源文件,编辑;
找到如下一段:
BEGIN
BLOCK"StringFileInfo"
BEGIN
BLOCK"040904e4" //修改BLOCK 一定要是"040904e4"
BEGIN
VALUE"CompanyName","TODO: <公司名>"
VALUE"FileDescription","TODO: <文件说明>"
VALUE"FileVersion","1.0.0.1"
VALUE"InternalName","nptestde.dll"
VALUE"LegalCopyright","Copyright (C) 2017"
// MIMEType是plugin的唯一标示,需要自己定义
//通常的格式是"application/“+ [plugin name]
//本例中定义为"application/demo-plugin"
VALUE "MIMEType","application/nptestdemo-plugin"
VALUE"OriginalFilename","nptestde.dll"
VALUE"ProductName","TODO: nptestdemo.dll_mkx"
VALUE"ProductVersion","1.0.0.1"
END
END
BLOCK"VarFileInfo"
BEGIN
VALUE"Translation",0x409,1252 //修改VALUE值这两个值
END
END
5. 添加最关键的部分:Plugin实现类
添加一个C++类: CPlugin. 要继承自nsPluginInstanceBace. (注意包含父类文件)
编辑Plugin.h文件:
重写父类三个虚函数:
#pragma once
#include "pluginbase.h"
class CPlugin :public nsPluginInstanceBase
{
public:
CPlugin(NPP pNPInstance);
~CPlugin();
//重写以下三个虚函数
NPBoolinit(NPWindow* pNPWindow)
{
m_bInitialized = TRUE; return TRUE;
}
voidshut() { m_bInitialized = FALSE; }
NPBoolisInitialized() { return m_bInitialized; }
private:
NPPm_pNPInstance;
NPBoolm_bInitialized;
};
编辑Plugin.cpp文件,实现四个全局函数;
NPError NS_PluginInitialize()
{
returnNPERR_NO_ERROR;
}
void NS_PluginShutdown()
{
}
nsPluginInstanceBase *NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct)
{
if(!aCreateDataStruct)
return NULL;
CPlugin * plugin = new CPlugin(aCreateDataStruct->instance);
returnplugin;
}
voidNS_DestroyPluginInstance(nsPluginInstanceBase * aPlugin)
{
if(aPlugin)
delete(CPlugin *)aPlugin;
}
6. 修改项目属性,加入SDK链接
在第5步之前,应该将工程属性修改,C++->常规->附加包含目录中加入sdk的包含目录;本项目按照SDK路径加入路径为:..\ffplugin\sdk\samples\include; ..\ffplugin\base\public
加入添加预编译宏X86
C++->预处理器->预处理器定义中加入 _X86_
这个宏要是没有加,会报出错误 1 error C1189: #error : "No Target Architecture"等错误还有会报很多vs库文件中的类型错误。所以该编译宏必须添加;
7. 注册、测试
本例编译后,生成文件为nptestdemo.dll
要向注册表注册该文件,打开注册表,注册路径为:
[HKEY_CURRENT_USER\Software\MozillaPlugins]
在该路径下新建@mozilla.com.cn/demo(该名称可以自定义创建,名称自定义)
新建字符串数据“Path” 值为nptestdemo.dll的绝对路径。本例为:F:\Propram Code Lib\nptestdemo\Debug\nptestdemo.dll. 下图:
打开chrome浏览器(版本为开发环境中说明的版本)在地址栏输入“about:plugins” 。如果在plugin列表中有本例的nptestdemo.dll即说明我们的plugin示例已经成功完成,并且浏览器已经成功加载。(360浏览器极速模式亦可完成此动作)
至此,一个简单的流程就完成了。这只是完成了一个基本简单的框架,实际上什么也做不了。浏览器只是加载了插件,实际上相当于和浏览器没发生任何交互动作。
写一个简单的html页面来调用该插件;
1.
2.
3.
4. window.onready = function(){
5.
6. }
7. function toDoSt(){
8. var plugin = document.getElementById("plugin");
9. alert(plugin.version);
10. }
11.
12. //插件以这种方式加载
13. id="plugin" type="application/x-npTest" > //这里填入的是插件的 唯一标识,还是的吗就是资源文件中添加的那个,即VALUE "MIMEType","application/nptestdemo-plugin"这个东东;
14.
15.
16. type="button" onclick="toDoSt()" value="test">
17.
18.
8.
9. S
10. S
11. S
前面说插件就是动态库,就从定义接口之处对插件进行寻根探源,不管哪个插件代码中相信都可以找到.def文件中如下描述:
EXPORTS
NP_GetEntryPoints @1
NP_Initialize @2
NP_Shutdown @3
可能有的还有另外一个:
NP_GetMIMEDescription @4 //这一项可有可无,一般不加这个;
SDK中主要文件包括:np_entry.cpp、npn_gate.cpp、npp_gate.cpp、basic.def、pluginbase.h等几个主要文件;
其中有很多接口;
接口说明:
NP_XXX 是npapi的插件库提供给浏览器的最上层的接口,
NPP_XXX即NP Plugin是插件本身提供给浏览器调用的接口, 主要被用来填充NPPluginFuncs的结构体.
NPN_XXX即NP Netscape ,是浏览器提供给插件使用的接口,这些接口一般都在NPNetscapeFuncs结构体中。
NP_XXX类接口一般为动态链接库的导出接口,对应文件为:np_entry.cpp,主要有NP_Initialize、NP_GetMIMEDescription、 NP_GetEntryPoints、NP_GetValue、NP_Shutdown的等几个函数,不同平台的接口可能略有不同,但基本功能都是一样的,都是通过接口来初始化、销毁以及认知此动态库。
有必要看看这几个接口实现了什么功能:
NP_GetEntryPoints的代码如下
很简单,所调用的函数名已经说明了这个接口的功能:填充插件函数表。
接下来看看NP_Initialize,代码如下:
同样的,所调用的函数名已经说明了该接口的功能:填充浏览器端函数表。
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 就是NPPluginFuncs结构的指针,上面这两条语句就将NPP_New和 NPP_Destroy(这些是需要我们插件开发者来实现的函数)与NPPluginFuncs结构中的newp和destroy联系起来了。浏览器调用NPPluginFuncs结构的newp指针就是在调用我们实现的NPP_New。实际上我们需要实现的就是这些功能函数。
NPP_XXX类接口一般为提供给浏览器引擎调用的接口,即用于定义浏览器调用插件的方法,对应文件为:npp_gate.cpp, 主要包括: NPP_New、NPP_Destroy、NPP_SetWindow、NPP_GetMIMEDescription 、NPP_NewStream、NPP_DestroyStream、NPP_StreamAsFile、NPP_WriteReady、
NPP_Write、NPP_Print、NPP_HandleEvent、NPP_URLNotify、NPP_GetValue、NPP_SetValue等
NPP_GetValue是一个笔很重要的函数, 因为在这里常见交互的js脚本实例。
经过NP_GetEntryPoints的初始化后,当特定事件发生时,浏览器将会调用这些方法,然后需要注意的是该文件引用了plugin.h文件,创建实例。
NPP_New用于创建插件实例,该方法中通过nsPluginInstanceBase* plugin = NS_NewPluginInstance(&ds); 来创建一个实例,NS_NewPluginInstance()是我们在前面Plugin.cpp中实现的一个全局函数,还记得不,就是它,就是用它来创建一个实例的。
NPP_Destroy方法,用于销毁插件实例,刷新页面,关闭页面等操作会触发
该方法会调用CPlugin的shut方法再delete掉实例。
NPP_SetWindow方法,插件窗口发生任何变化都会调用该方法,window创建时会调用一次,如果初始化失败则delete掉实例然后返回错误。
NPP_GetValue方法,当获取插件有关的一些信息时会触发该方法调用(如获取插件名,插件实例),当javascript操作插件对象时,该方法调用CPlugin的GetScriptableObject方法,需要自己实现,返回一个脚本操作对象(NPObject),在这里返回到CPlugin类,添加GetScriptableObject方法并实现。
NPP_HandleEvent方法,处理事件,该方法调用CPlugin的handleEvent方法,继续添加实现吧
NPN_XXX类接口一般为webkit引擎提供给plugin调用的接口,对应文件npn_gate.cpp,该文件实现了对浏览器的一些操作的函数, 主要包括:NPN_GetURL、NPN_PostURL、NPN_RequestRead、NPN_NewStream、NPN_Write、NPN_DestroyStream、NPN_Status、NPN_UserAgent、NPN_MemAlloc、NPN_MemFree、 NPN_MemFlush、NPN_ReloadPlugins、NPN_GetJavaEnv、NPN_GetJavaPeer、 NPN_GetURLNotify、NPN_PostURLNotify、NPN_GetValue、NPN_SetValue、 NPN_InvalidateRect、NPN_InvalidateRegion、NPN_ForceRedraw、 NPN_GetStringIdentifier、NPN_GetStringIdentifiers等函数接口。
本部分主要分析NPAPi 插件的函数调用流程。将详细分析插件的代码是如何执行的,主要分析np_entry.cpp、npn_gate.cpp和npp_gate.cpp。
在windows平台下,插件就是一个dll,注意到这个dll的def文件内容是:
NP_GetEntryPoints
NP_Initialize
NP_Shutdown
这是整个dll的导出接口,也是调用的入口函数。
NP_GetEntryPoints – 在插件加载之后立即调用该接口,用于浏览器获取所有可能需要调用的API函数的指针。
NP_Initialize – 为插件提供全局初始化。
NP_Shutdown – 为插件提供全局反初始化。
这三个函数的定义是在np_entry.cpp文件中的:
NPError OSCALL NP_GetEntryPoints(NPPluginFuncs* aNPPFuncs)
{
returnfillPluginFunctionTable(aNPPFuncs);
}
其中fillPluginFunctionTable为:
static NPError fillPluginFunctionTable(NPPluginFuncs* aNPPFuncs)
{
if (!aNPPFuncs)
returnNPERR_INVALID_FUNCTABLE_ERROR;
//这些函数是需要在插件中加以实现的。
// Set up the plugin functiontable that Netscape will use to call us.
aNPPFuncs->version = (NP_VERSION_MAJOR << 8) |NP_VERSION_MINOR;
aNPPFuncs->newp = NPP_New;
aNPPFuncs->destroy = NPP_Destroy;
aNPPFuncs->setwindow = NPP_SetWindow;
aNPPFuncs->newstream = NPP_NewStream;
aNPPFuncs->destroystream = NPP_DestroyStream;
aNPPFuncs->asfile = NPP_StreamAsFile;
aNPPFuncs->writeready = NPP_WriteReady;
aNPPFuncs->write = NPP_Write;
aNPPFuncs->print = NPP_Print;
aNPPFuncs->event = NPP_HandleEvent;
aNPPFuncs->urlnotify =NPP_URLNotify;
aNPPFuncs->getvalue = NPP_GetValue;
aNPPFuncs->setvalue = NPP_SetValue;
return NPERR_NO_ERROR;
}
其中aNPPFuncs 是全局变量类型为:NPNetscapeFuncsNPNFuncs;
fillPluginFunctionTable函数设置了一系列的函数入口,都是很熟悉的在mozilla的开发文档中经常提到的以NPP开头的函数。这些函数是需要在插件中加以实现的。
第二个:
NPError OSCALL NP_Initialize (NPNetscapeFuncs* aNPNFuncs)
{
NPError rv = fillNetscapeFunctionTable(aNPNFuncs);
if (rv != NPERR_NO_ERROR)
return rv;
return NS_PluginInitialize();
}
其调用的fillNetscapeFunctionTable为:
static NPError fillNetscapeFunctionTable(NPNetscapeFuncs* aNPNFuncs)
{
if (!aNPNFuncs)
return NPERR_INVALID_FUNCTABLE_ERROR;
if (HIBYTE(aNPNFuncs->version) > NP_VERSION_MAJOR)
return NPERR_INCOMPATIBLE_VERSION_ERROR;
if (aNPNFuncs->size < sizeof(NPNetscapeFuncs))
return NPERR_INVALID_FUNCTABLE_ERROR;
NPNFuncs.size = aNPNFuncs->size;
NPNFuncs.version = aNPNFuncs->version;
NPNFuncs.geturlnotify = aNPNFuncs->geturlnotify;
NPNFuncs.geturl = aNPNFuncs->geturl;
NPNFuncs.posturlnotify = aNPNFuncs->posturlnotify;
NPNFuncs.posturl = aNPNFuncs->posturl;
NPNFuncs.requestread = aNPNFuncs->requestread;
NPNFuncs.newstream = aNPNFuncs->newstream;
NPNFuncs.write = aNPNFuncs->write;
NPNFuncs.destroystream = aNPNFuncs->destroystream;
NPNFuncs.status = aNPNFuncs->status;
NPNFuncs.uagent = aNPNFuncs->uagent;
NPNFuncs.memalloc = aNPNFuncs->memalloc;
NPNFuncs.memfree = aNPNFuncs->memfree;
NPNFuncs.memflush = aNPNFuncs->memflush;
NPNFuncs.reloadplugins = aNPNFuncs->reloadplugins;
NPNFuncs.getvalue = aNPNFuncs->getvalue;
NPNFuncs.setvalue = aNPNFuncs->setvalue;
NPNFuncs.invalidaterect = aNPNFuncs->invalidaterect;
NPNFuncs.invalidateregion = aNPNFuncs->invalidateregion;
NPNFuncs.forceredraw = aNPNFuncs->forceredraw;
return NPERR_NO_ERROR;
}
这里获取一系列函数的入口,这些函数是浏览器中实现的。
NP_Initialize还调用了一个函数NS_PluginInitialize(),NS_PluginInitialize是我们在plugin.cpp中实现的,随便提一句,NP_Shutdown调用的NS_PluginShutdown也是在plugin.cpp中由我们自己去实现的。
通过对上面的代码的分析,可以发现虽然在def里面只定义了三个接口,但实际上却包括浏览器实现的由浏览器调用的接口21个以及浏览器要调用的由插件实现的接口13个(实际上NPNetscapeFuncs结构定义了55个函数指针,NPPluginFuncs结构定义了19个函数指针)。换句话说,我们开发插件就是要来实现这13个接口。接口的标准已经由浏览器定义好了,我们怎么去实现以及要实现什么样的功能就全凭我们自己了。
np_entry.cpp这个文件已经分析得差不多了,这个文件包含了两个头文
这个文件包含了两个头文件,"npplat.h"和"pluginbase.h",打开这两个文件来看看里面定义了些什么。
pluginbase.h,该文件定义了nsPluginInstanceBase类并声明了四个全局函数:NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct)
NS_DestroyPluginInstance(nsPluginInstanceBase *aPlugin);
NS_PluginInitialize();
NS_PluginShutdown();
这四个函数都是由我们在plugin.cpp中去实现的,前面的分析中已经知道,NS_PluginInitialize()和NS_PluginShutdown()是在np_entry.cpp文件中调用的。
由此观之,我们要开发插件,建立的类首先要继承nsPluginInstanceBase类根据需要实现其中的某些虚函数,另外还需要实现上面这四个全局函数。
接下来研究npn_gate.cpp文件和npp_gate.cpp文件。
打开npn_gate.cpp文件,就可以发现其中实现了20个函数,都是fillNetscapeFunctionTable中的函数,(之前我们发现其中有21个函数,这里为什么只有20个呢?看看NPNetscapeFuncs的定义可以发现,size是个变量不是函数。)而这里的函数名就是以NPN_开头了,按说这是浏览器实现函数不需要在这些地方来实现了呀,这是什么原因呢。来看看其中的函数吧。
以一个简单的函数NPN_GetURL为例:其实现代码如下:
NPError NPN_GetURL(NPP instance, const char *url, const char*target)
{
return(*NPNFuncs.geturl)(instance, url, target);
}
这个一看咱就明白了,NPN_GetURL确实可以说是浏览器实现的,因为这个函数直接调用了NPNetscapeFuncs的一个函数地址处的函数。
换句话说,在插件实例初始化的时候,将浏览器实现的这些函数的入口地址保存到一个NPNetscapeFuncs结构中,NPN_开头的这些函数名其实是开发插件时需要由开发者定义的,这些函数的实现就直接根据NPNetscapeFuncs结构中的入口地址调用浏览器实现的相关功能。但这些基本都是固定不变的,因此sdk中已经帮我们开发者写好了这些代码,在开发插件时只需要调用NPN_开头的全局函数即可。
npp_gate.cpp文件中实现了13个函数,这13个函数就是NP_GetEntryPoints中fillPluginFunctionTable需要实现的函数。
在npp_gate.cpp文件中我们可以发现分别在NPP_New和NPP_Destroy中调用了plugin.cpp中实现的另外两个全局函数NS_NewPluginInstance和NS_DestroyPluginInstance。
这个文件中实现的13个函数基本上都调用了nsPluginInstanceBase类对应的函数,因此这些NPP_开头的函数就相当于是插件开发者实现的。实现这些函数就是要实现nsPluginInstanceBase类的成员函数,可以看到nsPluginInstanceBase的成员函数都是定义为虚函数的,其中init(NPWindow* aWindow) 、shut()、 isInitialized()三个函数是纯虚函数,在nsPluginInstanceBase类的派生类中必须进行实现,而其他函数就可以根据需要加以实现。
通过上面的一番分析,要写出一个NPAPI的插件,利用这个框架必须至少要做的工作有:从nsPluginInstanceBase类派生一个类并至少实现其中init() 、shut()、 isInitialized()这三个成员函数。
声明并实现四个全局函数
nsPluginInstanceBase *NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct);
void NS_DestroyPluginInstance(nsPluginInstanceBase * aPlugin);
NPError NS_PluginInitialize();
void NS_PluginShutdown();
到这里就对插件的接口分析完毕了,开发插件的过程就是实现nsPluginInstanceBase类的部分成员函数以实现需要的功能。要想得心应手的开发插件就必须准确的把握浏览器调用这些NPP函数的机制和流程。
接下来分析浏览器在何时调用何种接口的问题。
首先来理一下nsPluginInstanceBase类的成员函数是如何被NPP_开头的函数所调用的。(注:plugin表示一个nsPluginInstanceBase对象)
NPP接口函数 |
可能调用的nsPluginInstanceBase成员函数或全局函数 |
备注 |
NPP_New |
NS_NewPluginInstance |
创建插件实例 |
NPP_Destroy |
NS_DestroyPluginInstance、plugin->shut |
删除插件实例 |
NPP_SetWindow |
plugin->SetWindow、plugin->isInitialized、plugin->init、NS_DestroyPluginInstance |
窗口创建、移动、改变大小或销毁时调用 |
NPP_NewStream |
plugin->NewStream |
通知插件实例有新的数据流 |
NPP_WriteReady |
plugin->WriteReady |
确定插件是否准备好接收数据(以及其准备接收的最大字节数) |
NPP_Write |
plugin->Write |
调用以将数据读入插件this might be better named “NPP_DataArrived” |
NPP_DestroyStream |
plugin->DestroyStream |
通知插件实例数据流将要关闭或销毁 |
NPP_StreamAsFile |
plugin->StreamAsFile |
为创建流数据提供本地文件名 |
NPP_Print |
plugin->Print |
为嵌入或全屏插件请求平台特定的打印操作 |
NPP_URLNotify |
plugin->URLNotify |
通知插件已完成URL请求 |
NPP_GetValue |
plugin->GetValue |
调用以查询插件信息(还用来获取NPObject/Scriptable 插件的实例) |
NPP_SetValue |
plugin->SetValue |
这是用来为浏览器提供插件变量信息的 |
NPP_HandleEvent |
plugin->HandleEvent |
事件处理函数,对windowed的插件只在MAC操作系统上可用,对于winless的插件所有平台都可用 |
可见,NPP接口基本上与nsPluginInstanceBase类的成员函数一一对应。
初始化入口NP_Initialize和NP_GetEntryPoints的调用顺序不确定(根据相关文档);但是在实际中,Windows平台上貌似NP_GetEntryPoints先被调用。记住这一点,下面就是Windows平台上基本的windowed插件初始化的调用顺序:
1. NP_GetEntryPoints – 插件用NPP_New,NPP_Destroy, NPP_SetWindow等函数的入口地址填充一个函数表。
2. NP_Initialize – 插件存储一个NPN_CreateObject,NPN_MemAlloc,等函数入口地址组成的函数表的拷贝。
3. NPP_New – 插件创建一个新的插件实例并初始化
4. NPP_SetWindow – 每个实例都会多次调用这个函数——每次实例窗口创建、改变大小或者其他变化都会调用。
5. NPP_GetValue (Variable = NPPVpluginScriptableNPObject) – 插件创建一个支持脚本的NPObject并返回其指针(调用NPN_RetainObject)。
6. — 标准的插件活动—
创建脚本之后会生成一个staticNPClass objectClass;对象,这里会实现对应的方法, 这个对象里面会有页面点击等的响应处理, 在invoke中调用需要封装的dll的接口。(脚本创建等方法在下节说明)
7. NPP_Destroy – 销毁插件实例
8. NP_Shutdown – 销毁所有遗留的插件资源
大概就是这么一个顺序了。
Plugin的注册需要写注册表,路径为:
[HKEY_CURRENT_USER\Software\MozillaPlugins\@mozilla.com.cn/demo]
"Path"="E:\\work_space\\timevalePdf_3.0\\out\\debug\\npplugin.dll"
其中:@mozilla.com.cn/demo是新建项,需要自己定义,即新建一个项,名字自定义,然后新建一个字符串值Path, 值为上面的dll绝对路径。
这样就将plugin注册了。然后打开chrom,网址栏输入:about:plugins。插件中出现插件名称,说明chrome已经加载了插件。然后在chrome 打开调用的页面即可调用插件。
如果插件没能加载,则可能是因为浏览器阻止了插件,在网址栏输入chrome://flags/#enable-npapi,选择npapi 启用。具体情况根据浏览器版本不同而不同。
新建一个C++类,该示例命名为PluginObject,继承NPObject,我们需要添加静态方法,用于创建该脚本操作的对象。
class CPluginObject : publicNPObject
{
public:
CPluginObject(NPP npp);
~CPluginObject();
public:
static NPObject* Allocate(NPP npp, NPClass* aClass);
static void Deallocate(NPObject *npobj);
static void Invalidate(NPObject *npobj);
static bool HasMethod(NPObject* obj, NPIdentifier methodName);
static bool InvokeDefault(NPObject *obj, const NPVariant *args, uint32_targCount, NPVariant *result);
static bool Invoke(NPObject* obj, NPIdentifier methodName, constNPVariant *args, uint32_t argCount, NPVariant *result);
static bool HasProperty(NPObject *obj, NPIdentifier propertyName);
static bool GetProperty(NPObject *obj, NPIdentifier propertyName,NPVariant *result);
static bool SetProperty(NPObject *npobj, NPIdentifier name, constNPVariant *value);
static bool RemoveProperty(NPObject *npobj, NPIdentifier name);
static bool Enumerate(NPObject *npobj, NPIdentifier **identifier,uint32_t *count);
static bool Construct(NPObject *npobj, const NPVariant *args, uint32_targCount, NPVariant *result);
static NPClass objectClass;
private:
//函数接口
NPIdentifier id_func;
NPIdentifier methodId[NUM_METHODS]; //方法数组
NPIdentifier propertyId[NUM_PROPERTIES]; //属性
NPP m_npp;
TGEsignProMedicalCtrlLib::ITGEsignProMedicalPlugInPtr m_plugin;
};
将以上声明的static NPClass objectClass,对象在类外初始化,就是用类中声明的那些方法填充对objectClass对象的成员。
NPClass CPluginObject::objectClass =
{
NP_CLASS_STRUCT_VERSION,
CPluginObject::Allocate,
CPluginObject::Deallocate,
CPluginObject::Invalidate,
CPluginObject::HasMethod,
CPluginObject::Invoke,
CPluginObject::InvokeDefault,
CPluginObject::HasProperty,
CPluginObject::GetProperty,
CPluginObject::SetProperty,
CPluginObject::RemoveProperty,
CPluginObject::Enumerate,
CPluginObject::Construct
};
没错,这个对象就是我们脚本类交互的重点,他会依次调用这里面的方法。
然后我们回到CPlugin中,这个类中我们实现了一个GetScriptableObjec方法,该方法中通过NPNCreateObject方法创建该对象。
1. NPObject* CPlugin::GetScriptableObject(){
2. return NPN_CreateObject(this->instance,&objectClass);
3. }
this->instance在构造函数时获取并保存下来的NPP对象.
NPN_CreateObject会在浏览器中做一些操作然后回来调用objectClass中的_allocate方法,需要实现该静态方法,new 一个PluginObject对象。
1. NPObject* PluginObject:: Allocate (NPP npp,NPClass* aClass){
2. return new PluginObject(npp);
3. }
后面的操作中,浏览器调用了NPNFunc中以上的一些方法则会来调用这些静态方法,并将_allocate返回的值NPObject*指针作为参数传到其他函数中,作为入参,标识该对象。
接下来的实现就是按照我们的业务流程来实现了,可以直接在这些静态方法中实现想要的效果,也可以在PluginObject中创建对应的成员函数实现,然后在静态方法中通过nobj参数转换为(PluginObject)类型调用相应成员函数,建议使用这种方式。
例如在构造函数中:
//将npp实例强制转换成TGPlugin的对象,就可以访问TGPlugin的成员
CTGPlugin* pPlugin = (CTGPlugin*)m_npp->pdata;
if (nullptr != pPlugin)
{
m_plugin =pPlugin->GetActiveXPlugin();
}
我们注意到:在CPluginObject类中定义了NPIdentifier id_func 成员,没有错这就是供js脚本调用的类函数接口的一个东西,这个成员在构造中初始化:
id_func = NPN_GetStringIdentifier("func");
初始化后,id_func指向一个需要封装的或调用的dll中的函数接口: func。
我们可以在js中这样调用:var Res =plugin.func (参数);//func就是初始化的那个了。
其中几个函数比较重要:
HasMethod判断参见是否有该函数,hasMethod等方法的类似于参数methodName都是以identifier作为判断的,可以调用NPN_GetStringIdentifier获取。
HasProperty判断是否有某些属性, getProperty则是获取属性。
invoke调用相应方法,invoke调用dll中的实际的接口,该接口原型为:
boolInvoke(NPObject* obj, NPIdentifier methodName, const NPVariant *args, uint32_targCount, NPVariant *result)
参数说明:
NPObject*obj 为Allocate返回的对象指针 //可以将其转换成PluginObject对象使用。
NPIdentifiermethodName 为此次调用的方法名。
NPVariant*args 为一个数组指针 //通过他可以获取对象位置参数的值,下标从0开始。
uint32_targCount 为本次调用接口中的参数个数;
NPVariant*result 为接口调用后的返回值。可以返回到上层,被前面js调用中的var Res接受。
注:这里的函数调用也有顺序, 调用发生时,首先会调用HasMethod和HasProperty来检测是否有该方法和某属性,如果有才会调用Invoke,否则Invoke函数不会被调用。
这两个方法如果只是单纯的返回true,不足以说明什么,虽然返回成功, 不一定说明函数调用成功,也就是说它必须有函数体执行检测方法是否存在,并检测是否有某属性,否则Invoke并不会调用。
invokeDefault可以在invoke中调用NPN_InvokeDefault来访问,最好不要直接调用,(见API,原因未知,一般浏览器都要做进一步操作)
JS中加载一个插件对象可以用下面的方法:
然后通过使用 document.getElementsByTagName 或者 document.getElementById来获取页面中已经存在的插件对象,还可以在JS中使用document.createElement("object");来动态创建对象,并为该对象设置type属性,接着将创建的这个对象添加到页面中,这样就动态创建了一个插件对象。如下JS函数可以根据传入的mimetype创建一个插件对象(chrome、firefox测试有效,其他未测试):
如通过objname来获取:
function getPluginObjcet(objname) {
if(!isIEBrowser()) {
if (document.embeds && document.embeds[objname])
return document.embeds[objname];
}
else{
return document.getElementById(objname);
}
}
这部分在第二部分的插件的调用中已经做了说明和注释。
然后就可以在js中调用插件了,如下:
function func()
{
Var Res =plugin.Init(参数);
}
那么浏览器是如何完成将插件转换为JS能够识别的对象的呢?我们发现,在NPP_GetValue的实现中有:
没有错这里创建了一个脚本类的对象,前面也已经说过。
也就是说,浏览器会调用NPP_GetValue(instance,NPPVpluginScriptableNPObject, value)并将来获取插件的scriptable对象。进一步看看plugin是如何获取scriptable对象的:前面已经脚本化接口部分说过,通过NPN_CreateObject来创建对象。
当我们在JS中设置/获取属性或者调用方法时,都会在这个scriptable对象中操作,在使用结束时(CPlugin的析构函数中)使用NPN_ReleaseObject(m_pScriptableObject);来释放这个对象。注:发现这样释放对象,接口出现奔溃,原因未知。
可以使用一个宏定义来创建对象:DECLARE_NPOBJECT_CLASS_WITH_BASE及GET_NPOBJECT_CLASS
#defineDECLARE_NPOBJECT_CLASS_WITH_BASE(_class, ctor) \
static NPClasss##_class##_NPClass = { \
NP_CLASS_STRUCT_VERSION_CTOR, \
Ctor,\
ScriptablePluginObjectBase::_Deallocate, \
ScriptablePluginObjectBase::_Invalidate, \
ScriptablePluginObjectBase::_HasMethod, \
ScriptablePluginObjectBase::_Invoke, \
ScriptablePluginObjectBase::_InvokeDefault, \
ScriptablePluginObjectBase::_HasProperty, \
ScriptablePluginObjectBase::_GetProperty, \
ScriptablePluginObjectBase::_SetProperty, \
ScriptablePluginObjectBase::_RemoveProperty,\
ScriptablePluginObjectBase::_Enumerate, \
ScriptablePluginObjectBase::_Construct \
}
#defineGET_NPOBJECT_CLASS(_class) &s##_class##_NPClass
当然从名称可以知道是用基类声明一个变量,并用GET_NPOBJECT_CLASS来引用这个变量,这就相当于是我在基类中定义的nsScriptObjectClass。
实现一个scriptable对象的类其实并不难,只需要从NPObject派生一个类并逐一实现NPClass中的几个函数指针所需要的函数。这里搞得如此复杂就是为了能够设计一个基类,并一劳永逸的不再修改这个基类。本章最后一个示例会给出实现一个最简单的scriptable对象的例子。
前面说了加载插件的方法,和注册方法,等这些都做好了的话, 就可以对插件进行调试,插件运行起来后有他自己的进程, 本文档中到的插件件进程是:chrome大进程底下的动态库为nptestdemo.dll调用的一个子进程,确定该进程PID,然后就可以进行调试。
将该进程附加到vs进程即可。
然后就可以像调试ocx或者动态库一样的方式调试插件了;
注:务必确定加载进程是不是当前进程,并且,代码务必编译过,否则无法命中断点。
NPAPI文档讲解连接: https://wiki.mozilla.org/NPAPI#NPAPI_SDK