程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 自行設計NPAPI開發框架

自行設計NPAPI開發框架

編輯:C++入門知識

    經歷了一年有余的插件開發,對插件的工作機制也比較熟悉了,在開發插件的過程中使用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的代碼如下: [cpp]   NPError OSCALL NP_GetEntryPoints(NPPluginFuncs* pFuncs)   {       if (!fillPluginFunctionTable(pFuncs)) {          return NPERR_INVALID_FUNCTABLE_ERROR;       }          return NPERR_NO_ERROR;   }     NPError OSCALL NP_GetEntryPoints(NPPluginFuncs* pFuncs) {     if (!fillPluginFunctionTable(pFuncs)) {        return NPERR_INVALID_FUNCTABLE_ERROR;     }       return NPERR_NO_ERROR; }     很簡單,所調用的函數名已經說明了這個接口的功能:填充插件函數表。接下來看看NP_Initialize,代碼如下: [cpp]   NPError OSCALL NP_Initialize(NPNetscapeFuncs* aNPNFuncs)   {       NPError rv = fillNetscapeFunctionTable(aNPNFuncs);       if (rv != NPERR_NO_ERROR)           return rv;          return NS_PluginInitialize();   }     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函數中很多類似如下的語句: [cpp]  pFuncs->newp = NPP_New;   pFuncs->destroy = NPP_Destroy;     pFuncs->newp = NPP_New; pFuncs->destroy = NPP_Destroy; 其中pFuncs 就是NPPluginFuncs結構的指針,上面這兩條語句就將NPP_New和 NPP_Destroy(這些是需要我們插件開發者來實現的函數)與NPPluginFuncs結構中的newp和destroy聯系起來了。浏覽器調用NPPluginFuncs結構的newp指針就是在調用我們實現的NPP_New。           再來看看NPN_函數的實現,以NPN_GetValue為例,代碼如下: [cpp]   NPError NPN_GetValue(NPP instance, NPNVariable variable, void* value)   {           return sBrowserFuncs->getvalue(instance, variable, value);   }     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中是如何做的: [cpp]   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);   }     NPError NPP_NewStream(NPP instance, NPMIMEType type, NPStream* stream, NPBool seekable, uint16_t* stype) { if (!instance) return NPERR_INVALID_INSTANCE_ERROR; www.2cto.com 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中的代碼片段: [cpp]  nsPluginInstanceBase * plugin = NS_NewPluginInstance(&ds);   if (!plugin)       return NPERR_OUT_OF_MEMORY_ERROR;   instance->pdata = (void *)plugin;     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。內容如下: [cpp]   #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     #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!         如圖是該工具的一個截圖:    \         需要填寫的信息包括文件名,mimetype類型,以及插件實例類名稱,其他各項都有預設默認值,或者會根據填寫這幾項信息時自動填寫相關信息。自動填寫或預設默認值都可以手動修改。         最上面一行可以選擇生成的文件類型,選擇第一項則生成的文件中不包含scriptable的接口,文件非常簡潔。選擇第二項則生成包含scriptable相關的內容,而且沒有添加ENABLE_SCRIPT_OBJECT預編譯宏;選擇第三項則會生成帶 ENABLE_SCRIPT_OBJECT預編譯宏的文件,默認是不支持腳本化接口的。要使其支持只需要在項目屬性中設置預編譯宏ENABLE_SCRIPT_OBJECT即可。 www.2cto.com         復選框生成VS工程選項可選擇是否生成VS2010版的項目文件,若選擇該項則自動選擇將框架文件同時生成在項目文件中,生成的項目文件可以直接用VS2010打開,設置好包含目錄之後即可生成最簡單的插件dll。也可以在VS中將該項目添加到其他解決方案中。若不選擇該項,則生成必要的文件,自行創建新的項目,這種方式可以用於其他版本的VS,但需要注意def文件不能直接在新建的項目中添加現有項來完成,否則生成的dll在測試頁面中會有問題,正確的做法是用VS添加新建項,添加一個空白的def文件,然後將本工具生成的def文件內容復制到空白def文件中;其他文件則可以直接在VS中添加現有項來進行。建議不要用本工具生成VS項目,因為每個項目文件都有不同的GUID,而本工具沒有自動生成GUID的功能,其中使用的GUID是試驗的時候創建項目所采用的GUID。  

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved