Click here to Skip to main content
15,883,791 members
Articles / Desktop Programming / WTL

Creating JavaScript arrays and other objects from C++

Rate me:
Please Sign up or sign in to vote.
4.12/5 (11 votes)
26 Jun 2010CPOL5 min read 70.3K   1.2K   22   21
How to create JavasSript arrays and other objects from C++ code and pass them to the script.

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:

JavaScript
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:

  1. 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").
  2. The returned property is asked for an IDispatchEx interface.
  3. 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:

C++
// pScriptEngine is of type IActiveScript
// SCRIPT_ITEMNAME is the name you specified in IActiveScript::AddNamedItem
//     can be NULL to retreive the the object containing all global members
CComPtr<IDispatch> pScriptDisp;
HRESULT hr = pScriptEngine->GetScriptDispatch(SCRIPT_ITEMNAME, &pScriptDisp);
if (FAILED(hr))
  return hr;
// pScriptDisp is now the scripting object

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:

C++
// spBrowser is of type IHTMLWindow2
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;
// pScriptDisp should now be scripting object

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:

JavaScript
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":

C++
// get DISPID for "Array"
DISPID did = 0;
LPOLESTR lpNames[] = {L"Array"};
hr = pScriptDisp->GetIDsOfNames(IID_NULL, lpNames, 1, 
                     LOCALE_USER_DEFAULT, &did);
if (FAILED(hr))
  return hr;

// invoke pScriptDisp with DISPATCH_PROPERTYGET for "Array"
CComVariant vtRet;
DISPPARAMS params = {0};
CComVariant vtResult;
hr = pScriptDisp->Invoke(did, IID_NULL, LOCALE_USER_DEFAULT, 
                   DISPATCH_PROPERTYGET, &params
  , &vtResult, NULL, NULL);
if (FAILED(hr))
  return hr;
// vtResult should be of type VT_DISPATCH and contain the creator for Array

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:

C++
// get IDispatchEx on returned IDispatch
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:

C++
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:

C++
// creator is of type IDispatchEx
DISPPARAMS params = {0};
CComVariant vtResult;
HRESULT hr = creator->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT, DISPATCH_CONSTRUCT
  , &params, &vtResult, NULL, NULL);
// vtResult should contain the newly created object

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.

JavaScript
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:

C++
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:

C++
// theObject is of type IDispatchEx and is the js-Array
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, &params,
  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.

C++
HRESULT CJsArrayView::PutJsArray(LPOLESTR lpsName, CAtlVariantArray& data)
{
  // get script dispatch
  CComPtr<IDispatch> scriptDispatch;
  HRESULT hr = GetScriptDispatch(&scriptDispatch);
  if (FAILED(hr))
    return hr;
  ATLASSERT(scriptDispatch);

  // get DISPID for "Array"
  DISPID did = 0;
  LPOLESTR lpNames[] = {L"Array"};
  hr = scriptDispatch->GetIDsOfNames(IID_NULL, lpNames, 1, 
                       LOCALE_USER_DEFAULT, &did);
  if (FAILED(hr))
    return hr;

  // invoke scriptdispatch with DISPATCH_PROPERTYGET for "Array"
  CComVariant vtRet;
  DISPPARAMS params = {0};
  CComVariant vtResult;
  hr = scriptDispatch->Invoke(did, IID_NULL, LOCALE_USER_DEFAULT, 
          DISPATCH_PROPERTYGET, &params, &vtResult, NULL, NULL);
  if (FAILED(hr))
    return hr;
  // check result: should be a VT_DISPATCH
  if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
    return DISP_E_TYPEMISMATCH;

  // get IDispatchEx on returned IDispatch
  CComQIPtr<IDispatchEx> prototype(vtResult.pdispVal);
  if (!prototype)
    return E_NOINTERFACE;

  // call InvokeEx with DISPID_VALUE
  // and DISPATCH_CONSTRUCT to construct new array
  vtResult.Clear();
  hr = prototype->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT, 
             DISPATCH_CONSTRUCT, &params, &vtResult, NULL, NULL);
  if (FAILED(hr))
    return hr;

  // vtresult should contain the new array now.
  if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
    return DISP_E_TYPEMISMATCH;

  // get IDispatchEx on returned IDispatch
  CComQIPtr<IDispatchEx> theObject(vtResult.pdispVal);
  if (!theObject)
    return E_NOINTERFACE;

  // add values by invoking InvokeEx
  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;
  }

  // now call the js-function in lpsName with the array as parameter
  params.cArgs = 1;
  params.rgvarg = &vtResult;

  hr = CallJs(scriptDispatch, lpsName, &params);

  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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Germany Germany
Born in 1968 I do programming since over 25 years now. I started with Basic on a ZX81 and with hacking hexcodes in a Microprofessor before I switched to C++ and other languages.

Since more than 10 years I work as a professional software developer, currently for Salsitasoft in Prague.

Comments and Discussions

 
QuestionThanks a lot ! Pin
PlutusLEE23-Feb-18 21:54
PlutusLEE23-Feb-18 21:54 
QuestionMulti-dimensional arrays Pin
hyzerfool15-Dec-14 7:51
hyzerfool15-Dec-14 7:51 
I needed to send back a 2-dimensional array from C++ to javascript. In case it's useful to anyone, here's the code that finally worked for me. I post it simply because it took me a long time to figure out, so hopefully it can be useful to someone.

Note that, in javascript, my 2 dimensional array really comes as an Object, not an array. So, isArray returns false:
oMyResult = window.external.VPQuery (strQuery);
if (Array.isArray (oMyResult))
    alert ("This is an array!");

But this will be successful:
if( (typeof arrVPResult === "object") && (arrVPResult !== null) )
    alert ("This is an object!");

So I can iterate through it like this:
for (var strProp in arrVPResult) {
    if (arrVPResult.hasOwnProperty (strProp)) {
          //  do something...
    }
}


Edit: If the call to theObject->GetDispID uses a numeric value rather than a text value (where CComBSTR(rarrData[n]) is way down below) then the created object will all act like an array in javascript (and have a .length value). If you use a text value, it will act like an object (and will not have a .length value).


Here's my C++ code:

bool CMyHtmlDispatch::BuildJsArrayTest  (CStringArray& rarrData, VARIANT& vtResult)
{
    CComQIPtr<IDispatchEx>  theObject;
    DISPID                  didMainArray    = 0;
    DISPID                  didCurSubArray  = 0;
    DISPID                  didTemp         = 0;

    //
    //  First create our main array.  The elements in this array will be 
    //  arrays, too, giving us our 2 dimensional array.  We'll operate on
    //  this array using theObject.  As far as I can tell, we don't need didMainArray (the dispatch ID)

    if (! BuildJsArray_CreateObject (vtResult, theObject, didMainArray))
        return false;

    if (NULL == theObject)
        return false;

    //
    // Now create our sub-arrays.  Each of them will contain an array of strings.

    CString strIndex;
    for(size_t n = 0; n < rarrData.GetCount(); n++)
    {
        CComQIPtr<IDispatchEx>  theObjectSubArray;
        _variant_t              vtSubArray;

        //
        //  Just like above, this creates our javascript array object.  We'll refer to it
        //  using theObjectSubArray.  We won't need to use didCurSubArray (the dispatch ID)

        if (! BuildJsArray_CreateObject (vtSubArray, theObjectSubArray, didCurSubArray))
            return false;

        //
        //  Add our data to the sub-array, then we'll add the sub-array to the main array
        //  In our case, we'll just create some test data.  We'll use the value in rarrData 
        //  as the Key name and fill the sub-array values with variations on the key name.

        for (int x = 0; x < 3; x ++) {
            //
            //  First create the key for our index in our sub array.  We'll just use the index
            //  number as the key, though we have to pass it to GetDispID as a string.
            //  fdexNameEnsure makes theObjectSubArray create it for us.  It sends the dispatch ID 
            //  back that we'll use below to set the value for our key in InvokeEx

            strIndex.Format (_T("%d"), x);
            if (FAILED (theObjectSubArray->GetDispID (CComBSTR(strIndex), fdexNameEnsure, &didTemp)))
                break;

            //
            //  Now add our sub-item value string to the sub-array object.   We'll call
            //  InvokeEx on theSubObjectArray and need to pass in didTemp so it knows which
            //  key we're setting the value for.  

            CString s; s.Format (_T("%s - %d"), rarrData[n], x);
            CComVariant vArg (s);

            DISPID      namedArgs[] = {DISPID_PROPERTYPUT};
            DISPPARAMS  p           = {&vArg, namedArgs, 1, 1};

            if (FAILED (theObjectSubArray->InvokeEx(didTemp, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, &p, NULL, NULL, NULL)))
                return false;
        }

        //
        //  OK!  Now, in theory, all the data for rarrData[n] is in didCurSubArray.  Let's
        //  add that to the main array.  Remember that GetDispID returns a dispatch ID that
        //  we'll use in our call to InvokeEx.

        if (FAILED (theObject->GetDispID (CComBSTR(rarrData[n]), fdexNameEnsure, &didTemp)))
            break;

        CComVariant vArg (vtSubArray);

        DISPID      namedArgs[] = {DISPID_PROPERTYPUT};
        DISPPARAMS  p           = {&vArg, namedArgs, 1, 1};

        //
        //  Because our vArg really contains an IDispatch, we have to use DISPATCH_PROPERTYPUTREF 
        //  rather than DISPATCH_PROPERTYPUT.  So here we're associating that sub array with the
        //  key whose dispatch ID is didTemp.

        if (FAILED (theObject->InvokeEx (didTemp, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUTREF, &p, NULL, NULL, NULL))) // DISPATCH_PROPERTYPUT
            return false;

    } // end loop through main data

    return true;
} // end build js array int





//////////////////////////////////////////////////////////////////////////////
//
//  This does the grunt work
//
//  This function turns our MFC array into a native Javascript array.  
//  Javascript can't handle SAFEARRAYS directly.  It _can_ handle them 
//  by using the javascript object VBArray, like this:
//  var arrResult = new VBArray(window.external.CB_CustomFunctionWithParams (oRed, nPi)).toArray()
//
//  But by doing the BuildJsArray stuff it lets the javascript side
//  treat it just like a native javascript array.
//
//  I got this code from:
//  http://www.codeproject.com/Articles/88753/Creating-JavaScript-arrays-and-other-objects-from
//
bool CMyHtmlDispatch::BuildJsArray_CreateObject (VARIANT& vtResult, CComQIPtr<IDispatchEx>& rpObject, DISPID& did)
{
    if (NULL == m_pHtmlView)
        return false;

    CWebDocument2Ptr    pWebDocument2 = m_pHtmlView->GetWebDocument2 ();
    if (NULL == pWebDocument2)
        return false;

    CComQIPtr<IDispatch> scriptDispatch;
    pWebDocument2->get_Script (&scriptDispatch);

    if (!scriptDispatch)
        return false;

    //
    // get DISPID for "Array"

    did = 0;
    LPOLESTR lpNames[] = {L"Array"};
    HRESULT hr = scriptDispatch->GetIDsOfNames(IID_NULL, lpNames, 1, LOCALE_USER_DEFAULT, &did);
    if (FAILED(hr))
        return false;

    //
    // invoke scriptdispatch with DISPATCH_PROPERTYGET for "Array"

    CComVariant vtRet;
    DISPPARAMS params = {0};
    hr = scriptDispatch->Invoke(did, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, ¶ms, &vtResult, NULL, NULL);
    if (FAILED(hr))
        return false;

    //
    // check result: should be a VT_DISPATCH

    if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
        return false;

    //
    // get IDispatchEx on returned IDispatch

    CComQIPtr<IDispatchEx> prototype(vtResult.pdispVal);
    if (!prototype)
        return false;

    //
    // call InvokeEx with DISPID_VALUE
    // and DISPATCH_CONSTRUCT to construct new array

    VariantClear (&vtResult);
    hr = prototype->InvokeEx(DISPID_VALUE, LOCALE_USER_DEFAULT, DISPATCH_CONSTRUCT, ¶ms, &vtResult, NULL, NULL);
    if (FAILED(hr))
        return false;

    //
    // vtresult should contain the new array now.

    if ((VT_DISPATCH != vtResult.vt) || (NULL == vtResult.pdispVal))
        return false;

    //
    // get IDispatchEx on returned IDispatch

    CComQIPtr<IDispatchEx> theObject(vtResult.pdispVal);
    if (!theObject)
        return false;

    rpObject = theObject;
    return true;
}


modified 16-Dec-14 13:04pm.

QuestionTrying to use your code, but get DISP_E_UNKNOWNNAME Pin
pyva.net6-Feb-12 21:36
pyva.net6-Feb-12 21:36 
AnswerRe: Trying to use your code, but get DISP_E_UNKNOWNNAME Pin
imagiro7-Feb-12 6:12
imagiro7-Feb-12 6:12 
GeneralOk, solved the problem! Pin
pyva.net7-Feb-12 21:52
pyva.net7-Feb-12 21:52 
GeneralStunning! Problem not solved (for me) Pin
Elmue19-Feb-14 19:34
Elmue19-Feb-14 19:34 
GeneralRe: Stunning! Problem not solved (for me) Pin
afirst20-Oct-15 16:45
afirst20-Oct-15 16:45 
GeneralPutting a VT_DISPATCH into the array PinPopular
Member 62577428-Feb-11 0:10
Member 62577428-Feb-11 0:10 
GeneralRe: Putting a VT_DISPATCH into the array Pin
imagiro14-Mar-11 7:38
imagiro14-Mar-11 7:38 
GeneralRe: Putting a VT_DISPATCH into the array Pin
dc_20005-May-14 23:16
dc_20005-May-14 23:16 
GeneralCall JS function with single argument (string type) Pin
Ful4-Aug-10 12:22
Ful4-Aug-10 12:22 
GeneralRe: Call JS function with single argument (string type) Pin
imagiro5-Aug-10 10:25
imagiro5-Aug-10 10:25 
QuestionThe same for Mozilla plugin ? Pin
Alkalinou7-Jul-10 1:09
Alkalinou7-Jul-10 1:09 
AnswerRe: The same for Mozilla plugin ? [modified] Pin
imagiro8-Jul-10 8:03
imagiro8-Jul-10 8:03 
GeneralDefinitely looks like looking into Pin
JohnWallis4221-Jun-10 3:49
JohnWallis4221-Jun-10 3:49 
GeneralRe: Definitely looks like looking into Pin
imagiro21-Jun-10 3:56
imagiro21-Jun-10 3:56 
GeneralRe: Definitely looks like looking into Pin
JohnWallis4221-Jun-10 4:01
JohnWallis4221-Jun-10 4:01 
GeneralRe: Definitely looks like looking into Pin
JohnWallis4221-Jun-10 4:07
JohnWallis4221-Jun-10 4:07 
GeneralRe: Definitely looks like looking into Pin
imagiro21-Jun-10 4:09
imagiro21-Jun-10 4:09 
GeneralRe: Definitely looks like looking into Pin
JohnWallis4221-Jun-10 4:17
JohnWallis4221-Jun-10 4:17 
GeneralRe: Definitely looks like looking into Pin
JohnWallis4221-Jun-10 4:24
JohnWallis4221-Jun-10 4:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.