Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Adding Macro Scripting language support to existing MFC Application

0.00/5 (No votes)
18 Jul 2003 7  
Introduction to Microsoft Script Hosting and Adding Macro Scripting language support to existing MFC Application

Introduction

Writing applications that host a script engine to enable people to write scripts to customize and extend applications has proven to be very successful. There are thousands of developers using the Windows® Script engines in their applications and make no mistake about it, the Microsoft implementation is quite adequate if you want to add script (macro) capabilities to your application. The one good choice is to use the Active Scripting technology. First of all, Active Scripting technology uses existing scripting language, so you don't need to learn any new language. If you know how to program with VBScript, JavaScript or even PerlScript, that is all you need. In this article, I will present a simple alternative that allows you to add Scripting support to your application (even existing one).

Description

The Active Scripting architecture consists of a family of COM interfaces that defines a protocol for connecting a scripting engine to a host application. In the world of Active Scripting, a script engine is just a COM object that's capable of executing script code dynamically in response to either direct parsing or loading of script statements, explicit calls to the script engine's IDispatch interface, or outbound method calls (events) from the host application's objects. The host application can expose its automation interfaces to the script engine's namespace, allowing the application's objects to be accessed as programmatic variables from within dynamically executed scripts. A client application that needs to add script support only needs to implement Host portion of this technology. Different vendors may implement their own implementation of Engine, giving you now alternative to use other language that you already know. A good example is the PerlScript engine. A company may decide to use it instead of using JavaScript or VBScript in order to maintain existing code base.

Figure 1: Active Scripting architecture

Figure 2: Active Scripting COM Interaction

Figure 1 shows the basic architecture of Active Scripting, and Figure 2 shows sequence diagram detail of the COM interfaces defined by the architecture. Your client application that needs to use Active Scripting technology, creates and initializes a scripting engine based on the scripting language you want to parse, and you connect your application to the engine via the SetScriptSite method. You can then feed the engine script code that it can execute either immediately (not a function) or at some point in the future (function call), based on the script engine content and its state. For example, the following script text contains only global statements, and therefore could execute at parse time:

ScriptHost.Display("Hello CodeProject guru around the world.");

This statement would force the application to display a message box with the provide text (using the MFCScriptHost.exe Application) but:

function HostDisplay()
{
  ScriptHost.Display("Hello CodeProject guru around the world.");
}

would force the application to display this message only when HostDisplay() is called. But the good news is that this new method can also be accessed by your application whenever you want. To execute this function, your client application (Site object) needs to call GetIDsOfNames and Invoke of the IDispatch pointer of the script engine being used to force the execution of this function. Another cool feature of the Active Scripting technology is that you can add any automation object to the script engine items list and access its methods and properties from your script. In fact, these features are being used inside of Microsoft Office applications, Internet Explorer and Visual Studio. For example, you could have an item named 'Documents' and expose the list of opened documents in your application. Your implementation of the Script Site would call AddNamedItem("Documents") on the script engine interface pointer. For example, in our last example, the script engine gets a dispatch pointer of the "ScriptHost" named-item and call the "Display" method. But internally a lot of this process depends on the state of the Scripting Engine. That is the state of the engine must be started (SCRIPTSTATE_STARTED). At this point (when the engine is started), the engine would query the ActiveScriptSite object to resolve a named-item to a IDispatch interface pointer. It will then access that interface properties and methods by calling GetIDsOfNames and Invoke. This new item then becomes just like an internal variable that can be accessed whenever it needs it. Also, it becomes apparent how simple it is for the script engine to access the named-item properties and methods thanks to these two IDispatch interface methods.

But connecting event function to the script engine is a bit more tricky for many reasons. The more apparent reason is that the script engine must be able to do late-binding event support on a named-item. To support binding events to host named-item, Active Scripting engines use connection points to map outbound method calls/events from the host application's objects onto script functions. The way the named-item event function is called depends on the script language. VBScript uses a whole different approach to bind event call than JavaScript. Andrew Clinick's Scripting Events article give a lot more details than what I could cover here, so you may want to check it out.

I think this may be enough to get you start, now if you want to learn more, please check first the following References at the bottom of this article. This article was updated to show how the ScriptHost object can trigger event to the script.

Adding Scripting Support to your application

Now I have built this procedure that you can use to add scripting support to new or existing MFC application.

  1. The first step consists to creating an .ODL file (if your application doesn't have one). Advanced MFC developers could also fake this process by some others means (since we will not register this library) but I will not cover this here. You will have to generate a GUID by using GUIDGen.exe (available in tools folder of Visual Studio). A typical .ODL file would look like this
    // YourAppName.odl : type library source for YourAppName.exe
    
    // This file will be processed by the MIDL compiler to produce the
    // type library (YourAppName.tlb).
    
    [ uuid(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX), version(1.0) ]
    library YourAppName
    {
        importlib("stdole32.tlb");
        importlib("stdole2.tlb");
    
        //{{AFX_APPEND_ODL}}
        //}}AFX_APPEND_ODL}}
    };
    
  2. Use ClassWizard (Ctrl+W) to create an Automation object derived from CCmdTarget that will be the host object (ActiveScriptSite's client object). Define methods that you want to make available from your host. For example, you may have methods like: CreateActiveX(strProgID),DisplayMessage(strMessage), etc. Typically, these new dispatch methods would be called from a script.
  3. In your new class files, replace all references of CCmdTarget with CActiveScriptHost. At this point you will have to include ActiveScriptHost.h/cpp to your project.
  4. *UPDATED* Override the virtual function HRESULT GetClassID( LPCLSID pclsid ). It must return successfully the CLSID of your host object. The CSLID can be found in the .ODL file once you create the automation object in step 2. Typical implementation will look like this:
    HRESULT CHost_Proxy::GetClassID( LPCLSID pclsid )
    {
        *pclsid = CLSID_Host_Proxy;
        return S_OK;
    }
    Note also that at this point our object is really a COM object and ActiveX control but we will not register as we would normally do.
  5. *UPDATED* You will need to declare the type library that you are using. This step is tricky but using MFC macro, it is a lot easier. Just add DECLARE_OLETYPELIB(CYourHost_Proxy) in the header file of your proxy class and IMPLEMENT_OLETYPELIB(CYourHost_Proxy, _tlid, _wVerMajor, _wVerMinor) in the .cpp file. _tlid is the GUID of the typelibrary (step 1) and _wVerMajor/_wVerMinor represent the version number of your typelibary. Also, use the resource include editor to add these directives.
    #ifdef _DEBUG
    1 TYPELIB "Debug\\YourAppName.tlb"
    #else
    1 TYPELIB "Release\\YourAppName.tlb"
    #endif
  6. *NEW* Now Add an event-source object, for example:
    [ uuid(740C1C2D-692F-43F8-85FF-38DEE1742819) ]
    dispinterface IHostEvent
    {
        properties:
        methods:
        [id(1)] void OnRun();
        [id(2)] void OnAppExit();
    };
    //  Class information for CHost_Proxy
    [ uuid(F8235A29-C576-439D-A070-6E7980C9C3F6) ]
    coclass Host_Proxy
    {
        [default] dispinterface IHost_Proxy;
        [default, source] dispinterface IHostEvent;
    };
    
    As you can see in this example, our Host now supports two events that we can trigger directly from our code by using COleControl::FireEvent function. Such functions are very simple. For example:
    void FireOnRun()
        {FireEvent(eventidOnRun,EVENT_PARAM(VTS_NONE));}
    void FireOnAppExit()
        {FireEvent(eventidOnAppExit,EVENT_PARAM(VTS_NONE));}
    
  7. Create an instance of your host object (can also be a dynamically-created class by using MFC macro) and call CYourHostProxy::CreateEngine( 'Language ProgID' ) which can be 'JavaScript' or 'VBScript' if you want to use these engine.
  8. Add implementation code to your proxy methods to do what you wish to let the advanced users do with your application.
  9. Add any additional named-item object that you wish to access from script language
  10. Provide way for the user to create script or load script text from disk. CActiveScriptHost class provide helper functions that you may want to reuse based on functionalities that you want to give in your application. By the way, it is not safe to let user create Inproc-ActiveX object but Local-server is generally good. One good reason not to let the user create ActiveX control is that, if a crash occurred inside of the ActiveX, your application should not. Local-server give you freely this kind of safety.

Revision History

////////////////////////////////////////////////////////
//  Version history
// v1.01 : Bug fix with accessing object info (ITypeInfo)
// v1.10 : Add 'InvokeFuncHelper' allows to call script 
//         function directly from c++
// v1.5  : Add support for Host event (now derive from COleControl 
//         instead of CCmdTarget)
////////////////////////////////////////////////////////

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here