為什么會隔了那么久?由於本來打算太監的,但是看到熱情的網友們的眼神,從期望變成了失望,在我的心里激起了層層波瀾。
兩年后的今天,還是堅持把它寫了出來。事實上當時剛寫完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轉載請注明出處
