In RAD Studio 10.2.1 we added support for debug visualizers for Delphi generic and C++ template types. A debug visualizer is an IDE plugin that allows you to change the display of a variable in the various debug windows, such as Local Variables and Watches.
"Great!", you may think, "but how do I actually write one of these visualizers?"
Pretty easily, as it turns out.
A debug vizualiser is an interfaced class that implements some specific Open Tools API interfaces (see toolsapi.pas), located in a DLL or package, and an instance of which is registered with the IDE when the package is loaded. Those interfaces are:
- IOTADebuggerVisualizer, IOTADebuggerVisualizer250: name and description of the visualizer, list of types it's interested in; version 250 adds support for generics or templates by allowing you to specify that a type string is for a generic or template type.
- IOTADebuggerVisualizerValueReplacer: this lets you replace the result of an expression returned by the debug evaluator with some other content – whatever you want. You can get more advanced than this, such as showing an external window for the results. This is just a simple example.
- IOTAThreadNotifier, IOTAThreadNotifier160: let you handle an evaluation completing. A notifier, by convention in the ToolsAPI, also always has four other methods to do with saving, destroying and modification that are not meaningful in this example.
Generic and template debug visualizer example
Here's a complete visualizer code snippet. To test this out, create a new package, and add designide to the Requires. Then add a unit called GenericVisualizer.pas to it and paste in the following source code. To test it out, just right-click and Install, and then it's running right there inside your local IDE instance. (You can also debug the IDE with IDE if you want to do some more advanced work and load it while debugging an instance of the IDE with your first copy of the IDE.)
Source code
. unit GenericVisualizer; interface procedure Register; implementation uses Classes, SysUtils, ToolsAPI; type TGenericVisualizer = class(TInterfacedObject, IOTADebuggerVisualizer, IOTADebuggerVisualizer250, IOTADebuggerVisualizerValueReplacer, IOTAThreadNotifier, IOTAThreadNotifier160) private FCompleted: Boolean; FDeferredResult: string; public { IOTADebuggerVisualizer } function GetSupportedTypeCount: Integer; procedure GetSupportedType(Index: Integer; var TypeName: string; var AllDescendants: Boolean); overload; function GetVisualizerIdentifier: string; function GetVisualizerName: string; function GetVisualizerDescription: string; { IOTADebuggerVisualizer250 } procedure GetSupportedType(Index: Integer; var TypeName: string; var AllDescendants: Boolean; var IsGeneric: Boolean); overload; { IOTADebuggerVisualizerValueReplacer } function GetReplacementValue(const Expression, TypeName, EvalResult: string): string; { IOTAThreadNotifier } procedure EvaluteComplete(const ExprStr: string; const ResultStr: string; CanModify: Boolean; ResultAddress: Cardinal; ResultSize: Cardinal; ReturnCode: Integer); procedure ModifyComplete(const ExprStr: string; const ResultStr: string; ReturnCode: Integer); procedure ThreadNotify(Reason: TOTANotifyReason); procedure AfterSave; procedure BeforeSave; procedure Destroyed; procedure Modified; { IOTAThreadNotifier160 } procedure EvaluateComplete(const ExprStr: string; const ResultStr: string; CanModify: Boolean; ResultAddress: TOTAAddress; ResultSize: LongWord; ReturnCode: Integer); end; type TGenericVisualierType = record TypeName: string; IsGeneric: Boolean; end; const GenericVisualizerTypes: array [0 .. 4] of TGenericVisualierType = ( (TypeName: 'MyGenericType.IGenericInterface<T,T2>'; IsGeneric: True), (TypeName: 'MyGenericType.TGenericClass<System.Integer>'; IsGeneric: False), (TypeName: 'MyGenericType.TGenericClass<T>'; IsGeneric: True), (TypeName: 'std::vector<T, Allocator>'; IsGeneric: True), (TypeName: 'std::map<K, V, Compare, Allocator>'; IsGeneric: True) ); { TGenericVisualizer } procedure TGenericVisualizer.AfterSave; begin // don't care about this notification end; procedure TGenericVisualizer.BeforeSave; begin // don't care about this notification end; procedure TGenericVisualizer.Destroyed; begin // don't care about this notification end; procedure TGenericVisualizer.Modified; begin // don't care about this notification end; procedure TGenericVisualizer.ModifyComplete(const ExprStr, ResultStr: string; ReturnCode: Integer); begin // don't care about this notification end; procedure TGenericVisualizer.EvaluateComplete(const ExprStr, ResultStr: string; CanModify: Boolean; ResultAddress, ResultSize: Cardinal; ReturnCode: Integer); begin EvaluateComplete(ExprStr, ResultStr, CanModify, TOTAAddress(ResultAddress), LongWord(ResultSize), ReturnCode); end; procedure TGenericVisualizer.EvaluateComplete(const ExprStr, ResultStr: string; CanModify: Boolean; ResultAddress: TOTAAddress; ResultSize: LongWord; ReturnCode: Integer); begin FCompleted := True; if ReturnCode = 0 then FDeferredResult := ResultStr; end; procedure TGenericVisualizer.ThreadNotify(Reason: TOTANotifyReason); begin // don't care about this notification end; function TGenericVisualizer.GetReplacementValue(const Expression, TypeName, EvalResult: string): string; begin Result := 'Generic visualizer:' + '; ' + 'Expression: ' + Expression + '; ' + 'Type name: ' + TypeName + '; ' + 'Evaluation: ' + EvalResult; end; function TGenericVisualizer.GetSupportedTypeCount: Integer; begin Result := Length(GenericVisualizerTypes); end; procedure TGenericVisualizer.GetSupportedType(Index: Integer; var TypeName: string; var AllDescendants: Boolean); begin AllDescendants := True; TypeName := GenericVisualizerTypes[index].TypeName; end; procedure TGenericVisualizer.GetSupportedType(Index: Integer; var TypeName: string; var AllDescendants: Boolean; var IsGeneric: Boolean); begin AllDescendants := True; TypeName := GenericVisualizerTypes[index].TypeName; IsGeneric := GenericVisualizerTypes[index].IsGeneric; end; function TGenericVisualizer.GetVisualizerDescription: string; begin Result := 'Sample on how to register a generic visualizer'; end; function TGenericVisualizer.GetVisualizerIdentifier: string; begin Result := ClassName; end; function TGenericVisualizer.GetVisualizerName: string; begin Result := 'Sample Generic Visualizer'; end; var GenericVis: IOTADebuggerVisualizer; procedure Register; begin GenericVis := TGenericVisualizer.Create; (BorlandIDEServices as IOTADebuggerServices).RegisterDebugVisualizer(GenericVis); end; procedure RemoveVisualizer; var DebuggerServices: IOTADebuggerServices; begin if Supports(BorlandIDEServices, IOTADebuggerServices, DebuggerServices) then begin DebuggerServices.UnregisterDebugVisualizer(GenericVis); GenericVis := nil; end; end; initialization finalization RemoveVisualizer; end.
To test
For Delphi
Define some types:
. type IGenericInterface<T,T2> = interface ['{2395EEA7-7E2E-485E-B6D3-C424A12FAE7F}'] end; TGenericClassFromInterface<T,T2> = class(TInterfacedObject, IGenericInterface<T,T2>) end; TGenericClass<T> = class(TObject) end; TGenericClassDescendant<T> = class(TGenericClass<T>) end; TGenericClassDescendantInt = class(TGenericClass<Integer>) end;
This example is longer than the C++ one, because it demonstrates both generic classes and interfaces. It also shows some descendant types, where you need to register the descendant type separately. (For a non-generic-type debug visualizer, the visualizer is called for descendants of the registered type automatically.)
And create a new console app, giving it the contents:
. procedure foo; var a: TGenericClassFromInterface<Integer, string>; a1: IGenericInterface<Integer, string>; b: TGenericClass<string>; c: TGenericClass<Integer>; d: TGenericClassDescendant<string>; e: TGenericClassDescendantInt; begin a := TGenericClassFromInterface<Integer, string>.Create; a1 := a; b := TGenericClass<string>.Create; c := TGenericClass<Integer>.Create; d := TGenericClassDescendant<string>.Create; e := TGenericClassDescendantInt.Create; writeln('abc'); // set breakpoint here end; begin try foo; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
For C++
Create a new console app (and some shiny new template types as well, if you wish) and give it the contents:
. int _tmain(int argc, _TCHAR* argv[]) { std::vector<int> myVec; std::map<int, std::string> myMap; return 0; // BP here }
To extend
This is the exciting bit! Try changing the data returned from GetReplacementValue(), and also try evaluating expressions yourself in the visualizer to use in the returned replacement.
Have fun!