Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

Extending JScript


Jan99: Extending JScript

Extending JScript

Paul is a project manager at Superscape, working on the Viscape Universal browser. He can be reached at [email protected].


Sidebar: Threading Models and ActiveX Scripting

Microsoft's ActiveX Scripting specification defines a standard mechanism, based upon COM, that lets scripting engines be embedded within host applications. To implement ActiveX Scripting, Microsoft provides a scaled-down version of Visual Basic called "VBScript," and JScript, an implementation of ECMAScript (previously called "Java-Script"). Both scripting engines are included as part of Internet Explorer or can be installed separately (see http://www.microsoft.com/scripting/).

In this article, I'll discuss how you can extend JScript's capabilities by adding support for constructors and arrays when embedding JScript. In the process, I'll present a program called "JScriptTest," which displays a user interface that lets users type and execute JScript source. Although JScriptTest is written with the ActiveX Template Library (ATL), the techniques behind it are not ATL specific and apply to hosts written with MFC or any other library that supports COM. For instance, I've successfully used these techniques to implement ECMAScript support within a VRML97 browser. The complete source code and related files for JScriptTest are available electronically; see "Resource Center," page 5.

Hosting JScript

Listing One is the declaration of class CScriptEngine, which JScriptTest uses to encapsulate an instance of either VBScript or JScript.

JScriptTest communicates with the script via global variables and functions exposed within the namespace of the script. All these variables and functions are members of a single Automation object of the class CGlobals. An instance of CGlobals is passed to CScriptEngine::AddGlobals, which, in turn, calls IActiveScript::Add-NamedItem with the argument SCRIPTITEM_GLOBALMEMBERS.

One of CGlobals' member functions is messageBox (which calls the Win32 MessageBox API). This allows

var x = 42;

messageBox(x);

to display the message "42." (I'll use messageBox throughout this article to show that code is working as expected.)

Constructors

Scripts written in JScript perform tasks by interacting with Automation objects provided by the host application. Such objects export methods via IDispatch or IDispatchEx and are called by the script. Objects may be created by the host and provided to the script (the CGlobals object is an example), or they may be created dynamically by the script itself.

JScript provides the ActiveXObject constructor for dynamically creating ActiveX objects. This can be used as in var x = new ActiveXObject("ServerName.ObjectType");. This approach has some limitations that may make it unacceptable in certain cases. First, there is no way to provide additional arguments to the ActiveXObject constructor, meaning that objects may have to be initialized in a separate step. Next, using this constructor makes it clear that objects created in this way are not normal JScript objects. Finally, it may be necessary to support a particular syntax; the VRML97 specification (http:// www.vrml.org/Specifications/VRML97/), for example, requires that scripts be able to create instances of the SFRotation type as in: var r = new SFRotation(0, 1, 0, 0);. If you wanted to implement the SFRotation type via an ActiveX object, you would not be able to use the ActiveXObject constructor.

To illustrate support for constructors, I've added support for complex numbers to JScript. This lets var c = new Complex(3, 5); create an instance of the complex number 3+5i. Calling the Complex constructor with no arguments creates a complex number initialized to zero. Complex numbers support two properties: r (which represents the real part) and i (which represents the imaginary part). They support three methods: add, multiply, and toString. Example 1, for instance, displays the answer 4+6i. Listing Two is an IDL fragment describing the Complex class.

To support constructors, you'll need to support the IDispatchEx interface, which adds eight functions to IDispatch -- DeleteMemberByDispID, DeleteMemberByName, GetDispID, GetMemberName, GetMemberProperties, GetNameSpaceParent, GetNextDispID, and InvokeEx. As the names of these functions suggest, IDispatchEx adds a great deal of functionality to IDispatch, allowing methods to be added to or removed from an Automation interface at run time. I'll only scratch the surface of the features provided by IDispatchEx, since all that we're interested in is the DISPATCH_CONSTRUCT flag supported by InvokeEx.

Before JScript can call my implementation of IDispatchEx, it needs to obtain a pointer to it. I achieve this by adding a read-only property called Complex of type IDispatch* to the CGlobals object. When JScript comes across the code fragment new Complex(), it queries the CGlobals object for the value of its Complex property. It then calls QueryInterface on that pointer to retrieve a pointer to its IDispatchEx interface. Finally, it calls the InvokeEx function with a dispatch ID of DISPID_VALUE and the DISPATCH_CONSTRUCT flag.

JScriptTest uses an instance of class CJScriptTypeConstructor to implement constructor properties. Instances of this class maintain a pointer to an instance-creation function in the member variable m_pfnCreateObject, allowing the same class to be used to construct multiple types. In the case of Complex, the instance-creation function used is CComplex::Create, which creates instances of the CComplex class. All of CJScriptTypeConstructor's member functions apart from InvokeEx simply return E_NOTIMPL or S_OK as appropriate. Listing Three is the implementation of InvokeEx. The first few lines of InvokeEx check its arguments. If they are okay, it passes a pointer to the parameters passed to the constructor in JScript to the instance-creation function; see Example 2.

Listing Four presents the implementation of CComplex::Create. First, it creates an instance of CComplex and then it examines the DISPPARAMS* passed from JScript. If there are no arguments, it initializes the m_r and m_i members of the new object to zero. If there are two arguments, it attempts to convert them into numbers (with VariantChangeType) and uses them to initialize m_r and m_i.

The implementations of the other members of CComplex are straightforward, with one exception. Because CComplex communicates with JScript via Automation, only types compatible with Automation can be used as function parameters. This means that CComplex functions that take instances of CComplex as arguments are actually passed an IDispatch*. Somehow, given an IDispatch*, you need to get hold of a CComplex*.

This is done using the function DispatchToComplex (see Listing Five). Given an IDispatch*, this function first calls QueryInterface to see if it supports IComplex. If it does not, then it cannot be an instance of CComplex and the function fails. If the call to QueryInterface succeeds, the function uses static_cast to cast the IComplex* to a CComplex*. You cannot immediately use static_cast as the pointer may not have originated within your address space. This approach takes considerable liberties with the COM specification. In this particular case, you know it will work because the JScript engine is an in-process server that supports the "Both" threading model and, therefore, that interface pointers are simply raw C++ vtbl pointers. This technique is not guaranteed to work in the general case, however.

If you feel uneasy about taking such liberties, an alternative approach would be to have one CComplex object access another's state via its IComplex interface. While this is fine for a simple example, it may result in inefficiencies (because inline functions cannot be used). It may also require that you expose more functionality to external clients than you would otherwise. The latter objection may be addressed by supporting a second back door interface (IComplexInternal, perhaps) that is made available only to your own code.

Arrays

At first glance, it appears that arrays in JScript are similar to those provided by other languages. When you look deeper, however, you quickly discover that they are strange beasts. Although it is customary to think of objects and arrays as being distinct types in JScript, they are actually identical -- objects can be treated as arrays and vice versa.

Objects in JScript are a collection of properties indexed by names. These properties can be of any type, including functions (methods are actually properties of functional type). Properties may be accessed in one of two different styles. Example 3(a), for instance, assigns the value 25 to the property indexed by the string salary of the person object. Arrays are objects that have properties whose names are numbers. Names are always strings, so it is a string representation of the array index that is used. Example 3(b), for example, writes the string Fred to the third element of the forenames array. If an expression that evaluates to a number (1+1, for instance) is placed inside the square brackets, it is first evaluated, then automatically converted into a string.

Arrays support a special property, length, the value of which is one greater than the largest array index in use. As Example 4 illustrates, length is automatically adjusted as the array grows, or can be explicitly assigned to. Assigning to length grows or truncates the array as appropriate.

JScriptTest adds support for three-dimensional vectors to JScript. The Vector constructor (implemented via the same method that we used for Complex) creates an array of three elements. These objects support three methods: add, cross (which calculates the vector cross product), and toString. Example 5(a), for instance, prints the answer 0, 0, 1, while Example 5(b) prints the answer 0.2, 0, 0.3.

Listing Six is an IDL fragment describing the Vector class. There is nothing in this fragment that addresses array access. In general, it is not possible to describe JScript array support in IDL, as we would have to provide one property per array element. Although in the specific case of this Vector type, you know that there are only three array elements and you could potentially implement this as three properties. But for other arrays, this will not necessarily be the case. JScriptTest makes use of the class CDispatchWithArrayAccess to provide support for JScript arrays. It defines two pure virtual functions ArrayPut and ArrayGet, which will be called whenever an element of the array is written or read, respectively. Any class that provides support for arrays should derive from this class and provide implementations of these two functions.

CDispatchWithArrayAccess derives from ATL's IDispatchImpl class, and overrides both GetIDsOfNames and Invoke. Whenever JScript attempts to resolve a name it calls GetIDsOfNames. This initially delegates the call to the base class. If the base class cannot resolve the name (it is not declared in the IDL file), it is not the special-case length property and a few basic sanity checks are passed, Example 6 is executed. This code examines the name to see whether all its characters are digits. If they are, then we convert the name into an integer with _wtoi and return a dispatch ID of this number plus the constant, DISPID_ARRAY. DISPID_ARRAY is a large number chosen to be larger than any of the dispatch IDs defined in the IDL file.

When JScript calls Invoke, the function examines the dispatch ID to see if it is greater than or equal to DISPID_ARRAY. If it is, then the call to Invoke represents an array access. You can calculate the array index by subtracting DISPID_ARRAY from the given dispatch ID, and then call either ArrayPut or ArrayGet as appropriate; see Example 7.

Conclusion

The techniques I've described here let host applications implement a more natural interface to JScript than straightforward JScript embedding would provide. It allows experienced JScript programmers to treat objects implemented by the host as though they were native JScript objects.

DDJ

Listing One

class CScriptEngine :    public CComObjectRootEx<CComMultiThreadModelNoCS>,
    public IActiveScriptSite
{
    public:
        CScriptEngine();
        virtual ~CScriptEngine();
        enum EngineType { EngineType_JScript, EngineType_VBScript };
        HRESULT Create(EngineType type);
        HRESULT Close();
        HRESULT ParseScript(const OLECHAR* pszScriptText);
        HRESULT AddGlobals(IDispatch* pDispatch);
        ...
};

Back to Article

Listing Two

[    object,
    uuid(FCC9CDD3-EFFF-11d1-A9F0-00A0244AC403),
    dual
]
interface IComplex : IDispatch
{
    [propget, id(1)] HRESULT r([out, retval] double* pVal);
    [propput, id(1)] HRESULT r([in] double Val);
    [propget, id(2)] HRESULT i([out, retval] double* pVal);
    [propput, id(2)] HRESULT i([in] double Val);
    [id(3)] HRESULT add([in] IDispatch* pArg, 
                                [out, retval] IDispatch** pRetVal);
    [id(4)] HRESULT multiply([in] IDispatch* pArg, 
                                [out, retval] IDispatch** pRetVal);
    [id(5)] HRESULT toString([out, retval] BSTR* pRetVal);
};

Back to Article

Listing Three

HRESULT STDMETHODCALLTYPECJScriptTypeConstructor::InvokeEx(/* [in] */ DISPID id,
                           /* [in] */ LCID /*lcid*/,
                           /* [in] */ WORD wFlags,
                           /* [in] */ DISPPARAMS* pdp,
                           /* [out] */ VARIANT * pvarRes,
                           /* [out] */ EXCEPINFO * /*pei*/,
                           /* [unique][in] */ IServiceProvider* /*pspCaller*/)
{
    // We only know how to handle DISPID_VALUE
    if(id != DISPID_VALUE)
    {
        _ASSERT(false);
        return DISP_E_MEMBERNOTFOUND;
    }
    // This had better be a call to a constructor!
    if(!(wFlags & DISPATCH_CONSTRUCT))
    {
        _ASSERT(false);
        return DISP_E_MEMBERNOTFOUND;
    }
    // We don't know how to handle named arguments
    if(pdp->cNamedArgs != 0)
    {
        _ASSERT(false);
        return DISP_E_NONAMEDARGS;
    }
    // Construct an object and return its IDispatch pointer
    IDispatch* pdisp;
    HRESULT hr = m_pfnCreateObject(pdp, &pdisp);
    if(SUCCEEDED(hr))
    {
        pvarRes->vt = VT_DISPATCH;
        pvarRes->pdispVal = pdisp;
    }
    return hr;
}

Back to Article

Listing Four

HRESULTCComplex::Create(DISPPARAMS* pdp, IDispatch** ppdisp)
{
    // Create the object
    CComObject<CComplex>* pNewObject = new CComObject<CComplex>;


</p>
    if(pdp->cArgs == 0)
    {
        // No arguments - initialize to default (zero)
        pNewObject->m_r = pNewObject->m_i = 0.0;
    }
    else if(pdp->cArgs == 2)
    {
        // Two arguments - first argument is r, second is i (remember that
        // the DISPPARAMS::rgvarg array contains arguments in reverse order)
        VARIANTARG vaTemp;
        VariantInit(&vaTemp);


</p>
        HRESULT hr = VariantChangeType(&vaTemp, &(pdp->rgvarg[0]), 0, VT_R8);
        if(!SUCCEEDED(hr))
        {
            return hr;
        }
        pNewObject->m_i = vaTemp.dblVal;


</p>
        hr = VariantChangeType(&vaTemp, &(pdp->rgvarg[1]), 0, VT_R8);
        if(!SUCCEEDED(hr))
        {
            return hr;
        }
        pNewObject->m_r = vaTemp.dblVal;
    }
    else
    {
        // Number of arguments must be 0 or 2
        return DISP_E_BADPARAMCOUNT;
    }
    pNewObject->AddRef();
    *ppdisp = pNewObject;
    return S_OK;
}

Back to Article

Listing Five

HRESULTCComplex::DispatchToComplex(IDispatch* pdisp,CComObject<CComplex>** ppComplex)
{
  // Given an IDispatch*, convert it (if possible) to a CComObject<CComplex>*
  IComplex* pinterface;
  if(FAILED(pdisp->QueryInterface(IID_IComplex,
        reinterpret_cast<void**>(&pinterface))))
  {
        return DISP_E_TYPEMISMATCH;
  }
  *ppComplex = static_cast<CComObject<CComplex>*>(pinterface);
  pinterface->Release();
 return S_OK;
}

Back to Article

Listing Six

    [        object,
        uuid(FCC9CDD4-EFFF-11d1-A9F0-00A0244AC403),
        dual
    ]
    interface IVector : IDispatch
    {
        [id(1)] HRESULT add([in] IDispatch* pArg, 
                                  [out, retval] IDispatch** pRetVal);
        [id(2)] HRESULT cross([in] IDispatch* pArg,
                                  [out, retval] IDispatch** pRetVal);
        [id(3)] HRESULT toString([out, retval] BSTR* pRetVal);
    };

Back to Article

DDJ


Copyright © 1999, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.