any developers today use scripting in their applications to add software functionality such as macro language facilities and other user-customizable features. Microsoft has fueled this trend in recent years with their release of the Active Scripting COM interfaces. One of the questions that arises when scripting support is integrated into a product is how to debug it. For certain apps, the use of Microsoft's free script debugger (available at https://msdn.microsoft.com/scripting) is a good choice. However, you may have applications that need an integrated debugging solution.
Microsoft was sensitive to this need when they designed the Active Scripting framework and included hooks to allow third-party applications to debug script code within their own application space. These hooks took the form of a set of COM interfaces (more than 45 of them) spread across the various components within the framework to help provide debugging capability.
This article explores the COM interfaces that provide debugging services within the Active Scripting framework. It also explains how to implement certain interfaces and write a generic debugger application based solely on Active Scripting debugging interfaces. A complete discussion of all COM interfaces in the Active Scripting debugging interface set is beyond the scope of this article. However, I will describe a subset of those interfaces that specifically helps to implement the sample debugger application provided with this article.
Active Debugging Framework
When Microsoft designed the Active Scripting framework, they were very careful about partitioning the debugging functionality of the system. They split the various functions commonly used in a debugger system (starting the application, object browsing, and so on) into five different components within the framework: the application debugger, the language engine, the process debug manager (PDM), the machine debug manager (MDM), and the host (see Figure 1). Each component has its own family of COM interfaces that it implements, and each is responsible for providing a certain set of services within the Active Scripting framework.
Figure 1Active Scripting Debugger Framework
In the next five sections, I'll briefly explore each component, then use them to create a real application debugger.
The application debugger component of the Active Scripting framework represents what typically comes to mind when you think of a debugger. It's responsible for providing all of the UI features that you normally use when debugging: breakpoint enumeration, call stack viewing, and variable browsing.
The term "application" is used loosely here. An application doesn't necessarily need to be an EXE or DLL; rather, it can be one of many different kinds of self-contained executable entities. Depending on how the application's interfaces are implemented, it may be a block of script code or a full-blown Visual C++Â®-based application. Active Scripting makes no distinction among themâ€"it only knows about application interface pointers.
A typical implementation of the framework's components puts the application debugger in a separate process from the host, as is the case with MicrosoftÂ® Internet Explorer and the Microsoft Script Debugger. However, they can also be integrated. In my demonstration application, SampleDebugger, I've taken the liberty of integrating the host and application debugger into the same physical entity. This allows the application to be both the host and the debugger at the same time so it can edit script code and then immediately execute and debug it.
The interfaces that need to be implemented by an application debugger are shown in Figure 2.
Optionally, the application debugger can provide external components with the ability to control which documents have focus within the application debugger user interface. The interface that provides that additional functionality is IApplicationDebuggerUI.
Considering all the work that you typically think of a debugger doing, you may wonder why only two interfaces are needed. The reason is that the application debugger only really needs to be started and made aware of the occurrences of certain key events within the system (for example, a breakpoint gets hit). The rest of the work of object browsing, call stack viewing, source text navigation, and so on is accomplished by the application debugger using COM interfaces implemented by the other four components of the Active Scripting framework.
The language engine is responsible for parsing and executing all code in a given language. Additionally, the language engine must provide facilities for performing debugger-type functions, such as stack frame enumeration, expression evaluation, variable inspection, and both compile-time and runtime error notifications. The language engine also provides mechanisms for performing syntax coloring for all source code characters loaded into the language engine. The interfaces implemented by the language engine are shown in Figure 3. I'll cover the usage of these interfaces when I explain the sample application.
Process Debug Manager
The PDM is responsible for managing all the various process-related issues within the Active Scripting framework. A given process can support one or more Active Scripting applications, and it is the responsibility of the PDM to manage those applications. The PDM tracks the applications and the process they are running in, tracks all threads and their parent applications, and coordinates communication among the machine debug manager, application debugger, and language engine. In addition, the PDM also provides helper interfaces for document management within simple smart hosts, which I'll explain in the host component section.
Figure 4 shows a list of the interfaces implemented by the PDM.
The implementation of these interfaces is somewhat involved, so I won't discuss it in this article. Microsoft ships a PDM with its scripting components (PDM.DLL in the Windows system directory). This PDM is an in-process COM server that implements the interfaces listed in Figure 4. Other script components shipped by Microsoft are written to interact directly with their version of the PDM. This is okay from the perspective of writing a debugger because my sample debugger really doesn't need to manage the process's applications, threads, and so on. I can let the Microsoft PDM do the work. To use the Microsoft PDM, all I need to do is use the PDM's CLSID when accessing it.
Machine Debug Manager
The MDM is a task manager that manages all currently running applications on a given machine. It serves as a runtime repository that other components within the Active Scripting framework will call on when new applications come into existence. For example, when the PDM creates an application that it will manage within a given process, it registers this application with the MDM. This makes the application visible to all other processes on the machine because it is an out-of-process server and has a lifetime independent of other transient processes that may interact with it. This is not true of the PDM; it is an in-process server and its lifetime is tied to its host process. Figure 5 lists the interfaces implemented by the MDM.
While relatively straightforward, I'll skip the implementation of an MDM. Microsoft ships one with its scripting components (MDM.EXE in the Windows system directory). This MDM is an out-of-process COM server that manages all the applications that it's aware of on a given machine. Other script components shipped by Microsoft, such as the language engine and script debugger, are written to directly interact with their version of the MDM. That's fine from my perspective because my debugger really doesn't need to be the central application repository for the machine. My sample debugger just needs to interface with MDM.EXE to see what applications are registered. To use the MDM, all I need to do is use the MDM CLSID when accessing it.
The host controls the language engine by providing it with all the necessary script code and objects that are needed to execute that code. When the language engine needs to resolve external references to named objects within the code, it calls on the host. The language engine also notifies the host when script states change, when script parsing and execution errors occur, and when it needs locale information from the host.
The basic set of interfaces a host needs to implement includes all interfaces typically needed for Active Scripting (IActiveScriptSite and IActiveScriptSiteWindow) and IActiveScriptSiteDebug. IActiveScriptSiteDebug basically tells other framework components that the host is debugging-aware.
In addition, the host can optionally expose a tree of documents that can be debugged. The term "document" is used loosely here. For example, a document may be a traditional file-based document (such as VBScript.vbs), a Web page, or a portion of text within a larger document (such as a selection within a text editor). This document tree support is optional because the Active Debugging framework supports two kinds of hosts: dumb hosts and smart hosts. The key difference is whether the host chooses to directly implement or provide an implementation for (via a helper interface from the PDM) a set of COM interfaces that it can expose to other components within the Active Debugging framework for document management.
Dumb hosts implement the most basic set of Active Scripting interfaces and do not expose or provide any means for supporting document management. While simple, this functionality is also very basic and limited. For example, an application debugger cannot browse the source documents provided by a dumb host. All an application debugger can see is whatever the language engine provided when script text was sent to the language engine.
Smart hosts provide a much larger range of document management functionality. These hosts provide a tree of documents that can be navigated by components such as the application debugger within the Active Debugging framework. Smart hosts may also provide more advanced control over features such as syntax coloring and the ability to receive script text incrementally. A Web browser may use the latter functionality to provide script text in sections as it receives it over the Internet.
Each document within this tree is a node and can have zero or more child nodes (see Figure 6). Each document within this tree may represent an entire document or a portion of some larger document. Once documents are arranged into this tree, they can be navigated by enumerating the children at each node. Each child node is itself a node that has traversable children. Components that typically navigate this document tree include the host and the application debugger.
Figure 6Document Tree
Let's move from an abstract to a concrete discussion of document trees using Internet Explorer as an example. Think of the document tree representing a Web page with frames containing other Web pages. The top-level Web page is the root page. That root page, in turn, includes frames with links to other Web pages that are contained within the root page. Frames can then be nested further. In essence, there is a tree of Web page documents, all linked to a root Web page document. In this case, Internet Explorer is the host and is responsible for providing this physical arrangement of all of these Web pages to components within the Active Debugging framework.
A smart host will implement or provide support for the COM interfaces shown in Figure 7 (in addition to those needed for basic scripting support, such as IActiveScriptSite). That's quite a list of interfaces to implement! For many applications, the contents of documents within the tree never change once they are created (like Web pages). In these cases, the host would need to implement all these interfaces even though they are boilerplate. Luckily, I can rely on the PDM because it provides a set of helper functions via another COM interface that implements much of what is seen here.
These helper functions are feature-rich; they not only support static documents, but documents that expand incrementally. This feature, known as deferred text, allows a host to provide code to the language engine on an as-needed basis, instead of preloading the language engine at the beginning of an execution session. This type of functionality is especially handy for Web browsers that download content incrementally from some remote location.
Now that you have a brief understanding of how each Active Scripting component functions and interacts, it's time to put your newfound knowledge to work by looking at a concept application. SampleDebugger is an application that I wrote specifically to demonstrate how to use the Active Scripting Debugging interfaces to perform de bugger-type actions. Figure 8 illustrates a basic app.
Figure 8Basic Debugger Application
SampleDebugger sets breakpoints, views the call stack, inspects variables, evaluates expressions (via an immediate window), enumerates application threads, and enumerates applications. All of this is achieved using Active Scripting's debugging facilities.
Before you can start playing around with SampleDebugger, you'll need to download the components needed for compiling against the Active Scripting framework. Please refer to Knowledge Base article Q223389 for instructions on how to download these scripting components.
SampleDebugger is an MFC application built using the Single Document Interface (SDI) document view architecture. An edit control is used as the display for SampleDebugger because it provides an easy text editor to work with. The contents of the edit control are fed to the language engine when it's time to debug some code. ATL is used to help implement COM interfaces that SampleDebugger must implement, as I described earlier in the sections on the Active Scripting framework.
Of the five components present within the Active Scripting framework, I will implement two: the host and the application debugger. In addition, I will use the VBScript language engine that shipped with Internet Explorer (VBScript.DLL). The Microsoft implementation of the PDM and the Microsoft MDM round out all the components within the framework.
SampleDebugger implements the IActiveScriptSite, IActiveScriptSiteWindow, IActiveScriptSiteDebug, IApplicationDebugger, IDebugSessionProvider, and IDebugExpressionCallBack interfaces. The first three interfaces are used to implement the host functionality. IApplicationDebugger and IDebugSessionProvider are used to implement the application debugger. Finally, IDebugExpressionCallBack is used to receive notification of a debug expression evaluation completion, which is needed when implementing the immediate and variables windows. Let's look more closely at how each of these six interfaces is implemented in SampleDebugger.
IActiveScriptSite is required for the host to be able to execute code with the language engine. Typically, it provides notification mechanisms for the language engine to notify the host when script execution states change. It is also used to resolve references in code to external named objects (such as external COM object instances). SampleDebugger provides a very basic implementation of IActiveScriptSite. In this case, SampleDebugger implements just enough of IActiveScriptSite to make the language engine parse and execute code. This means that SampleDebugger can get away with doing nothing more than returning E_NOTIMPL or S_OK HRESULTS on all methods within IActiveScriptSite. It also means that named objects can't be added to the script, but for the purposes of building the sample debugger that trade-off is acceptable.
IActiveScriptSiteWindow is used to let the language engine know that a particular host supports the display of some form of user interface element (such as MsgBox in VBScript). IActiveScriptSiteWindow is then used by the language engine to get a parent window to display the user interface elements. In SampleDebugger, IActiveScriptSiteWindow is implemented so that message boxes can be displayed from within the script. Again, only a minimal set of functionality is provided there.
IActiveScriptSite and IActiveScriptSiteWindow only allow the application to run scriptâ€"they do not give the language engine any indication that SampleDebugger can actually perform debugging. This is where IActiveScriptSiteDebug comes in. When SampleDebugger registers its site with the language engine via IActiveScript::SetScriptSite, the language engine queries the IActiveScriptSite pointer that SampleDebugger gave it to see whether it supports IActiveScriptSiteDebug. If it does, then the language engine knows that the host is debugging-aware and can make sure things are in place to facilitate debugging code.
IApplicationDebugger allows SampleDebugger to trap debug events that occur while executing script. Like the IActiveScriptSite interfaces, only a basic set of functionality is provided for the majority of the interface methods. OnHandleBreakPoint is the only method that gets a nontrivial implementation. This is because all the real work that SampleDebugger can perform is triggered by a single call to onHandleBreakPoint. The PDM calls this method when it hits a breakpoint within the script and sends the pointer to the thread where the breakpoint was hit.
The next interface that's implemented is IDebugSessionProvider. The debugger uses this interface to connect to a particular application. IDebugSessionProvider has only one method, StartDebugSession, which was designed specifically for connecting to an application to start the debugging process. I implement StartDebugSession by connecting to the application provided in the parameter list. This application then uses the debugger whenever debugging events happen while the application is running (for instance, when a breakpoint is hit).
The last interface implemented in SampleDebugger is IDebugExpressionCallBack. This interface is important when performing expression evaluation because expression evaluation is an asynchronous operation that requires a callback interface pointer to be registered. This is necessary so that the debugger can be notified when an expression evaluation has completed. IDebugExpressionCallBack is implemented for those occasions when expressions will be evaluated (in the variables window, immediate window, and so on). I'll explain more about my implementation a little later when I discuss the immediate window.
In the next three sections I'll describe how the SampleDebugger application works. I'll explain how the application gets information about breakpoints and how it handles them. Then I'll explain the code behind each of the windows in the application.
Debugging Script with SampleDebugger
When SampleDebugger first loads, it displays an empty text editor. Code can be entered directly into this text editor, or loaded from a file that contains the code to be debugged. In this context, SampleDebugger behaves as a host in the Active Scripting framework. When debugging begins, the contents of this text editor are fed to the language engine by the host. Once the script is fed to the language engine, then SampleDebugger behaves as an application debugger in the framework.
The first thing that SampleDebugger does when it begins a debug session is to spawn a thread that it can use to communicate with the language engine. This is important because SampleDebugger wants to keep the UI active. If SampleDebugger did not create this thread, all calls to the language engine would occur in the thread context of SampleDebugger's user interface. The net result would be that when breakpoints are hit, SampleDebugger would receive notification from the language engine, but it wouldn't be possible to resume the code via its menu system because the UI thread would not be processing messages within the breakpoint notification handler.
Once in the new thread, SampleDebugger has a fair amount of work to perform before the code is actually executed. That code is quite meaty and can be seen by perusing the sample source code for CSampleDebuggerView::StartDebugging in the file SampleDebuggerView.cpp, which is downloadable from the link at the top of this article. As you will see in the code, SampleDebugger needs to prepare both the host and the application debugger framework components and then execute the code within the language engine. Logically, the steps are as follows:
- Create an instance of the PDM and get an application object from it.
- Prepare the application debugger framework component.
- Prepare the host framework component.
- Prepare the language engine and load it with code.
Any host implementation of IActiveScriptSiteDebug needs to give the language engine a reference to an IRemoteDebugApplication interface pointer. This interface pointer represents the code to be executed. It is also used to begin a debugging session on the code it represents. Therefore, in order to get the application debugger component ready, an instance of the PDM is created in StartDebugging because it provides the IRemoteDebugApplication object that's needed. An IRemoteDebugApplication interface pointer is acquired from the PDM by either calling IProcesDebugManager::CreateApplication or IProcessDebugManager::GetDefaultApplication. My approach is to call IProcessDebugManager::GetDefaultApplication because Microsoft's current implementation of IProcessDebugManager::CreateApplication within the PDM will fail after approximately 50 calls per process. (I verified this using version 6.00.8169 of the Process Debug Manager, PDM.DLL.)
Calling GetDefaultApplication on the PDM registers the application with the PDM by default. However, if I had used a call to CreateApplication instead, I would need to finish the preparation of the application debugger by registering the application with the PDM via IProcessDebugManager::AddApplication. This would in turn register the application with the MDM.
After assigning the application a name (used later when enumerating applications), SampleDebugger creates an instance of its application debugger COM object. I use CComObject::CreateInstance as opposed to ::CoCreateInstance because this object is internal to SampleDebugger. However, had the host been separate from the application debugger (like Internet Explorer is), ::CoCreateInstance would have been called with the CLSID of the application debugger to use.
Once the object has been created, I use QueryInterface to query the object for IDebugSessionProvider. As I mentioned earlier, the IDebugSessionProvider interface has a single method, StartDebugSession, that is used to prepare the application for debugging. StartDebugSession merely connects the application debugger to the remote application object, as you can see in this code from ApplicationDebugger.cpp: