NPAPI插件與JS交互開發詳細記錄

在JS中使用document.getElementsByTagName或者document.getElementById來獲取頁面中已經存在的插件對象,還可以在JS中使用document.createElement(“object”);來動態創建對象,併爲該對象設置type屬性,接着將創建的這個對象添加到頁面中,這樣就動態創建了一個插件對象。如下JS函數可以根據傳入的mimetype創建一個插件對象(chrome、firefox測試有效,其他未測試):
function newNPObject(mimetype)
{
var obj = document.createElement(“object”);
obj.type = mimetype;
document.body.appendChild(obj);
return obj;
}

那麼瀏覽器是如何完成將插件轉換爲JS能夠識別的對象的呢?我們發現,在NPP_GetValue的實現中有:
if (variable==NPPVpluginScriptableNPObject)
{
(NPObject *)value = plugin->GetScriptableObject();
}

也就是說,瀏覽器會調用NPP_GetValue (instance, NPPVpluginScriptableNPObject, value)並將來獲取插件的scriptable對象。進一步看看plugin是如何獲取scriptable對象的:

NPObject* CPlugin::GetScriptableObject()
{
    if (!m_pScriptableObject) {
       m_pScriptableObject = NPN_CreateObject(m_pNPInstance, &CScriptObject::nsScriptObjectClass);
    }

    if (m_pScriptableObject) {
       NPN_RetainObject(m_pScriptableObject);
    }

    return m_pScriptableObject;
}

對象存在時用NPN_RetainObject來獲取對象,對象不存在時用NPN_CreateObject來創建一個對象。
當我們在JS中設置/獲取屬性或者調用方法時,都會在這個scriptable對象中操作,在使用結束時(CPlugin的析構函數中)使用NPN_ReleaseObject(m_pScriptableObject);來釋放這個對象。
簡單解釋一下對象是如何創建的(一般情況下我們可以不知道,只需要按照demo中的代碼使用就可以了,如果只想知道如何實現與JS的交互請跳至下一部分),看看MDN上相關說明:
NPObject *NPN_CreateObject(NPP npp, NPClass *aClass);
The function has the following parameters:
npp
The NPP indicating which plugin wants to instantiate the object.
aClass
The class to instantiate an object of.
第一個參數很好搞定,第二個參數比較費解,創建時傳入的&CScriptObject::nsScriptObjectClass實際上是基類nsScriptObjectBase的NPClass變量,結合說明可以知道,NPN_CreateObject是根據所傳入的NPClass類創建一個NPObject並返回這個對象的指針。NPN_CreateObject中調用NPClass類的NPAllocateFunctionPtr成員來爲NPObject分配內存,看到NPClass的NPAllocateFunctionPtr成員是nsScriptObjectBase::_Allocate函數,該函數則是調用nsScriptObjectBase::AllocateScriptPluginObject來實現的,AllocateScriptPluginObject的實現在Plugincpp中,可以看到其實現代碼就是return (NPObject*)new CScriptObject(npp);也就是創建一個新的CScriptObject對象,這裏繞過來繞過去這麼複雜,其實就是要做這樣一件事情:我們設計scriptableobject類時會新建一個類,而基類nsScriptObjectBase卻需要用我們設計的scriptableobject類的構造函數來分配內存並轉換成NPObject,最終由NPP_GetValue返回給瀏覽器,JS實際上就是與瀏覽器獲取到的這個對象來交互的。
仔細研究過npruntime代碼的人可能會發現,npruntime中有一個很晦澀的宏DECLARE_NPOBJECT_CLASS_WITH_BASE及GET_NPOBJECT_CLASS,當然從名稱可以知道是用基類聲明一個變量,並用GET_NPOBJECT_CLASS來引用這個變量,這就相當於是我在基類中定義的nsScriptObjectClass。
實現一個scriptable對象的類其實並不難,只需要從NPObject派生一個類並逐一實現NPClass中的幾個函數指針所需要的函數。這裏搞得如此複雜就是爲了能夠設計一個基類,並一勞永逸的不再修改這個基類。本章最後一個示例會給出實現一個最簡單的scriptable對象的例子。
屬性

在JS中一個對象具有的屬性可以比較靈活的設置,比如一個對象obj本來不具有屬性kit,調用obj.kit會是undefined,然而當我們設置obj.kit=some_val之後,再次調用obj.kit就會有相應的屬性了。
另一方面,在實現插件dll的代碼中(後文稱爲C++代碼中),插件對象是一個派生自NPObject的對象,我們也可以很方便的爲其設置成員變量,要在插件中實現與JS交互,那麼就需要C++代碼中的屬性(變量)與JS中屬性能夠互相訪問。
可以進行交互的屬性分爲一般屬性及只讀屬性,只讀屬性是對於JS來說的,畢竟插件中的代碼相對於JS來說是更加底層的,可以不允許JS修改C++中保持的變量,但若想要防止C++更改JS中的變量值卻是比較不現實的。
從NPObject的定義可以看到,NPObject包括一個指向NPClass對象的指針和一個引用計數器。NPClass則由諸如hasProperty、hasMethod等函數指針。要實現一個可以與JS交互的插件,就需要實現hasProperty、hasMethod等接口。前文我們知道瀏覽器調用NPN_CreateObject創建scriptable對象,這裏介紹我們在scriptable對象中實現可交互屬性。
大概過程是這樣的:瀏覽器在獲取到scriptable對象之後,就會調用對象的hasProperty、hasMethod來判斷該對象是否具有某個屬性或方法,當JS中訪問屬性或調用函數是就會調用scriptable對象的getProperty、involve等函數來獲取屬性值或者執行函數。
要設置一個屬性(這裏以foo爲例),首先需要定義一個NPIdentifier來方便保存屬性的標識,一般的插件中是設置爲全局static變量,我將其設置爲CScriptObject類的static成員變量,不設置爲全局的,如下:
static NPIdentifier foo_id;

類的中聲明的static變量並不會初始化,還需要在cpp文件中,對其初始化:
NPIdentifier CScriptObject::foo_id;

另外我設置一個變量來保存屬性值,這個變量要CScriptObject類可以訪問,或者通過某個函數訪問,簡便起見直接設置爲CScriptObject類的私有成員變量:
int m_vfoo;

接下來在適當的位置對這個id與要設置的屬性關聯起來,我選擇在CPlugin類的構造函數中執行:
CScriptObject::foo_id = NPN_GetStringIdentifier(“foo”);

如前所述,瀏覽器會調用CScriptObject類的HasProperty來判斷是否具有某屬性,那麼我們在HasProperty中如下實現:
bool CScriptObject::HasProperty(NPIdentifier name)
{
return name == foo_id;
}

在JS中可以設置屬性,需要實現SetProperty
bool CScriptObject::SetProperty(NPIdentifier name, const NPVariant *value)
{
if(name==foo_id){
if(value->type == NPVariantType_Int32)
m_vfoo = NPVARIANT_TO_INT32(*value);
return true;
}
}

     最後在GetProperty中返回屬性值:

bool CScriptObject::GetProperty(NPIdentifier name, NPVariant *result)
{
if (name == foo_id)
{
INT32_TO_NPVARIANT(m_vfoo,*result);
m_vfoo++;
return true;
}
}

     爲了與JS中設置屬性值進行區別,每次獲取之後,把屬性的值+1可以在JS中多次獲取該屬性值,發現每次獲取的值都會增加一個,說明確實是獲取到了插件中設置的屬性。
      在JS中設置/獲取foo屬性,HTML中相應代碼爲:
     property test:
    int property<input id = "fooinput" value="0"></input>
    <button onclick = "btnsetfoo();">set FOO</button>
    <button onclick = "btngetfoo();">get FOO</button><br />

     JS代碼(片段)如下:
function btnsetfoo()
{
    var val = document.getElementById("fooinput");
    obj.foo = parseInt(val.value) ;
}
function btngetfoo()
{
    alert(obj.foo);

}

    如果想實現只讀屬性,不實現SetProperty即可。

供JS調用的插件接口

    實現可以在JS中調用的接口,過程與屬性相似,這裏以實現一個函數func爲例,首先在CScriptObject類中聲明一個標識:

static NPIdentifier func_id;
初始化:
NPIdentifier CScriptObject::func_id;
接下來將這個id與要設置的函數名關聯起來:
CScriptObject::func_id = NPN_GetStringIdentifier(“func”);
瀏覽器會調用CScriptObject類的HasMethod來判斷是否具有某個函數,那麼我們在HasMethod中如下實現:
bool CScriptObject::HasMethod(NPIdentifier name)
{
return name == func_id;
}
JS中調用obj.func()時,會執行到Invoke,其中我們利用messagebox彈出一個消息框,實現如下:

    ```

bool CScriptObject::Invoke(NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (name == func_id)
{
MessageBox(NULL,_T(“func”),_T(“”),0);
return true;
}
return false;
}

     在JS中調用func函數,HTML中相應代碼爲:

FUNC
JS代碼(片段)如下:
function btnclick()
{
obj.func();
}
供插件調用的JS函數(JS callback)

可以將JS函數作爲回調供插件調用,假設我們需要插件調用JS函數如下:
function objJSfunc()
{
alert(“JS function called!”);
}

在JS中,函數其實也是一個object,那麼如何將這個object傳遞給插件,並在插件中執行呢?我們可以將這個object作爲插件的一個屬性,在執行的時候利用NPN_InvokeDefault來執行,以下是完整過程:
首先需要一個變量來保存這個JS函數或者函數的標識,JS函數作爲一個對象,因此可以將其保存爲一個NPObject對象,可以用全局變量也可以將其作爲某個類的成員變量,我將這個NPObject作爲CPlugin類的一個成員,聲明成員 變量:
NPObject* m_jsObj;
在構造函數中初始化爲NULL:
m_jsObj = NULL;
使用完畢之後需要釋放這個對象,我們在析構函數中執行:
if (m_jsObj!=NULL)
NPN_ReleaseObject(m_jsObj);
接下來,將JS函數作爲一個屬性,與前文設置一般屬性是一樣的,先聲明一個標識:

static NPIdentifier jsfunc_id;

初始化:
NPIdentifier CScriptObject::jsfunc_id;
與要設置的屬性名稱關聯起來:
CScriptObject::jsfunc_id = NPN_GetStringIdentifier(“OnJsFunc”);
接下來在HasProperty中:

bool CScriptObject::HasProperty(NPIdentifier name)
{
    return name == jsfunc_id;
}
      然後SetProperty:
bool CScriptObject::SetProperty(NPIdentifier name, const NPVariant *value)
{
    if (name == jsfunc_id)
    {
        CPlugin * plugin = (CPlugin*) m_npp->pdata;
        if (plugin->m_jsObj == NULL)
        {
            plugin->m_jsObj = NPN_RetainObject(NPVARIANT_TO_OBJECT(*value));
        }
        return true;
    }
}```
         當然這個就不需要實現GetProperty了。這樣就實現了JS回調函數的設置,只需要在JS中使用obj.OnJsFunc = objJSfunc;爲其設置好需要調用的JS函數就可以了。
         設置好之後,就是在C++中如何調用這個JS函數了,要調用這個函數,就需要訪問我們設置的m_jsObj,因此只要能夠訪問我們設置的這個變量的位置都可以執行這個JS函數,之前我們設置了一個func供JS調用,這裏我們假設func執行完畢之後需要調用我們設置的這個JS函數,可以在func的執行代碼最後加上調用JS函數的代碼,Invoke函數就變爲如下形式了:

bool CScriptObject::Invoke(NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (name == func_id)
{
MessageBox(NULL,_T(“func”),_T(“”),0);
CPlugin* plugin = (CPlugin*) m_npp->pdata;
if (!(!plugin->m_jsObj))
{
NPVariant result;
NPN_InvokeDefault(m_npp,plugin->m_jsObj,NULL,0,&result);
NPN_ReleaseVariantValue(&result);
}
return true;
}
return false;
}
“`
以上就是設置JS回調的完整過程,與JS交互有關的話題可能還包括編碼的轉換,在遇到中文時處理不好可能會導致亂碼,只要記住一個原則就是JS處理的字符都是UTF-8編碼的,而C++中的字符可能是unicode的也可能是ansi的,因此需要根據實際情況進行編碼的轉換就可以解決中文亂碼的問題。我給出的scriptdemo還包含:str屬性可以設置字符串類型的屬性,funci2i處理輸入爲int輸出爲int的函數,funcs2s處理輸入爲字符串輸出爲字符串的函數。目前沒有發現有亂碼的問題,因此這裏就不再對編碼轉換的話題做過多的介紹了,如果有朋友發現scriptdemo中有亂碼的問題,請及時反饋給我,需要的話以後再來補充。
接下來實現一個簡單的JS數組對象,可以說是一個簡化的scriptable對象的設計。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章