NPAPI & NPRuntime 简介 Scriptable Plugin

本篇文章在探讨 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 之间的运作过程:

  1. Browser lookup Plugin (.so, .dll) and load it.
  2. 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。
  3. Browser 呼叫 Plugin 的 NP_GetValue() 来得到 Plugin 的资讯,例如:版本资讯与是否支援 Scriptability 等。
  4. Browser 在网页中发现 Plugin 所支援的 Mime Type 时,呼叫 Plugin 的 NPP_New() 来建立新的 Plugin instance 来处理。
  5. 当网页被 Unload 前,Browser 则会呼叫 Plugin NPP_Destroy() 来通知 Plugin 应 Destroy 所对应的 Plugin instance 。
  6. 当 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:

  1. void *pdata : Plugin Private Data
  2. 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 )

  1. 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;
        ...
    };
  2. 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 这样的设计主要有两个原因:

  1. Less Memory Cost
    对于许多 Object 来说都有相同名称的 Method 或是 Property,若是将这些「名称字串」全都储存在 Object 的 Instance 中,对于 Memory 的消耗实在是一种浪费。
  2. Fast Lookup (ECMAScript Identifer Resolution)
    对于 Browser 或是 Plugin 在 JavaScript 执行时在 Lookup Object 的动作时,能够以 Lookup NPIdentifier 来取代 Name String Compare,可以大大增加 Lookup 的速度。

 

Reference

本文来自http://www.eifr.com/article.php?id=1273
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章