VC与JavaScript交互(三) ———— JS调用C++
为什么会隔了那么久?由于本来打算太监的,但是看到热情的网友们的眼神,从期望变成了失望,在我的心里激起了层层波澜。
两年后的今天,还是坚持把它写了出来。事实上当时刚写完VC与JavaScript交互(二)的时候,參考网上的资料,已经把JavaScript调用c++实现了。但是实现方法太恶心了。代码写出来太复杂太麻烦了,并且还涉及到了一大堆见都没见过的COM接口,每一个接口都是一大堆函数和一大堆參数。尽管实现代码写出来了。但是为什么这么写。根本讲不清楚,怕误人子弟,便可耻的太监了。
这两年期间,好几次想把 VC与JavaScript交互(三) 写出来,但是发现这个东西实在是太麻烦,太复杂。看不透。剪不断,理还乱。抽刀断水水更流。举杯消愁愁更愁。代码写出来以后我总是怀疑是不是搞错了。感觉是不是走了弯路,直到今天我仍然怀疑是不是有更好更简单的办法来实现JS调用C++。为什么说它很麻烦和复杂,能够看这里http://dgj0600.blog.163.com/blog/static/440604322012102325015495/
但略微深入后便发现了这样的闭源软件的弊端,难以扩展和改造!
比方要用WebBrowser开发一个多进程浏览器,怎样在进程间共享Cookie。比方要针对不用的URL设置不同的HTTP代理来訪问。
比方要让它支持须要usernamepassword验证的HTTP/SOCKS5代理等。WebBrowser根本没有提供这样的接口来实现这些功能。仅仅能是通过API Hook等办法来实现。既麻烦又不稳定可靠。并且WebBrowser这个东西还很慢。本来IE就已经够慢了,WebBrowser作为IE的简化版,当它嵌入到我们的程序中时,WebBrowser中的HTML排版、渲染引擎、JavaScript解释器竟然都是执行在我们程序的主线程(UI线程)中!所以你能够发现,假设WebBrowser载入一个内容许多。很复杂的页面时。在载入期间,你的程序就像假死了一样。相同假设HTML页面上的JavaScript代码在进行繁杂的运算时。你的程序界面又假死了。
由于你的UI线程在执行JS解释器,你的UI线程在解释JavaScript代码并执行。在那期间它抽不出来空来去处理Windows消息循环。便假死了。
Chromium就不用说了。它的快是很出名的,即便作为控件来使用。CEF也运用了多进程技术,HTML的渲染和JavaScript的解释运行都是在格外的进程中,不会影响你的UI线程,奔溃了也不会破坏你的进程。并且CEF是用C++写的,对外提供的原生接口就是C++接口,比起WebBrowser的那套COM接口来说不知道好用多少倍。
那么怎样构造这个IDispatch就是问题的关键点。
然后改动MFC为我们生成的对话框类CxxDlg(我的项目名为JsCallCpp,所以我的演示样例代码中就是CJsCallCppDlg):
class CJsCallCppDlg : public CDialogEx, public IDispatch { ... }
然后我写下了例如以下的HTML文件:
<html> <head> <meta charset="utf-8" /> <title></title> <script language="javascript"> function ShowMessageBox() { if (cpp_object != null) cpp_object.ShowMessageBox("你好,我是Javascript,你是谁?"); } function GetProcessID() { if (cpp_object != null) { var id = cpp_object.GetProcessID(); document.getElementById("process_info").innerText = "本进程ID为:" + id; } } function SaveCppObject(obj) { cpp_object = obj; } var cpp_object; </script> </head> <body> <p id="process_info"></p> <button type="button" onclick="ShowMessageBox()">MessageBox</button> <button type="button" onclick="GetProcessID()">Process ID</button> </body> </html>然后我在我的CxxDlg里写下了例如以下的两个成员函数:
DWORD CJsCallCppDlg::GetProcessID() { return GetCurrentProcessId(); } void CJsCallCppDlg::ShowMessageBox(const wchar_t *msg) { MessageBox(msg, L"这是来自javascript的消息"); }
接来下。我要用HTML中的这两个button,分别调用这两个C++函数,当中一个是ShowMessageBox。让Javascript调用它并传递一个字符串给它,终于C++这边通过Windows API的MessageBox实现弹出一个消息框。
另外一个是GetProcessID,Javascript调用它,终于C++这边通过Windows API的GetCurrentProcessId()获取本进程ID,并给Javascript返回这个ID值。然后显示到HTML中。
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *pctinfo); virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo); virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId); virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr); virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject); virtual ULONG STDMETHODCALLTYPE AddRef(); virtual ULONG STDMETHODCALLTYPE Release();
然后实现这七个虚函数:
//我自己给我的两个函数拟定的数字ID。这个ID能够取0-16384之间的随意数 enum { FUNCTION_ShowMessageBox = 1, FUNCTION_GetProcessID = 2, }; //不用实现,直接返回E_NOTIMPL HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfoCount(UINT *pctinfo) { return E_NOTIMPL; } //不用实现,直接返回E_NOTIMPL HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) { return E_NOTIMPL; } //JavaScript调用这个对象的方法时,会把方法名,放到rgszNames中,我们须要给这种方法名拟定一个唯一的数字ID。用rgDispId传回给它 //同理JavaScript存取这个对象的属性时。会把属性名放到rgszNames中,我们须要给这个属性名拟定一个唯一的数字ID,用rgDispId传回给它 //紧接着JavaScript会调用Invoke。并把这个ID作为參数传递进来 HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) { //rgszNames是个字符串数组。cNames指明这个数组中有几个字符串。假设不是1个字符串。忽略它 if (cNames != 1) return E_NOTIMPL; //假设字符串是ShowMessageBox。说明JavaScript在调用我这个对象的ShowMessageBox方法。我就把我拟定的ID通过rgDispId告诉它 if (wcscmp(rgszNames[0], L"ShowMessageBox") == 0) { *rgDispId = FUNCTION_ShowMessageBox; return S_OK; } //同理,假设字符串是GetProcessID。说明JavaScript在调用我这个对象的GetProcessID方法 else if (wcscmp(rgszNames[0], L"GetProcessID") == 0) { *rgDispId = FUNCTION_GetProcessID; return S_OK; } else return E_NOTIMPL; } //JavaScript通过GetIDsOfNames拿到我的对象的方法的ID后。会调用Invoke。dispIdMember就是刚才我告诉它的我自己拟定的ID //wFlags指明JavaScript对我的对象干了什么事情!//假设是DISPATCH_METHOD,说明JavaScript在调用这个对象的方法。比方cpp_object.ShowMessageBox(); //假设是DISPATCH_PROPERTYGET。说明JavaScript在获取这个对象的属性,比方var n = cpp_object.num; //假设是DISPATCH_PROPERTYPUT。说明JavaScript在改动这个对象的属性,比方cpp_object.num = 10; //假设是DISPATCH_PROPERTYPUTREF,说明JavaScript在通过引用改动这个对象,详细我也不懂 //演示样例代码并没有涉及到wFlags和对象属性的使用。须要的请自行研究,使用方法是一样的 //pDispParams就是JavaScript调用我的对象的方法时传递进来的參数,里面有一个数组保存着全部參数 //pDispParams->cArgs就是数组中有多少个參数 //pDispParams->rgvarg就是保存着參数的数组,请使用[]下标来訪问。每一个參数都是VARIANT类型,能够保存各种类型的值 //详细是什么类型用VARIANT::vt来推断,不多解释了。VARIANT这东西大家都懂 //pVarResult就是我们给JavaScript的返回值 //其他不用管 HRESULT STDMETHODCALLTYPE CJsCallCppDlg::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) { //通过ID我就知道JavaScript想调用哪个方法 if (dispIdMember == FUNCTION_ShowMessageBox) { //检查是否仅仅有一个參数 if (pDispParams->cArgs != 1) return E_NOTIMPL; //检查这个參数是否是字符串类型 if (pDispParams->rgvarg[0].vt != VT_BSTR) return E_NOTIMPL; //放心调用 ShowMessageBox(pDispParams->rgvarg[0].bstrVal); return S_OK; } else if (dispIdMember == FUNCTION_GetProcessID) { DWORD id = GetProcessID(); *pVarResult = CComVariant(id); return S_OK; } else return E_NOTIMPL; } //JavaScript拿到我们传递给它的指针后,由于它不清楚我们的对象是什么东西,会调用QueryInterface来询问我们“你是什么鬼东西?” //它会通过riid来问我们是什么东西。仅仅有它问到我们是不是IID_IDispatch或我们是不是IID_IUnknown时,我们才干肯定的回答它S_OK //由于我们的对象继承于IDispatch。而IDispatch又继承于IUnknown,我们仅仅实现了这两个接口,所以仅仅能这样来回答它的询问 HRESULT STDMETHODCALLTYPE CJsCallCppDlg::QueryInterface(REFIID riid, void **ppvObject) { if (riid == IID_IDispatch || riid == IID_IUnknown) { //对的,我是一个IDispatch,把我自己(this)交给你 *ppvObject = static_cast<IDispatch*>(this); return S_OK; } else return E_NOINTERFACE; } //我们知道COM对象使用引用计数来管理对象生命周期,我们的CJsCallCppDlg对象的生命周期就是整个程序的生命周期 //我的这个对象不须要你JavaScript来管,我自己会管。所以我不用实现AddRef()和Release()。这里乱写一些。
//你要return 1;return 2;return 3;return 4;return 5;都能够 ULONG STDMETHODCALLTYPE CJsCallCppDlg::AddRef() { return 1; } //同上。不多说了 //题外话:当然假设你要new出一个c++对象来并扔给JavaScript来管,你就须要实现AddRef()和Release(),在引用计数归零时delete this; ULONG STDMETHODCALLTYPE CJsCallCppDlg::Release() { return 1; }
该讲的都在代码凝视中讲了。简单来说。当JavaScript运行如cpp_object.GetProcessID();的代码时,会先调用GetIDsOfNames,并把"GetProcessID"这个字符串传递进来,我们给它分配一个自拟的ID,紧接着JavaScript会拿着这个ID来调用Invoke。至于參数和返回值怎样传递。代码和凝视写得非常清楚了。
//调用JavaScript的SaveCppObject函数,把我自己(this)交给它。SaveCppObject会把我这个对象保存到全局变量var cpp_object;中 //以后JavaScript就能够通过cpp_object来调用我这个C++对象的方法了 void CJsCallCppDlg::OnBnClickedOk() { CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document(); CComDispatchDriver script; document->get_Script(&script); CComVariant var(static_cast<IDispatch*>(this)); script.Invoke1(L"SaveCppObject", &var); }
只是那个实现实在是太麻烦太恶心了,又会引入一大堆我解释不清楚的东西,所以还是作罢了,这样才是最简洁的实现。
//载入资源文件里的HTML,IDR_HTML1就是HTML文件在资源文件里的ID wchar_t self_path[MAX_PATH] = { 0 }; GetModuleFileName(NULL, self_path, MAX_PATH); CString res_url; res_url.Format(L"res://%s/%d", self_path, IDR_HTML1); m_webbrowser.Navigate(res_url, NULL, NULL, NULL, NULL);
CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document(); CComDispatchDriver script; document->get_Script(&script);这样获取其接口指针进行C++调用Javascript操作,这样往往会取到空指针,由于m_webbrowser.Navigate()调用完成,并不意味着HTML文档已经载入、渲染完成,m_webbrowser.Navigate()实际上是一个异步操作,调用以后仅仅是发出了一个命令,让WebBrowser去载入这个HTML文档。至于何时载入完成,能够处理WebBrowser的DocumentComplete事件来获知,仅仅有在触发DocumentComplete事件后,才干够获取其接口指针进行操作。所以在上面的演示样例中,假设想让HTML文档载入完成后就自己主动用C++调用Javascript的SaveCppObject()函数,把C++对象传递过去,仅仅需把上面演示样例程序中我写在button响应函数中的代码写到DocumentComplete事件的响应函数中就可以(Github上的演示样例代码已经更新成这样了)。
本系列其他文章:
《VC与JavaScript交互(一) ———— 怎样实现》
《VC与JavaScript交互(二) ———— 调用JS函数》
本文由CharlesSimonyi发表于CSDN博客:http://blog.csdn.net/charlessimonyi/article/details/50984903转载请注明出处