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代理來訪問。

比方要讓它支持須要usernamepassword驗證的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):

class CJsCallCppDlg : public CDialogEx, public IDispatch
{
...
}
    將其多重繼承於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>
<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中。

    因為我的CxxDlg繼承了IDispatch。那么我須要實現IDispatch中的七個純虛函數。所以在CxxDlg類的聲明中加入例如以下七個虛函數的聲明:
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。至於參數和返回值怎樣傳遞。代碼和凝視寫得非常清楚了。

    注意我的HTML中的JavaScript代碼中,我用一個 var cpp_object;全局變量來保存C++對象。然后我還寫了一個 SaveCppObject()函數給C++調用。在WebBrowser載入完成HTML文檔后。須要先用C++調用JavaScript的這個SaveCppObject()函數,並把C++對象指針傳遞給JavaScript。這樣JavaScript才干把它保存到var cpp_object;中,才干進行接下來的JavaScript調用C++。C++調用JavaScript的SaveCppObject()方法代碼例如以下:

//調用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);
}
    好了,至此。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()中 使用例如以下代碼:

//載入資源文件里的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);

常見問題:
①調用m_webbrowser.Navigate()載入一個HTML文檔后,不要緊接着就:
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上的演示樣例代碼已經更新成這樣了)。

怎么加入DocumentComplete事件響應函數?看下圖,先選中WebBrowser控件,再到屬性對話框里找想處理的事件,全部的Activex控件的事件響應函數都能夠在這里加入。



本系列其他文章:

《VC與JavaScript交互(一) ———— 怎樣實現》

《VC與JavaScript交互(二) ———— 調用JS函數》


本文由CharlesSimonyi發表於CSDN博客:http://blog.csdn.net/charlessimonyi/article/details/50984903轉載請注明出處


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM