本篇文章在探討 NPAPI 與 NPRuntime 的設計,並非 Plugin 教學。
當時因為看到公司內部寫出來的 Plugin 問題不少,而且網路上說明太少,特地寫來給大家看的~
故本篇沒有詳細介紹每個 API 的使用與功能,請見諒囉!
This article was written in 2009/04/08.
NPAPI & NPRuntime 簡介
Netscape Plugin Application Programming Interface (NPAPI)
NPAPI 原本是由 Netscape 所制定的一組單純的 C Plugin API,起初是無法支援 Scriptability;於是到了 2004 年底時,各家 Browser (IE, Opera, Mozilla 等) 都同意支援 NPRuntime 延伸 API 以支援 Scriptability,所以目前若是想寫 Plugin 則應該以 NPRuntime API 才能跨不同的 Browsers。
Plugin Life Cycle
上面的 Sequence Diagram 說明了 Browser 與 Plugin 之間的運作過程:
-
Browser lookup Plugin (.so, .dll) and load it.
-
Browser 呼叫 Plugin 的 NP_Initialize() 來交換彼此所需的 API Function Pointers。
-
將 Browser Side 的 NPN_ API function table (NPNetscapeFuncs *aNPNFuncs) 傳給 Plugin (Binding)。
-
Plugin 應將其自身所定義好的 NPP API functions 填入 NPPluginFuncs *aNPPFuncs 中,好讓 Browser 得到 Plugin Side 的 Function pointers。
-
-
Browser 呼叫 Plugin 的 NP_GetValue() 來得到 Plugin 的資訊,例如:版本資訊與是否支援 Scriptability 等。
-
Browser 在網頁中發現 Plugin 所支援的 Mime Type 時,呼叫 Plugin 的 NPP_New() 來建立新的 Plugin instance 來處理。
-
當網頁被 Unload 前,Browser 則會呼叫 Plugin NPP_Destroy() 來通知 Plugin 應 Destroy 所對應的 Plugin instance 。
-
當 Browser 程式結束前會呼叫 Plugin 的 NP_Shutdown() 做 Destruction,結束整個 Plugin Life Cycle 。
以下為 API 宣告:
char* NP_GetMIMEDescription() // Unix only
NPError NP_GetValue(void*, NPPVariable, void* out)
NPError NP_Initialize(NPNetscapeFuncs*, NPPluginFuncs*)
NPError OSCALL NP_Shutdown()
NPNetscapeFuncs (NPN_XXXXX API)
NPNetscapeFuncs 是一個 Function pointer table,是 Browser 傳給 Plugin 使用的 NPN_XXXXX API。
宣告如下:
typedef struct _NPNetscapeFuncs {
uint16 size;
uint16 version;
NPN_GetURLProcPtr geturl;
NPN_PostURLProcPtr posturl;
NPN_RequestReadProcPtr requestread;
NPN_NewStreamProcPtr newstream;
NPN_WriteProcPtr write;
NPN_DestroyStreamProcPtr destroystream;
NPN_StatusProcPtr status;
NPN_UserAgentProcPtr uagent;
NPN_MemAllocProcPtr memalloc;
NPN_MemFreeProcPtr memfree;
NPN_MemFlushProcPtr memflush;
NPN_ReloadPluginsProcPtr reloadplugins;
NPN_GetJavaEnvProcPtr getJavaEnv;
NPN_GetJavaPeerProcPtr getJavaPeer;
NPN_GetURLNotifyProcPtr geturlnotify;
NPN_PostURLNotifyProcPtr posturlnotify;
NPN_GetValueProcPtr getvalue;
NPN_SetValueProcPtr setvalue;
NPN_InvalidateRectProcPtr invalidaterect;
NPN_InvalidateRegionProcPtr invalidateregion;
NPN_ForceRedrawProcPtr forceredraw;
NPN_GetStringIdentifierProcPtr getstringidentifier;
NPN_GetStringIdentifiersProcPtr getstringidentifiers;
NPN_GetIntIdentifierProcPtr getintidentifier;
NPN_IdentifierIsStringProcPtr identifierisstring;
NPN_UTF8FromIdentifierProcPtr utf8fromidentifier;
NPN_IntFromIdentifierProcPtr intfromidentifier;
NPN_CreateObjectProcPtr createobject;
NPN_RetainObjectProcPtr retainobject;
NPN_ReleaseObjectProcPtr releaseobject;
NPN_InvokeProcPtr invoke;
NPN_InvokeDefaultProcPtr invokeDefault;
NPN_EvaluateProcPtr evaluate;
NPN_GetPropertyProcPtr getproperty;
NPN_SetPropertyProcPtr setproperty;
NPN_RemovePropertyProcPtr removeproperty;
NPN_HasPropertyProcPtr hasproperty;
NPN_HasMethodProcPtr hasmethod;
NPN_ReleaseVariantValueProcPtr releasevariantvalue;
NPN_SetExceptionProcPtr setexception;
NPN_PushPopupsEnabledStateProcPtr pushpopupsenabledstate;
NPN_PopPopupsEnabledStateProcPtr poppopupsenabledstate;
NPN_EnumerateProcPtr enumerate;
NPN_PluginThreadAsyncCallProcPtr pluginthreadasynccall;
NPN_ConstructProcPtr construct;
NPN_ScheduleTimerProcPtr scheduletimer;
NPN_UnscheduleTimerProcPtr unscheduletimer;
NPN_PopUpContextMenuProcPtr popupcontextmenu;
} NPNetscapeFuncs;
NPPluginFuncs (NPP_XXXX API)
NPPluginFuncs 也是一個 Function pointer table,是由 Plugin 傳回給 Browser 使用的 NPP_XXXXX API。
宣告如下:
typedef struct _NPPluginFuncs {
uint16 size;
uint16 version;
NPP_NewProcPtr newp;
NPP_DestroyProcPtr destroy;
NPP_SetWindowProcPtr setwindow;
NPP_NewStreamProcPtr newstream;
NPP_DestroyStreamProcPtr destroystream;
NPP_StreamAsFileProcPtr asfile;
NPP_WriteReadyProcPtr writeready;
NPP_WriteProcPtr write;
NPP_PrintProcPtr print;
NPP_HandleEventProcPtr event;
NPP_URLNotifyProcPtr urlnotify;
JRIGlobalRef javaClass;
NPP_GetValueProcPtr getvalue;
NPP_SetValueProcPtr setvalue;
} NPPluginFuncs;
Plugin Instance Construction and Destruction
當 Browser 在 HTML 中發現 Plugin 所對應的 Mime Type 時,會呼叫 NPP_New() 來向 Plugin 要求一個 Plugin Instace 服務。
NPP_New() 定義如下:
#include <npapi.h>
NPError NPP_New(NPMIMEType pluginType,
NPP instance,
uint16 mode,
int16 argc,
char* argn[],
char* argv[],
NPSavedData* saved);
NPError NPP_Destroy(NPP instance,
NPSavedData **save);
NPP 即為 Plugin Instance 資料結構,由 Browser 所建立,透過 NPP_New() 傳送給 Plugin。
NPP 的 資料結構很簡單,僅包含兩個 void pointer:
- void *pdata : Plugin Private Data
- void *ndata : Browser Private Data
宣告如下:
/*
* NPP is a plug-in's opaque instance handle
*/
typedef struct _NPP
{
void* pdata; /* plug-in private data */
void* ndata; /* netscape private data */
} NPP_t;
typedef NPP_t* NPP;
而 Browser 在某個 Page 被 Unload 之前,則會呼叫 NPP_Destroy() 來通知 Plugin 結束所對應的 Plugin Instance。
Scriptability
Scriptability 就是讓 JavaScript 可以將 Plugin 當作 JavaScript Object 來使用,而 NPRuntime 定義了 NPObject 與 NPClass 兩個結構來建立 Browser 能夠了解的 Scriptable Object 。
Multiple NPObject Instances
該注意的一點是,NPObject 本身也是需要支援 Multiple Instance,原因很簡單,因為 Plugin Instance 都應該擁有自己的 NPObject,若是 NPObject 不設計成 Multiple Instance,就得所有 Plugin Instance 「共用」一組 NPObject,將會帶來很多擴充性上的困難。
Scriptable Object Model (NPObject & NPClass design with UML)
以下是個人從 NPRuntime 設計中理解出的 Scriptable Object Model (名字取不好,多見諒。)
What is NPClass?
NPClass 是一組 Interface (function pointer table),代表某個 NPObject 在建立 Instance 時所需要的動作(ex: Constructor/Desctructor),也就是說 Browser 只透過 NPClass 所指定的 Methods 來建立新的 NPObject Instance。舉例來說,當 Browser 透過 NPP_GetValue() 來向 Plugin 要一個 Property 時,Plugin 可以傳回一個 NPObject 給 Browser ,讓 Browser 知道其實這個 Plugin Property 其實是一個 Scriptable Object。
NPClass 其實就是所謂的 Marshaling Functions,這個原本由 RPC 發展出來的方法已經在很多地方都可以看到,幾乎只要是 Virtual Machine 相關的系統都會用這個方式來達到模擬 Calling Convention 的目的。
不過也有例外的,像是 Mozilla XPCOM 的 xptcall 就是直接從 register/stack 來做 Marshaling 的動作。
PluginObject 是我們實際上想建立的 Object,它應該具有我們想要的 Custome Properties 與 Methods,而 PluginObject 所擁有的 NPClass 其實就是 PluginClass ,也是由我們設計的,因為只有設計者才知道 Plugin Object 應該如何 construct/destruct/etc…)。
當 Browser 需要建立 NPObject Instance 時,會呼叫 NPObject→NPClass→allocate(),也就會呼叫到 PluginClass→pluginAllocate(),我們就可以 new PluginObject() 傳回給 Browser 了。簡單的說,Browser 想要建立或是存取任何 PluginObject,都得透過 PluginClass 中的 API,Browser 是無法直接存取 PluginObject 的 Custom Property/Methods 。
雖然 NPRuntime 的呼叫都是 C API,但是實際上這組 API 想完成的事就是上面的 Scriptable Object Model。若只是了解 Call Flow 是不夠的,要能「讀出」原來設計這組 API 的人在「想」的是什麼。
NPClass 與 NPObject 的 C 宣告如下:
struct NPClass
{
uint32_t structVersion;
NPAllocateFunctionPtr allocate;
NPDeallocateFunctionPtr deallocate;
NPInvalidateFunctionPtr invalidate;
NPHasMethodFunctionPtr hasMethod;
NPInvokeFunctionPtr invoke;
NPInvokeDefaultFunctionPtr invokeDefault;
NPHasPropertyFunctionPtr hasProperty;
NPGetPropertyFunctionPtr getProperty;
NPSetPropertyFunctionPtr setProperty;
NPRemovePropertyFunctionPtr removeProperty;
NPEnumerationFunctionPtr enumerate;
};
struct NPObject {
NPClass *_class;
uint32_t referenceCount;
// Additional space may be allocated here by types of NPObjects
}
When should NPObject be created?
當 NPP_New() 要求建立 Plugin Instance ,就需要建立我們的 PluginObject(which is a NPObject) Instance,在此同時,應該直接以 NPN_CreateObject() 來建立相對應的 NPObject,因為 NPObject 是由 Browser 來主動 Allocate/Free Memory,所以 reference count 也會紀錄在 NPObject 中。
以下為使用 NPN_CreateObject() 來建立 NPObject 的範例:
#include <npruntime.h>
NPObject *NPN_CreateObject(NPP npp, NPClass *aClass);
NPError NPP_New(NPMIMEType pluginType,
NPP instance,
uint16 mode,
int16 argc,
char* argn[],
char* argv[],
NPSavedData* saved)
{
// Scripting functions appeared in NPAPI version 14
if (browser->version >= 14)
instance->pdata = NPN_CreateObject ((NPP) instance,
(NP_Class *) PluginClass);
}
NPN_CreateObject() 是由 Plugin 向 Browser 要求建立一個 NPObject,此 NPObject 的 NPClass 就是我們所指定的 PluginClass。
若是 PluginClass 中有指定 allocate(),則 Browser 會使用 PluginClass -> allocate() 來做來替 NPObject allocate Memory ,否則 Browser 會以 malloc() 來 Allocate NPObject。傳回的 NPObject 的 reference count 會變成 1。
這裡有一件很重要的技巧,PluginClass 中的 pluginAllocate() 應該要 new PluginObject 傳回給 Browser,因為 PluginObject 「繼承自」 NPObject, 從 Browser 的角度來看,PluginObject 就只是個 NPObject;但是對於 Plugin 來說,instance→pdata 所存放的其實是 PluginObject;之後 Browser 呼叫 PluginClass 中的 Methods 時,我們只需要取得 instance→pdata 就可以當做是 PluginObject,並直接存由 Plugin 自己定義的 Custom Properties/Methods 了。如此一來,就不用一堆 Variable 指來指去的了,這是簡化支援 Multiple Instance 的一個重點。
這樣的技巧在 Apple Objective-C 與 Object-Oriented Language (ex: C++ vtable) 裡是很常見的,可惜的是我們目前的實作完全沒有 OO 的思考。
instance->pdata 反正是個 void *,可以任意 casting 成任一種 type,Browser 與 Plugin 就像一個中國各自表述啦~
Browser ask for NPObject
Browser 在 NPP_New() 建立 Plugin Instance 之後,還會以
NPP_GetValue(npp, NPPVpluginScriptableNPObject, void* value);
來詢問 Plugin 是否支援 Scriptable,此時再把我們先前建立好的 PluginObject (stored in instance->pdata) 透過 value 傳回給 Browser 即可。
範立如下:
NPError NPP_GetValue(NPP instance, NPPVariable variable, void *value)
{
if (variable == NPPVpluginScriptableNPObject) {
void **v = (void **)value;
PluginObject *obj = instance->pdata;
if (obj)
NPN_RetainObject((NPObject*)obj);
*v = obj;
return NPERR_NO_ERROR;
}
return NPERR_GENERIC_ERROR;
}
NPRuntime API 中規定,在傳回 NPObject 之前,應該先以 NPN_RetainObject() 來增加 NPObject.refCount,這對 JavaScript Engine 的 Garbage Collection 機制很重要,千萬別忘了。
How NPObject was used by JavaScript?
2 Types of Marshaling Functions (Method/Property)
在上面建立完 Scriptable NPObject 後,等於是建立了一個相對應的 JavaScript Object 。於是 JavaScript 可以對 JavaScript Object 做其它的存取動作,而這些動作則被對應到 NPObject 的兩類 Marshaling Functions:
(提醒一下:NPObject->_class 就是 NPClass )
- Method 呼叫
typedef bool (*NPHasMethodFunctionPtr)(NPObject *obj, NPIdentifier name);
typedef bool (*NPInvokeFunctionPtr)(NPObject *obj, NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result);
typedef bool (*NPInvokeDefaultFunctionPtr)(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result);
struct NPClass
{
...
NPHasMethodFunctionPtr hasMethod;
NPInvokeFunctionPtr invoke;
NPInvokeDefaultFunctionPtr invokeDefault;
...
}; - Property 存取
typedef bool (*NPHasPropertyFunctionPtr)(NPObject *obj, NPIdentifier name);
typedef bool (*NPGetPropertyFunctionPtr)(NPObject *obj, NPIdentifier name, NPVariant *result);
typedef bool (*NPSetPropertyFunctionPtr)(NPObject *obj, NPIdentifier name, const NPVariant *value);
typedef bool (*NPRemovePropertyFunctionPtr)(NPObject *npobj, NPIdentifier name);
struct NPClass
{
...
NPHasPropertyFunctionPtr hasProperty;
NPGetPropertyFunctionPtr getProperty;
NPSetPropertyFunctionPtr setProperty;
NPRemovePropertyFunctionPtr removeProperty;
...
};
從以下範例說明會比較清楚:
<script>
var myPlugin = document.getElementByID("FooPlugin");
myPlugin.fooMethod();
myPlugin.fooProperty = "hello world";
</script>
從 ECMAScript 的角度說明如下:
- myPlugin : "myPlugin" 是一個 JavaScript Object 的名字(Identifier),之後可以將 myPlugin 想像成 NPObject。(實際上 JavaScript VM 內部的對應要複雜許多)
- fooMethod : "fooMethod" 是我們 Plugin 所提供的 Method 的名字(Identifier),則 myPlugin.fooMethod(); 會轉換成對 NPObject 內 NPClass 的 function call ,動作如下:
1. 透過 hasMethod(NPObject, NPIdentifier of "fooMethod") 詢問 Plugin 是否提供名稱為 "fooMethod" 的 Method,若有則到 2.
2. 透過 invokeMethod(NPObject, NPIdentifier of "fooMethod", …) 來傳送參數給 Plugin,而 Plugin 則可由 NPIdentifier 得知 Browser 希望呼叫的 method 為何,再去執行所對應的功能,最後再傳回值 (result)。 - fooProperty : "fooProperty" 是我們 Plugin 所提供的 Property 的名字(Identifier),則 myPlugin.fooProperty = "hello world"; 會轉換成為以下動作:
1. 透過 hasProperty(NPObject, NPIdentifier of "fooProperty"); 詢問 Plugin 是否有提供名稱為 "fooProperty" 的 Property,若有則到 2.
2. 透過 setProperty(NPObject, NPIdentifier of "fooProperty", "hello world"); 要求 Plugin 執行將 fooProperty 的值更改為 "hello world" 的動作。
以上說明著動在流程上,細節上並非完全正確,因為 JavaScript (ECMAScript) 內部有許多針對 Objects, Properties, Attributes 等細節,可以說上三天三夜了吧!
NPVariant (Parameters Serialization between JavaScript and C)
在 Marshaling Functions 中,會以 NPVariant 來傳送真正的參數資料。
NPVariant 就是參數的 Serialized Data Type 。
typedef struct _NPVariant {
NPVariantType type;
union {
bool boolValue;
int32_t intValue;
double doubleValue;
NPString stringValue;
NPObject *objectValue;
} value;
} NPVariant;
typedef enum {
NPVariantType_Void,
NPVariantType_Null,
NPVariantType_Bool,
NPVariantType_Int32,
NPVariantType_Double,
NPVariantType_String,
NPVariantType_Object
} NPVariantType;
Data Type Mapping Between JavaScript and NPVariant
NPVariant 所封裝的資料型態會對應到 JavaScript 資料型態。
對應如下:
JavaScript | C (NPVariant with type:) |
---|---|
undefined | NPVariantType_Void |
null | NPVariantType_Null |
Boolean | NPVariantType_Bool |
Number | NPVariantType_Double or NPVariantType_Int32 |
String | NPVariantType_String |
Object | NPVariantType_Object |
Marshaling Macro
為了方便 NPVariant 與 JavaScript 間的資料轉換, NPRuntime 也定義了一組轉換的 Macro 方便程式設計。
#define NPVARIANT_IS_VOID(_v) ((_v).type == NPVariantType_Void)
#define NPVARIANT_IS_NULL(_v) ((_v).type == NPVariantType_Null)
#define NPVARIANT_IS_BOOLEAN(_v) ((_v).type == NPVariantType_Bool)
#define NPVARIANT_IS_INT32(_v) ((_v).type == NPVariantType_Int32)
#define NPVARIANT_IS_DOUBLE(_v) ((_v).type == NPVariantType_Double)
#define NPVARIANT_IS_STRING(_v) ((_v).type == NPVariantType_String)
#define NPVARIANT_IS_OBJECT(_v) ((_v).type == NPVariantType_Object)
#define NPVARIANT_TO_BOOLEAN(_v) ((_v).value.boolValue)
#define NPVARIANT_TO_INT32(_v) ((_v).value.intValue)
#define NPVARIANT_TO_DOUBLE(_v) ((_v).value.doubleValue)
#define NPVARIANT_TO_STRING(_v) ((_v).value.stringValue)
#define NPVARIANT_TO_OBJECT(_v) ((_v).value.objectValue)
#define NP_BEGIN_MACRO do {
#define NP_END_MACRO } while (0)
#define VOID_TO_NPVARIANT(_v) NP_BEGIN_MACRO (_v).type = NPVariantType_Void; (_v).value.objectValue = NULL; NP_END_MACRO
#define NULL_TO_NPVARIANT(_v) NP_BEGIN_MACRO (_v).type = NPVariantType_Null; (_v).value.objectValue = NULL; NP_END_MACRO
#define BOOLEAN_TO_NPVARIANT(_val, _v) NP_BEGIN_MACRO (_v).type = NPVariantType_Bool; (_v).value.boolValue = !!(_val); NP_END_MACRO
#define INT32_TO_NPVARIANT(_val, _v) NP_BEGIN_MACRO (_v).type = NPVariantType_Int32; (_v).value.intValue = _val; NP_END_MACRO
#define DOUBLE_TO_NPVARIANT(_val, _v) NP_BEGIN_MACRO (_v).type = NPVariantType_Double; (_v).value.doubleValue = _val; NP_END_MACRO
#define STRINGZ_TO_NPVARIANT(_val, _v) NP_BEGIN_MACRO (_v).type = NPVariantType_String; NPString str = { _val, strlen(_val) }; (_v).value.stringValue = str; NP_END_MACRO
#define STRINGN_TO_NPVARIANT(_val, _len, _v) NP_BEGIN_MACRO (_v).type = NPVariantType_String; NPString str = { _val, _len }; (_v).value.stringValue = str; NP_END_MACRO
#define OBJECT_TO_NPVARIANT(_val, _v) NP_BEGIN_MACRO (_v).type = NPVariantType_Object; (_v).value.objectValue = _val; NP_END_MACRO
Why need NPVariant (Serialization) ?
在 JavaScript 中,使用者可以任意撰寫任何 function,而這些 function 的參數個數,型態,排列順序等,都是任意的;我們不可能寫出一個 C function 來對應到所有的 JavaScript function,C function 是 compile time 時就必須決定參數個數,型態與順序;因此必須要透過 Marshaling (Serialization) 的方式來取得 JavaScript 的參數後,再轉換成 C 語言中相對應的資料型態來處理。
NPIdentifier
NPObject 的 Method 與 Property 的皆是由 NPIdentifier 來指定,NPIdentifier 對於相同名稱的 Method 或是 Property 會有一個 Unique 值。而 NPIdentifier 的值是由 Browser 所提供,也就是說 Browser 內部有一個 (Hash) Table 來儲存所有的 NPIdentifier 。
typedef void *NPIdentifier;
/*
NPObjects have methods and properties. Methods and properties are
identified with NPIdentifiers. These identifiers may be reflected
in script. NPIdentifiers can be either strings or integers, IOW,
methods and properties can be identified by either strings or
integers (i.e. foo["bar"] vs foo[1]). NPIdentifiers can be
compared using ==. In case of any errors, the requested
NPIdentifier(s) will be NULL.
*/
NPIdentifier NPN_GetStringIdentifier(const NPUTF8 *name);
void NPN_GetStringIdentifiers(const NPUTF8 **names, int32_t nameCount,
NPIdentifier *identifiers);
NPIdentifier NPN_GetIntIdentifier(int32_t intid);
bool NPN_IdentifierIsString(NPIdentifier identifier);
/*
The NPUTF8 returned from NPN_UTF8FromIdentifier SHOULD be freed.
*/
NPUTF8 *NPN_UTF8FromIdentifier(NPIdentifier identifier);
/*
Get the integer represented by identifier. If identifier is not an
integer identifier, the behaviour is undefined.
*/
int32_t NPN_IntFromIdentifier(NPIdentifier identifier);
Browser 另外還提供了 NPIdentifier 的轉換函數,供 Plugin 方便使用。
Why use NPIdentifier?
有 NPIdentifier 這樣的設計主要有兩個原因:
-
Less Memory Cost
對於許多 Object 來說都有相同名稱的 Method 或是 Property,若是將這些「名稱字串」全都儲存在 Object 的 Instance 中,對於 Memory 的消耗實在是一種浪費。 -
Fast Lookup (ECMAScript Identifer Resolution)
對於 Browser 或是 Plugin 在 JavaScript 執行時在 Lookup Object 的動作時,能夠以 Lookup NPIdentifier 來取代 Name String Compare,可以大大增加 Lookup 的速度。