1. 程式人生 > >VC與JavaScript互動(三) ———— JS呼叫C++

VC與JavaScript互動(三) ———— JS呼叫C++

太監的原因:     時隔兩年,VC與JavaScript互動系列的最後一篇關於JavaScript如何呼叫c++的文章終於出爐了。為什麼會隔了那麼久?因為本來打算太監的,可是看到熱情的網友們的眼神,從期望變成了失望,在我的心裡激起了層層波瀾。兩年後的今天,還是堅持把它寫了出來。其實當時剛寫完VC與JavaScript互動(二)的時候,參考網上的資料,已經把JavaScript呼叫c++實現了。可是實現方法太噁心了,程式碼寫出來太複雜太麻煩了,而且還涉及到了一大堆見都沒見過的COM介面,每個介面都是一大堆函式和一大堆引數,雖然實現程式碼寫出來了,但是為什麼這麼寫,根本講不清楚,怕誤人子弟,便可恥的太監了。
    當初為了寫自動開啟網頁,自動填單,自動提交的小程式,看了一下這方面的東西,由於當時只涉及到了VC呼叫JavaScript,沒有涉及到JavaScript呼叫VC,所以也沒有花時間去深入了。這兩年期間,好幾次想把 VC與JavaScript互動(三) 寫出來,可是發現這個東西實在是太麻煩,太複雜,看不透,剪不斷,理還亂,抽刀斷水水更流,舉杯消愁愁更愁。程式碼寫出來以後我總是懷疑是不是搞錯了,感覺是不是走了彎路,直到今天我仍然懷疑是不是有更好更簡單的辦法來實現JS呼叫C++。 這是網上找到的一段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):
  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檔案:

  1. <html>
  2. <head>
  3.     <metacharset="utf-8"/>
  4.     <title></title>
  5.     <scriptlanguage="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.     <pid="process_info"></p>
  28.     <buttontype="button"onclick="ShowMessageBox()">MessageBox</button>
  29.     <buttontype="button"onclick="GetProcessID()">Process ID</button>
  30. </body>
  31. </html>
    然後我在我的CxxDlg裡寫下了如下的兩個成員函式:
  1. DWORD CJsCallCppDlg::GetProcessID()  
  2. {  
  3.     return GetCurrentProcessId();  
  4. }  
  5. void CJsCallCppDlg::ShowMessageBox(constwchar_t *msg)  
  6. {  
  7.     MessageBox(msg, L"這是來自javascript的訊息");  
  8. }  

    接來下,我要用HTML中的這兩個按鈕,分別呼叫這兩個C++函式,其中一個是ShowMessageBox,讓JavaScript呼叫它並傳遞一個字串給它,最終C++這邊通過Windows API的MessageBox實現彈出一個訊息框。另外一個是GetProcessID,Javascript呼叫它,最終C++這邊通過Windows API的GetCurrentProcessId()獲取本程序ID,並給Javascript返回這個ID值,然後顯示到HTML中。

    由於我的CxxDlg繼承了IDispatch,那麼我需要實現IDispatch中的七個純虛擬函式,所以在CxxDlg類的宣告中新增如下七個虛擬函式的宣告:
  1. virtualHRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *pctinfo);  
  2. virtualHRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);  
  3. virtualHRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);  
  4. virtualHRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);  
  5. virtualHRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject);  
  6. virtualULONG STDMETHODCALLTYPE AddRef();  
  7. virtualULONG STDMETHODCALLTYPE Release();  

    然後實現這七個虛擬函式:

  1. //我自己給我的兩個函式擬定的數字ID,這個ID可以取0-16384之間的任意數
  2. enum
  3. {  
  4.     FUNCTION_ShowMessageBox = 1,  
  5.     FUNCTION_GetProcessID = 2,  
  6. };  
  7. //不用實現,直接返回E_NOTIMPL
  8. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfoCount(UINT *pctinfo)  
  9. {  
  10.     return E_NOTIMPL;  
  11. }  
  12. //不用實現,直接返回E_NOTIMPL
  13. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)  
  14. {  
  15.     return E_NOTIMPL;  
  16. }  
  17. //JavaScript呼叫這個物件的方法時,會把方法名,放到rgszNames中,我們需要給這個方法名擬定一個唯一的數字ID,用rgDispId傳回給它
  18. //同理JavaScript存取這個物件的屬性時,會把屬性名放到rgszNames中,我們需要給這個屬性名擬定一個唯一的數字ID,用rgDispId傳回給它
  19. //緊接著JavaScript會呼叫Invoke,並把這個ID作為引數傳遞進來
  20. HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)  
  21. {  
  22.     //rgszNames是個字串陣列,cNames指明這個陣列中有幾個字串,如果不是1個字串,忽略它
  23.     if (cNames != 1)