Code for this article: Oct99VP.exe (230KB)
George Shepherd is a Senior Software Engineer at Stingray Software (www.stingsoft.com), an MFC/Java class library company, where he develops Visual CASE. George co-wrote MFC Internals (Addison-Wesley, 1996).|
Q I've heard about a COM-based technology called
Active Scripting. What is it, and how can I use it in my
A Scripting has been a part of COM since its inception. You can go back and read Inside OLE, by Kraig Brockschmidt (or any of the older COM literature), for extensive descriptions rationalizing the IDispatch interface. In the early days of COM, the story usually went something like: "Each application has its own scripting language interface. Rather than having multiple applications each expose their own macro language, let's separate the language you use to script your application from the application itself. It would be better for applications to provide a single interface that some script-based environment (like Visual Basic® or Visual Basic for Applications) can understand so that high-level application users can easily script their applications."
Thus was born Automation, which is based on an interface named IDispatch. If your application (or objects within your app) implements the IDispatch interface, then it is said to be scriptable. That is, you can write some scripting code (like Visual Basic) and have the script code access methods and properties on COM objects.
Of course, scripting your application is useful in itself. However, these days Visual Basic has gone from being a simple interpreted language to being a full-blown development tool. But scripting is still a valuable tool, and it's used all over the place, especially in Web pages, as scriptlets buried inside some HTML code.
In addition to providing the ability for someone to script your application or object using IDispatch, sometimes you want to incorporate the scripting engine as part of your application. For example, your users may not have access to a scripting environment. In that case, you would want to include a script parser with your application. Also, there are other scripting languages out there (including JScript®, VBScript, and more esoteric languages like Perl and Lisp) that you might want to use for writing scripts.
That's where Active Scripting comes in. The idea behind Active Scripting is to abstract the mechanics of scripting behind some COM interfaces so that it's easy to create the client side of a scripting environment. In the early days of COM, your applications and objects became the objects used within a script. Active Scripting adds a new dimension by allowing you to include the script interpreter with your application. This month, I'll look at what it takes to implement an Active Scripting host.
Active Script Engines
There are two sides to the Active Scripting story: Active Scripting engines and Active Scripting hosts. An Active Scripting engine is a COM component that implements the scripting interfaces and understands how to parse the syntax of a certain scripting language. Under Windows NT 4.0, Microsoft provides scripting engines for VBScript and JScript. Because the Active Scripting mechanism is hidden behind COM interfaces, you can include multiple, disparate scripting engines in your application. In fact, you don't even need to use Microsoft scripting engines; you can use one from another vendor, or you can write your own.
An Active Scripting host is responsible for instantiating one of the available scripting engines and instructing the engine to run a script. There are Active Scripting hosts out there, including Microsoft® Internet Explorer.
Looking in the registry of a machine running Windows NT 4.0, you'll see that Microsoft provides two Active Scripting engines: one for VBScript and one for JScript. Examining their entries in the registry using OLEViewer, you'll see the details of these engines. The VBScript engine lives in a file named VBScript.DLL and the JScript engine lives in a file name JScript.DLL. They implement the Active Script Engine and Active Script Engine With Parsing categories.
The VBScript engine implements the following interfaces: IActiveScript, IActiveScriptDebug, IActiveScriptParse, IActiveScriptStats, IObjectSafety, IRemoteApplicationDebugEvents, and IVariantChangeType. The JScript engine implements all the interfaces listed previously as well as IActiveScriptParseProcedure and IActiveScriptParseProcedureOld.
For just getting a host up and running, IActiveScript and IActiveScriptParse are the most important interfaces. Let's take a deeper look at these interfaces. Figure 1 shows the IActiveScript interface, which is the core interface implemented by a scripting engine. Hosts acquire this interface first and use it to initialize and drive the scripting engine. The most important methods that I'll look at from this interface include SetScriptSite, AddNamedItem, SetScriptState, and GetScriptDispatch.
Figure 2 shows the IActiveScriptParse interface. The main job of IActiveScriptParse is to get script code into the scripting engine. As it turns out, there are two ways to do this. You can ask the scripting engine to load the script text from some persistent medium (through one of the IPersistXXX interfaces), or you can represent the scripting text as a BSTR and load it into the engine using IActiveScriptParse (that's how it works in this month's sample). The main method I'll look at within IActiveScriptParse is ParseScriptText.
An Active Scripting engine is just like any other COM objectyou create the object using CoCreateInstance and acquire interfaces through QueryInterface. Like many other COM-based technologies, Active Scripting depends upon bidirectional communication between the Active Scripting engine and the Active Scripting host. For scripting to work, the host needs to implement an interface named IActiveScriptSite so the scripting engine has a way to call back to the host.
The Active Script Site
When writing an Active Scripting host, your job is to implement the IActiveScriptSite interface and plug it into the scripting engine. Figure 3 shows the IActiveScriptSite interface, which the scripting host implements. The scripting engine then calls on the script site from time to time to get information about the objects being scripted, inform the site about changes in the state of the scripting engine, and inform the site when there's an error running the script.
To illustrate Active Scripting, I've created an ATL-based composite control that behaves as a scripting host. Figure 4 shows the control being used within a dialog box. The control includes an edit box to let you edit script text, a listbox showing the subroutines found within the script, and buttons to load the script, connect to the script, retrieve a list of subroutines within the script, test the events, and reset the script. The control is simply loaded into an MFC-based dialog application.
Figure 4 The Active Script Host Composite Control
The Active Script Host
The Active Script host is where all the meat is. The job of the Active Script host is to:
A C++ class named CActiveScriptSite implements the IActiveScriptSite interface. This is a simple C++-based COM class. I didn't use ATL because CActiveScriptSite is fairly straightforward, implementing only one interface named IActiveScriptSite. Figure 5 shows the most important parts of CActiveScriptSite.
- Instantiate the scripting engine and set the script site
- Parse (or load) the script
- Inform the scripting engine about items used in the scripting (add named items) and run the script
The CActiveScriptSite class includes a function named InitScriptEngine that creates the VBScript engine component and retrieves the interface pointers necessary to drive the scripting engine. Figure 6 shows the InitScriptEngine function.
Named Items and Type Information
Once the scripting engine has been initialized, the host will want to add named items to the scripting engine. When doing this, you're simply telling the scripting engine that you have an IUnknown pointer and some type information about a scripting item.
Another good way to think about adding named items is that you're setting up a namespace for the scripting engine. The scripting engine wants to see a name for each top-level item imported into the scripting engine's namespace. For example, a top-level object in VBScript is often the form. Hosts call IActiveScript::AddNamedItem to create entries in the engine's namespace. You only have to add named items for top-level itemsyou don't have to name sublevel items (such as controls on an HTML page). To bring this into perspective, a Visual Basic form could be considered a named item.
Whenever the scripting engine needs information about the items you've named, the scripting engine calls back to the site through IActiveScriptSite::GetNamedItem whenever the scripting engine needs either an interface pointer to the named item or some type information about the named item. The scripting engine knows only about the name of the itemit's the scripting host's responsibility to keep track of the interface pointer and type information of a named item.
The sample app associates named items, interface pointer, and type information in a structure called NamedItem. Then the script maintains a collection of these structures. In this sample, the site includes a wrapper function that takes the name of the item and an IUnknown pointer. When a named item is added, the site creates a NamedItem structure, uses the IUnknown pointer to retrieve the item's ITypeInfo pointer (through IProvideClassInfo2), caches the item information in a collection, and then registers the named item with the scripting engine. As you'll see shortly, the script engine will consult the scripting host for type information of the named items. Figure 7 shows how to add a named item.
The Active Scripting Callbacks
The IActiveScript interface includes methods for calling into the scripting engine to set it up. Once you've created the scripting engine and connected the site to it, the scripting engine will start calling back to the site through OnStateChange and GetItemInfo as things happen within the scripting engine. Figure 8 shows an implementation of OnStateChange that prints out a debugging string whenever the state of the scripting engine changes. By implementing this function, you know when the script has been loaded, when the event sink has been set up, and so forth. Figure 9 lists the states exhibited by the scripting engine.
The other important IActiveScriptSite function called by the scripting engine is GetItemInfo. The scripting engine calls this function whenever the script needs the type information (represented by an ITypeInfo pointer) of a named item. Figure 10 shows how the sample control implements GetItemInfo. The control implements GetItemInfo by searching through the list of named items held by the CActiveScriptSite object, looking for the named item represented by the name given during AddNamedItem. For example, the scripting engine needs the item's interface pointer to set up an event sink.
A script isn't very useful without a way of receiving events. That is, you want to be able to fire events over to the script engine to tell the script to do certain things. For example, one event you may want to fire to the script engine is the Load event to let the script know that it's been loaded and is ready to run. The named items that you add typically support connections (IConnectionPointContainer and IConnectionPoint). Getting the script engine connected to the named items is simply a matter of calling IActiveScript::SetScriptState and passing SCRIPTSTATE_
CONNECTED (after loading and parsing the script text). From there you can have your named items fire events to the scripting engine to get the ball rolling.
In addition to kicking off the script by firing an event to the script engine, you can invoke the individual subroutines defined in the script. You can get the names of the subroutines defined in the script by getting the script engine's IDispatch pointer (by calling IActiveScript::GetScriptDispatch) then asking for the type information (through IDispatch::GetTypeInfo). Unfortunately, ITypeInfo is a huge and gnarly interface. However, you can get the code names of the subroutines through ITypeInfo.
The sample code has a couple of smart classes that wrap the type information and extract method names easily. Once you have the names of the methods defined in the script, it's a simple matter to call the scripting engine's IDispatch::Invoke function and run the method. Figure 11 shows the code for calling a subroutine within the script.
So there it isActive Scripting in a nutshell. The basic idea is that the mechanics of parsing and running a script are hidden behind abstract interfaces. Just create an instance of the script engine, load some script text, and kick the script off by firing an event.
There are actually some more issues involved in Active Scripting that I haven't had the opportunity to address here. These include window management and script debugging, which I'll take a look at in a later column.
If you're looking for more examples of Active Scripting hosts, check out the ActiveX control test container (TSTCON32.EXE) and a program named MFCAXSVB.EXE available at http://support.microsoft.com/download/support/mslfiles/MFCAxs.exe.
Have a question about programming in
Visual Basic, Visual FoxPro, Microsoft
Access, Office, or stuff like that? Send
your questions via email to George Shepherd at email@example.com.
From the October 1999 issue of Microsoft Systems Journal.
Get it at your local newsstand, or better yet, subscribe.