Adding Macro Scripting language support to existing MFC Application





5.00/5 (14 votes)
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.
- 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}} };
- 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. - In your new class files, replace all references of
CCmdTarget
withCActiveScriptHost
. At this point you will have to include ActiveScriptHost.h/cpp to your project. - *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. - *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 andIMPLEMENT_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
- *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 usingCOleControl::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));}
- 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. - Add implementation code to your proxy methods to do what you wish to let the advanced users do with your application.
- Add any additional named-item object that you wish to access from script language
- 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) ////////////////////////////////////////////////////////