VC与JavaScript交互(三) ———— JS调用C++

太监的原因:

    时隔两年,VC与JavaScript交互系列的最后一篇关于JavaScript如何调用c++的文章终于出炉了。为什么会隔了那么久?因为本来打算太监的,可是看到热情的网友们的眼神,从期望变成了失望,在我的心里激起了层层波澜。两年后的今天,还是坚持把它写了出来。其实当时刚写完VC与JavaScript交互(二)的时候,参考网上的资料,已经把JavaScript调用c++实现了。可是实现方法太恶心了,代码写出来太复杂太麻烦了,而且还涉及到了一大堆见都没见过的COM接口,每个接口都是一大堆函数和一大堆参数,虽然实现代码写出来了,但是为什么这么写,根本讲不清楚,怕误人子弟,便可耻的太监了。

    当初为了写自动打开网页,自动填单,自动提交的小程序,看了一下这方面的东西,由于当时只涉及到了VC调用JavaScript,没有涉及到JavaScript调用VC,所以也没有花时间去深入了。这两年期间,好几次想把 VC与JavaScript交互(三) 写出来,可是发现这个东西实在是太麻烦,太复杂,看不透,剪不断,理还乱,抽刀断水水更流,举杯消愁愁更愁。代码写出来以后我总是怀疑是不是搞错了,感觉是不是走了弯路,直到今天我仍然怀疑是不是有更好更简单的办法来实现JS调用C++。 为什么说它非常麻烦和复杂,可以看这里http://dgj0600.blog.163.com/blog/static/440604322012102325015495/
这是网上找到的一段JS调用C++的代码,密密麻麻的,根本不知道该怎么把它解释清楚。

    实际上关于VC与JavaScript交互,最熟悉它的人应该是开发Activex控件及IE的BHO插件的程序员,他们一定能讲清楚其中的原理,讲清楚每一个API和接口的用法。不过搞这些的人越来越少了,现在WEB上的Activex控件也是越来越少了,关于ATL的书都在10年前就绝版了,可想而知现在还有多少人研究这个东西。

吐槽WebBrowser:

    WebBrowser这个东西真是让人爱又让人恶心,刚开始使用觉得挺简单的,导航、刷新、前进、后退、获取其中的HTML,都还比较易用,很快就爱上了它。但稍微深入后便发现了这种闭源软件的弊端,难以扩展和改造!比如要用WebBrowser开发一个多进程浏览器,如何在进程间共享Cookie。比如要针对不用的URL设置不同的HTTP代理来访问。比如要让它支持需要用户名密码验证的HTTP/SOCKS5代理等。WebBrowser根本没有提供这种接口来实现这些功能,只能是通过API Hook等办法来实现,既麻烦又不稳定可靠。而且WebBrowser这个东西还非常慢,本来IE就已经够慢了,WebBrowser作为IE的简化版,当它嵌入到我们的程序中时,WebBrowser中的HTML排版、渲染引擎、JavaScript解释器居然都是运行在我们程序的主线程(UI线程)中!所以你可以发现,如果WebBrowser加载一个内容非常多,非常复杂的页面时,在加载期间,你的程序就像假死了一样,同样如果HTML页面上的JavaScript代码在进行繁杂的运算时,你的程序界面又假死了。因为你的UI线程在运行JS解释器,你的UI线程在解释JavaScript代码并执行,在那期间它抽不出来空来去处理Windows消息循环,便假死了。

点赞CEF:

    在此强烈推荐CEF(Chromium Embedded Framework),即Chromium版的WebBrowser。Chromium就不用说了,它的快是非常出名的,即便作为控件来使用,CEF也运用了多进程技术,HTML的渲染和JavaScript的解释执行都是在格外的进程中,不会影响你的UI线程,奔溃了也不会破坏你的进程。而且CEF是用C++写的,对外提供的原生接口就是C++接口,比起WebBrowser的那套COM接口来说不知道好用多少倍。

JavaScript调用C++的一个相对简单的实现:

简述:
    上一章说到,一个 JavaScript对象传到了C++这边以后,就变成了一个IDispatch*, 然后我们用CComDispatchDriver接管这个IDispatch*后,就可以调用这个JavaScript对象的方法,获取这个JavaScript对象的属性,实际上CComDispatchDriver就是对IDispatch的包装,最终都是调用IDispatch::Invoke。同理,如果我们在C++这边构造出一个IDispatch*并传递给JavaScript,那么JavaScript就可以把这个IDispatch*当做一个JavaScript对象来使用,自然它就可以调用这个对象的方法,修改这个对象的属性,最终就可以实现调用C++函数,修改C++对象的成员变量,实际上JavaScript调用C++也是通过IDispatch::Invoke来调用。那么如何构造这个IDispatch就是问题的关键点。

实现:
    直接上代码,首先我建的是一个MFC对话框项目,WebBrowser已经拖上去了,添加为成员变量 m_webbrowser。然后修改MFC为我们生成的对话框类CxxDlg(我的项目名为JsCallCpp,所以我的示例代码中就是CJsCallCppDlg):
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class CJsCallCppDlg : public CDialogEx, public IDispatch  
  2. {  
  3. ...  
  4. }  
    将其多重继承于IDispatch。啊!多重继承?怎么把这种坑爹的东西搞出来?NO NO NO,不要谈多重继承就色变,这里的IDispatch里面的所有成员函数都是纯虚函数,本质上IDispatch就是个接口,C++的实现接口的方式就是多重继承,虽然不鼓励用多重继承来继承实现代码,但是像这样用来实现接口是面向对象中非常常用的。当然你也可以class MyIDispatch : public IDispatch,然后把MyIDispatch实例化成一个对象后传递给 JavaScript来调用。这里之所以用CxxDlg来实现IDispatch,是为了方便,因为待会儿,我只要把CxxDlg的this指针传递给JavaScript,它就可以调用我的CxxDlg从IDispatch处继承来的虚函数Invoke,也就是说JavaScript就可以直接调用CxxDlg::Invoke,然后在CxxDlg::Invoke中可以很方便的调用我CxxDlg的其它成员函数。

    然后我写下了如下的HTML文件:

[html]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <html>  
  2. <head>  
  3.     <meta charset="utf-8" />  
  4.     <title></title>  
  5.     <script language="javascript">  
  6.         function ShowMessageBox()  
  7.         {  
  8.             if (cpp_object != null)  
  9.                 cpp_object.ShowMessageBox("你好,我是Javascript,你是谁?");  
  10.         }  
  11.         function GetProcessID()  
  12.         {  
  13.             if (cpp_object != null)  
  14.             {  
  15.                 var id = cpp_object.GetProcessID();  
  16.                 document.getElementById("process_info").innerText = "本进程ID为:" + id;  
  17.             }  
  18.         }  
  19.         function SaveCppObject(obj)  
  20.         {  
  21.             cpp_object = obj;  
  22.         }  
  23.         var cpp_object;  
  24.     </script>  
  25. </head>  
  26. <body>  
  27.     <p id="process_info"></p>  
  28.     <button type="button" onclick="ShowMessageBox()">MessageBox</button>  
  29.     <button type="button" onclick="GetProcessID()">Process ID</button>  
  30. </body>  
  31. </html>  
    然后我在我的 CxxDlg里写下了如下的两个成员函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. DWORD CJsCallCppDlg::GetProcessID()  
  2. {  
  3.     return GetCurrentProcessId();  
  4. }  
  5.   
  6. void CJsCallCppDlg::ShowMessageBox(const wchar_t *msg)  
  7. {  
  8.     MessageBox(msg, L"这是来自javascript的消息");  
  9. }  

    接来下,我要用HTML中的这两个按钮,分别调用这两个C++函数,其中一个是ShowMessageBox,让JavaScript调用它并传递一个字符串给它,最终C++这边通过Windows API的MessageBox实现弹出一个消息框。另外一个是GetProcessID,Javascript调用它,最终C++这边通过Windows API的GetCurrentProcessId()获取本进程ID,并给Javascript返回这个ID值,然后显示到HTML中。

    由于我的CxxDlg继承了IDispatch,那么我需要实现IDispatch中的七个纯虚函数,所以在CxxDlg类的声明中添加如下七个虚函数的声明:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *pctinfo);  
  2.   
  3. virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);  
  4.   
  5. virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);  
  6.   
  7. virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);  
  8.   
  9. virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject);  
  10.   
  11. virtual ULONG STDMETHODCALLTYPE AddRef();  
  12.   
  13. virtual ULONG STDMETHODCALLTYPE Release();  

    然后实现这七个虚函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //我自己给我的两个函数拟定的数字ID,这个ID可以取0-16384之间的任意数  
  2. enum  
  3. {  
  4.     FUNCTION_ShowMessageBox = 1,  
  5.     FUNCTION_GetProcessID = 2,  
  6. };  
  7.   
  8. //不用实现,直接返回E_NOTIMPL  
  9. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfoCount(UINT *pctinfo)  
  10. {  
  11.     return E_NOTIMPL;  
  12. }  
  13.   
  14. //不用实现,直接返回E_NOTIMPL  
  15. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)  
  16. {  
  17.     return E_NOTIMPL;  
  18. }  
  19.   
  20. //JavaScript调用这个对象的方法时,会把方法名,放到rgszNames中,我们需要给这个方法名拟定一个唯一的数字ID,用rgDispId传回给它  
  21. //同理JavaScript存取这个对象的属性时,会把属性名放到rgszNames中,我们需要给这个属性名拟定一个唯一的数字ID,用rgDispId传回给它  
  22. //紧接着JavaScript会调用Invoke,并把这个ID作为参数传递进来  
  23. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)  
  24. {  
  25.     //rgszNames是个字符串数组,cNames指明这个数组中有几个字符串,如果不是1个字符串,忽略它  
  26.     if (cNames != 1)  
  27.         return E_NOTIMPL;  
  28.     //如果字符串是ShowMessageBox,说明JavaScript在调用我这个对象的ShowMessageBox方法,我就把我拟定的ID通过rgDispId告诉它  
  29.     if (wcscmp(rgszNames[0], L"ShowMessageBox") == 0)  
  30.     {  
  31.         *rgDispId = FUNCTION_ShowMessageBox;  
  32.         return S_OK;  
  33.     }  
  34.     //同理,如果字符串是GetProcessID,说明JavaScript在调用我这个对象的GetProcessID方法  
  35.     else if (wcscmp(rgszNames[0], L"GetProcessID") == 0)  
  36.     {  
  37.         *rgDispId = FUNCTION_GetProcessID;  
  38.         return S_OK;  
  39.     }  
  40.     else  
  41.         return E_NOTIMPL;  
  42. }  
  43.   
  44. //JavaScript通过GetIDsOfNames拿到我的对象的方法的ID后,会调用Invoke,dispIdMember就是刚才我告诉它的我自己拟定的ID  
  45. //wFlags指明JavaScript对我的对象干了什么事情!  
  46. //如果是DISPATCH_METHOD,说明JavaScript在调用这个对象的方法,比如cpp_object.ShowMessageBox();  
  47. //如果是DISPATCH_PROPERTYGET,说明JavaScript在获取这个对象的属性,比如var n = cpp_object.num;  
  48. //如果是DISPATCH_PROPERTYPUT,说明JavaScript在修改这个对象的属性,比如cpp_object.num = 10;  
  49. //如果是DISPATCH_PROPERTYPUTREF,说明JavaScript在通过引用修改这个对象,具体我也不懂  
  50. //示例代码并没有涉及到wFlags和对象属性的使用,需要的请自行研究,用法是一样的  
  51. //pDispParams就是JavaScript调用我的对象的方法时传递进来的参数,里面有一个数组保存着所有参数  
  52. //pDispParams->cArgs就是数组中有多少个参数  
  53. //pDispParams->rgvarg就是保存着参数的数组,请使用[]下标来访问,每个参数都是VARIANT类型,可以保存各种类型的值  
  54. //具体是什么类型用VARIANT::vt来判断,不多解释了,VARIANT这东西大家都懂  
  55. //pVarResult就是我们给JavaScript的返回值  
  56. //其它不用管  
  57. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid,  
  58.     WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)  
  59. {  
  60.     //通过ID我就知道JavaScript想调用哪个方法  
  61.     if (dispIdMember == FUNCTION_ShowMessageBox)  
  62.     {  
  63.         //检查是否只有一个参数  
  64.         if (pDispParams->cArgs != 1)  
  65.             return E_NOTIMPL;  
  66.         //检查这个参数是否是字符串类型  
  67.         if (pDispParams->rgvarg[0].vt != VT_BSTR)  
  68.             return E_NOTIMPL;  
  69.         //放心调用  
  70.         ShowMessageBox(pDispParams->rgvarg[0].bstrVal);  
  71.         return S_OK;  
  72.     }  
  73.     else if (dispIdMember == FUNCTION_GetProcessID)  
  74.     {  
  75.         DWORD id = GetProcessID();  
  76.         *pVarResult = CComVariant(id);  
  77.         return S_OK;  
  78.     }  
  79.     else  
  80.         return E_NOTIMPL;  
  81. }  
  82.   
  83. //JavaScript拿到我们传递给它的指针后,由于它不清楚我们的对象是什么东西,会调用QueryInterface来询问我们“你是什么鬼东西?”  
  84. //它会通过riid来问我们是什么东西,只有它问到我们是不是IID_IDispatch或我们是不是IID_IUnknown时,我们才能肯定的回答它S_OK  
  85. //因为我们的对象继承于IDispatch,而IDispatch又继承于IUnknown,我们只实现了这两个接口,所以只能这样来回答它的询问  
  86. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::QueryInterface(REFIID riid, void **ppvObject)  
  87. {  
  88.     if (riid == IID_IDispatch || riid == IID_IUnknown)  
  89.     {  
  90.         //对的,我是一个IDispatch,把我自己(this)交给你  
  91.         *ppvObject = static_cast<IDispatch*>(this);  
  92.         return S_OK;  
  93.     }  
  94.     else  
  95.         return E_NOINTERFACE;  
  96. }  
  97.   
  98. //我们知道COM对象使用引用计数来管理对象生命周期,我们的CJsCallCppDlg对象的生命周期就是整个程序的生命周期  
  99. //我的这个对象不需要你JavaScript来管,我自己会管,所以我不用实现AddRef()和Release(),这里乱写一些。  
  100. //你要return 1;return 2;return 3;return 4;return 5;都可以  
  101. ULONG STDMETHODCALLTYPE CJsCallCppDlg::AddRef()  
  102. {  
  103.     return 1;  
  104. }  
  105.   
  106. //同上,不多说了  
  107. //题外话:当然如果你要new出一个c++对象来并扔给JavaScript来管,你就需要实现AddRef()和Release(),在引用计数归零时delete this;  
  108. ULONG STDMETHODCALLTYPE CJsCallCppDlg::Release()  
  109. {  
  110.     return 1;  
  111. }  

    该讲的都在代码注释中讲了,简单来说,当JavaScript执行如cpp_object.GetProcessID();的代码时,会先调用GetIDsOfNames,并把"GetProcessID"这个字符串传递进来,我们给它分配一个自拟的ID,紧接着JavaScript会拿着这个ID来调用Invoke。至于参数和返回值如何传递,代码和注释写得很清楚了。

    注意我的HTML中的JavaScript代码中,我用一个 var cpp_object;全局变量来保存C++对象,然后我还写了一个 SaveCppObject()函数给C++调用,在WebBrowser加载完毕HTML文档后,需要先用C++调用JavaScript的这个SaveCppObject()函数,并把C++对象指针传递给JavaScript,这样JavaScript才能把它保存到var cpp_object;中,才能进行接下来的JavaScript调用C++。C++调用JavaScript的SaveCppObject()方法代码如下:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //调用JavaScript的SaveCppObject函数,把我自己(this)交给它,SaveCppObject会把我这个对象保存到全局变量var cpp_object;中  
  2. //以后JavaScript就可以通过cpp_object来调用我这个C++对象的方法了  
  3. void CJsCallCppDlg::OnBnClickedOk()  
  4. {  
  5.     CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document();  
  6.     CComDispatchDriver script;  
  7.     document->get_Script(&script);  
  8.     CComVariant var(static_cast<IDispatch*>(this));  
  9.     script.Invoke1(L"SaveCppObject", &var);  
  10. }  
    好了,至此,JavaScript调用C++已经完成了。这种方法,需要先把IDispatch*(示例代码中是this,但因为this是CJsCallCppDlg的实例,而CJsCallCppDlg多重继承于IDispatch,实际this就是IDispatch*了)传递给JavaScript,JavaScript把它保存好,然后调用它。网上还有一种方法是,在C++这边再实现IDocHostUIHandler接口,然后通过一系列麻烦的操作,JavaScript那边就可以直接通过window.external来调用C++,而不用var cpp_object;了。不过那个实现实在是太麻烦太恶心了,又会引入一大堆我解释不清楚的东西,所以还是作罢了,这样才是最简洁的实现。

    最后晒上一张运行效果图:



示例代码的整个VisualStudio项目文件可以到这里下载和查看(版本VS2015): https://github.com/charlessimonyi/javascript_call_cpp


    对了,还有一点,写好的HTML文件不仅可以直接和EXE放在一个目录下使用,也可以在VisualStudio中把HTML文件作为资源添加到项目中,这样最终写出来的程序只有一个EXE,HTML文件已经在EXE里面了,至于如何让WebBrowser加载这个HTML文件,可以在 CxxDlg::OnInitDialog()中 使用如下代码:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //加载资源文件中的HTML,IDR_HTML1就是HTML文件在资源文件中的ID  
  2. wchar_t self_path[MAX_PATH] = { 0 };  
  3. GetModuleFileName(NULL, self_path, MAX_PATH);  
  4. CString res_url;  
  5. res_url.Format(L"res://%s/%d", self_path, IDR_HTML1);  
  6. m_webbrowser.Navigate(res_url, NULL, NULL, NULL, NULL);  

常见问题:
①调用m_webbrowser.Navigate()加载一个HTML文档后,不要紧接着就:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document();  
  2. CComDispatchDriver script;  
  3. document->get_Script(&script);  
这样获取其接口指针进行C++调用Javascript操作,这样往往会取到空指针,因为m_webbrowser.Navigate()调用完毕,并不意味着HTML文档已经加载、渲染完毕,m_webbrowser.Navigate()实际上是一个异步操作,调用以后只是发出了一个命令,让WebBrowser去加载这个HTML文档,至于何时加载完毕,可以处理WebBrowser的 DocumentComplete事件来获知,只有在触发DocumentComplete事件后,才可以获取其接口指针进行操作。 所以在上面的示例中,如果想让HTML文档加载完毕后就自动用C++调用Javascript的 SaveCppObject()函数,把C++对象传递过去,只需把上面示例程序中我写在按钮响应函数中的代码写到 DocumentComplete事件的响应函数中即可(Github上的示例代码已经更新成这样了)。
怎么添加DocumentComplete事件响应函数?看下图,先选中WebBrowser控件,再到属性对话框里找想处理的事件,所有的Activex控件的事件响应函数都可以在这里添加。


  • 1
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值