NPAPI——實現非IE瀏覽器的類似ActiveX的本地程式(外掛)呼叫
一.Netscape Plugin Interface(NPAPI)
大致的說明可以看下官方文件Plugin
本文主要針對於javascript與外掛互動部分做一些交流,比如用於數字證書的操作(淘寶和支付寶的外掛),用於播放的flash player外掛等
與javascript的互動需要用到NPAPI中的
下面的部分將以示例的方式說明整個過程如何去實現
在開始前需要從火狐瀏覽器原始碼中獲取介面標頭檔案火狐4.0.1原始碼下載
下載後在\firefox-4.0.1.source\mozilla-2.0\modules\plugin可以找到一些samples和標頭檔案
另外,基於NPAPI的一個跨瀏覽器外掛開發的框架FireBreath,非常容易上手而且據說跨瀏覽器的支援非常好,但是非常笨重,有些功能不需要的也不太容易去掉
還有一個基於NPAPI做的簡單的示例,結構非常簡單,不用繞來繞去,相對理解起來也簡單許多
二.外掛入門開發的示例
開發工具為visual studio 2010
1.新建一個Win32 project,命名以np開頭(目的是編譯完的Dll名必須以np開頭才能被識別為外掛)
型別為一個DLL的空工程即可
2.右鍵選中專案的屬性,在VC++ Directories目錄下,選擇Include Directories,Edit,
將plugin/base/public和plugin/sdk/samples/include新增到include
3.新建Version資原始檔
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // Chinese (Simplified, PRC) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Version // VS_VERSION_INFO VERSIONINFO FILEVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x40004L FILETYPE 0x2L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "WHU ISS" VALUE "FileDescription", "A new Plugin For test" VALUE "FileVersion", "1.0.0.1" VALUE "InternalName", "npTest.dll" VALUE "LegalCopyright", "Copyright (C) 2012" VALUE "MIMEType", "application/x-npTest" VALUE "OriginalFilename", "npTest.dll" VALUE "ProductName", "new Plugin Test" VALUE "ProductVersion", "1.0.0.1" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x804, 1200 END END #endif // Chinese (Simplified, PRC) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
需要注意的是Block 必須為040904e4,MIMEType為最後引用外掛的標誌
4.新建一個Module-Definition File(.def),定義入口函式
LIBRARY npTest
EXPORTS
NP_GetEntryPoints @1
NP_Initialize @2
NP_Shutdown @3
5.新建一個CPlugin類繼承nsPluginInstanceBase,作為外掛例項類(後面再說該類的作用)
確定之後,在plugin.h中#include <pluginbase.h>
類名為Cplugin,標頭檔案名為plugin.h,(npp_gate.cpp會使用到,不同可以修改)
修改建構函式的實現,帶引數NPP型別並新建一個屬性儲存該引數
實現父類的三個純虛擬函式
NPBool init(NPWindow* aWindow);//NPWindow用於外掛中繪畫部件的視窗
void shut();
NPBool isInitialized();
6.免得做過多操作,從samples中引入已經編寫好的入口函式
從plugin\sdk\samples\npruntime路徑新增np_entry.cpp(外掛入口函式),npn_gate.cpp(外掛呼叫瀏覽器的一些方法),npp_gate.cpp(瀏覽器呼叫外掛的一些方法)
新增後需要做一點修改,
1).np_entry.cpp和npn_gate.cpp的引用
#include "npapi.h"
#include "npfunctions.h"
換成
#include<pluginbase.h>
2).然後進入pluginbase.h,再進入npplat.h,將
#ifdef XP_WIN
#include "windows.h"
#endif
挪到
#include "npapi.h"
#include "npfunctions.h"
前面,
3).然後在專案屬性,Preprocessor,Preprocessor Definitions新增XP_WIN的定義
(這樣做的原因是windows.h需要在npapi.h前定義,自己在所有引用了npapi.h的前面加上windows.h的引用也可以)
4),np_entry.cpp中引入標頭檔案#include <stddef.h>
因為使用到offsetof
這三個檔案中的函式非常重要,首先來看下np_entry.cpp中的函式
NP_GetEntryPoints函式,為外掛入口的函式,外掛初始化將會首先呼叫該函式
該函式用於初始化瀏覽器呼叫外掛的函式表,以NPP(np plugin)開頭,
後面的外掛的一些事件(new等)發生時將會以這裡初始化的函式作為入口,比如
pFuncs->newp = NPP_New;初始化後將會在建立外掛例項時呼叫NPP_New的實現來建立.
NP_Initialize函式,初始化外掛時,在NP_GetEntryPoints後呼叫,
該函式用於初始化外掛呼叫瀏覽器的函式表,引數pFuncs帶有該函式表資訊,
我們自定義一個物件儲存這些資訊,今後就可通過該物件呼叫方法來實現對瀏覽器的一些操作
NP_Shutdown函式,與NP_Initialize對應,主要釋放資源等操作
再來看下npp_gate.cpp,這個檔案中的函式都以NPP開頭,用於定義瀏覽器呼叫外掛的方法
經過NP_GetEntryPoints的初始化後,當特定事件發生時,瀏覽器將會呼叫這些方法
然後需要注意的是該檔案引用了plugin.h,是我們第5步建立的檔案,名字不同可以改改
NPP_New方法,用於建立外掛例項
CPlugin * pPlugin = new CPlugin(instance);這句話為建立一個我們定義的CPlugin類物件,建構函式為NPP型別
NPP_Destroy方法,用於銷燬外掛例項,重新整理頁面,關閉頁面等操作會觸發
該方法會呼叫CPlugin的shut方法再delete掉例項
NPP_SetWindow方法,外掛視窗發生任何變化都會呼叫該方法
window建立時會呼叫一次,如果初始化失敗則delete掉例項然後返回錯誤
NPP_GetValue方法,當獲取外掛有關的一些資訊時會觸發該方法呼叫(如獲取外掛名,外掛例項)
當javascript操作外掛物件時,該方法呼叫CPlugin的GetScriptableObject方法,需要自己實現,返回一個指令碼操作物件(NPObject)
在這裡返回到CPlugin類,新增GetScriptableObject方法並實現(見第7步操作)
NPP_HandleEvent方法,處理事件,該方法呼叫CPlugin的handleEvent方法,繼續新增實現吧
該檔案中其他方法暫時沒什麼可說的,需要用到的可以查下API並實現出來就行了.
再看下npn_gate.cpp,該檔案實現了對瀏覽器的一些操作的函式,都以NPN(np netscape)開頭
其中有一些帶有NPObject*引數的與GetScriptableObject方法建立的指令碼操作物件有關,將在第7步做說明
該檔案中用到的NPNetscapeFuncs NPNFuncs;在NP_Initialize中初始化完成
7.封裝一個指令碼操作物件
Add一個C++類,該示例命名為PluginObject,繼承NPObject
新增靜態方法,用於建立該指令碼操作的物件
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_t argCount, NPVariant *result);
static bool _invoke(NPObject* obj, NPIdentifier methodName, const NPVariant *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,const NPVariant *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_t argCount, NPVariant *result);
在PluginObject.h中宣告一個NPClass物件,使用上面的靜態方法將該NPClass物件初始化
#ifndef __object_class
#define __object_class
static NPClass objectClass = {
NP_CLASS_STRUCT_VERSION,
PluginObject::_allocate,
PluginObject::_deallocate,
PluginObject::_invalidate,
PluginObject::_hasMethod,
PluginObject::_invoke,
PluginObject::_invokeDefault,
PluginObject::_hasProperty,
PluginObject::_getProperty,
PluginObject::_setProperty,
PluginObject::_removeProperty,
PluginObject::_enumerate,
PluginObject::_construct
};
#endif
回到第6步中在CPlugin類中實現的GetScriptableObject方法
在該方法中通過NPNCreateObject方法建立該物件
NPObject* CPlugin::GetScriptableObject(){
return NPN_CreateObject(this->instance,&objectClass);
}
this->instance在建構函式時獲取並儲存下來的NPP物件.
NPN_CreateObject會在瀏覽器中做一些操作然後回來呼叫objectClass中的_allocate方法
需要實現該靜態方法,new 一個PluginObject
新建一個NPP npp屬性,和一個NPP引數的建構函式
NPObject* PluginObject::_allocate(NPP npp,NPClass* aClass){
return new PluginObject(npp);
}
後面的操作中,瀏覽器呼叫了NPNFunc中以上的一些方法則會來呼叫這些靜態方法,並將_allocate返回的值作為引數傳到其他函式中
接下來的實現就相對比較隨意了,可以直接在這些靜態方法中實現想要的效果,
也可以在PluginObject中建立對應的成員函式實現,然後在靜態方法中通過nobj引數轉換為(PluginObject)型別呼叫相應成員函式
其中幾個函式比較重要,_hasMethod判斷參見是否有該函式,_getProperty則是判斷屬性,invoke呼叫相應方法,
invokeDefault可以在invoke中呼叫NPN_InvokeDefault來訪問,最好不要直接呼叫,(見API,原因未知,一般瀏覽器都要做進一步操作)
hasMethod等方法的類似於引數methodName都是以identifier作為判斷的,可以呼叫NPN_GetStringIdentifier獲取
例如:
PluginObject::PluginObject(NPP npp)
{
this->npp = npp;
id_func_add = NPN_GetStringIdentifier("add");
id_property_version = NPN_GetStringIdentifier("version");
}
bool PluginObject::hasMethod(NPObject* obj, NPIdentifier methodName)
{
if(methodName==this->id_func_add)
return true;
return false;
}
多說下enumerate方法或者說是NPN_XXX的方法,因為就這個東西折騰我完完整整的兩天時間...
enumerate方法的引數有個指標陣列,但是他的結構是
而且初始化的時候一定要用NPN_MemAlloc來操作....API上只有關於指標陣列的結構說明,而且很簡單的提了一句,折騰兩天才發現非得用NPN來分配記憶體- -||
弱弱的總結下,應該是需要給Firefox用到的東西或者說從引數傳進來需要你分配記憶體的都得用NPN_MemAlloc分配記憶體
如果出現Access Violation,首先想到什麼地方應該用NPN_MemAlloc....
bool PluginObject::enumerate(NPIdentifier **identifier,uint32_t *count)
{
*count = 1;
NPIdentifier *outList(NULL);
outList = (NPIdentifier*)NPN_MemAlloc((uint32_t)(sizeof(NPIdentifier) * *count));
outList[0] = id_property_version;
*identifier = outList;
return true;
}
測試的時候在firebug的控制檯輸入 plugin. 就會去呼叫enumerate了
三.註冊及安裝
1.登錄檔註冊位置
HKEY_CURRENT_USER\Software\MozillaPlugins
新增一個項@whuiss.com/npTest
新增字串值
"Description"="code project test"
"Path"="path to npTest.dll"
"ProductName"="npdemo Dynamic Library"
"Vendor"="zsy"
"Version"="1.0.0.1"
新增子項MIMETypes
新增MIMETypes的子項application/x-npTest
但是實際上只需要一個項@whuiss.com/npTest以及一個Path字串值,其他可有可無
在firefox位址列輸入about:plugins可查到你的外掛了
2.使用安裝檔案註冊
visual studio新建一個set up project
FileSystem View中選中dll或者某個工程的輸出
Registry View中按照上面的位置給新增上相應資訊即可
四.使用外掛
<html>
<head>
<script>
window.onready = function(){
}
function toDoSt(){
var plugin = document.getElementById("plugin");
alert(plugin.version);
}
</script>
<embed id="plugin" type="application/x-npTest" src="file:///path to npTest.dll" pluginspage="http://xxxx">
</head>
<body>
<input type="button" onclick="toDoSt()" value="test">
</body>
</html>
其中embed的src和pluginspage可有可無
五.除錯外掛
先前一直弄錯了,以為是指向Firefox.exe,查了好久,發現原來在Firefox4之後新建了一個plugin-container.exe程序
除錯目標指向plugin-container.exe 或者 tools->attach to process選中plugin-container.exe程序 或者debug->attach to process
六.附上示例工程
開啟工程後需要修改include directory
------------------------------------------------------分割線-----------------------------------------------------
發現個新問題,NPAPI執行函式返回值不支援帶中文的麼?
除錯很多次了,也不知道是配置問題還是什麼問題,NPVariant *result中帶有值返回的
但是到瀏覽器就變成空字串,去掉中文的就能正常顯示
Firebreath的也試過了,也不支援中文字元
沒辦法,只好將返回的值轉成base64再在瀏覽器解碼,這樣倒是可以正常
------------------------------------------------------分割線-----------------------------------------------------
firefox新版本 彈出winform(例如訪問某些智慧卡私鑰會需要輸入PIN)的時候導致假死的情況,在火狐社群提問了,能夠解決
大致意思就是修改config裡面的dom.ipc.plugins.enabled.your-plugin.dll=false