Say Goodbye to Macro Envy with Active Scripting

Say Goodbye to Macro Envy with Active Scripting


Don Box    


With Active Scripting, your application can easily implement a full macro language like VBScript in the comfort of your own code.    


In the past, if you wanted to provide an integrated scripting language in your applications you had two choices. Ambitious developers would invent a new scripting language, create a parser, and integrate it into their product. This was the approach taken by AutoCAD, GNU Emacs, and a myriad of terminal emulation programs. Less ambitious developers would buy an off-the-shelf solution (some of which carried licensing fees in excess of the cost of a BMW) and would integrate the parser into their product according to the language vendor's proprietary API. The ambitious approach often resulted in yet-another-way-to-write-a-for-loop languages that offered little syntactic benefit over other languages. The latter approach implied a varying degree of proprietary integration work that essentially married the developer to one particular language product.
With the advent of ActiveX™, developers have a third choice: Active Scripting. Active Scripting uses existing scripting languages (so you don't need to learn a new syntax for writing for-loops) and uses the Component Object Model (COM), which lets you treat all scripting languages uniformly (so you aren't tied to one particular language implementation or vendor).
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.
   
   
Figure 1: Active Scripting architecture    


 Figure 1 shows the basic architecture of Active Scripting, and Figure 2 and Figure 3 detail the COM interfaces defined by the architecture. Your application 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 text that it can execute either immediately or at some point in the future, based on both the script content (does the script contain procedure definitions or actual executable statements at global scope?) and the state of the engine. For example, the following script text contains only global statements, and therefore could execute at parse time:    


 MsgBox "Hello!"
This statement would display a message box as soon as the statement is parsed. In contrast, this next chunk of script text contains only a procedure definition:    


 Sub showMe()
   MsgBox "I'm shown!"
 End sub
This statement would simply add the showMe routine as a method on the dispatch interface exposed by the engine. To call this routine from your application, you need to use the script engine's GetIDsOfNames and Invoke methods to trigger the execution of the MsgBox statement.
To make an automation interface in your application available as a programmatic scripting variable, you call AddNamedItem to insert a new name into the symbol table of the engine. As the script engine executes code, your GetItemInfo method will be called to look up the IDispatch interface that corresponds to the registered variable name. Assuming that your application calls AddNamedItem("bob") on a script engine, the script code    


 MsgBox bob.GetCurrentTitle()
would request an IDispatch interface for bob through your site object's GetItemInfo method. Once the engine acquires the named dispatch pointer, it dynamically calls the GetCurrentTitle method via GetIDsOfNames and Invoke.
In Figure 1 there are two Active Scripting interfaces (IActiveScript and IActiveScriptParse) that are implemented by language providers to expose their parsing engine to host applications. Another pair of interfaces (IActiveScriptSite and IActiveScriptSiteWindow) must be implemented by host applications to provide a scripting engine with a standard mechanism for accessing the application's automation interfaces and to allow for very simple UI integration with the runtime of the language. One additional interface (IActiveScriptError) allows scripting engines to describe runtime errors in a uniform manner, allowing parsing errors and IDispatch-style exceptions to be communicated to the host application.
In addition to defining these standard interfaces, Microsoft provides two implementations of the script engine interfaces: one that parses and executes JScript and another that parses and executes VBScript. Third-party software vendors plan to provide implementations that parse and execute other scripting languages such as Perl, Rexx, and Python. Any of these languages can be accessed polymorphically via COM and thus selected dynamically at runtime. This makes it possible for the user to write scripts in the language they prefer instead of imposing one that may or may not be desirable in a particular situation.
For simplicity, Active Scripting uses the ProgID to denote the language of the engine, so allowing the client to indicate which language a script is written against is fairly simple. The code below illustrates how to instantiate the scripting engine for JavaScript:    


 HRESULT GetJavaScript(IActiveScript **ppas)
 {
   *ppas = 0;
   OLECHAR wszProgID[] = OLESTR("JavaScript");
   CLSID clsid;
   HRESULT hr = CLSIDFromProgID(wszProgID, &clsid);
   if (SUCCEEDED(hr))
   {
     hr = CoCreateInstance(clsid, 0, CLSCTX_ALL,
                          IID_IActiveScript,
                           (void**)ppas);
   }
   return hr;
 }
This is precisely the technique Microsoft® Internet Explorer 3.0 (IE 3.0) uses when the HTML <SCRIPT> tag includes the LANGUAGE parameter.
Given that users may have different scripting languages installed on their machines, how can an application find out which languages are available? Enter component categories.
Component Categories are a standard COM technique that allows implementation classes (CLSIDs) to be grouped into categories or groups, each of which is identified by a GUID (CATID). With the release of the ActiveX SDK, COM provides a standard mechanism for registering the CATIDs of a class and enumerating all CLSIDs that are in a category. Active Scripting introduces two categories: CATID_ ActiveScript (Active Scripting engines) and CATID_ ActiveScriptParse (Active Scripting engines with parsing). All scripting engines must belong to CATID_ ActiveScript. Scripting engines that support runtime parsing of script text via the IActiveScriptParse interface must also belong to CATID_ActiveScriptParse. Scripting engines that are not directly parsable must provide an alternative mechanism for loading a script state. The current documentation implies that a standard persistence interface can be used, but neither of the two standard script engines supports this technique. Figure 4 shows how to use CATIDs to enumerate all parsable script engines on the current machine.
Once a script engine is created by the host application using CoCreateInstance, the engine will begin in the uninitialized state and will transition between the script states listed in Figure 5. These states indicate the host application's readiness for execution. It is the host application's job to control this state by calling the SetScriptState method at the appropriate times, passing as a parameter the enumerated value that corresponds to the desired state. The system header files define an enumerated type SCRIPTSTATE that has values corresponding to the script states in Figure 5. By allowing the host to explicitly control the engine's state, the host application can reliably prime the engine by populating its namespace prior to executing the first line of script text. This ensures that all referenced script variables are immediately available to the engine prior to starting the first line of script execution. This also allows the host application to suspend execution while entering protected regions of code (such as displaying a modal dialog). The state machine is designed with persistent script engines in mind and is somewhat more complex than you need for the current generation of script engine implementations.
Like many COM objects, script engines require two-phase construction. The first phase, CoCreateInstance, returns an uninitialized object. The second phase of construction requires that the host application call either the InitNew or Load method, depending on whether or not the engine is being loaded from a persistent state. After one of these calls has been made, the host application must attach its site interface to the engine by calling the SetScriptSite method. Once these two actions have been taken, the engine is in the initialized state. Since the current generation of scripting engines does not support persistence, the engine will be initialized by an IActiveScriptParse::InitNew call and will contain no executable script state. When in this state, the engine does not know of any named objects that are exposed by the host and will not execute any script code.
To allow the engine to begin executing code, the host application must force the engine into the started state by calling the SetScriptState method. This brings the engine to life and allows it to dynamically execute code as it parses script text or responds to Invoke calls on the engine's IDispatch interface. Since a nonpersistent engine enters the started state with no initial script parsed, this call to SetScriptState will have no effect other than to allow subsequent parsing operations to become active. To prime the engine with script code, the host application can call the engine's ParseScriptText method. The following C++ code shows how two small fragments of VBScript can be added to an engine:    


 OLECHAR wszScript1[] = L"Sub Foo()/n"
                       L"  MsgBox /"Hello!/"/n"
                        L"End sub";
 
 OLECHAR wszScript2[] = L"Foo/n"
                       L"MsgBox /"Do it now!/"/n";

 HRESULT AddCodeToEngine(IActiveScriptParse *pas) {
   HRESULT hr;
 // add wszScript1 to engine's namespace
   hr = pas->ParseScriptText(wszScript1, 0, 0, 0,
                0, 0, SCRIPTTEXT_ISVISIBLE, 0, 0);
   if (FAILED(hr)) return hr;
 // execute statement(s) in wszScript2
   hr = pas->ParseScriptText(wszScript2, 0, 0, 0,
                0, 0, SCRIPTTEXT_ISVISIBLE, 0, 0);
   return hr;
 }
The first call to ParseScriptText simply adds the procedure definition for Foo to the engine's runtime state. Besides becoming available to subsequent script statements that may be parsed in later calls to ParseScriptText—as is the case in the second ParseScriptText call above—the procedure definition can be called by the host application directly. This is accomplished with the engine's IDispatch interface, which can be obtained via the GetScriptDispatch method. The dispatch interface is created dynamically by the engine as script text is parsed, and it exports each parsed procedure definition as an IDispatch-based method that is accessible via GetIDsOfNames/Invoke. The following code could be used to execute a procedure called Foo that has been previously parsed with ParseScriptText:    


 HRESULT DoFoo(IActiveScript *pas)
 {
   IDispatch *pd = 0;
   HRESULT hr = pas->GetScriptDispatch(0, &pd);
   if (SUCCEEDED(hr)) {
     DISPID id; OLECHAR *rgwsz [] = { L"Foo" };
 // look up Sub name
     hr = pd->GetIDsOfNames(IID_NULL, rgwsz, 1,
                           0, &id);
     DISPPARAMS dp = { 0, 0, 0, 0 };
 // call Foo script
     if (SUCCEEDED(hr))
       hr = pd->Invoke(id, IID_NULL, 0,
                      DISPATCH_METHOD, &dp,
                      0, 0, 0);
     pd->Release();
   }
   return hr;
 }
Since script code can be added in several passes (IE 3.0 calls ParseScriptText once per <SCRIPT> tag), the dispatch interface may support additional DISPIDs as new procedure definitions are parsed.
As shown in the code fragments above, adding support for basic script execution is as simple as creating a COM object and calling a few methods. The interesting aspect of scripting is how a host application's internal object model is exposed to the script programmer. To do this, you use the engine's AddNamedItem method. Host applications typically call AddNamedItem for each top-level application object that will be made available to the engine. To ensure proper initialization, this must be done after the engine enters the initialized state but prior to parsing the first script fragment using ParseScriptText.
It isn't necessary to call AddNamedItem for each automation interface that your application exports. Any subobjects that are managed by your top-level object(s) can be made available as named properties. For example, when IE 3.0 initializes a script engine, it makes its top-level automation interface available as a variable named window. Any subobjects are available as named properties of the object variable called window, such as the document or location properties. So, for the following line of VBScript to work:    


 window.document.write("Hello")
only the variable window needs to be added through AddNamedItem. Figure 6 shows how an engine would acquire the dispatch pointer for window.document and execute the method shown above.
The AddNamedItem method takes only an object name and some flags to control the visibility and scope of the name. The actual IDispatch pointer to the application's named object is not bound until the script engine enters the started state. Any names that are added while the engine is in the initialized state are noted but not resolved until the started state transition. To resolve a name to its IDispatch pointer, the script engine calls the GetItemInfo method on the host application's site object (which was connected just after the call to InitNew). It's in the host application's implementation of GetItemInfo that the namespace of the scripting engine is merged with that of the application itself. Once the script engine acquires an IDispatch pointer to a named item, it can access the properties and methods of the object as seamlessly as any other native variable. This is not unlike how Visual Basic® accesses any other automation interface. The interesting difference lies in how the variable name is resolved to an IDispatch pointer. In traditional Visual Basic, the programmer must declare an object reference variable and initialize it by calling CreateObject or GetObject. In Active Scripting, the host application injects implicit variable definitions through the use of AddNamedItem/GetItemInfo.
It is fairly obvious how a script engine accesses the properties and methods of an application's dispatch interfaces. A somewhat less apparent aspect of scripting is how user-defined scripts can be bound to particular events that occur in the host application's object hierarchy. To support binding events to host objects, Active Scripting engines use connection points to map outbound method calls/events from the host application's objects onto script procedures. This mapping can take place in one of two ways. The simplest way is based on the Visual Basic convention for naming event handlers. If your host application exposes a named object called bob that supports an outbound method called eat, the VBScript (though not the JScript) scripting engine will map this event to a procedure named bob_eat. A somewhat more interesting technique for accomplishing the same effect is to use the IActiveScriptParse::AddScriptlet method. This method allows the caller to bind some script text to an event sent by a named object or subobject. The following code binds a line of VBScript to the Split event from a named subobject stocks.MSFT:    


 hr = pasp->AddScriptlet(
 
     L"littleScript",      // host's name for script
     L"MsgBox /"Yippee/"", // script code
     L"stocks",            // top-level object
     L"MSFT",              // sub-object
     L"Split",             // event name
     L"</SCRIPT>",         // end delimiter
     0,                    // cookie
     0,                    // line number of 1st line
     0,                    // flags
     &bstrName,            // real name
     0);                   // EXCEPINFO
AddScriptlet is useful for binding arbitrary script statements to an application-based event.
 Figure 7 shows the basic sequence of events that take place in a host/engine conversation. The exact order in which each action is taken can vary based on host application idiosyncrasies. Figure 8 shows a very simple script hosting application that allows the user to run arbitrary scripts from the command line. The program exposes one object named shell to the script. The shell object supports the IShell interface to give scripts access to the standard output of the console, and provides the ability to launch arbitrary programs. The shell object also supports an outbound interface, IShellNotify, which defines events to notify scripts when an end-of-line character is written to the console or when a game application is launched. In eval.cpp, note that the scripting language is the first command-line argument, and that one or more script files can be parsed. It expects one of the script files to define a procedure called main that it executes once all scripts have been parsed and the shell object has been connected.
As the eval program illustrates, it's fairly straightforward to support scripting from within your application using Active Scripting. Basic script execution requires very minimal effort and virtually no real integration work unless you want to make your automation interfaces available to user scripts. Once your application has gone to the trouble of exposing automation interfaces for use by external scripting clients (you did this two years ago, right?), implementing the GetItemInfo method allows you to make your application's namespace available to user scripts with very little additional work. Making application-defined events scriptable requires that your application's automation objects support outbound dispatch interfaces using connection points. While few applications currently support this, frameworks like MFC and the ActiveX Template Library provide at least some minimal support for connectable interfaces.

From the February 1997 issue of Microsoft Interactive Developer. Get it at your local newsstand, or better yet, subscribe.

 

another article

Active debugging environment for debugging mixed-language scripting code
United States Patent 6353923

http://www.freepatentsonline.com/6353923.html

發佈了26 篇原創文章 · 獲贊 2 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章