NPAPI插件开发详细记录:与JS交互

        插件主要用于HTML页面中增强HTML页面可以支持的资源类型,在HTML页面中最重要的一个特性就是可以利用脚本语言来实现与用户的交互,之前的文章也提及过相关的议题,不过在交流过程中还是发现好多开发者对于这个主题有不太明白的地方,这里再次详细介绍一下在插件中如何与JS进行交互。

以前的文章可以与本文互为参考:脚本化接口、插件接口脚本化。

本文的例子代码可以下载:无boost需安装CMake、原来的scriptable demo。

插件对象

          可以在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中相应代码为:
<button onclick ="btnclick();" >FUNC</button>
         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对象的设计。

简单的JS数组对象

        这里设计的一个JS数组对象是比较简单的,因为我在C++中将数据保存好,然后以数组对象的方式返回到JS中,JS中只需要调用Size方法获取数组中元素的个数,并使用At方法获取数组中的某一个元素。当然网上可能有更完善的JS数组的实现,或者你可以在这个代码的基础上完善更多的功能,在此仅贴出代码。
头文件:
class JsArrayObject : public NPObject
{
private:
	vector<std::wstring> array_;
	static NPIdentifier method_At;
	static NPIdentifier method_Size;

public:
	static NPClass nsJsObject;
	JsArrayObject();
	~JsArrayObject(){}

public:
	std::wstring At(size_t idx); 
	size_t Size();
	void Clear();
	bool Empty();
	void PushBack(std::wstring val);
	static NPObject *_Allocate(NPP npp, NPClass *aClass);
	static void _Deallocate(NPObject *npobj);
	static void _Invalidate(NPObject *npobj);
	static bool _HasMethod(NPObject *npobj, NPIdentifier name);
	static bool _Invoke(NPObject *npobj, NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result);
	static bool _InvokeDefault(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result);
	static bool _HasProperty(NPObject *npobj, NPIdentifier name);
	static bool _GetProperty(NPObject *npobj, NPIdentifier name, 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 **value, uint32_t *count);
	static bool _Construct(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result);
};

实现文件:
NPIdentifier JsArrayObject::method_At;
NPIdentifier JsArrayObject::method_Size;

JsArrayObject::JsArrayObject()
{
	method_At = NPN_GetStringIdentifier("At");
	method_Size = NPN_GetStringIdentifier("Size");
}

NPObject* JsArrayObject::_Allocate(NPP npp, NPClass *aClass)
{
	return (NPObject*)new JsArrayObject();
}

void JsArrayObject::_Deallocate(NPObject *npobj)
{
	delete (JsArrayObject*)npobj;
}

void JsArrayObject::_Invalidate(NPObject *npobj)
{

}

bool JsArrayObject::_HasMethod(NPObject *npobj, NPIdentifier name)
{
	return name == method_At || name ==method_Size ;
}

bool JsArrayObject::_Invoke(NPObject *npobj, NPIdentifier name, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
	if (name == method_At)
	{
		if(argCount < 1) return false;
		int val = args[0].value.intValue;
		std::wstring str = ((JsArrayObject*)npobj)->At(val);
		std::string pstr = ult::UnicodeToUtf8(str);
		char* npOutString = (char*) NPN_MemAlloc(pstr.length() + 1);
		if (!npOutString)
			return false;
		strcpy(npOutString, pstr.c_str());
		STRINGZ_TO_NPVARIANT(npOutString,*result);
		return true;
	}
	if (name == method_Size)
	{
		int val = ((JsArrayObject*)npobj)->Size();
		INT32_TO_NPVARIANT(val, *result);
		return true;
	}	
	return false;
}

bool JsArrayObject::_InvokeDefault(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
	return false;
}

bool JsArrayObject::_HasProperty(NPObject *npobj, NPIdentifier name)
{
	return false;
}

bool JsArrayObject::_GetProperty(NPObject *npobj, NPIdentifier name, NPVariant *result)
{
	return false;
}

bool JsArrayObject::_SetProperty(NPObject *npobj, NPIdentifier name, const NPVariant *value)
{
	return false;
}

bool JsArrayObject::_RemoveProperty(NPObject *npobj, NPIdentifier name)
{
	return false;
}

bool JsArrayObject::_Enumerate(NPObject *npobj, NPIdentifier **value, uint32_t *count)
{
	return false;
}

bool JsArrayObject::_Construct(NPObject *npobj, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
	return false;
}

NPClass JsArrayObject::nsJsObject = {
	NP_CLASS_STRUCT_VERSION_CTOR,
	JsArrayObject::_Allocate,
	JsArrayObject::_Deallocate,
	JsArrayObject::_Invalidate,
	JsArrayObject::_HasMethod,
	JsArrayObject::_Invoke,
	JsArrayObject::_InvokeDefault,
	JsArrayObject::_HasProperty,
	JsArrayObject::_GetProperty,
	JsArrayObject::_SetProperty,
	JsArrayObject::_RemoveProperty,
	JsArrayObject::_Enumerate,
	JsArrayObject::_Construct
};

std::wstring JsArrayObject::At(size_t idx)
{
	return array_.at(idx);
}


你可能感兴趣的:(NPAPI插件开发详细记录:与JS交互)