Introduction
JavaScript has some built-in classes like Object
, Array
, and Date
. It is often asked how to create instances of these classes from C++. Assuming you are familiar with COM, you should be able to understand this solution.
How does it work?
To create new instances, you would usually write JavaScript code like:
var o = new Array();
If you host JScript in a C++ application (by using the scripting host directly or by using the WebBrowser control to display a website with JavaScript code embedded), you can't simply call new
from your C++ code. Also, there is no way to create an array via CoCreateInstance
since you don't have a CLSID
. Instead, you have to manually do what JScript does when you construct a new object.
The following happens when you use the new
-operator in JScript:
- The object on which
new
is called will be asked for a property with the name of the class of which you want a new instance (e.g., "Array
").
- The returned property is asked for an
IDispatchEx
interface.
InvokeEx
is called on the returned interface with a DISPID
of 0 and the flag DISPATCH_CONSTRUCT
. This will construct the instance and return the newly created object.
After these steps, you will have your Array
.
Obtain the scripting object
There are usually two situations where you have to deal with JavaScript from your C++ application. One is that you host a WebBrowser control to display HTML pages, the second is that you use the JScript scripting host directly. In the second case, you will have some IActiveScript
pointer somewhere. Just call GetScriptDispatch()
on that interface to get the scripting object:
CComPtr<IDispatch> pScriptDisp;
HRESULT hr = pScriptEngine->GetScriptDispatch(SCRIPT_ITEMNAME, &pScriptDisp);
if (FAILED(hr))
return hr;
If you host a WebBrowser control, you will have a IWebBrowser2
pointer. Ask for the current IHTMLDocument2
, ask this document for the parent window (IHTMLWindow2
interface). This will be the scripting object:
CComPtr<IDispatch> spDoc;
hr = spBrowser->get_Document(&spDoc);
if (FAILED(hr))
return hr;
CComQIPtr<IHTMLDocument2> spHTMLDoc(spDoc);
if (!spHTMLDoc)
return E_NOINTERFACE;
CComPtr<IHTMLWindow2> spWindow;
hr = spHTMLDoc->get_parentWindow(&spWindow);
if (FAILED(hr))
return hr;
CComQIPtr<IDispatch> pScriptDisp(spWindow);
if (!pScriptDisp)
return E_NOINTERFACE;
Obtaining the object constructor
Using the new
operator in JavaScript first asks for a property contained in the object on which new
is called. So if you say new Array()
inside the JavaScript of an HTML page, the window
object is the one you ask for a property with the name "Array". Everything inside JavaScript is in fact a property of something. If you write the following Javascript code inside an HTML page:
function doSomething()
{
}
you add a property of type function
to your window
object with the name of "doSomething".
If you then construct a new instance by saying var o = new doSomething();
, you call this property in a "special way" which is different by then just saying var o = doSomething();
. The latter will obtain the DISPID for doSomething
and call Invoke
on the window-object with this DISPID and the flag DISPATCH_METHOD
. Use var o = new doSomething();
queries for a property named "doSomething
" (which is also an object) and then calls InvokeEx
on this object with the flag DISPATCH_CONSTRUCT
. But first, let's get the property named "Array":
DISPID did = 0;
LPOLESTR lpNames[] = {L"Array"};
hr = pScriptDisp->GetIDsOfNames(IID_NULL, lpNames, 1,
LOCALE_USER_DEFAULT, &did);
if (FAILED(hr))
return hr;
CComVariant vtRet;
DISPPARAMS params = {0};
CComVariant vtResult;
hr = pScriptDisp->Invoke(did, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET, ¶ms
, &vtResult, NULL, NULL);
if (FAILED(hr))
return hr;
Now we have something like a creator for Array
objects. The creator will query now for a new Array
. This is done by first getting an IDispatchEx
interface from the creator:
CComQIPtr<IDispatchEx> creator(vtResult.pdispVal);
if (!creator)
return E_NOINTERFACE;
The IDispatchEx
interface extends IDispatch
and supports objects that can be dynamically expanded with new properties. This is exactly the way our new property doSomething
is added to the window, by using its IDispatchEx
interface. (See http://msdn.microsoft.com/en-us/library/sky96ah7%28VS.85%29.aspx for details about IDispatchEx
.) Here we only need the InvokeEx
method, which is:
HRESULT InvokeEx(
DISPID id,
LCID lcid,
WORD wFlags,
DISPARAMS *pdp,
VARIANT *pVarRes,
EXCEPINFO *pei,
IServiceProvider *pspCaller
);
Using id = DISPID_VALUE
and wFlags = DISPATCH_CONSTRUCT
does the constructor magic:
DISPPARAMS params = {0};
CComVariant vtResult;
HRESULT hr = creator->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT, DISPATCH_CONSTRUCT
, ¶ms, &vtResult, NULL, NULL);
If everything went well, you have the newly created object now, whether it is a built-in-object or some object you defined in JavaScript. Yes, of course, you can also construct objects you defined in your JS-code.
function MyObject()
{
this.foo = "bar";
}
by asking for a property named "MyObject
". And that's more ore less all.
Oh, yes, you might want to pass some parameters to your constructor. Use the DISPPARAMS
parameter in the InvokeEx
call to pass arguments. Remember that the parameters have to be given in right-to-left-order, so the first parameter that arrives at the constructor is the last one in DISPPARAMS::rgvarg
.
Adding values
If you created a new Object
or Array
, you may want to add values to the new object. This is also done via the IDispatchEx
interface using GetDispID
and InvokeEx
. First, we need the DISPID
for the name (or the index in case of an array) of the property to be added. IDispatchEx
offers the method GetDispID
for this:
HRESULT GetDispID(
BSTR bstrName,
DWORD grfdex,
DISPID *pid
);
This property doesn't exist yet. To create it, pass the flag fdexNameEnsure
in grfdex
that will ensure that a property with the name bstrName
will be created if it does not exist already. Initially, this new property will be a VARIANT
of type VT_EMPTY
. After you have the new DISPID
, all you have to do is call InvokeEx
with DISPATCH_PROPERTYPUT
to add the value:
DISPID did = 0;
hr = theObject->GetDispID(CComBSTR(L"0"), fdexNameEnsure, &did);
if (FAILED(hr))
return hr;
CComVariant data(_T("bar"));
DISPID namedArgs[] = {DISPID_PROPERTYPUT};
DISPPARAMS params = {&data, namedArgs, 1, 1};
hr = theObject->InvokeEx(did, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, ¶ms,
NULL, NULL, NULL);
As you can see, the rgdispidNamedArgs
member of DISPPARAMS
is used here; otherwise, the call will fail with DISP_E_PARAMNOTOPTIONAL
.
Putting it all together, our PutJsArray
looks like the following now. I use a CAtlArray<CComVariant>
which is typedef
'ed to CAtlVariantArray
to pass the values and a JS-function named lpsName
that is called to pass the newly created array to JavaScript.
HRESULT CJsArrayView::PutJsArray(LPOLESTR lpsName, CAtlVariantArray& data)
{
CComPtr<IDispatch> scriptDispatch;
HRESULT hr = GetScriptDispatch(&scriptDispatch);
if (FAILED(hr))
return hr;
ATLASSERT(scriptDispatch);
DISPID did = 0;
LPOLESTR lpNames[] = {L"Array"};
hr = scriptDispatch->GetIDsOfNames(IID_NULL, lpNames, 1,
LOCALE_USER_DEFAULT, &did);
if (FAILED(hr))
return hr;
CComVariant vtRet;
DISPPARAMS params = {0};
CComVariant vtResult;
hr = scriptDispatch->Invoke(did, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET, ¶ms, &vtResult, NULL, NULL);
if (FAILED(hr))
return hr;
if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
return DISP_E_TYPEMISMATCH;
CComQIPtr<IDispatchEx> prototype(vtResult.pdispVal);
if (!prototype)
return E_NOINTERFACE;
vtResult.Clear();
hr = prototype->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT,
DISPATCH_CONSTRUCT, ¶ms, &vtResult, NULL, NULL);
if (FAILED(hr))
return hr;
if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
return DISP_E_TYPEMISMATCH;
CComQIPtr<IDispatchEx> theObject(vtResult.pdispVal);
if (!theObject)
return E_NOINTERFACE;
CString sName;
for(size_t n = 0; n < data.GetCount(); n++)
{
sName.Format(_T("%i"), n);
hr = theObject->GetDispID(CComBSTR(sName), fdexNameEnsure, &did);
if (FAILED(hr))
break;
DISPID namedArgs[] = {DISPID_PROPERTYPUT};
DISPPARAMS p = {&data[n], namedArgs, 1, 1};
hr = theObject->InvokeEx(did, LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYPUT, &p, NULL, NULL, NULL);
if (FAILED(hr))
break;
}
params.cArgs = 1;
params.rgvarg = &vtResult;
hr = CallJs(scriptDispatch, lpsName, ¶ms);
return hr;
}
The sample code
The Zip contains a sample project based on WTL and ATL. All the important code is contained in the view class CJsArrayView
. It can easily be changed to MFC, or for usage with either MFC or ATL, or adapted to create other objects than Array
or Object
.