So schreibt man Debugger für das Active Scripting
Die Active Scripting-Schnittstellen erlauben es nicht nur, beliebige Anwendungen mit beliebigen Skriptsprachen auszustatten, sondern auch noch, eigene Debugger dafür zu schreiben. In diesem Artikel stellen wir einen Beispieldebugger vor.
Auf dieser Seite
Das Active Debugging Framework
Anwendungsdebugger
Die Sprachenmaschine
Prozess-Debug-Manager
Maschinen-Debug-Manager
Host-Schnittstellen
Der SampleDebugger
Fehlersuche im Skript mit dem SampleDebugger
Beschaffung von Informationen über Haltepunkte
Reaktion auf Haltepunkte
Wiederaufnahme des Programmlaufs
Das Breakpoints-Fenster
Das Call Stack-Fenster
Immediate-Fenster
Das Variables-Fenster
Das Threads-Fenster
Das Applications-Fenster
Ein Wort zum Schluss
Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:
Heutzutage setzen viele Entwickler in Ihren Anwendungen Skriptsprachen ein, um die Funktionalität zu erweitern, zum Beispiel in Form von Makro-Sprachen. Microsoft hat diesen Trend in den letzten Jahren durch die Einführung der Active-Scripting-Schnittstellen gefördert.
Wenn ein Produkt nun eine Skriptsprache anbietet, stellt sich sofort die Frage, wie man eigentlich die Fehler im Skript findet. Für bestimmte Anwendungen ist der unter http://msdn.microsoft.com/scripting frei erhältliche Skriptdebugger von Microsoft eine gute Wahl. Andere Anwendungen verlangen dagegen nach einer integrierten Lösung für die Fehlersuche.
Diesen Anforderungen war man sich bei Microsoft schon beim Entwurf des Active Scripting Frameworks bewusst und hat entsprechende Möglichkeiten vorgesehen, die es den Anwendungen von Drittanbietern erlaubt, den Skriptcode im eigenen Adressraum zu debuggen. Der erforderliche Zugriff erfolgt über spezielle COM-Schnittstellen (und davon gibt es über 45), die sich über die verschiedenen Komponenten des Grundgerüsts verteilen und bei der Implementierung der Debugging-Fähigkeiten helfen.
In diesem Artikel geht es um die COM-Schnittstellen, die im Active-Scripting-Grundgerüst für die Debug-Dienstleistung sorgen. Im folgenden wird erklärt, wie man bestimmte Schnittsstellen implementiert und einen Debugger schreibt, der die Debug-Schnittstellen des Active-Scripting-Gerüsts ausnutzt. Obwohl dieser Artikel nicht gerade kurz ist, würde eine vollständige Beschreibung aller Debug-Schnittstellen des Active-Scripting trotzdem den Rahmen sprengen. Daher möchte ich mich insbesondere auf die Teilmenge dieser Schnittstellen beschränken, die für das vorgestellte Debugger-Beispiel gebraucht werden. (Dieses Beispiel finden Sie, wie üblich, auf der Begleit-CD dieses Hefts).
Das Active Debugging Framework
Beim Entwurf des Active-Scripting-Grundgerüsts hat Microsoft sehr sorgfältig auf die Aufteilung der Debug-Funktionalität des Systems geachtet. Die verschiedenen Funktionen, die üblicherweise in einem Debugger-System gebraucht werden (Start der Anwendung, Untersuchung der Objekte und so weiter) wurden in fünf verschiedenen Komponenten untergebracht, nämlich im Anwendungsdebugger, in der Sprachenmaschine (language engine, zum Beispiel die VBScript-Maschine), im Prozess-Debug-Manager (PDM), im Maschinen-Debug-Manager (MDM) und im Host (Bild B1). Jede Komponente implementiert ihre eigene Schnittstellenfamilie und muss innerhalb des Active-Scripting-Grundgerüsts eine bestimmte Leistung anbieten.
In den nächsten fünf Abschnitten möchte ich kurz auf jede Komponente eingehen und sie anschließend zur Entwicklung eines richtigen Anwendungsdebuggers verwenden.
B1 Der Debugger im Active Scripting Framework.
(??? Host = Host Language Engine = Sprachmaschine (language engine) Process Debug Manager = Prozess-Debug-Manager Machine Debug Manager = Maschinen-Debug-Manager Application Debugger = Anwendungsdebugger ???)
Anwendungsdebugger
Die Anwendungsdebuggerkomponente aus dem Active Scripting Framework stellt ungefähr das dar, was man sich vorstellt, wenn man an einen Debugger denkt. Sie ist für die Leistungen zuständig, die man in der UI eines Debuggers erwartet: Aufzählung der Haltepunkte, Anzeige des Aufrufstapels und das Herumblättern in den Variablen.
Den Begriff "Anwendung" sollte man sowieso nicht auf die Goldwaage legen und in diesem Zusammenhang schon gar nicht. Eine Anwendung braucht nicht zwangsläufig die Form einer EXE oder DLL zu haben. Sie kann sich aus den unterschiedlichsten Arten von eigenständigen lauffähigen Einheiten zusammensetzen. Je nach der Implementierung der Schnittstellen kann es sich um einen Block Skriptcode handeln oder um eine ausgewachsene Anwendung auf Basis von C++. Das Active Scripting macht da keine Unterschiede. Es kennt nämlich nur Zeiger auf die Schnittstellen der Anwendung.
Eine typische Implementierung der Komponenten bringt den Anwendungsdebugger in einem vom Host getrennten Prozess unter, wie es zum Beispiel beim Internet Explorer und dem Script Debugger der Fall ist. Allerdings kann der Debugger auch im selben Adressraum laufen. In meinem Programmbeispiel, dem SampleDebugger, habe ich mir die Freiheit erlaubt, sowohl die den Host als auch den Anwendungsdebugger in derselben logischen Einheit unterzubringen. Dadurch kann die Anwendung gleichzeitig Host und Debugger sein und zum Beispiel die Bearbeitung des Skriptcodes ermöglichen und ihn dann sofort ausführen und debuggen.
Tabelle T1 zeigt die Schnittstellen, die der Anwendungsdebugger implementieren muss.
T1 Die Schnittstellen der Anwendungsdebugger-Komponente
|
Schnittstelle |
Beschreibung |
|
IDebugSessionProvider |
Die Schnittstelle wird von Clients benutzt, um eine Debug-Sitzung für eine laufende Anwendung zu eröffnen. |
|
IApplicationDebugger |
Das ist die Hauptschnittstelle, die aufgerufen wird, wenn im System ein Debug-Ereignis eintritt. |
Bei Bedarf kann der Anwendungsdebugger den externen Komponenten noch die Gelegenheit zur Entscheidung geben, welche Dokumente in der Anwenderschnittstelle des Anwendungsdebuggers gerade im Fokus liegen sollen. Die entsprechende Schnittstelle, die für diese Funktionalität sorgt, heißt IApplicationDebuggerUI.
Wenn Sie die Arbeit bedenken, die ein Debugger eigentlich zu erledigen hat, fragen Sie sich vielleicht, warum dafür nur zwei Schnittstellen gebraucht werden. Nun, eigentlich braucht man den Anwendungsdebugger nur zu starten und ihn über das Auftreten bestimmter Schüsselereignisse im System zu informieren, zum Beispiel über das Auflaufen des Programms auf einen Haltepunkt. Den Rest der Arbeit, also das Blättern in den Objekten, die Anzeige des Aufrufstapels, die Navigation im Quelltext und so weiter delegiert der Anwendungsdebugger an die anderen vier Komponenten des Active-Scripting-Grundgerüsts, indem er die entsprechenden COM-Schnittstellen aufruft.
Die Sprachenmaschine
Die Sprachenmaschine ist für die Analyse und Ausführung des Codes in einer gegebenen Sprache zuständig. Außerdem muss die Sprachenmaschine für die Durchführbarkeit bestimmter Arbeiten sorgen, auf die der Debugger angewiesen ist, zum Beispiel die Aufzählung des Stapelrahmens, die Bewertung von Ausdrücken, die Untersuchung von Variablen und die Information über Fehler, die beim Kompilieren oder bei der Ausführung auftreten. Außerdem bietet die Sprachenmaschine einen Mechanismus zur farbigen Hervorhebung der Sprachelemente im geladenen Quelltext an (syntax coloring). Tabelle T2 zeigt die Schnittstellen, die von der Sprachenmaschine implementiert werden. Bei der Beschreibung des Beispielprogramms werde ich noch darauf zurückkommen, wie man diese Schnittstellen benutzt.
T2 Die Schnittstellen der Sprachenmaschine
|
Schnittstelle |
Beschreibung |
|
IActiveScriptDebug |
Bietet Syntaxfärbung und Code-Kontextaufzählung |
|
IActiveScriptErrorDebug |
Kapselt Fehler, die beim Kompilieren und zur Laufzeit eintreten |
|
IDebugCodeContext |
Virtueller Befehlszeiger, der zum Setzen von Haltepunkten benutzt wird. Beschaffung eines Kontextes für die Property-Aufzählung |
|
IEnumDebugCodeContexts |
Aufzähler, der Debug-Codekontexte verwaltet |
|
IDebugStackFrame |
Repräsentiert einen gegebenen Stapelrahmen (und Ausführungskontext) in der Anwendung |
|
IDebugExpressionContext |
Liefert den Laufzeitkontext für die Bewertung von Ausdrücken |
|
IDebugExpression |
Ein Ausdruck soll in einem gegebenen Ausdruckskontext bewertet werden |
|
IDebugExpressionCallBack |
Benachrichtigungsmechanismus, mit dem die Sprachenmaschine über den Abschluss der Bewertung eines Ausdrucks informiert |
Prozess-Debug-Manager
Der PDM kümmert sich im Active Scripting Framework um die verschiedenen prozessbezogenen Dinge. Ein gegebener Prozess kann eine oder mehrere Active-Scripting-Anwendungen fahren und der PDM ist für die Verwaltung dieser Anwendungen zuständig. Der PDM verfolgt das Treiben der Anwendungen und des Prozesses, in dem sie laufen, überwacht alle Threads und deren Elternanwendungen und koordiniert die Kommunikation zwischen Maschinen-Debug-Manager, Anwendungsdebugger und Sprachenmaschine. Außerdem bietet der PDM Hilfsschnittstellen für die Dokumentverwaltung in einfachen "schlauen" Hosts (darauf komme ich im Abschnitt über die Host-Komponente noch zurück.)
Tabelle T3 zeigt eine Liste der Schnittstellen, die der PDM implementiert.
T3 Die Schnittstellen des Prozess-Debug-Managers (PDM)
|
Schnittstelle |
Beschreibung |
|
IProcessDebugManager |
Dient zur Erzeugung, zum Hinzufügen und Entfernen von virtuellen Anwendungen aus einem Prozess, zur Aufzählung der Stapelrahmen, der Threads und so weiter |
|
IRemoteDebugApplication |
Kapselt eine Anwendung, die auf einer Maschine laufen kann |
|
IDebugApplication |
Liefert einen spezifischeren maschinenabhängigen Zugriff auf eine IRemoteDebugApplication |
|
IRemoteDebugApplicationThread |
Kapselt einen Thread im Betriebssystem, auf dem eine Anwendung läuft |
|
IDebugApplicationThread |
Liefert einen spezifischeren maschinenabhängigen Zugang auf einen IRemoteDebugApplicationThread |
|
IEnumRemoteDebug-ApplicationThreads |
Aufzähler, der für eine Gruppe von Anwendungsthreads zuständig ist |
|
IDebugApplicationNode |
Verwaltet die Position eines Dokuments in einem Dokumentbaum |
|
IEnumDebugApplicationNodes |
Aufzähler, der für eine Gruppe von Anwendungsknoten zuständig ist |
|
IEnumDebugStackFrames |
Aufzähler, der für eine Gruppe von Stapelrahmen zuständig ist |
|
IDebugDocumentHelper |
Gibt Hilfestellung bei der Implementierung von Smart Hosts |
Die Implementierung dieser Schnittstellen ist relativ aufwendig, so dass ich in diesem Artikel nicht näher darauf eingehen kann. Microsoft liefert mit den Skriptkomponenten einen PDM aus (PDM.DLL im Systemverzeichnis von Windows). Dieser PDM ist ein prozessinterner COM-Server, der die in Tabelle T3 angeführten Schnittstellen implementiert. Andere Skriptkomponenten von Microsoft arbeiten direkt mit der Microsoft-Version des PDM zusammen. Für die Entwicklung des Beispiel-Debuggers spielt das keine Rolle, weil sich mein Debugger nicht um die Anwendungen oder um die Threads im Prozess zu kümmern braucht. Diese Arbeit kann ich getrost dem PDM von Microsoft überlassen. Um mit dem Microsoft-PDM arbeiten zu können, brauche ich beim Zugriff nur dessen CLSID.
Maschinen-Debug-Manager
Der MDM ist ein Taskmanager, der alle derzeit auf einer gegebenen Maschine laufenden Anwendungen verwaltet. Er dient als eine Art Meldestelle, die von anderen Komponenten aus dem Active-Scripting-Grundgerüst aufgerufen wird, wenn neue Anwendungen entstehen. Wenn der PDM zum Beispiel eine Anwendung anlegt, die er in einem gegebenen Prozess verwaltet, meldet er diese Anwendung beim MDM an. Dadurch wird die Anwendung für alle anderen Prozesse auf der Maschine sichtbar, weil der MDM als prozessfremder Server läuft, dessen Lebensdauer von den anderen mehr oder weniger temporären Prozessen, die mit ihm zusammenarbeiten, unabhängig ist. Das gilt nicht für den PDM, der als prozessinterner Server läuft und dessen Lebensdauer daher an den gastgebenden Prozess gebunden ist. Tabelle T4 nennt die Schnittstellen, die vom MDM implementiert werden.
T4 Die Schnittstellen des Maschinen-Debug-Managers
|
Schnittstelle |
Beschreibung |
|
IEnumRemoteDebugApplications |
Aufzähler, der für eine Gruppe von Anwendungen zuständig ist |
|
IMachineDebugManager |
Hauptschnittstelle, die zum Eintragen und Löschen von Anwendungen aus der MDM-Taskliste dient |
|
IMachineDebuggerEvents |
Meldet Ereignisse, wenn sich die MDM-Taskliste ändert |
|
IMachineDebuggerCookie |
Hat dieselbe Funktionalität wie IMachineDebugManager, benutzt zum Eintragen und Löschen von Anwendungen aber Cookies statt Schnittstellenzeiger |
Obwohl sie relativ übersichtlich ist, möchte ich die Implementierung eines MDM überspringen. Microsoft liefert einen mit den Skripting-Komponenten mit (MDM.EXE im Systemverzeichnis von Windows). Dieser MDM ist ein prozessfremder COM-Server, der alle anderen Anwendungen auf einer gegebenen Maschine verwaltet, von denen er Kenntnis erhält. Andere Skriptkomponenten aus dem Hause Microsoft, wie zum Beispiel die Sprachenmaschine und der Skriptdebugger, wurden so geschrieben, dass sie direkt mit der Microsoft-Version des MDM zusammenarbeiten. Aus meiner Sicht ist das völlig in Ordnung, denn mein Debugger braucht wirklich nicht die zentrale Meldestelle auf der Maschine zu sein. Mein Beispiel-Debugger benutzt den MDM.EXE einfach nur, um herauszufinden, welche Anwendungen registriert sind. Um mit dem MDM arbeiten zu können, brauche ich nur die CLSID des MDM zu kennen.
Host-Schnittstellen
Der Host steuert die Sprachenmaschine, indem er sie mit dem erforderlichen Skriptcode versorgt und mit den Objekten, die zur Ausführung des Codes erforderlich sind. Wenn die Sprachenmaschine im Code externe Referenzen auf benannte Objekte auflösen muss, ruft sie den Host auf. Außerdem informiert die Sprachenmaschine den Host, wenn sich Skriptzustände ändern, wenn Fehler bei der Analyse oder Ausführung des Skripts auftreten und wenn sie Gebietsinformationen (Locales) vom Host braucht.
Ein Host, also das "gastgebende Programm", muss zumindest die Basisschnittstellen für das Active Scripting implementieren (IActiveScriptSite und IActiveScriptSiteWindow), sowie IActiveScriptSiteDebug. IActiveScriptSiteDebug teilt den anderen Komponenten aus dem Grundgerüst im wesentlichen nur mit, dass der Host beim Debuggen helfen kann.
Außerdem kann der Host optional einen Dokumentenbaum anbieten, der sich debuggen lässt. Auch die Bezeichnung "Dokument" sollte man nicht allzu wörtlich nehmen. Bei solch einem Dokument kann es sich zum Beispiel um ein herkömmliches Dokument auf Dateibasis handeln (wie VBScript.vbs), um eine Webseite oder um einen Textausschnitt aus einem umfangreicheren Dokument (zum Beispiel um den Text, der in einem Texteditor markiert wurde). Optional ist dieser Dokumentenbaum deswegen, weil das Active Debugging Framework zwei Arten von Hosts kennt, nämlich dumme und schlaue. Der wesentliche Unterschied liegt darin, ob sich der Host dazu entschließt, den anderen Komponenten im Active-Debugging-Gerüst eine Reihe von COM-Schnittstellen zur Dokumentenverwaltung anzubieten oder nicht. Diese Schnittstellen kann er direkt implementieren oder über eine Hilfsschnittstelle des PDM anbieten.
Einfache Hosts beschränken sich auf die grundlegenden Active-Scripting-Schnittstellen und kümmern sich nicht weiter um die Dokumentenverwaltung. Diese Lösung ist zwar einfach, aber auch sehr beschränkt. So kann ein Anwendungsdebugger zum Beispiel nicht in den Quelldokumenten blättern, die von einem einfachen Host angeboten werden. Der Anwendungsdebugger kann dann nur das sehen, was ihm die Sprachenmaschine zukommen lässt, wenn sie Skripttexte erhält.
Schlaue Hosts haben eine wesentlich umfassendere Dokumentenverwaltung zu bieten. Solche Hosts stellen die Dokumente zu einem Baum zusammen, in dem andere Komponenten navigieren können, zum Beispiel der Anwendungsdebugger aus dem Active-Debugging-Gerüst. Schlaue Hosts können auch eine bessere Kontrolle über bestimmte Leistungen wie zum Beispiel die Syntax-Einfärbung bieten und die Möglichkeit, den Skripttext in kleineren Abschnitten zu übermitteln. Mit dieser Funktionalität könnte ein Webbrowser zum Beispiel den Skripttext in dem Umfang weiterreichen, in dem er ihn über das Internet erhält.
Jedes Dokument in diesem Baum lässt sich als Knoten auffassen, der null oder mehr Kindknoten hat (Bild B2). Jedes Dokument in diesem Baum könnte ein vollständiges Dokument darstellen oder einen Ausschnitt aus einem umfangreicheren Dokument. Sobald die Dokumente in Baumform gebracht sind, kann die Navigation durch die Aufzählung der Kinder eines Knotens erfolgen. Jeder Kindknoten ist selbst wieder ein Knoten, der Kinder haben kann, die sich aufzählen lassen. Zu den Komponenten, die in solch einem Dokumentenbaum herumklettern, zählen zum Beispiel der Host und der Anwendungsdebugger.
B2 Ein Dokumentenbaum setzt sich aus Knoten zusammen
Wenden wir uns von der abstrakten Beschreibung der Dokumentenbäume ab und einem konkreten Beispiel zu, nämlich dem Internet Explorer. Stellen Sie sich vor, der Dokumentenbaum repräsentiere eine Webseite mit verschiedenen Rahmen, die wiederum andere Webseiten enthalten. Die oberste Webseite ist die Wurzelseite. Sie enthält Rahmen, in denen es Verknüpfungen zu anderen Webseiten gibt, die nun Bestandteil der Wurzelseite sind. Die Rahmen lassen sich auch noch weiter schachteln. Im Endeffekt entsteht ein Baum mit Webseitendokumenten, die alle mit dem Wurzeldokument verknüpft sind. In diesem Fall ist der Internet Explorer der Host und dafür verantwortlich, dass die anderen Komponenten aus dem Active-Debugging-Gerüst Zugang zum Webseitenbaum erhalten.
Ein schlauer Host implementiert außer den grundlegenden Schnittstellen wie IActiveScriptSite für das Active Scripting auch noch die COM-Schnittstellen aus Tabelle T5 oder unterstützt sie zumindest. Und das ist schon eine stolze Liste von Schnittstellen, die implementiert werden wollen. Bei vielen Anwendungen ändert sich der Inhalt der Dokumente aus dem Baum nicht mehr, sobald sie erst einmal erstellt sind (zum Beispiel Webseiten). Auch in diesen Fällen müsste der Host alle diese Schnittstellen implementieren, wenn auch nur mit einer Art Standardimplementierung. Zum Glück kann ich mich auf den PDM verlassen, weil er über eine andere COM-Schnittstelle eine Reihe von Hilfsfunktionen anbietet, die einen Großteil von dem bieten, was hier gefordert wird.
T5 Ein schlauer Host implementiert diese Schnittstellen
|
Schnittstelle |
Beschreibung |
|
IDebugDocumentInfo |
Liefert Informationen über eingegebenes Dokument (zum Beispiel einen Namen) |
|
IDebugDocumentProvider |
Erlaubt die "faule" Instanzenbildung für Dokumente |
|
IDebugDocument |
Abstrakte Schnittstelle, die ein Dokument im System darstellt |
|
IDebugDocumentText |
Repräsentiert den Textinhalt eines Dokuments |
|
IDebugDocumentTextEvents |
Verbindungspunktschnittstelle, die zur Information über Änderungen in einem gegebenen Dokument dient |
|
IDebugDocumentContext |
Stellt eine bestimmte Stelle in einem gegebenen Dokument dar |
|
IDebugApplicationNode |
Repräsentiert in einer Dokumenthierarchie ein einzelnes Dokument |
|
IDebugApplicationNodeEvents |
Verbindungspunktschnittstelle, die zur Information über Änderungen der Dokumenthierarchie dient |
Diese Hilfsfunktionen sind sehr leistungsstark. Sie können nicht nur mit statischen Dokumenten umgehen, sondern auch mit Dokumenten, die sich inkrementell vergrößern. Dieser Mechanismus, der als "verzögerter Text" (Deferred Text) bekannt ist, erlaubt es dem Host, den Code nach Bedarf an die Sprachmaschine weiterzugeben, statt sie schon vor Beginn der Codeausführung mit dem gesamten Code zu füttern. Diese Funktionalität ist besonders für Webbrowser sehr praktisch, die ihren Inhalt in kleineren Stückchen herunterladen.
Der SampleDebugger
Nach diesem kurzen Überblick über die Funktionen und Aktionen der Active Scripting-Komponenten wird es Zeit, das frisch erworbene Wissen in die Praxis umzusetzen und sich ein funktionierendes Programmbeispiel anzusehen. Ich habe den SampleDebugger speziell geschrieben, um den Einsatz der Active Scripting Debugging-Schnittstellen bei Debug-Aktionen zu zeigen. Bild B3 skizziert eine einfache Anwendung dieser Art.
B3 Der SampleDebugger.
Der SampleDebugger setzt Haltepunkte, zeigt den Aufrufstapel, untersucht Variablen, bewertet Ausdrücke (in einem Direktfenster), zählt die Threads der Anwendung auf und auch die Anwendungen. All dies mit dem Debug-Angebot von Active Scripting.
Bevor Sie aber mit dem SampleDebugger spielen können, müssen Sie sich die Komponenten beschaffen, die für die Arbeit mit dem Active-Scripting-Gerüst erforderlich sind. Diese finden Sie auf der CD zum Heft in der Datei Scriptng.exe.
Der SampleDebugger ist eine MFC-Anwendung, die sich auf die SDI-Doc/View-Architektur stützt (Single Document Interface). Zur Anzeige dient ein Edit-Steuerelement, weil es einen einfachen Texteditor anbietet, mit dem man schon recht ordentlich arbeiten kann. Der Inhalt dieses Eingabefelds wird an die Sprachenmaschine verfüttert, wenn es an der Zeit ist, Fehler im Code zu suchen. Die COM-Schnittstellen, die der SampleDebugger implementieren muss, werden mit Hilfe der ATL implementiert.
Von den fünf Komponenten, die es im Active-Scripting-Gerüst gibt, möchte ich zwei implementieren, nämlich den Host und den Anwendungsdebugger. Außerdem möchte ich die VBScript-Sprachmaschine aus dem Internet Explorer einsetzen (VBScript.DLL). Die Microsoft-Implementierung des PDM und des MDM runden die Komponenten ab.
Der SampleDebugger implementiert die Schnittstellen IActiveScriptSite, IActiveScriptSiteWindow, IActiveScriptSiteDebug, IApplicationDebugger, IDebugSessionProvider und IDebugExpressionCallBack. Die ersten drei Schnittstellen werden für die Grundfunktionalität des Host gebraucht. IApplicationDebugger und IDebugSessionProvider dienen zur Implementierung des Anwendungsdebuggers. IDebugExpressionCallBack schließlich wird zum Empfang der Hinweisnachricht gebraucht, die den Abschluss der Bewertung eines Ausdrucks verkündet. Sie ist erforderlich, wenn man das Direktfenster oder das Variablenfenster implementiert. Schauen wir uns jede dieser sechs Schnittstellen, die der SampleDebugger implementiert, etwas genauer an.
IActiveScriptSite versetzt den Host in die Lage, den Code auf der Sprachenmaschine auszuführen. Sie bietet der Sprachenmaschine einen Hinweismechanismus an, mit der die Sprachenmaschine den Host informieren kann, wenn sich der Zustand der Skriptausführung ändert. Außerdem wird sie gebraucht, um Bezüge auf externe benannte Objekte aufzulösen, zum Beispiel auf externe COM-Objektinstanzen. Der SampleDebugger implementiert IActiveScriptSite nur in einer ziemlich einfachen Weise. In diesem Fall implementiert der SampleDebugger nur soviel von der IActiveScriptSite, dass die Sprachenmaschine den Code analysieren und ausführen kann. Das bedeutet, dass sich der SampleDebugger in allen Funktionen der IActiveScriptSite-Schnittstelle auf die Rückgabe der HRESULT-Werte E_NOTIMPL oder S_OK schränken kann. Das bedeutet zwar, dass man im Skript nicht mit benannten Objekten arbeiten kann, aber für einen Beispiel-Debugger ist diese Beschränkung akzeptabel.
IActiveScriptSiteWindow lässt die Sprachenmaschine wissen, ob der Host die Anzeige von bestimmten Elementen aus der Anwenderschnittstelle erlaubt (zum Beispiel eine MsgBox in VBScript). Dann beschafft sich die Sprachenmaschine mit IActiveScriptSiteWindow ein Elternfenster, in dem sie das Schnittstellenelement anzeigen kann. Im SampleDebugger wird IActiveScriptSiteWindow so implementiert, dass sich vom Skript aus Meldungsfenster anzeigen lassen. Ansonsten gilt auch hier der Grundsatz der minimalen Funktionalität.
IActiveScriptSite und IActiveScriptSiteWindow ermöglichen es der Anwendung nur, ein Skript zu fahren. Sie geben der Sprachenmaschine keinen Hinweis darauf, dass der SampleDebugger tatsächlich das Skript debuggen kann. Das ist nun die Stelle, an der IActiveScriptSiteDebug ihren Auftritt hat. Wenn der SampleDebugger seine Site via IActiveScript::SetScriptSite bei der Sprachmaschine anmeldet, befragt die Sprachmaschine den IActiveScriptSite-Zeiger, den sie von SampleDebugger erhalten hat, nach IActiveScriptSiteDebug. Wenn der SampleDebugger IActiveScriptSiteDebug anbietet, weiß die Sprachenmaschine, dass der Host debuggen kann, und kann selbst entsprechende Vorbereitungen treffen.
IApplicationDebugger erlaubt es dem SampleDebugger, Debug-Ereignisse abzufangen, die bei der Ausführung des Skripts auftreten. Wie bei IActiveScriptSite erhalten die meisten ihrer Schnittstellenmethoden nur eine sehr bescheidene Funktionalität. onHandleBreakPoint ist die einzige Funktion mit einer nicht trivialen Implementierung. Das liegt einfach daran, dass sich die ganze Arbeit, die SampleDebugger ausführt, durch einen einzelnen Aufruf von onHandleBreakPoint einleiten lässt. Der PDM ruft diese Methode auf, wenn er im Skript auf einen Haltepunkt aufläuft, und schickt einen Zeiger auf den Thread, in dem der Haltepunkt angeschlagen hat.
Bei der nächsten implementierten Schnittstelle handelt es sich um IDebugSessionProvider. Der Debugger benutzt diese Schnittstelle, um die Verbindung zu einer bestimmten Anwendung herzustellen. IDebugSessionProvider hat nur eine Methode, nämlich StartDebugSession, die speziell für die Verbindung mit einer Anwendung konzipiert wurde, die getestet werden soll. Ich habe StartDebugSession so implementiert, dass ich die Verbindung zu einer Anwendung herstelle, die in der Parameterliste angegeben wird. Anschließend benutzt die Anwendung immer dann den Debugger, wenn während ihres Laufs ein Debug-Ereignis eintritt (zum Beispiel dann, wenn ein Haltepunkt anschlägt).
Die letzte im SampleDebugger implementierte Schnittstelle ist IDebugExpressionCallBack. Diese Schnittstelle ist für Bewertung von Ausdrücken wichtig, weil das ein asynchroner Vorgang ist, der die Anmeldung eines speziellen Callback-Schnittstellenzeigers erfordert. Das ist nötig, damit der Debugger informiert werden kann, sobald die Bewertung eines Ausdrucks abgeschlossen ist. IDebugExpressionCallBack wird für die Gelegenheiten implementiert, bei denen Ausdrücke ausgewertet werden, zum Beispiel im Variablenfenster, im Direktfenster und so weiter. Ich werde noch etwas näher auf meine Implementierung eingehen, wenn ich über das Direktfenster spreche.
In den nächsten drei Abschnitten möchte ich darauf eingehen, wie die SampleDebugger-Anwendung funktioniert. Ich werde erläutern, wie die Anwendung Informationen über Haltepunkte einholt und wie sie mit den Haltepunkten umgeht. Anschließend komme ich auf den Code zu sprechen, der hinter jedem Fenster der Anwendung steht.
Fehlersuche im Skript mit dem SampleDebugger
Nach dem Laden zeigt der SampleDebugger einen leeren Texteditor an. Der gewünschte Code lässt sich direkt in diesem Texteditor eingeben oder aus einer Datei laden, die den zu untersuchenden Code enthält. In diesem Zusammenhang nimmt der SampleDebugger die Rolle des Hosts im Active Scripting Framework ein. Sobald die Fehlersuche beginnt, wird der Inhalt dieses Texteditors vom Host an die Sprachmaschine weitergeleitet. Sobald die Sprachmaschine das Skript übernommen hat, nimmt der SampleDebugger im Active Scripting Framework die Rolle des Anwendungsdebuggers ein.
Wenn der SampleDebugger eine Debug-Sitzung beginnt, startet er als erstes einen neuen Thread, den er zur Kommunikation mit der Sprachenmaschine benutzt. Das ist wichtig, weil der SampleDebugger die UI funktionsfähig halten will. Würde der SampleDebugger nicht diesen Thread erzeugen, ereigneten sich alle Aufrufe der Sprachmaschine im Threadkontext der Anwenderschnittstelle des SampleDebuggers. Und das geht schief. Sobald das Skript auf einen Haltepunkt läuft, erhält der SampleDebugger einen entsprechenden Hinweis von der Sprachenmaschine. Es wäre aber nicht möglich, im Menüsystem des SampleDebuggers die Ausführung des Skripts wieder aufzunehmen, weil der UI-Thread in der Empfängerfunktion für die Haltepunktsnachricht keine Nachrichtenpumpe betreibt, also keine Nachrichten aus der Warteschlange holt.
Einmal im neuen Thread, hat der SampleDebugger eine ganze Menge Arbeit zu erledigen, bevor der Code tatsächlich ausgeführt wird. Der Code gestaltet sich entsprechend umfangreich und ist in der Datei SampleDebuggerView.cpp zu finden, in der Funktion CSampleDebuggerView:: StartDebugging. (Die Quelltexte finden Sie auf der Begleit-CD dieses Hefts.) Wie Sie im Code sehen werden, muss der SampleDebugger den Host und den Anwendungsdebugger vorbereiten und dann den Code in der Sprachenmaschine ausführen lassen. Das macht er in folgenden Schritten:
-
Lege eine Instanz des PDM an und besorge von ihm ein Anwendungsobjekt.
-
Bereite die Anwendungsdebugger-Komponente vor.
-
Bereite die Host-Komponente vor.
-
Bereite die Sprachenmaschine vor und gib ihr den Code.
-
Lege die gewünschten Haltepunkte fest.
-
Führe den Code aus.
-
Räume auf.
Jede Host-Implementierung von IActiveScriptSiteDebug muss der Sprachenmaschine eine Referenz auf einen IRemoteDebugApplication-Schnittstellenzeiger geben. Dieser Schnittstellenzeiger repräsentiert den auszuführenden Code. Er wird auch dazu benutzt, die Debug-Sitzung mit dem betreffenden Code einzuleiten. Zur Vorbereitung des Anwendungsdebuggers wird daher in StartDebugging eine Instanz des PDM angelegt, weil sie das erforderliche IRemoteDebugApplication-Objekt liefern kann. Einen IRemoteDebugApplication-Schnittstellenzeiger erhält man entweder mit IProcessDebugManager::CreateApplication vom PDM oder mit IProcessDebugManager::GetDefaultApplication. Meine Wahl fiel auf IProcessDebugManager::GetDefaultApplication, weil Microsofts aktuelle Implementierung von IProcessDebugManager::CreateApplication im PDM anscheinend nach ungefähr 50 Aufrufen per Prozess versagt. (Davon konnte ich mich mit der Version 6.00.8169 des Prozess-Debug-Managers PDM.DLL überzeugen. Woran es aber genau liegt, weiß man natürlich immer erst, wenn man den Fehler tatsächlich gefunden hat.)
Der Aufruf der Methode GetDefaultApplication des PDM führt automatisch zur Anmeldung der Anwendung beim PDM. Hätte ich dagegen CreateApplication eingesetzt, müsste ich die Vorbereitung des Debuggers mit der Anmeldung der Anwendung beim PDM abschließen, und zwar durch den Aufruf von IProcessDebugManager::AddApplication. Dadurch wiederum würde die Anwendung beim MDM angemeldet.
Nachdem er der Anwendung einen Namen gegeben hat (der später bei der Aufzählung der Anwendungen benutzt wird), legt der SampleDebugger eine Instanz seines Anwendungsdebuggerobjekts an. Ich benutze CComObject::CreateInstance statt ::CoCreateInstance, weil dieses Objekt ein internes Objekt des SampleDebuggers ist. Wäre der Host dagegen vom Anwendungsdebugger getrennt (wie im Internet Explorer), müsste ::CoCreateInstance aufgerufen werden, und zwar mit der CLSID des zu benutzenden Anwendungsdebuggers.
Sobald das Objekt fertig ist, fordere ich von ihm mit QueryInterface einen Zeiger auf IDebugSessionProvider an. Wie schon erwähnt, hat IDebugSessionProvider nur eine Methode, nämlich StartDebugSession, die zur Vorbereitung der Anwendung auf das Debuggen dient. StartDebugSession verbindet den Anwendungsdebugger einfach mit dem Anwendungsobjekt, wie die folgenden Zeilen zeigen. Sie stammen aus der Datei ApplicationDebugger.cpp:
STDMETHODIMP CApplicationDebugger::StartDebugSession(
/* [in] */ IRemoteDebugApplication __RPC_FAR *pda)
{
HRESULT hRes;
hRes = pda->ConnectDebugger(this);
return SUCCEEDED(hRes) ? S_OK : E_FAIL;
} // end CApplicationDebugger::StartDebugSession
Die Implementierung dieses Schritts ist ziemlich leicht und für das Debuggen einer Anwendung entscheidend. Übersieht man diesen Schritt, wird die Anwendung den Debugger nicht informieren, wenn sie auf einen Haltepunkt läuft. Unter Umständen werden die Debug-Vorgänge an einen Standarddebugger des Systems weitergeleitet.
Zur Implementierung der Host-Funktionalität lege ich mit CComObject::CreateInstance ein Skriptsite-Objekt an und gebe ihm eine Referenz auf das Anwendungsobjekt, dessen Vorbereitung ich gerade abgeschlossen habe. Ich brauche diese Referenz auf das Anwendungsobjekt in der Skriptsite, damit ich den Aufruf von IActiveScriptSiteDebug::GetApplication implementieren kann. GetApplication ist in den folgenden Zeilen zu sehen, die aus der Datei ActiveScriptSite.h stammen:
STDMETHOD(GetApplication)(
/* [out] */ IDebugApplication __RPC_FAR *__RPC_FAR *ppda)
{
return m_debugApplication->QueryInterface(IID_IDebugApplication,
(LPVOID*)ppda
);
} // end GetApplication
Es ist wichtig, dass sämtliche Vorbereitungen der Hostsite vor dem Aufruf der Methode IActiveScript::SetScriptSite von der Sprachenmaschine abgeschlossen sind, weil die Sprachenmaschine direkt IActiveScriptSite::GetApplication aufruft, noch bevor SetScriptSite zum Aufrufer zurückkehrt. Falls die Anwendung erst nach der Anmeldung der Hostsite erzeugt wird, erhält die Sprachenmaschine keine gültige Referenz auf die Anwendung und kann den Code nicht debuggen.
Nun kann der Host mit der Sprachenmaschine kommunizieren und eine Instanz der Sprachenmaschine anlegen. In dieser speziellen Anwendung war zwar VBScript die Skriptsprache der Wahl, aber die Anwendung sollte im Prinzip mit jeder Sprachenmaschine laufen. Das ist tatsächlich eine ziemlich coole Sache, was das Active Scripting Framework da zu bieten hat. Ändern Sie einfach die CLSID der Sprachenmaschine von VBScript auf eine Sprache Ihrer Wahl (zum Beispiel JScript) und der SampleDebugger kann den Code für diese Sprache debuggen.
Der Host führt einige Vorbereitungen durch, während derer er die Skriptsite setzt und dann die Sprachenmaschine via IActiveScriptParse mit Code versorgt. Es ist wichtig, dass der Aufruf von IActiveScriptParse::ParseScriptText erfolgreich verläuft, sonst kommt es beim späteren Versuch, Haltepunkte zu setzen, zu Fehlern. Nachdem das alles erledigt ist, kann der Anwendungsdebugger mit der Sprachenmaschine Haltepunkte setzen.
Beschaffung von Informationen über Haltepunkte
Nun muss ich die Haltepunkte setzen, die der Anwender bereits in seinem Code gesetzt hatte. Und woher kommen diese Haltepunkte? Der Anwender kann sie mit einem Menüpunkt namens InsertBreakpoint in seinen Code eintragen. Und dann werden die Dinge etwas komplizierter. Die Sprachenmaschine bietet eigentlich keinen zufriedenstellenden Mechanismus zur Verwaltung der Haltepunkte an. Also habe ich selbst den Code geschrieben, der die aktuelle Cursorposition ermittelt, sobald der Anwender einen neuen Haltepunkt festlegt, und in der folgenden Struktur ablegt, die in StdAfx.h definiert wird:
typedef struct
{
int m_line; // Auf dieser Quelltextzeile liegt der Haltepunkt
bool m_enabled; // Zustand des Haltepunkts, freigeschaltet oder
} BREAKPOINT; // nicht
Der SampleDebugger definiert diese Struktur und füllt ihre beiden Datenelemente mit Inhalt. Das erste Datenelement gibt die Codezeile an, auf welcher der Haltepunkt liegt, und das zweite den Zustand des Haltepunkts (freigeschaltet oder gesperrt). Dann wird die Struktur in einem von der STL abgeleiteten Vektor abgelegt. Später, wenn der Code ausgeführt werden soll, werden diese Haltepunkte der Sprachenmaschine mitgeteilt. Schauen wir uns einmal an, wie das abläuft.
An diesem Punkt muss ich zuerst von der Sprachenmaschinen einen IActiveScriptDebug-Zeiger anfordern. Diese Schnittstelle, die via IActiveScript von QueryInterface erhältlich ist, bietet eine Funktion zur Aufzählung der Codekontexte an. Was ist ein Codekontext? Stellen Sie sich solch einen Kontext als einen virtuellen Befehlszeiger mit Code vor. Jede Anweisung in diesem Code enthält einen oder mehrere Befehle, mit denen die in der Hochsprache gegebene Anweisung ausgeführt wird. Also stellt ein Codekontext die Adresse eines Befehls im Code dar, der ausgeführt wird. Auf jeden dieser Codekontexte kann ich einen Haltepunkt setzen.
Und wie komme ich vom Quelltext zum Codekontext? Das ist die große Stunde von IActiveScriptDebug. Diese Schnittstelle wird von der Sprachenmaschine implementiert und hat eine Methode namens EnumCodeContextsOfPosition. Diese Methode benutzt der SampleDebugger zur Umwandlung einer Hochsprachenabstraktion (wie einer Quelltextzeile) in einen Codekontext. EnumCodeContextsOfPosition hat folgende Signatur:
HRESULT EnumCodeContextsOfPosition(
/* [in] */ DWORD dwSourceContext,
/* [in] */ ULONG uCharacterOffset,
/* [in] */ ULONG uNumChars,
/* [out] */ IEnumDebugCodeContexts __RPC_FAR *__RPC_FAR *ppescc);
Der erste Parameter ist der Quellkontext. Das ist derselbe Kontext, der beim Aufruf von ParseScriptText an die Sprachenmaschine übergeben wird. Der zweite Parameter ist die Position innerhalb des Codes, der zu diesem Kontext gehört. Der SampleDebugger speichert die Nummer der Zeile, auf der ein Haltepunkt liegt, und ermittelt dann mit Hilfe des Edit-Steuerelements die Position des ersten Zeichens auf dieser Zeile, und zwar in Bezug auf den Anfang des Dokuments. Beim dritten Parameter handelt es sich um die Zahl der Zeichen, die von dieser Position aus verwendet werden. In meiner Anwendung ist dies die Länge der Quelltextzeile. Der letzte Parameter schließlich ist eine Schnittstelle zum Aufzählen der Codekontexte, die von der Sprachenmaschine für diese bestimmte Zeichenposition zusammengestellt werden (oder wurden). Da ich nur einen einzelnen Haltepunkt an dieser Position brauche, reicht der erste Kontext aus, den die Schnittstelle liefert.
IDebugCodeContext hat zwei interessante Methoden. Die erste namens GetDocumentContext liefert eine IDebugDocumentContext. Auf diese Schnittstelle werde ich bei der Besprechung der Hinweisnachrichten für Haltepunkte noch näher eingehen. Die andere Methode heißt SetBreakPoint und bietet die Möglichkeit, auf einen gegebenen Codekontext einen Haltepunkt zu setzen. Wie erwähnt, stellt solch ein Codekontext einen virtuellen Befehlszeiger dar. Indem ich also SetBreakPoint aufrufe, weise ich die Sprachenmaschine an, den Haltepunkt auf diesem speziellen Befehl freizuschalten oder zu sperren. Sobald die Sprachenmaschine später diesen Befehl erreicht, kann sie ermitteln, ob der Client über das Erreichen eines Haltepunkts informiert werden muss oder nicht.
Nachdem Sie nun erfahren haben, wie Codepositionen in Haltepunkte umgewandelt werden, brauchen Sie im Prinzip nur noch die Liste mit den BREAKPOINT-Strukturen durchzugehen, in der es für jeden Haltepunkt einen BREAKPOINT gibt, und die Codekontexte aufzuzählen, um sie freizuschalten oder zu sperren.
Nach der Vorbereitung der Haltepunkte ist der Host so weit, dass er der Sprachenmaschine den Befehl zur Ausführung des Codes geben kann. Der Vorgang ist ziemlich einfach. Ich beschaffe mir einen Zeiger auf eine IDispatch-Schnittstelle, mit der ich eine Funktion namens Main im globalen Namensraum des Codes aufrufen kann. Sobald ich die Schnittstelle und die Dispatch-ID von Main habe, rufe ich Main auf und die Sprachenmaschine beginnt mit der Ausführung des Skripts. Anschließend wartet die Anwendung darauf, etwas von der Sprachenmaschine zu hören, sofern Haltepunkte gesetzt sind. Sind keine gesetzt, läuft der Code vollständig durch.
Sobald der Code vollständig ausgeführt wurde, gebe ich alle Objekte wieder ab, die ich in den bisherigen Schritten angelegt habe. Dazu gehört die Streichung der Anwendung aus dem PDM, die Trennung des Debuggers von der Anwendung, das Schließen der Sprachenmaschine und die Release-Aufrufe für alle verbliebenen Schnittstellenzeiger.
Reaktion auf Haltepunkte
Sobald die Sprachenmaschine auf einen Haltepunkt läuft, muss sie eine andere Komponente über dieses Ereignis informieren. Die Sprachmaschine kennt ein IRemoteDebugApplication-Objekt, dass sie vom SampleDebugger bei der Anmeldung seiner Skriptsite erhielt. Sobald sie auf einen Haltepunkt läuft, ruft sie die Methode IRemoteDebugApplication::HandleBreakPoint auf. Anschließend ist das Anwendungsobjekt für die Benachrichtigung des Debuggers über das Ereignis zuständig. Da der PDM die Schnittstelle IRemoteDebugApplication implementiert, ist es also eigentlich der PDM, der den SampleDebugger über Haltepunkte informiert, und nicht die Sprachenmaschine. Das Anwendungsobjekt verfügt bereits über einen Zeiger auf die IApplicationDebugger-Schnittstelle, den es im Rahmen der Verbindungsaufnahme mit dem Debugger via IDebugSessionProvider erhielt. Also ruft der PDM die Methode onHandleBreakPoint von IApplicationDebugger auf.
Die Methode onHandleBreakPoint hat folgende Signatur:
STDMETHODIMP CApplicationDebugger::onHandleBreakPoint(
/* [in] */ IRemoteDebugApplicationThread __RPC_FAR *prpt,
/* [in] */ BREAKREASON br,
/* [in] */ IActiveScriptErrorDebug __RPC_FAR *pError);
Der erste Parameter ist für den SampleDebugger ziemlich wichtig. Dieser harmlos aussehende Zeiger auf eine Thread-Schnittstelle stellt für den SampleDebugger sozusagen das Tor zur Sicht auf den Aufrufstapel dar, zur Bewertung von Ausdrücken und so weiter. Es bietet nämlich über den Aufruf von IRemoteDebugApplicationThread::GetApplication den Zugriff auf die Elternanwendung. Sobald der Zugriff auf die Elternanwendung möglich ist, ist im Prinzip auch der Zugriff auf alle Aspekte des Debug-Gerüsts möglich. Daher speichere ich den Threadzeiger für die spätere Verwendung.
Die Sprachenmaschine kann in verschiedenen Weisen auf Haltepunkte treffen. Falls der Debugger in den verschiedenen Fällen auch verschieden reagieren soll, wird es erforderlich, zwischen den Haltepunktarten zu unterscheiden. Das ist die Aufgabe des zweiten Parameters von onHandleBreakPoint. Im Active Scripting werden sieben verschiedene Umstände definiert, unter denen der Aufruf von onHandleBreakPoint erfolgt. Diese Umstände werden vom Aufzählwert BREAKREASON erfasst (Tabelle T6).
T6 Die Gründe für den Aufruf von onHandleBreakPoint
|
Aufzählwert |
Beschreibung |
|
BREAKREASON_STEP |
Die Wiederaufnahme der Anwendung nach dem vorigen Haltepunkt erfolgte mit einem Schritt in die nächste Prozedur hinein oder einem Schritt aus der aktuellen Prozedur heraus |
|
BREAKREASON_BREAKPOINT |
Ein mit IDebugCodeContext::Set-BreakPoint explizit konfigurierter Haltepunkt hat angeschlagen |
|
BREAKREASON_DEBUGGER_BLOCK |
Ein anderer Thread in der untersuchten Anwendung hat die Unterbrechung ausgelöst |
|
BREAKREASON_HOST_INITIATED |
Ein Host hat die Unterbrechung gefordert |
|
BREAKREASON_LANGUAGE_INITIATED |
Eine Anweisung im Code hat den Halt ausgelöst (zum Beispiel die Stop-Anweisung in VBScript) |
|
BREAKREASON_DEBUGGER_HALT |
Die Debugger-IDE hat eine Unterbrechung gefordert |
|
BREAKREASON_ERROR |
Im Code ist ein Fehler aufgetreten |
Der dritte Parameter von onHandleBreakPoint ist ein Zeiger auf ein Fehlerobjekt. Der Parameter ist nur dann nicht NULL, wenn im BREAKREASON-Parameter der Wert BREAKREASON_ERROR steht. In diesem Fall enthält der dritte Parameter einen Zeiger auf eine IActiveScriptErrorDebug-Schnittstelle. Mit diesem Zeiger auf IActiveScriptErrorDebug ist der Zugang zu den vorhandenen Fehlerinformationen möglich. Außerdem lässt sich der Stapelrahmen und der Dokumentkontext zum Zeitpunkt des Auftretens des Fehlers ermitteln. Daher kann der Anwendungsdebugger nicht nur die Position im Quelltext dokumentieren, an welcher der betreffende Fehler aufgetreten ist, sondern auch die Laufzeitbedingungen, unter denen es zum Fehler kam. Im Falle des SampleDebuggers interessiert mich aber nur BREAKREASON_BREAKPOINT, weil der SampleDebugger daran erkennt, dass der Code auf einen Haltepunkt aufgelaufen ist, den der Anwender gesetzt hat. Sobald der Hinweis auf den Haltepunkt eintrifft, wird die Nummer der dazugehörigen Quelltextzeile ermittelt und ein Dialog wie in Bild B4 angezeigt.
B4 Das Skript ist auf einen Haltepunkt aufgelaufen.
Es ist übrigens unerwartet aufwendig, die Nummer der Quelltextzeile zu ermitteln. An dieser Stelle macht sich die Implementierung eines schlauen Hosts tatsächlich bezahlt, weil man die Funktionalität solch eines schlauen Hosts braucht, um herauszufinden, in welcher Zeile der betreffende Haltepunkt liegt. Nun ist der SampleDebugger als "Gastgeber" zwar ziemlich dumm, aber zum Glück bietet die Sprachenmaschine von sich aus auch die erforderliche Funktionalität an.
CApplicationDebugger::onHandleBreakPoint in der Datei ApplicationDebugger.cpp unternimmt zur Ermittlung der Quelltextzeile folgende Schritte:
-
Zähle den Stapelrahmen des Threads auf, den onHandleBreakPoint liefert.
-
Beschaffe die erste IDebugCodeContext vom ersten aufgezählten Stapelrahmen.
-
Beschaffe die IDebugDocumentContext vom Codekontext.
-
Beschaffe die IDebugDocument vom Dokumentkontext.
-
Fordere von der QueryInterface-Methode der Schnittstelle IDebugDocument die Schnittstelle IDebugDocumentText an.
-
Übergibt den Debug-Dokumentkontext aus Schritt 3 an die Methode IDebugDocumentText::GetPositionOfContext.
-
Rufe IDebugDocumentText::GetLineOfPosition mit der Zeichenposition auf, die in Schritt 6 ermittelt wurde.
Würden Host und Anwendungsdebugger nicht in derselben Anwendung liegen, wie es beim SampleDebugger der Fall ist, wäre es recht praktisch, wenn der Debugger den Quelltext besorgen könnte. Der Zugriff auf den Quelltext erfolgt über die Schnittstelle IDebugDocumentText. Aus dem Schritt 5 haben Sie bereits einen IDebugDocumentText-Schnittstellenzeiger, mit dem Sie Zugang zum Quelltext erhalten. Wenn Sie also den Quelltext brauchen und kein entsprechendes Textfenster in der Anwendung haben, aus dem Sie diesen Text wie im SampleDebugger auslesen können, hilft Ihnen vielleicht das Beispiel aus Listing L1 weiter.
L1 Beschaffung des Quelltextes
ULONG numLines;
ULONG numChars;
ULONG charsRcd = 0;
// Die Größe des erforderlichen Speicherblocks richtet sich nach dem
// Textumfang.
hRes = ddt->GetSize(&numLines,&numChars);
WCHAR *sourceText = new WCHAR[numChars];
SOURCE_TEXT_ATTR *sourceTextAttr = new SOURCE_TEXT_ATTR[numChars];
hRes = ddt->GetText(position, // cCharacterPosition
sourceText, // pcharText
sourceTextAttr, // pstaTextAttr
&charsRcd, // pcNumChars
numChars // cMaxChars
);
// tue etwas mit dem beschafften Quelltext
delete [] sourceText;
delete [] sourceTextAttr;
Wiederaufnahme des Programmlaufs
Sobald die Anwendung auf einen Haltepunkt aufläuft, wartet sie auf Anweisungen vom Anwendungsdebugger, wie denn weiter zu verfahren sei. Es ist nun Sache des Anwendungsdebuggers, dafür zu sorgen, dass die Anwendung wieder ihren Betrieb aufnimmt. Tut er das nicht, kann sie ihren Lauf nicht abschließen. Also sollte der Debugger unabhängig von dem Grund für die Breakpoint-Nachricht dafür sorgen, dass die Anwendung weiterläuft.
Wenn Sie sich die Schnittstelle IRemoteDebugApplication genauer anschauen, werden Sie auf die Methode ResumeFromBreakPoint stoßen, die genau für diesem Zweck implementiert wurde. Das Problem ist, dass der PDM keine Referenz auf eine IRemoteDebugApplication liefert. Allerdings liefert der PDM eine Referenz auf den Thread, der vom Haltepunkt unterbrochen wurde. Wie schon erwähnt, ist es mit diesem Thread möglich, die Elternanwendung zu ermitteln. Anschließend kann man den Thread mit der IRemoteDebugApplication-Schnittstelle der Elternanwendung wieder anwerfen, wie es die folgenden Zeilen aus der Datei SampleDebuggerView.cpp zeigen:
IRemoteDebugApplication *rda;
HRESULT hRes;
hRes = m_remoteDebugApplicationThread->GetApplication(&rda);
hRes = rda->ResumeFromBreakPoint(m_remoteDebugApplicationThread,
BREAKRESUMEACTION_CONTINUE,
ERRORRESUMEACTION_SkipErrorStatement
);
rda->Release();
Der obige Code zeigt einen typischen Mechanismus, der einen Anwendungsthread dazu veranlasst, die Arbeit wieder aufzunehmen. Es ist sehr wichtig, dass der entsprechende Aufruf, mit dem die Anwendung den Thread anwerfen möchte, nicht im Kontext des onHandleBreakPoint-Hinweises vom PDM erfolgt. Sonst hätte man ein ausgewachsenes Wiedereintrittsproblem, weil ein weiterer Haltepunkt anschlagen könnte, der zu einem weiteren onHandleBreakPoint-Aufruf führen würde. Da der erste Aufruf noch nicht abgeschlossen ist, würde die Funktion onHandleBreakPoint ein zweites Mal aufgerufen. Das Problem wird noch verstärkt, wenn die Haltepunkte ständig anschlagen und der Aufrufstapel wächst. Dann wird der Debugger über kurz oder lang abstürzen.
Zur Implementierung der üblichen Debug-Vorgänge wie Schrittweise hineingehen, mit einem Schritt überspringen, Fortfahren und so weiter benutze ich dieselbe Funktion ResumeFromBreakPoint von IRemoteDebugApplication. Allerdings ändere ich das zweite Argument. Es enthält Anweisungen für den PDM, wie die Anwendung den Betrieb wieder aufnehmen soll. Tabelle T7 zeigt die Wiederaufnahme- oder Fortführungsflags vom Active Scripting und ihre Wirkungen. Durch die Angabe von verschiedenen Fortführungsflags kann der SampleDebugger dem Anwender ein umfassendes Angebot an Varianten für die Fortführung des Programms machen.
T7 Flags zur Steuerung des weiteren Programmlaufs
|
Aufzählwert |
Beschreibung |
|
BREAKRESUMEACTION_ABORT |
Bewirkt den sofortigen Abbruch der Anwendung. |
|
BREAKRESUMEACTION_CONTINUE |
Veranlasst die Anwendung zum Weiterlaufen (bis zum Ende oder bis zum nächsten Haltepunkt). |
|
BREAKRESUMEACTION_STEP_INTO |
Veranlasst die Anwendung, einen Schritt in die nächste verfügbare Prozedur hineinzugehen. Das löst einen weiteren onHandleBreakPoint-Aufruf aus, sobald der Schritt abgeschlossen ist. |
|
BREAKRESUMEACTION_STEP_OVER |
Veranlasst die Anwendung, in einem Schritt über die nächste verfügbare Prozedur hinwegzugehen. Das löst einen weiteren onHandleBreakPoint-Aufruf aus, sobald der Schritt abgeschlossen ist. |
|
BREAKRESUMEACTION_STEP_OUT |
Veranlasst die Anwendung, die aktuelle Prozedur zu verlassen. Das löst einen weiteren onHandleBreakPoint-Aufruf aus, sobald der Schritt abgeschlossen ist. |
Nachdem die Arbeitsweise des SampleDebuggers nun hinreichend geklärt ist, möchte ich kurz auf den Code für die verschiedenen Fenster der Anwendung eingehen. Wie schon gesagt, setzt der SampleDebugger Haltepunkte, zeigt den Aufrufstapel an, untersucht Variablen, bewertet Ausdrücke (in einem Direktfenster) und zählt Anwendungen sowie Anwendungsthreads auf. Daher habe ich ein Breakpoint-Fenster implementiert, ein Call Stack-Fenster, das Immediate-Fenster, das Variables-Fenster, das Threads-Fenster und das Applications-Fenster.
Das Breakpoints-Fenster
Im Breakpoints-Fenster können Sie die Haltepunkte freischalten, sperren oder entfernen, die mit dem Menüpunkt Insert Breakpoint eingefügt wurden. Das Breakpoints-Fenster zeigt die Liste der Haltepunkte an, indem es von jedem Haltepunkt die entsprechende Zeilennummer angibt und mit den Buchstaben E und D ausdrückt, ob er freigeschaltet (enabled) oder gesperrt ist (disabled). Außerdem bietet das Fenster einige Schaltflächen an, mit denen die Haltepunkte freigeschaltet, gesperrt oder aus der Liste entfernt werden können. Das Breakpoints-Fenster ist in Bild B5 zu sehen.
B5 Das Breakpoints-Fenster zeigt die aktuellen Haltepunkte an.
Seine Haltepunkte beschafft sich das Breakpoints-Fenster wie schon beschrieben aus der Haltepunktsliste und bereitet sie in der Funktion OnInitDialog auf die Anzeige vor. Der Code zum Freischalten, Sperren und Entfernen der Haltepunkte aus der Liste ist nahezu identisch mit dem Code, mit dem die Haltepunkte anfangs hinzugefügt wurden. Der wichtigste Unterschied zwischen diesen drei Vorgängen liegt in den Informationen, die an IDebugDocumentContext::SetBreakPoint weitergegeben werden. Im Active Scripting wurden drei Werte für SetBreakPoint definiert: BREAKPOINT_DELETED zum Löschen eines Haltepunkts, BREAKPOINT_DISABLED zum Sperren eines Haltepunkts und schließlich BREAKPOINT_ENABLED zum Freischalten.
Das Call Stack-Fenster
Wenn das Programm auf einen Haltepunkt gelaufen ist, gibt es meistens Gründe genug, um sich den Aufrufstapel der Anwendung anzusehen. Das entsprechende Call Stack-Fenster (Bild B6) lässt sich sogar ziemlich einfach produzieren.
B6 Der Call Stack-Dialog zeigt den Aufrufstapel.
Der SampleDebugger weiß, auf welchem Thread die Anwendung lief, als der Haltepunkt anschlug, weil der PDM den Thread im Zuge des onHandleBreakPoint-Aufrufs bekannt gibt, mit dem er das Ereignis meldet. Also zählt der SampleDebugger einfach mit IRemoteDebugApplicationThread::EnumStackFrames die Stapelrahmen für den Thread auf, auf dem das Ereignis stattfand. Diese Funktion liefert einen Zeiger auf die Schnittstelle IEnumDebugStackFrames, mit dem sich alle Stapelrahmen des Threads aufzählen lassen. Leider gibt IEnumDebugStackFrames nicht gleich ein Array mit IDebugStackFrame-Zeigern zurück. Statt dessen liefert es ein Array mit DebugStackFrameDescriptor-Strukturen. Jede dieser Stapelrahmenstrukturen stellt eine Funktionsebene im Aufrufbaum dar. Mit dem Aufzähler lässt sich also der Stapel einer Anwendung rekonstruieren. Die Struktur wird folgendermaßen definiert:
typedef struct tagDebugStackFrameDescriptor
{
IDebugStackFrame __RPC_FAR *pdsf;
DWORD dwMin;
DWORD dwLim;
BOOL fFinal;
IUnknown __RPC_FAR *punkFinal;
} DebugStackFrameDescriptor;
Der erste Parameter ist der Stapelrahmen und die anderen dienen zum Sortieren der Stapelrahmen. Für die Sortierung dieser Stapelrahmen ist der PDM zuständig. Er sortiert die Rahmen mit Hilfe dieser zusätzlichen Felder so, dass der erste Eintrag bei der Aufzählung die Spitze des Aufrufstapels darstellt, während der letzte Eintrag den Boden des Aufrufstapels bildet.
Sobald ich den Aufrufstapel habe, brauche ich noch eine Beschreibung der aktuellen Funktion in Textform und ihre Adresse. Die Ergebnisse zeige ich in einem normalen Listenfeld an. Der entsprechende Code steht in der OnInitDialog-Funktion des Dialogs in der Datei CallStackDlg.cpp (Listing L2). Übrigens kann IEnumDebugStackFrames mit Stapelrahmen von verschiedenen Sprachmaschinen umgehen. Und das sogar gleichzeitig. Sie können also einen Stapelrahmen anzeigen, an dem mehrere Quelltextsprachen beteiligt sind.
L2 OnInitDialog bereitet den Stapelrahmen für die Anzeige vor
BOOL CCallStackDlg::OnInitDialog()
{
// lokale Variablen
HRESULT hRes; // Rückgabewert der COM-Aufrufe
BSTR functionName; // Name der Funktion Rahmen
IEnumDebugStackFrames *stackFrameEnum; // Rahmenaufzähler
// für Thread
DebugStackFrameDescriptor stackFrame; // Bestimmter Stapel-
// rahmen in Aufzählung
ULONG numFetched; // Zahl der Stapelrahmen aus
// der Aufzählung
// Anfang
USES_CONVERSION;
CDialog::OnInitDialog();
hRes = m_remoteApplicationThread->EnumStackFrames(&stackFrameEnum);
hRes = stackFrameEnum->Reset();
while(stackFrameEnum->Next(1,
&stackFrame,
&numFetched
) == S_OK
)
{
CString callStackEntry;
// Beschaffe eine Beschreibung für die Funktion im Stapel-
// rahmen. Das true als erstes Argument bedeutet, dass die
// Informationen eine variable Länge haben dürfen.
stackFrame.pdsf->GetDescriptionString(true,
&functionName
);
callStackEntry.Format(_T("0x%08X\t%s"),
stackFrame.dwMin,
OLE2T(functionName)
);
m_callStackList.AddString(callStackEntry);
// Die Parameter aus der Stapelrahmenbeschreibung müssen wir
// manuell entsorgen
::SysFreeString(functionName);
stackFrame.pdsf->Release();
} // Ende while(dsfs->Next(nextFrame,&dsf,&nextFrame) == S_OK)
stackFrameEnum->Release();
return TRUE; // geben Sie TRUE zurück, sofern Sie nicht den Fokus
// auf ein Steuerelement setzen. AUSNAHME:
// OCX-Propertypages müssen FALSE zurückgeben
} // Ende CCallStackDlg::OnInitDialog
Der Call Stack-Dialog ließe sich erweitern, so dass der Host einmal zeigen kann, was ein richtiger Host ist. Der Anwender könnte im Dialog zum Beispiel eine Zeile mit einem Doppelklick anklicken und dadurch den Debugger veranlassen, das entsprechende Dokument zu öffnen und den Quelltext von der betreffenden Funktion anzuzeigen. Vorausgesetzt, er hat einen schlauen Host. Das wäre eine Art natürlicher Erweiterung dessen, was das Active-Debugging-Grundgerüst bereits bietet. Um den Quelltext einer aufgerufenen Funktion zu finden, brauche ich einen Codekontext. Da ich bereits einen DebugStackFrameDescriptor habe, kann ich den Stapelrahmen mit dem IDebugStackFrame-Schnittstellenzeiger aus dieser Struktur anfordern. Habe ich den Stapelrahmen, kann ich den Codekontext des Stapelrahmens anfordern und dann das Quelldokument (das läuft in ähnlicher Weise ab wie die Ermittlung der Quelltextzeile nach dem Anschlagen des Haltepunkts).
Immediate-Fenster
Ein Direktfenster wird in Entwicklungsumgebungen wie Visual Basic normalerweise dazu benutzt, den Inhalt eines Programms zur Laufzeit zu ändern, Ausdrücke zu bewerten oder Variablen zu untersuchen. Alle drei Operationen haben dem Umstand gemein, dass sie sich in der Host-Sprache als Ausdrücke formulieren lassen. Es ist diese Gemeinsamkeit, die es dem SampleDebugger ermöglicht, das Direktfenster mit vergleichsweise wenigen Codezeilen zu implementieren. Das Direktfenster aus Bild B7 ermöglicht dem Anwender im oberen Expression-Feld die Eingabe eines beliebigen Ausdrucks. Sobald der Anwender dann die Evaluate-Schaltfläche drückt, wird der Ausdruck zur Bewertung an die Sprachenmaschine weitergeleitet. Das Ergebnis dieser Bewertung erscheint im Result-Feld.
B7 Das Immediate-Fenster.
Das Direktfenster ist ein ziemlich einfacher Dialog. Der einzige nennenswerte Code in ImmediateDlg.cpp ist die Funktion, die für die Schaltfläche Evaluate zuständig ist (Listing L3). Dieser Code führt ein noch nicht besprochenes Thema ins Active Scripting Framework ein, nämlich die Bewertung von Ausdrücken. Ausdrücke sind in diesem Sinne kleine Codeschnipsel, die im Kontext eines bestimmten Stapelrahmens ausgeführt werden. Dieser Stapelrahmenkontext ist wichtig. Er wird nämlich für die Zuordnung der lokalen Variablen gebraucht, die bei der Bewertung des Ausdrucks benutzt werden sollen.
L3 In OnBtnEvaluate werden Ausdrücke bewertet
void CImmediateDlg::OnBtnEvaluate()
{
// lokale Variablen
IEnumDebugStackFrames *stackFrames; // Aufzähler der Rahmen
DebugStackFrameDescriptor sfd; // Stapelrahmen für Bewertung
// des Ausdrucks
ULONG numFetched; // Zahl der aufgezählten
// Stapelrahmen
HRESULT hRes; // Ergebnisse der COM-Aufrufe
IDebugExpressionContext *ec; // Kontext für Bewertung
// des Ausdrucks
IDebugExpression *debugExpression; // zu bewertender Ausdruck
// Callback-Objekt, wird beim Abschluss der Bewertung aufgerufen
CComObject<CDebugExpressionCallback> *debugCallback;
// BEGIN
// Es beginnt alles mit einem Stapelrahmen...
hRes = m_remoteApplicationThread->EnumStackFrames(&stackFrames);
// Wir brauchen nur den obersten Stapelrahmen, weil dort alle
// Variablen liegen, die uns interessieren
hRes = stackFrames->Reset();
hRes = stackFrames->Next(1,
&sfd,
&numFetched
);
if(hRes == S_OK)
{
USES_CONVERSION;
CComObject<CDebugExpressionCallback>::
CreateInstance(&debugCallback);
// Der Ausdruckskontext hängt mit dem Stapelrahmen zusammen
hRes = sfd.pdsf->QueryInterface(IID_IDebugExpressionContext,
(LPVOID*)&ec
);
UpdateData(TRUE);
// Lade den Text des Ausdrucks in den Ausdruckskontext
hRes = ec->ParseLanguageText(
T2OLE(m_expression), // Ausdruck
10, // Basis (Radix)
NULL, // Textbegrenzer
DEBUG_TEXT_RETURNVALUE | // Wert zurückgeben
DEBUG_TEXT_ISEXPRESSION,
&debugExpression // Ergebnis
);
// Ausdruck bewerten.
hRes = debugExpression->Start(debugCallback);
// Wir warten mit unserer Rückrufklasse auf den Abschluss
// der Berechnung
debugCallback->WaitForCompletion();
HRESULT returnResu<
BSTR returnValue;
// Beschaffe die Ergebnisse der Bewertung
hRes = debugExpression->GetResultAsString(&returnResult,
&returnValue
);
m_result = OLE2T(returnValue);
// Gib die Ressourcen wieder ab
::SysFreeString(returnValue);
UpdateData(FALSE);
} // end if(hRes == S_OK)
} // Ende CImmediateDlg::OnBtnEvaluate
Ausgangspunkt für die Bewertung des Ausdrucks ist der Thread, der beim Aufruf von onHandleBreakPoint übergeben (und gespeichert) wurde. Von diesem Thread aus lassen sich die Stapelrahmen des Threads aufzählen. Mich interessiert insbesondere der oberste Stapelrahmen, weil er den Kontext darstellt, in dem ich die Ausdrücke bewerten möchte. Dieser Stapelrahmen lässt sich nach der Schnittstelle IDebugExpressionContext befragen, die zur Bewertung des Ausdrucks benutzt wird. Der Ausdruckskontext stellt den Laufzeitkontext dar, in dem die Ausdrücke im Direktfenster bewertet werden. Er hängt eng mit dem Stapelrahmen zusammen, weil der Stapelrahmen einen Teil des Laufzeitkontextes bildet, der von IDebugExpressionContext gebraucht wird.
Sobald ich den Ausdruckskontext habe, kann ich ihn mit dem Quelltext des Ausdrucks versorgen, und zwar durch den Aufruf der Methode ParseLanguageText. Die Methode ParseLanguageText ist sehr flexibel, weil sie der Sprachenmaschine mitteilen kann, wie der übergebene Ausdruck bewertet werden soll. Ihre Signatur sieht so aus:
HRESULT ParseLanguageText(
/* [in] */ LPCOLESTR pstrCode,
/* [in] */ UINT nRadix,
/* [in] */ LPCOLESTR pstrDelimiter,
/* [in] */ DWORD dwFlags,
/* [out] */ IDebugExpression __RPC_FAR *__RPC_FAR *ppe)
Der erste Parameter ist der Quelltext mit dem Ausdruck, der bewertet werden soll. Der zweite stellt die numerische Basis dar, die bei der Bewertung angewendet werden soll. Ich benutze die Basis 10 (dezimal). Allerdings könnte man auch mit einer gewissen Berechtigung die Basis 16 verwenden (hexadezimal). Der dritte Parameter ist ein Begrenzungszeichen für den Parser, falls der String mit dem Ausdruck nicht nullterminiert ist.
Die Flags für den vierten Parameter sind ziemlich interessant. Je nach dem Wert dieses Parameters kann sich der Laufzeitzustand des Codes durch die Bewertung des Ausdrucks ändern (zum Beispiel durch die Aktualisierung einer Variablen mit einem neuen Wert). In gewissem Umfang lässt sich dadurch das Verhalten des Direktfensters von Visual Basic nachahmen. Microsoft nennt diese Art von Aktualisierung "Seiteneffekt" (und es lohnt sich wirklich nicht, Haarspaltereien über solche Begriffe anzufangen). Wenn ein Ausdruck den Laufzeitzustand nicht ändern darf, sollte man das Flag DEBUG_TEXT_NOSIDEEFFECTS angeben. Im Direktfenster des SampleDebuggers soll das zurückgegebene Ergebnis sofort im Result-Feld des Fensters erscheinen. Daher benutzt der SampleDebugger die Flags DEBUG_TEXT_RETURNVALUE und DEBUG_TEXT_ISEXPRESSION. Der erste weist die Sprachenmaschine an, den Wert des Ausdrucks zurückzugeben, und der zweite, den Skripttext als Ausdruck zu analysieren, also nicht als Codeteil, der zum Anwendungscode gehört. Mit anderen Flags lässt sich festlegen, ob für den Code im Ausdruck Haltepunkte berücksichtigt werden sollen und ob Fehlermeldungen erfolgen sollen. Mit diesen Flags arbeitet der SampleDebugger aber nicht.
Der letzte Parameter schließlich ist eine Referenz auf eine IDebugExpression-Schnittstelle, mit der sich die Bewertung des Ausdrucks anwerfen lässt. Nach ihrer Einleitung erfolgt die Bewertung des Ausdrucks asynchron, zumindest aus der Sicht vom SampleDebugger. Über einen IDebugExpressionCallBack-Zeiger, den er in IDebugExpression::Start an die Sprachenmaschine übergibt, wird der SampleDebugger informiert, sobald die Bewertung abgeschlossen ist. Allerdings kann der SampleDebugger auch ein synchrones Verhalten simulieren. Zu diesem Zweck bietet die Klasse, die IDebugExpressionCallBack implementiert, eine Wartefunktion an, die WaitForCompletion heißt und solange auf einem Ereignisobjekt von Win32 blockiert, bis IDebugExpressionCallBack::onComplete aufgerufen wird. Sobald onComplete die Kontrolle erhält, setzt sie das Win32-Ereignisobjekt, das den SampleDebugger von anderen Aktivitäten abhält. Dadurch wird der SampleDebugger-Prozess aufgeweckt, kann sich die Ergebnisse der Bewertung besorgen und das Direktfenster aktualisieren.
Das Variables-Fenster
Das Variablenfenster erlaubt es dem Anwender, zur Laufzeit den Inhalt der im Code definierten Variablen zu untersuchen (Bild B8). Das Variablenfenster des SampleDebuggers stellt die Variablen aus einem gegebenen Kontext als Baum dar. Zur Darstellung gehört ein kurzer und ein langer Name für die jeweilige Variable, ihr Typ und ihr aktueller Wert. Außerdem ermöglicht das Variablenfenster die Aktualisierung des Variablenwerts, also den Ersatz des alten Werts durch einen neuen.
B8 Das Variables-Fenster.
Im Active Scripting werden alle Variablen als Properties betrachtet, und zwar auch solche, bei denen es sich eigentlich um Ergebnis/Rückgabewerte von Funktionen handelt. Daher gibt es im Active Scripting auch eine COM-Schnittstelle namens IDebugProperty, deren Aufgabe die Verwaltung von Properties ist. Und das kann ziemlich schnell kompliziert werden, weil Properties auch Subproperties haben können. Als Beispiel dafür möchte ich das Connection-Objekt von ADO nennen (ActiveX Data Objects). Das Connection-Objekt wird im Code als Property angelegt. Trotzdem implementiert das Connection-Objekt selbst Properties, die sich auch anzeigen lassen. Zu dem Subproperties eines Connection-Objekts gehören zum Beispiel CommandTimeout, ConnectionTimeout und Version. Diese Schachtelung von Properties braucht sich nicht auf ein oder zwei Ebenen zu beschränken. Die Schachtelungen reichen so tief wie die Vorstellungen des Software-Designers. Zum Glück ist IDebugProperty für (fast) alle Fälle gerüstet.
IDebugProperty, definiert wie in Listing L4, kann mit Hilfe einer DebugPropertyInfo-Struktur Informationen über das Property selbst liefern, seinen aktuellen Wert ändern, sein Elternproperty herausfinden und seine Subproperties aufzählen. Es mag bestimmte Propertyarten geben, die ihre Strukturen mit den Datenelementen von DebugPropertyInfo nicht angemessen wiedergeben können. Für solche Fälle bietet IDebugProperty dem Aufrufer GetExtendedInfo an. Die Aufrufer übergeben GUIDs, welche die erweiterten Parameter repräsentieren, an denen sie interessiert sind, und erhalten die Ergebnisse über den Parameter rgvar zurück.
L4 Die Definition der Schnittstelle IDebugProperty
IDebugProperty : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetPropertyInfo(
/* [in] */ DBGPROP_INFO_FLAGS dwFieldSpec,
/* [in] */ UINT nRadix,
/* [out] */ DebugPropertyInfo __RPC_FAR *pPropertyInfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetExtendedInfo(
/* [in] */ ULONG cInfos,
/* [size_is][in] */ GUID __RPC_FAR *rgguidExtendedInfo,
/* [size_is][out] */ VARIANT __RPC_FAR *rgvar) = 0;
virtual HRESULT STDMETHODCALLTYPE SetValueAsString(
/* [in] */ LPCOLESTR pszValue,
/* [in] */ UINT nRadix) = 0;
virtual HRESULT STDMETHODCALLTYPE EnumMembers(
/* [in] */ DBGPROP_INFO_FLAGS dwFieldSpec,
/* [in] */ UINT nRadix,
/* [in] */ REFIID refiid,
/* [out] */ IEnumDebugPropertyInfo __RPC_FAR *__RPC_FAR *ppepi) =
0;
virtual HRESULT STDMETHODCALLTYPE GetParent(
/* [out] */ IDebugProperty __RPC_FAR *__RPC_FAR *ppDebugProp) = 0;
};
Anzahl und Art der Properties, die an jedem Punkt eines Programms verfügbar sind, hängen sehr stark davon ab, welche Regeln in der verwendeten Sprache für die Sichtbarkeit von Variablen gelten. Ein Property mit dem Namen Prop kann in einer Funktion ein String sein und in einer anderen Funktion ein Integer. IDebugProperty kann den Kontext nicht erkennen, in dem sie liegt. Aber der Kontext lässt sich durch die Art festlegen, in der das Property ermittelt wird. Der Laufzeitkontext wird für jeden Punkt der Anwendung in einem Stapelrahmen gespeichert, in IDebugStackFrame. Wie die nähere Untersuchung von IDebugStackFrame zeigt, bietet diese Schnittstelle eine Methode namens GetDebugProperty an, von der man eine IDebugProperty-Schnittstelle erhält.
Wegen der hierarchischen Struktur der Properties zeigt der SampleDebugger die Variablen in einem Tree-Steuerelement. Die Variablen lassen sich im aktuellen Ausführungskontext benutzen. Das bedeutet, dass bei der Aufzählung der Properties der oberste Eintrag auf dem Stapel benutzt wird oder der erste Stapelrahmen, der bei der Aufzählung der Stapelrahmen vom aktuellen Thread genannt wird. Die Properties werden rekursiv bearbeitet und ins Tree-Steuerelement aufgenommen, damit auch Subproperties korrekt bearbeitet werden. Einzelheiten zeigt der Codeausschnitt aus VariablesDlg.cpp in Listing L5. Diese Implementierung taugt wohl kaum für kommerziellen Code (Subproperties können Properties haben, die auf ihre Eltern zurückverweisen). Für das SampleDebugger-Beispiel reicht der Code aber völlig aus.
L5 Die Funktion AddPropertyToList
void CVariablesDlg::AddPropertyToList(HTREEITEM parent,
IDebugProperty *debugProperty)
{
// lokale Variablen
IEnumDebugPropertyInfo *debugPropertyInfo;
HRESULT hRes;
ULONG numFetched = 0;
// BEGIN
USES_CONVERSION;
// Ermittle die bekannten Datenelemente
hRes = debugProperty->EnumMembers(DBGPROP_INFO_STANDARD |
DBGPROP_INFO_DEBUGPROP,
10,
IID_IEnumDebugPropertyInfo,
&debugPropertyInfo);
if(hRes == S_OK)
{
hRes = debugPropertyInfo->Reset();
ULONG propertyCount;
hRes = debugPropertyInfo->GetCount(&propertyCount);
// Lege ein Array an, in dem die Ergebnisse der Aufzählung
// abgelegt werden
DebugPropertyInfo *property = new
DebugPropertyInfo[propertyCount];
// Wir bearbeiten die Properties in einem Rutsch...
hRes = debugPropertyInfo->Next(propertyCount,
property,
&numFetched);
debugPropertyInfo->Release();
if(hRes == S_OK)
{
for(ULONG i = 0; i < numFetched; i++)
{
// Code für das Tree-Element gestrichen...
// Gibt es Subproperties?
if(property[i].m_pDebugProp != NULL)
{
AddPropertyToList(variableItem,
property[i].m_pDebugProp
);
} // end if(property[i].m_pDebugProp != NULL)
} // end for
} // end if(hRes == S_OK)
} // end if(hRes == S_OK)
} // Ende CVariablesDlg::AddPropertyToList
Zur Bearbeitung eines Properties gehört die Aufzählung aller seiner Datenelemente, die Beschaffung der entsprechenden Werte und die Bearbeitung der Subproperties. Für die Ermittlung der Werte von den Datenelementen wird eine weitere Datenstruktur gebraucht, nämlich DebugPropertyInfo. Microsoft hat sie in der Header-Datei dbgprop.h definiert:
typedef struct tagDebugPropertyInfo
{
DBGPROP_INFO_FLAGS m_dwValidFields;
BSTR m_bstrName;
BSTR m_bstrType;
BSTR m_bstrValue;
BSTR m_bstrFullName;
DBGPROP_ATTRIB_FLAGS m_dwAttrib;
IDebugProperty __RPC_FAR *m_pDebugProp;
} DebugPropertyInfo;
Die meisten Felder von DebugPropertyInfo beschreiben die Struktur eines gegebenen Properties. Das letzte Feld mit Namen m_pDebugProp enthält einen Zeiger auf die Schnittstelle IDebugProperty. Dieser Zeiger verweist auf Subproperties des aktuellen Properties. Wenn dieser Wert ungleich NULL ist, geht der SampleDebugger rekursiv in die Hierarchie hinein und bearbeitet die neu gefundenen Properties. Alle Ressourcen in der DebugPropertyInfo-Struktur müssen übrigens vom Aufrufer freigegeben werden.
Damit wäre geklärt, wie das Variablenfenster seine Nutzlast erhält. Und wie erhalten die Variablen ihre neuen Werte? Nun, dafür bieten sich zwei verschiedene Wege an. So können Sie zum Beispiel die Methode SetValueAsString von IDebugProperty aufrufen. Das funktioniert in einfachen Fällen, wenn man zum Beispiel einem Property namens Counter einen einfachen Wert wie 34 zuweisen möchte. Allerdings versagt diese Methode, sobald dem Property ein komplizierterer Ausdruck zugewiesen werden soll. So könnte SetValueAsString zum Beispiel keinen Ausdruck wie 34*56 korrekt zuweisen, weil SetValueAsString eine direkte Umsetzung in den Zieldatentyp vornimmt und sich der String "34*56" nicht unmittelbar in eine Zahl umwandeln lässt.
Für die Zuweisung von Werten an Properties geht der SampleDebugger etwas andere Wege. Er stellt einen Ausdruck zusammen, der eine Zuweisung enthält, und bewertet diesen Ausdruck dann mit eingeschalteten Seiteneffekten. Wie Sie von der Arbeit im Direktfenster her wissen, lässt sich bei eingeschalteten Seiteneffekten der Laufzeitzustand der Anwendung ändern. Also beschafft sich der SampleDebugger einen Ausdruckskontext für den aktuellen Thread, stellt einen einfachen Zuweisungsausdruck als String zusammen (nach dem Muster "%s = %s", wobei der Propertyname auf der linken Seite des Ausdrucks steht und der zuzuweisende Ausdruck auf der rechten Seite) und bewertet diesen Ausdruck. Diese Verfahrensweise lässt auch Zuweisungen mit komplexeren Ausdrücken zu.
Das Threads-Fenster
Zu den wichtigste Verantwortlichkeiten des PDM gehört es, die verschiedenen Anwendungen zu verwalten, die im Prozess laufen. Die Anwendungsverwaltung umfasst auch die Überwachung der verschiedenen Threads, die mit jeder Anwendung im Prozess verknüpft sind. Daher bietet der PDM Funktionen an, mit denen der SampleDebugger die Threads aufzählen kann, die zu einer Anwendung gehören. Außerdem kann er den Zustand dieser Threads abfragen, sie anhalten oder weiterlaufen lassen.
Für diese Aktionen verwendet der SampleDebugger das Threads-Fenster aus Bild B9. Nach dem Muster des Threads-Fensters von Visual C++ gestaltet, zeigt der SampleDebugger eine Liste aller Threads an, die zur gerade untersuchten Anwendung gehören. Außerdem bietet das Threads-Fenster einen Mechanismus zum Anhalten und Weiterlaufen lassen bestimmter Anwendungsthreads an.
B9 Das Threads-Fenster.
Wie bei vielen anderen Dialogen wird auch der Inhalt des Threads-Fensters in der OnInitDialog-Funktion vorbereitet, die in diesem Fall in der Datei CallStackDlg.cpp zu finden ist. Um alle Threads der untersuchten Anwendung aufzählen zu können, braucht der SampleDebugger Zugriff auf die IRemoteDebugApplication-Schnittstelle dieser Anwendung. Nach dem bereits beschriebenen Verfahren ruft der SampleDebugger die Methode GetApplication der bereits gespeicherten Schnittstelle IRemoteDebugApplicationThread auf (den entsprechenden Zeiger übergab der PDM beim Aufruf von onHandleBreakPoint). In IRemoteDebugApplication gibt es eine Methode namens EnumThreads. Sobald also der Zeiger auf die Anwendung verfügbar ist, ruft der SampleDebugger diese Methode auf und erhält von ihr einen Zeiger auf die Schnittstelle IEnumRemoteDebugApplicationThreads. Dahinter verbirgt sich ein COM-üblicher Aufzähler. Folglich zählt der SampleDebugger die Threads der Anwendung mit IEnumRemoteDebugApplicationThreads durch und trägt die Ergebnisse ins Listenfeld ein.
Der Thread selbst hat eine umfangreiche Schnittstelle, über die sein Zustand abfragbar ist, seine ID und seine Nummer. Um die Threadposition zu ermitteln (also die Funktion, in welcher der Thread gerade läuft), muss der SampleDebugger etwas mehr Aufwand treiben. Sie wissen bereits, wie sich der Name einer gerade in Ausführung befindlichen Funktion ermitteln lässt (wie im Call Stack-Fenster). Dasselbe Verfahren möchte ich auch hier einsetzen. Das bedeutet, dass ich für jeden Thread in der Anwendung die Stapelrahmen des jeweiligen Threads aufzähle. Sobald das erledigt ist, nehme ich den ersten Stapelrahmen aus dem Aufzähler (also die Spitze des Stapels) und beschaffe mir den Namen via DebugStackFrameDescriptor.
Nachdem die Daten eingesammelt sind, werden sie in einer Struktur untergebracht, die ich später noch für die Schaltflächen Suspend und Resume brauche. Um einen Thread anzuhalten oder weiterlaufen zu lassen, brauche ich einfach nur einen Zeiger auf den betreffenden Thread (aus der Struktur, die in OnInitDialog ausgefüllt wird) und rufe dann IRemoteDebugApplicationThread::Suspend oder IRemoteDebugApplicationThread::Resume auf.
Das Applications-Fenster
Zur Aufzählung der Anwendungen, die auf der Maschine laufen, wendet sich der SampleDebugger an den MDM. Die Anzeige erfolgt im Applications-Dialog (Bild B10). Der entsprechende Code demonstriert, wie man mit dem MDM kommuniziert.
B10 Das Applications-Fenster.
Vielleicht möchten Sie den Anwendungsdialog so erweitern, dass der Anwender eine Anwendung auswählen und debuggen kann. Das würde dem Verhalten "Attach To Process" des Visual C++-Debuggers ähneln. In der Schnittstelle IRemoteDebugApplication gibt es eine Methode, mit der sich das Debuggen kontrollieren lässt. Sie heißt ConnectDebugger und hat folgende Signatur:
HRESULT ConnectDebugger([in] IApplicationDebugger *pad)
Hat der Anwender also eine Anwendung ausgesucht, die er debuggen möchte, könnte der SampleDebugger die Methode ConnectDebugger aufrufen und ihr eine Referenz auf seine IApplicationDebugger-Schnittstelle übergeben. Anschließend muss der Debugger die ferne Anwendung noch irgendwie dazu bringen, so schnell wie möglich die Kontrolle an ihn abzugeben. Auch dafür gibt es in der Schnittstelle IRemoteDebugApplication eine passende Methode. Sie heißt CauseBreak.
HRESULT CauseBreak(void)
Wenn diese Methode aufgerufen wird, weist die ferne Anwendung die Sprachenmaschine an, die Ausführung der Anwendung zum nächsten möglichen Zeitpunkt abzubrechen. Sobald dieser Punkt erreicht ist, wird die onHandleBreakPoint-Methode der Schnittstelle IApplicationDebugger aufgerufen, die der Anwendung übergeben wurde, als der Debugger mit ihr verbunden wurde. Anschließend kann der Debugger die Anwendung so steuern, als wäre diese Anwendung direkt im Debugger gestartet und nicht erst später mit ihm verbunden worden.
Ein Wort zum Schluss
Können Sie noch? Keine Angst, es ist gleich vorbei. Ich habe eine enorme Materialmenge verarbeitet und konnte trotzdem nur an der Oberfläche kratzen, wenn man das Potential berücksichtigt, das hinter den Debug-Schnittstellen des Active Scriptings steckt. Der SampleDebugger ließe sich in viele Richtungen erweitern, angefangen von der vollständigen Implementierung eines "schlauen Hosts" bis hin zum Debuggen von bereits laufenden Prozessen.
Außerdem lassen sich die hier vorgestellten Konzepte in vielen Anwendungen einsetzen, in denen Skripte zum Einsatz kommen und integrierte Debug-Dienste gebraucht werden.
Literatur:
Christian Andritzky: "Ein Makro-Interpreter in MFC-Anwendungen", System Journal 06/1998, S. 102
George Sheperd: "Active Scripting im praktischen Einsatz", System Journal 01/2000, S. 96
Marcus Heege: "MFC-Dokumente mit dualen ATL Schnittstellen", System Journal 06/1998, S. 96
Dharma Shukla, Chris Sells: "So schreiben Sie praxisgerechte Control-Container", System Journal 02/2000, S. 84