| 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 http://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 1 Active Scripting Debugger Framework
In the next five sections, I'll briefly explore each component, then use them to create a real application debugger.
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.
Language Engine 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.
Host Interfaces 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 6 Document 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.
SampleDebugger 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 8 Basic 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:
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.)
- 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.
- Insert breakpoints.
- Execute the code.
- Clean up.
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:
This is a fairly easy step to implement and it's crucial to debugging an application. If this step is overlooked, the application will not notify the debugger of breakpoint events and may defer debugging operations to a default system debugger.
/* [in] */ IRemoteDebugApplication __RPC_FAR *pda)
hRes = pda->ConnectDebugger(this);
return SUCCEEDED(hRes) ? S_OK : E_FAIL;
} // end CApplicationDebugger::StartDebugSession
To implement host functionality, I create the script site object using CComObject::CreateInstance and give it a reference to the application object I just finished preparing. I need a reference to this application object from within the script site so that I can implement the call to IActiveScriptSiteDebug::GetApplication, as seen in the following code from ActiveScriptSite.h.
It's important to do all the host site preparation before the call to IActiveScript::SetScriptSite on the language engine because the language engine will directly call IActiveScriptSite::GetApplication before the call to SetScriptSite returns. If the app was created after the host site was registered, the language engine would not receive a valid app reference and wouldn't be able to debug code.
/* [out] */ IDebugApplication __RPC_FAR *__RPC_FAR *ppda)
} // end GetApplication
Now the host can communicate with the language engine to create an instance of the language engine. VBScript was the script language of choice for this particular application, although this application will work with any language engine. This is a really cool aspect of the Active Scripting framework. Just change the CLSID of the language engine from VBScript to the language of choice (for example, JScriptÂ®) and SampleDebugger will debug code for that language! Some initial preparation is made by the host to set the script site and then load the language engine with code via IActiveScriptParse. It's imperative that IActiveScriptParse::ParseScriptText successfully executes, otherwise trying to add breakpoints later will result in errors. Once all that's done, the application debugger can add breakpoints to the language engine.
Getting Information about Breakpoints Now I need to set up all the breakpoints that the user added to their code. Where do breakpoints come from? The user adds a breakpoint to the code via a menu entry called InsertBreakpoint. From there, things start to get complicated. The language engine really doesn't provide a nice mechanism for managing breakpoints, so I need to write the code that takes the current cursor position and stuffs it into the following structure, defined in StdAfx.h, when the user inserts a new breakpoint:
SampleDebugger defines this structure and fills in its contents, which consist of two members. The first member is the line of code that the breakpoint is to occur on, and the second is a state for the breakpoint (enabled or disabled). Then the structure is thrown into an STL-derived vector. Later, when the code is executed, these breakpoints are added to the language engine. (See the sidebar "Conditional Breakpoints") Now let's take a look at how that's done.
int m_line; // Source line breakpoint is on
bool m_enabled; // State of the breakpoint, enabled or not
The first thing I need to do at this point is get an IActiveScriptDebug pointer from the language engine. This interface, which can be queried from IActiveScript via QueryInterface, has a member function on it to enumerate code contexts. What's a code context? Think of it as a virtual instruction pointer within code. Each statement within the code contains one or more instructions that implement the work of the higher-level language statement. So a code context represents an address of an instruction within code that is executed. At each code context, I have the option of configuring a breakpoint.
So how do I move from source code to code contexts? This is where IActiveScriptDebug comes in. IActiveScriptDebug, implemented by the language engine, has a method called EnumCodeContextsOfPosition. This is what SampleDebugger uses to take a high-level abstraction, like a line of source code, and get back a code context for it. The signature for EnumCodeContextsOfPosition is as follows:
The first parameter is the source context, which is the same parameter given to the language engine when ParseScriptText is called. The second parameter is the character offset within the code that belongs to that context. SampleDebugger stores the line number that a breakpoint is on, then it uses some facilities of the edit control to get the first character of the particular line, relative to the beginning of the document. The third parameter is the number of characters needed at that offset, which for my application is the source line's length. The final parameter is an enumerator of code contexts that the language engine has (or builds) for that particular character offset. That enumerator contains all the code contexts for that character position, represented by IDebugCodeContext interface pointers. Since I only want to enable a single breakpoint at that position, any context will do, so I use the first code context returned by the enumerator.
/* [in] */ DWORD dwSourceContext,
/* [in] */ ULONG uCharacterOffset,
/* [in] */ ULONG uNumChars,
/* [out] */ IEnumDebugCodeContexts __RPC_FAR *__RPC_FAR *ppescc);
IDebugCodeContext has two interesting methods. The first one, GetDocumentContext, returns an IDebugDocumentContext. I'll explore its use when I look at handling breakpoint notifications. The other method, SetBreakPoint, provides the capability to assign a breakpoint for a given code context. Remember that a code context represents a virtual instruction pointer. So when I call SetBreakPoint I tell the language engine to enable or disable a breakpoint for a particular instruction. When the language engine reaches that instruction, it can evaluate whether it needs to notify the client of a breakpoint event.
Now that you know how to translate source code positions into breakpoints, it's just a matter of iterating the list of BREAKPOINT structures, one for each BREAKPOINT, then enumerating the code contexts to enable or disable them.
Once breakpoints have been configured, the host is ready to instruct the language engine to execute some code. This is fairly straightforward. I get an IDispatch pointer to a procedure named Main within the global namespace of the code. Once I have that, I call Main and the language engine starts executing the script. Then, the application waits to hear back from the language engine (if a breakpoint is set), or the code executes in its entirety.
Finally, when the code has completed execution, I shut down all the objects that I created in the previous steps. This involves removing the application from the PDM, disconnecting the debugger from the application, closing the language engine, and calling release on all remaining interface pointers.
Responding to Breakpoints When the language engine encounters a breakpoint, it must notify some component of the breakpoint event. The language engine knows about an IRemoteDebugApplication object received from SampleDebugger when it registered its script site, and it will execute the IRemoteDebugApplication::HandleBreakPoint method when it hits a breakpoint. Then the application object is responsible for notifying the debugger of the breakpoint hit. Since the PDM implements the IRemoteDebugApplication interface, it's really the PDM that notifies SampleDebugger of breakpoints, not the language engine. The application object already has an IApplicationDebugger interface pointer from when the debugger was connected to it in IDebugSessionProvider, so the PDM executes the onHandleBreakPoint method in IApplicationDebugger.
The signature for onHandleBreakPoint is as follows:
The first parameter is rather important to SampleDebugger. This one innocent-looking thread interface pointer provides the gateway for SampleDebugger to do call stack viewing, expression evaluation, and so on because it provides access to its parent application (via a call to IRemoteDebugApplicationThread::GetApplication). Once the parent application can be accessed, there is complete access to all aspects of the debugging framework. Therefore I've cached the thread pointer so it can be used later.
/* [in] */ IRemoteDebugApplicationThread __RPC_FAR *prpt,
/* [in] */ BREAKREASON br,
/* [in] */ IActiveScriptErrorDebug __RPC_FAR *pError);
The language engine can encounter breakpoints in a number of different ways. It may be necessary to discern when different kinds of breakpoints are received in case the debugger needs to behave differently. That's the job of the second parameter of onHandleBreakPoint. Active Scripting defines seven different circumstances where it will call onHandleBreakPoint, and captures them with the BREAKREASON enumeration. These circumstances are described in Figure 9.
The third parameter in onHandleBreakPoint is a pointer to an error object. This parameter is NULL unless the BREAKREASON parameter is BREAKREASON_ERROR. In this case, the third parameter contains a pointer to an IActiveScriptErrorDebug interface. This IActiveScriptErrorDebug interface pointer provides access to the error condition that exists as well as the stack frame and document context where the error occurred. This allows the application debugger to document both the location in the source code in which a particular error occurred and the runtime conditions with which the error occurred. In the case of SampleDebugger, I'm only interested right now in BREAKREASON_BREAKPOINT because SampleDebugger uses it to provide an indication when breakpoints set by the user are being hit. Once a breakpoint notification has been received, the source line number for that breakpoint is extracted and a dialog like the one in Figure 10 is shown.
Figure 10 Breakpoint Hit!
The process by which SampleDebugger gets the source number is rather intricate. This is where an implementation of a smart host really pays off because smart host functionality is needed in order to determine the line number a breakpoint occurred on. Fortunately, even though SampleDebugger's host is dumb, the language engine by default still provides this functionality.
CApplicationDebugger:onHandleBreakpoint in ApplicationDebugger.cpp implements the following steps to get the source code line:
If the host and the application debugger weren't within the same application, as is the case with SampleDebugger, then it would be handy for the debugger to be able to get the source text. Source text access is provided via the IDebugDocumentText interface. You already have an IDebugDocumentText interface pointer from step 5, so you can easily get the source text using that pointer. Therefore, if you need to access the source and you don't have a text window in your application like SampleDebugger does, use code like that shown in Figure 11.
- Enumerate the stack frames on the thread provided by onHandleBreakPoint.
- Get the first IDebugCodeContext from the first enumerated stack frames.
- Get the IDebugDocumentContext from the code context.
- Get the IDebugDocument from the document context.
- Call QueryInterface for IDebugDocumentText on the IDebugDocument returned from the document context.
- Call IDebugDocumentText::GetPositionOfContext, passing in the debug document context from step 3.
- Using the character position from step 6, call IDebugDocumentText::GetLineOfPosition.
Resuming the Application Once a breakpoint is hit, the application waits for additional instructions from the application debugger on how to proceed. It is the responsibility of the application debugger to resume the application. If the application isn't resumed, then it just keeps waiting, which means it will never shut down. Therefore, regardless of the reason that a breakpoint was received, it is the debugger's responsibility to resume the execution of the application. If you inspect IRemoteDebugApplication, you'll see that it has a method, ResumeFromBreakPoint, specifically for doing this. The problem is that the PDM doesn't provide a reference to an IRemoteDebugApplication. However, the PDM does provide a reference to the thread that was interrupted by the breakpoint. As I mentioned earlier, from that thread it is possible to get its parent application and then resume the thread using its parent IRemoteDebugApplication, as seen in this code from SampleDebuggerView.cpp:
The previous code shows the typical mechanism used to resume an application thread. It is very important that when the application makes the call to resume, it does not occur within the context of the onHandleBreakPoint notification from the PDM. Doing so creates a reentrancy issue because another breakpoint could be hit, generating another notification to onHandleBreakPoint. Since the first call didn't complete, the onHandleBreakPoint handler would be accessed for a second time. Naturally, this problem gets worse as breakpoints continually get hit and the call stack continues to grow. In this case, the application debugger program eventually crashes.
hRes = m_remoteDebugApplicationThread->GetApplication(&rda);
hRes = rda->ResumeFromBreakPoint(m_remoteDebugApplicationThread,
To implement common debugging continuation operations like Step Into, Step Over, Continue, and so on, I use the same function: ResumeFromBreakPoint on IRemoteDebugApplication. However, I change the second parameter, which is a flag to the PDM that contains information about how to resume the application. Figure 12 shows the Active Scripting application resume flags and their effects. By using these different resume flags, SampleDebugger can provide the user with a very flexible array of resume operations.
Now that you know how SampleDebugger works, let's look at the code for each of the application's windows. As I've said, SampleDebugger sets breakpoints, views the call stack, inspects variables, evaluates expressions (via an immediate window), and enumerates application threads and applications. Therefore, the windows I've implemented are the Breakpoint window, the Call Stack window, the Immediate window, the Variables window, the Threads window, and the Applications window.
Breakpoints Window The Breakpoints window is what you use to enable, disable, or remove breakpoints that were inserted using the Insert Breakpoint menu item. The Breakpoints window presents a list of the breakpoints, stating the line number of a given breakpoint and whether the breakpoint is enabled or disabled (signified with an E or D, respectively). This window also displays buttons to enable, disable, or remove breakpoints from the list. My Breakpoints window is shown in Figure 13.
Figure 13 Breakpoints Window
The Breakpoints window gets its breakpoints from the breakpoint list discussed earlier, and prepares them for display in the OnInitDialog function. The code to enable, disable, or remove the breakpoints within that list is nearly identical to the code used initially to add breakpoints. The key difference between the three actions is the information passed to IDebugCodeContext::SetBreakPoint. Active Scripting defines three possible values that can be passed to SetBreakPoint: BREAKPOINT_DELETED, which deletes a breakpoint; BREAKPOINT_DISABLED, which disables a breakpoint; and finally BREAKPOINT_ENABLED, which enables a breakpoint.
Call Stack Window When a breakpoint is hit, there are many circumstances in which you want to peruse the call stack of the application. This call stack window (see Figure 14) is relatively straightforward to produce.
Figure 14 Call Stack Dialog
SampleDebugger knows what thread the application is in when a breakpoint is hit because PDM supplies the thread as part of the call to onHandleBreakPoint when a breakpoint is hit. Therefore, SampleDebugger enumerates the stack frames for the thread on which the breakpoint occurred using IRemoteDebugApplicationThread::EnumStackFrames. This function returns an IEnumDebugStackFrames interface pointer that is used to enumerate all stack frames for the thread. Unfortunately, IEnumDebugStackFrames doesn't directly return an array of IDebugStackFrame pointers. Rather, it returns an array of DebugStackFrameDescriptor structures. Each stack frame structure represents a function level within the call tree, so by iterating the enumerator the call stack can be rebuilt for the application. This structure can be seen here:
The first parameter is the stack frame, and the other parameters are used to help sort stack frames. The PDM is responsible for sorting these stack frames and uses those parameters to do so. The PDM sorts the stack frames so that the first entry within the enumerator represents the top of the call stack, while the last entry within the enumerator represents the bottom of the call stack.
typedef struct tagDebugStackFrameDescriptor
IDebugStackFrame __RPC_FAR *pdsf;
IUnknown __RPC_FAR *punkFinal;
Once I have the call stack, I query it for a textual description of the current function and its address, and output the results to a listbox. The code for this is in the dialog's OnInitDialog handler from CallStackDlg.cpp (see Figure 15). Another really neat feature of the Active Scripting framework is that IEnumDebugStackFrames can contain stack frames from different language engines. This means you can view a call stack that spans multiple source languages.
Note that the Call Stack dialog can be extended to take advantage of the host. For example, the user could double-click a given line within the dialog and have the debugger open the correct document to display the source text for that function using smart host functionality. This is really a natural extension to what is already provided by the active debugging framework. In order to get the source code for a given function call, I need a code context. Since I have a DebugStackFrameDescriptor, I can get the stack frame by using the IDebugStackFrame interface pointer in that structure. Once I have the stack frame, I can get the stack frame's code context and then the source document (similar to what I did to get the source line when a breakpoint was hit).
Immediate Window An Immediate window is typically used in environments like Visual Basic to allow the user to modify a program's contents at runtime, evaluate expressions, or inspect variable contents. The common underlying theme of these three operations is that typically they all can be represented in the host language as expressions. It is this common theme that allows SampleDebugger to implement an Immediate window with relatively few lines of code. The Immediate window shown in Figure 16 allows the user to enter some arbitrary expression into the top edit box. Then, when the user presses the Evaluate button, the expression is fed to the language engine for evaluation. The results of that expression evaluation are displayed in the Edit window.
Figure 16 Immediate Window
The Immediate window is a fairly lightweight dialog. The only real code occurs in a button handler in ImmediateDlg.cpp for the Evaluate button, as shown in Figure 17. This code introduces a new topic not yet explored in the Active Scripting framework: expression evaluation. Expressions are small snippets of code that can be executed within the context of a particular stack frame. The stack frame context is important; it's used to determine the local variables available for use in expressions evaluated within the context.
The starting point for expression evaluation is the thread that was returned in the call to onHandleBreakPoint (which was cached in the dialog implementation class). From that thread, the stack frames for that thread can be enumerated. In particular, I am interested in the topmost stack frame because it's the context I want to execute expressions in. The stack frame can be queried for IDebugExpressionContext, which is used for expression evaluation. The expression context represents the runtime context that the Immediate window expressions are evaluated in. It's closely related to the stack frame because the stack frame helps provide some of that runtime context needed by IDebugExpressionContext.
Once I have the expression context, I can load it with the expression source code with a call to ParseLanguageText. ParseLanguageText is versatile because it tells the language engine how to evaluate the provided expression. Its signature looks like this:
The first parameter is the expression code to be evaluated. The second parameter represents the numerical radix to use. I used a radix of 10 (decimal); however, 16 (hexadecimal) could have easily been used. The third parameter specifies a delimiter to use for parsing, in case the expression string wasn't NULL-terminated.
/* [in] */ LPCOLESTR pstrCode,
/* [in] */ UINT nRadix,
/* [in] */ LPCOLESTR pstrDelimiter,
/* [in] */ DWORD dwFlags,
/* [out] */ IDebugExpression __RPC_FAR *__RPC_FAR *ppe)
The fourth parameter flag is really interesting. Depending on the value of this flag, the expression evaluation can change the runtime state of the code (for example, update a variable with a new value). This mimics the behavior of the immediate window in Visual Basic. Microsoft calls this type of updating a side effect. If an expression should not change the runtime state, the flag DEBUG_TEXT_NOSIDEEFFECTS should be used. In SampleDebugger's implementation of the immediate window, the value returned should immediately display in the result portion of the window. Therefore, SampleDebugger uses the flags DEBUG_TEXT_RETURNVALUE and DEBUG_TEXT_ISEXPRESSION. The first tells the language engine to return the value to the expression, and the second tells the language engine to parse the script text as an expression, as opposed to code that is integrated with the applications' code. Other flags can be used as well that state whether breakpoints should be executed for code within the expression and whether errors should be immediately reported. SampleDebugger does not use these flags.
The final parameter is a reference to an IDebugExpression interface that is used to kick off the expression evaluation. Expression evaluation occurs asynchronously to the execution of SampleDebugger once it's initiated. SampleDebugger is notified when the evaluation of an expression is complete via an IDebugExpressionCallBack pointer that it hands the language engine in IDebugExpression::Start. SampleDebugger can use synchronous behavior here, so the class that implements IDebugExpressionCallBack also provides a wait function called WaitForCompletion that blocks on a Win32Â® event until IDebugExpressionCallBack::onComplete is called. When onComplete is called, it signals the Win32 event that is causing SampleDebugger to block other activity. This awakens the SampleDebugger process, and it can get the results of the expression evaluation and update the immediate window dialog controls.
Variables Window The Variables window allows the user to inspect the contents of variables declared in code at runtime (see Figure 18). SampleDebugger's variables window provides a tree view of all variables in a given context. As part of that view, SampleDebugger provides the short and long name for the variable, its type, and its current value. The variables window also provides a means for updating a variable's value to some new value.
Figure 18 Runtime Variables Window
Active Scripting considers all variables to be properties, including those that are function return values. Specifically, Active Scripting has a COM interface, IDebugProperty, that is designed for managing properties. Handling properties can easily become tricky because properties can have subproperties. I'll use the Connection object that is implemented in ActiveXÂ® Data Objects as an example of this. The Connection object is instantiated in code as a property, yet the connection itself implements properties that are viewable. Example subproperties of the Connection object include CommandTimeout, ConnectionTimeout, and Version. Note that the nesting of properties isn't limited to one or two levels. Property nesting can go as deep as a software designer's imagination. Fortunately, IDebugProperty is up to the challenge.
IDebugProperty, defined in Figure 19, provides services to get information on the property itself via a DebugPropertyInfo structure, sets its current value, gets its parent property, and enumerates its subproperties. There may be certain types of properties that cannot fully represent their structure in using members in DebugPropertyInfo. In these cases, IDebugProperty provides GetExtendedInfo for callers. Callers pass in GUIDs that represent extended parameters that they are interested in, and the results are received back in the rgvar parameter.
The number and type of properties available at any point in a program depend largely on the scoping semantics of the language; therefore, a property with a name of Prop can be a string within one function and an integer in another function. IDebugProperty can't distinguish the context it's in, but context can be established based on how the property is retrieved. The runtime context at any point within the application is stored in a stack frame, IDebugStackFrame. Inspection of the IDebugStackFrame shows that it supports a method called GetDebugProperty, which gives you an IDebugProperty interface. If it were important to know what context the property existed in, it would be as easy as creating a structure to map the property to its parent stack frame.
Because of the tree-like structure of properties, SampleDebugger uses a tree control to present code variables to the user. Variables can be pulled from the current execution context. This means that when enumerating properties, the top entry in the stack is used or the first stack frame returned from a stack frame enumeration on the current thread. Processing properties and adding them to the tree control is handled recursively to handle subproperty cases, as shown in the code from VariablesDlg.cpp in Figure 20. Note that this is probably not a good implementation for commercial code (subproperties may have properties that link back to their parents), but it works well for the SampleDebugger demonstration.
The actual processing of the property includes enumerating all of its members, getting the member values, then processing subproperties. Getting member values introduces yet another data structure, DebugPropertyInfo, into the Active Scripting mix. It's defined in the Microsoft-supplied header file dbgprop.h:
Most of the members of DebugPropertyInfo actually describe the physical structure of a given property. The final member, m_pDebugProp, contains an IDebugProperty pointer. This pointer references subproperties to the current property. SampleDebugger checks this value for NULL, and when it is non-NULL, it recursively calls itself to process the newly found property. Note that all parameters in a DebugPropertyInfo structure must be released by the caller.
typedef struct tagDebugPropertyInfo
IDebugProperty __RPC_FAR *m_pDebugProp;
Now that you know how the Variables window gets filled, let's discuss how to update variables with new values. There are two ways to do this. The first is to call the SetValueAsString method of IDebugProperty. This works for the simple case where you would want to assign a property named Counter a value like 34. However, it doesn't handle the case where a more complex expression is to be assigned to the property. For example, SetValueAsString wouldn't be able to assign Counter an expression like 34 * 56 because SetValueAsString will perform a direct translation of the string, and the string "34 * 56" doesn't directly translate to a number.
SampleDebugger takes a slightly different approach to property assignment. It creates an assignment expression and then evaluates the expression with side effects turned on. As you know from working with the Immediate window, enabling side effects will change the runtime state of the application. So SampleDebugger gets an expression context for the current thread, builds a simple assignment expression as a string (for example, "%s = %s", where the property name is on the left-hand side of the expression and the expression to assign is on the right-hand side), and executes the expression. This implementation permits more complex expressions to be assigned to variables.
Threads Window One of the key responsibilities of the PDM is to manage the various applications running within the process. That application management also includes monitoring the various threads that are associated with each application within the process. The PDM provides services to allow SampleDebugger to enumerate all threads that belong to an application, as well as suspend, resume, and query the state of those threads.
SampleDebugger uses the Threads window shown in Figure 21 to perform those actions. Modeled after the Threads window in Visual C++, SampleDebugger displays a listing of all the threads for the application currently being debugged. The Threads window also provides a mechanism for suspending and resuming particular application threads.
Figure 21 Threads Window
Like many of the other dialogs, the work for preparing the contents of the Threads window occurs in an OnInitDialog handler in CallStackDlg.cpp. In order to enumerate all of the threads for a given application, SampleDebugger needs access to IRemoteDebugApplication for the application it's debugging. Using the same technique defined earlier, SampleDebugger calls ::GetApplication on the cached IRemoteDebugApplicationThread pointer that was received back when the PDM called onHandleBreakPoint. IRemoteDebugApplication has a method called EnumThreads, so once the application pointer is acquired SampleDebugger calls this function to get an IEnumRemoteDebugApplicationThreads interface pointer. This is a standard COM enumerator, so SampleDebugger iterates the application's threads using IEnumRemoteDebugApplicationThreads and fills the listbox with the results.
The thread itself has a rich interface that provides its thread state, its OS thread ID, and its thread count. To get the thread location (the function in which it is currently executing), SampleDebugger has to do a little more work. You already know how to get the name of a function being executed (as in the call stack window), so I will use that same approach here. This means that for each thread in the application, I'll enumerate the thread's stack frames. Once that's done, I'll take the first stack frame from the enumerator (the top of the stack), and get the name from DebugStackFrameDescriptor.
Once all the information is collected, it's stuffed into a structure that I use later when I implement the Suspend and Resume buttons. To suspend or resume a thread, it's just a matter of getting a pointer to the thread to suspend or resume (yanked from the structure filled in OnInitDialog) and calling IRemoteDebugApplicationThread::Suspend or IRemoteDebugApplicationThread::Resume.
Applications Window SampleDebugger uses the MDM as a way to enumerate all of the applications running on the machine. See Figure 22 for an example of its application enumerator dialog. This dialog demonstrates how to communicate with the MDM using the interfaces supported by it.
Figure 22 Applications Window
You might want to extend this application dialog to allow the user to break into a given application to perform debugging. This behavior is similar to the Attach To Process functionality that is provided by the Visual C++ debugger. The IRemoteDebugApplication interface has a method on it that allows control of its debugging. That method is ConnectDebugger, and its signature looks like the following:
Assuming that the user has chosen an application to attach the debugger to, then SampleDebugger would call the ConnectDebugger function, passing it a reference to its IApplicationDebugger interface. Once that's done, the debugger would need to tell the remote application to break at the earliest possible execution moment. The IRemoteDebugApplication interface also provides a mechanism for doing this called CauseBreak.
HRESULT ConnectDebugger([in] IApplicationDebugger *pad)
When this interface function is called, the remote application will tell the language engine to break at the earliest possible moment in the application's execution. Once that point is reached, the IApplicationDebugger interface passed to the application when the debugger was connected will have its onHandleBreakPoint function called. The application debugger can then continue debugging this application as if it had been started directly from within the application debugger itself, as opposed to being attached to the application.
Wrap-up I've covered a huge amount of material, yet I've really only scratched the surface in terms of the potential that can be achieved using the Active Scripting debugging interfaces. SampleDebugger can be extended in many directions, from providing a more complete smart host implementation to attaching to processes already in execution. In addition, the concepts explored here can be employed with many applications where scripting support is used and where integrated debugging services are needed.