Adding a Script Engine to Assembly Applications
Copyright © 2000 by Ernest Murphy ernie@surfree.com
For educational use only. All commercial use only by written license.
ABSTRACT
Scripting engines are a fast, safe, and inexpensive way to add macro capability to an application. Microsoft supplies both the Visual Basic script engine and the Java script engines free of charge. These are the same engines that run inside Internet Explorer, and they can be used to good advantage.
A simple test application will be demonstrated.
INTRODUCTION
The script engines are packaged as ActiveX components in dynamic link libraries, vbscript.dll and jscript.dll. You probably have both already on your computer. They are quite simple to use, with the minor exception that they work through COM interfaces. (Well, that's minor to me anyway).
The script engine themselves are written to a coherent pattern, so that by simply invoking the coclass for either the VB or the Java script engine, the same application can change it's scripting language. This means we can change languages by simple changing a single constant in our application.
What's a Script Engine Anyway?
Microsoft is very concerned with leveraging their code. When it came time to provide scripting to the Internet Explorer project, they did a very good thing. They packaged the Visual Basic and the Java script as reusable ActiveX components. Then they went one step further: they documented how they worked and gave them away free.
Literally Free. Here is a direct quite from the Microsoft Scripting Technology page: "Licensing the JScript & VBScript binary for use in your application is very easy Simply acknowledge Microsoft in the About box of your application. There is no charge for this license."
These scripting languages are well documented and widely used. You would not be forcing your users to learn new tricks just to script your application, making for a wider user acceptance.
How to Connect a Script Engine
Any application that supports the IActiveScriptSite interface may use a script engine. If a visual interface is supported, IActiveSiteWindow should also be implemented.
A script engine connects in to an application in 8 quick and quite simple steps:

Create the Host Document: This is com-speak for start your program application.
Create the ActiveX Scripting Engine: More com-speak, for you only create the object from out of the dll with a call to CoCreateInstance for the IActiveScript interface. Once you have this interface, QueryInterface it for IActiveScriptParse.
Add ScriptSite Interface: So the engine can find your application, give it your IActiveScriptSite interface with a call to IActiveScript.SetScriptSite
Initialize the Engine: Give the engine a chance to initialize itself by calling IActiveScriptParse.InitNew
Add Object References: Should you wish the script to control your application's objects, pass it the name of your top level object with IActiveScript.AddNamedItem. The engine will reply with a request for the object's IUnknown through IActiveScriptSite.GetItemInfo.
Load The Script: Your application needs to have a script to run. Scripts are simply text files containing the source code. Scripting obviously uses run-time compilation. The IActiveScriptParse.
Run the Script: Finally, begin running the script with a call to IActiveScript.SetScriptState with SCRIPTSTATE_CONNECTED.
Supply your hwnd: If your script needs a window (for example, to display a message box), the engine will need the hwnd of your application. It will request this with a call to your applications IActiveSiteWindow.GetWindow method.
The script will now run, and may control your application through the top level object.
Mixing COM with Windows
COM and Windows have two different ways of accessing external data and events. One must understand first how Windows fires it's events so we can execute the COM methods in the proper order. In this case, we only need to do a few things:
Initialize: We load the script engine in the WM_CREATE message handler for the applications main window.
Clean-Up: We do all our clean up (releasing interfaces to unload the script engine) in the WM_DESTROY message handler.
Running The Script: For this demonstration, we put a big command button on the main window captioned "RUN SCRIPT." The handlers for this message loads and runs the script.
The initialization in WM_CREATE is helped along with two procedures, LoadScript and RunScript.
LoadScript looks for a file named "MyScript.vbs," converts it to Unicode and returns a pointer to this string. It also coppies the script directly to the edit box with a SendMessage, so you can see the script being run.
RunScript just keeps all the COM methods to start the script engine in one place for clarity.
You will also need a pair of objects with your application, ActiveScriptSiteClass for the IScriptSite and IScriptSiteWindow interfaces, and ScriptTextClass for the Application object The Application object is the one the script can take charge of to make changes in your application. Thanks to the colib, these objects are simple to define.
Perhaps surprisingly, the IScriptSite and IScriptSiteWindow interfaces are not dispinterfaces. They do not support IDispatch. This is because the script engine is designed to work with these pre-defined interfaces, and it has full prior knowledge of then, thus can early bind.
However, the IScriptText interface of ScriptTextClass MUST be a dispinterface, as the script engine has zero prior knowledge of it's composition. Thus, you will need to define an .IDL file and compile a type library for it, as colib needs the type library to run IDispatch. Additionally, the script engine is allowed to ask for an ITypeInfo interface from us, and again, that means having this library.
Those interested in the class structure definitions for these classes are invited to view the source. This is the same as presented before (See the MyCom2 and AsmCtrl tutorials), with the exception of we need not produce a ClassMap.
The ClassMap is only needed for DllGetClassObject to search for supported classes. Here, we simply pass a direct reference of the class to the colib method AllocObject, instead of having DllGetClassObject do this for us. We can do this because these interfaces are no longer belong to the client, they are part of the application, and the application sets the rules on when objects are instanced.
Implementing the Class Methods
A surprisingly large number of the interface members we need implement just return S_OK or E_NOTIMPLEMENTED and are quite trivial. These will not be discussed.
The ScriptTextClass was designed to allow the script to write a string to the edit control on the main window. Thus, the sole method of this interface is SetText.
The SetText method will the first clear the control of all text with SendMessage EM_SETSEL to select all text, followed by a SendMessage WM_CLEAR. The text from the script is passed to us in a BSTR. BSTRs are Unicode strings, so a buffer for the ASCII must be allocated, then WideCharToMultiByte called to perform the conversion. After this, it is simple to SendMessage WM_SETTEXT to change the text. Finally, the buffer we allocated and also the BSTR are released.
IActiveScriptSite has only a single non-trivial method, GetItemInfo, with of course the longest list of parameters. This method is used to link up an object name string with it's interfaces (as determined by the application).
Since we only support a single named object, the BSTR passed in is compared to our object name. In keeping with Microsoft naming conventions, I named this top object "Application" Rather grandiose, but in keeping with standards.
The engine may ask us for either an IUnknown pointer, or an ITypeInfo, as determined by the dwReturnMask flags. If we are asked for the "Application" object the flags are checked, and the appropriate pointers returned. Note that IDispatch has a GetTypeInfo method. This is used to supply the ITypeInfo pointer.
IActiveScriptSiteWindow is required if we wish the script to be able to be 'visual,' meaning display message boxes and such. Thus, the GetWindow method is filled in to return the hwnd of the main application window.
Really, that was just 3 procedures we needed to implement to get the script engine in charge of the application.
Building the ScripTxt Sample
The sample application may be build using the MASM32 package (cuttently version 6) with Service Pack 1 installed. The SP is needed for the COM extensions.
Two new COM include files are contained in the sample: activscp.inc and msscript.inc. These should be moved to /masm32/COM/include/.
ScripTxt.asm will then build properly from Quick Editor using the BUILD ALL setting.
Writing Scripts to Control the Application
You now have the full power of the VBScript engine to work for you. It is quite a powerful language.
Additionally, we have passed the engine a reference to our ScriptTextClass object, so the script may control the edit control text directly (as long as all it needs do is replace it all). Note in a REAL application more methods would have been added to the ScriptSiteClass to give it more 'scriptability.'
The ScriptSiteClass instance, named "Application" is passed to the script. Thus in VBScript we may access the edit box as so:
Application.SetText = "Some text here."
MyVariable = "Some other text"
Application.SetText = MyVariable
In the script sample provided, the script is used to do a simple calculation and display the result on the edit control. Also, some text input boxes are used to let you pick what the edit control should say, so you see the script is really doing as described.
Conclusion
Scripting engines provide a simple way to add a very powerful macro language to your application. The only cost is "About screen" credit to the company we all love to hate.