老谭笔记

浏览器插件编程NPAPI之进阶篇——NPAPI开发实战

此处的实战开发最终完成一款支持Mac OSX环境下Safari、Chrome、FireFox三款浏览器的一个小的插件,它模拟类型于下载工具需要实现的功能:当用户点击了网页上面的下载链接,插件弹出一个下载确实框(具体下载过程就未实现了)。

NPAPI的插件需要最终编译成Mach-O风格的二进制文件,在Mac OSX系统中,需要打包成plugin的包,为了更加方便的开发,你可以使用一个上一篇文件提供的官方Demo作为模板(Xcode中的新建工程时没有浏览器的模板,熟悉之后可以自己创建新项目然后创建相应的文件如info.plist、Target等)。

1.Info.plist中需要添加的插件信息

插件支持的MIME类型WebPluginMIMETypes,其值是一个字典,字典中的每一个Key为MIME的一种类型,Value为对该MIME描述的字典,其中可以包括一个Key为WebPluginTypeDescription的String,对MIME类型的具体描述。

插件的名称WebPluginName和插件的描述信息WebPluginDescription,其它还有Xcode打包项目中必有的Bundle identifier和Version等等信息。

该实例Demo中Info.plist中添加了几乎所有下载中常见的MIME类型,具体内容请直接查看末尾提供的源码。

2.插件代码的实现

因为要实现弹出一个下载的框,所以我们先创建了一个NSWindowController的类(DownloadWindowController),该类有一个NSString的属性url,当设置了url的值之后,就会在Window中间用一个NSTextField把url的值显示出来,具体代码见源码。

一个插件在创建之后可能会在使用的时候会创建多个实例(如每一下检测到需要弹出下载确认框便是完成一次实例的创建到销毁的过程)因为NPAPI使用C语言编程,所以我们都需要使用一个上下文来对保存对它的引用,此处我们使用一个结构体,代码如下:

1
2
3
4
5
6
7
// 每个实例存储结构
typedef struct PluginObject
{
NPP npp;
NPWindow window;
DownloadWindowController *dwc;
} PluginObject;

插件要与浏览器进行通信,所以必须实现以下的接口:

1
2
3
NPError NP_Initialize(NPNetscapeFuncs *browserFuncs);
NPError NP_GetEntryPoints(NPPluginFuncs *pluginFuncs);
void NP_Shutdown(void);

NP_Initialize是插件实例化时将浏览器的结构体指针(NPNetscapeFuncs*)传递给插件的方法,NP_GetEntryPoints方法是用于浏览器取得插件的结构体指针(NPPluginFuncs*),NS_Shutdown是指当插件关闭时调用的方法。

以下是接口的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//通过此方法将浏览器的对象返回给插件
NPError NP_Initialize(NPNetscapeFuncs* browserFuncs)
{
browser = browserFuncs;
return NPERR_NO_ERROR;
}
//通过此方法将插件的接口返回给浏览器(填充NPPluginFuncs结构体中的方法指针,让浏览器可以调用本插件)
NPError NP_GetEntryPoints(NPPluginFuncs* pluginFuncs)
{
pluginFuncs->version = 11;
pluginFuncs->size = sizeof(pluginFuncs);
pluginFuncs->newp = NPP_New;
pluginFuncs->destroy = NPP_Destroy;
pluginFuncs->setwindow = NPP_SetWindow;
pluginFuncs->newstream = NPP_NewStream;
pluginFuncs->destroystream = NPP_DestroyStream;
pluginFuncs->asfile = NPP_StreamAsFile;
pluginFuncs->writeready = NPP_WriteReady;
pluginFuncs->write = (NPP_WriteProcPtr)NPP_Write;
pluginFuncs->print = NPP_Print;
pluginFuncs->event = NPP_HandleEvent;
pluginFuncs->urlnotify = NPP_URLNotify;
pluginFuncs->getvalue = NPP_GetValue;
pluginFuncs->setvalue = NPP_SetValue;
return NPERR_NO_ERROR;
}
void NP_Shutdown(void)
{
browser = NULL;
}

其中NP_Initialize方法体中,我们使用了一个全局指针browser来保存对浏览器的引用,NP_Shutdown是当插件关闭时调用的方法。NP_GetEntryPoints方法体中,就是对参数中的pluginFuncs结构体进行填充,pluginFuncs结构体是插件提供给浏览器所有方法的集合,是浏览器对于插件的一个引用,浏览器也是通过pluginFuncs中包含的方法与插件进行交互。所以如NPP_New、NPP_Destroy等方法便对应了main中实现的这些方法:

NPP_New方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
NPError NPP_New(NPMIMEType pluginType, NPP instance, uint16_t mode, int16_t argc, char* argn[], char* argv[], NPSavedData* saved)
{
//创建一个插件实例对象
PluginObject *obj = (PluginObject *)malloc(sizeof(PluginObject));
bzero(obj, sizeof(PluginObject));
obj->npp = instance;
instance->pdata = obj;
//每一个实例对应了一个WindowController
obj->dwc = [[DownloadWindowController alloc] init];
// 询问浏览器是否支持 Core Animation 绘制模式
NPBool supportsCoreAnimation;
if (browser->getvalue(instance, NPNVsupportsCoreAnimationBool, &supportsCoreAnimation) != NPERR_NO_ERROR)
supportsCoreAnimation = FALSE;
if (!supportsCoreAnimation)
return NPERR_INCOMPATIBLE_VERSION_ERROR;
// 询问浏览器是否支持 Core Animation 绘制, 如果支持便开启它
browser->setvalue(instance, NPPVpluginDrawingModel, (void *)NPDrawingModelCoreAnimation);
// 询问浏览器是否支持 Cocoa event model, 如果支持便开启它
NPBool supportsCocoa;
if (browser->getvalue(instance, NPNVsupportsCocoaBool, &supportsCocoa) != NPERR_NO_ERROR)
supportsCocoa = FALSE;
if (!supportsCocoa)
return NPERR_INCOMPATIBLE_VERSION_ERROR;
browser->setvalue(instance, NPPVpluginEventModel, (void *)NPEventModelCocoa);
for (int16_t i = 0; i < argc; i++) {
//保存下载的链接地址
if (strcasecmp(argn[i], "src") == 0) {
NSString *urlString = [NSString stringWithUTF8String:argv[i]];
if (urlString)
{
[obj->dwc setUrl:[NSURL URLWithString:urlString]];
break;
}
}
}
return NPERR_NO_ERROR;
}

此方法主要是对浏览器环境的配置和对应一个实例的默认设置,在方法的最后面我们通过参数获取了下载地址的url,并设置了WindowController对应的值。

NPP_Destroy方法的实现

1
2
3
4
5
6
7
8
9
10
NPError NPP_Destroy(NPP instance, NPSavedData** save)
{
PluginObject *obj = instance->pdata;
[obj->dwc release];
free(obj);
return NPERR_NO_ERROR;
}

此方法主要完成对实例内存的释放,以完成内存清理。

NPP_GetValue方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NPError NPP_GetValue(NPP instance, NPPVariable variable, void *value)
{
PluginObject *obj = instance->pdata;
switch (variable) {
case NPPVpluginCoreAnimationLayer:
*(CALayer **)value = NULL;
[[obj->dwc window] center];
[[obj->dwc window] orderFrontRegardless];
return NPERR_NO_ERROR;
default:
return NPERR_GENERIC_ERROR;
}
return NPERR_NO_ERROR;
}

此处NPP_GetValue的实现完成了的功能是,当浏览器需要询问插件的显示视图时,我们在此处保持默认不显示,而是弹出我们的下载框。
到处为止主要的方法我们便都实现了,其它的几个方法实现都留了空或保持默认返回值,具体编码见源码。

插件的安装

到上面为止,一个非常简单,甚至比较撇脚的插件就完成了,那怎么使用呢,我们只需要将编译结果的.plugin文件拷贝到
~/Library/Internet Plugins/
或者
/Library/Internet Plugins/
目录便可以了,然后重新启动Safari、Chrome、FireFox,打开一个文件下载的地址,你将会看到浏览器显示黑色背景,并且弹出了一个显示了该地址的窗口。

该实战的工程源码下载:NPAPI_Download_Plugin