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.
Table of Contents
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
. 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 EvaluateComplete(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
. 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
. 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:
1 2 3 4 5 6 7 8 9 |
. 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!
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition
Visualization of STL types is paramount to debugging. Inability to inspect simple things like std::vector has frustrated me for years. If this mechanism enables such inspection options, I would highly suggest to integrate this with the IDE, for as many STL types as possible!
For C++builder users without Delphi license, can this be used?
Hi Frank,
We’ve actually replaced this system in 10.4, for the C++ Win64 debugger, and it works even better! You can read about it here: https://blogs.embarcadero.com/new-in-c-builder-10-4-a-new-debugger-for-win64-c/
The new Win64 debugger uses ‘formatters’ which are build into the debugger itself, rather than being a plugin in the IDE, as this post describes. It includes formatters for std::vector, as well as eight or nine other types, and we’ve added a couple more since then. You can also easily add your own data types too, eg if you have your own complex data structure.
And yes, both the system in this post, and the new system in 10.4.x, can be used with standalone C++Builder. We have a beta for 10.4.2 right now for active update subscription customers and if you’re interested, I recommend you try that out.
This is not working in Delphi 12.
There is a bug in the example:
procedure EvaluteComplete(const ExprStr: string; const ResultStr: string;
CanModify: Boolean; ResultAddress: Cardinal; ResultSize: Cardinal;
ReturnCode: Integer);
There is no procedure ´EvaluteComplete´. If it should be
EvaluateComplete
, “overload” is missing in the declaration.Hello — that is a good catch! This article was written in 2017. At some point since then, and I’m afraid I cannot remember the exact year, we fixed that typo. We don’t normally update fixed interfaces but a spelling error seemed worthwhile. If you’re using the latest version, you should hopefully see it as “Evaluate”, and the methods should be overloaded correctly. Re the last point, they’re not?
Thanks for letting us know Roland. I’ve corrected the typo; I appreciate you pointing it out. The blogging platform we currently use means code examples are pasted in or typed manually and that can occasionally lead to some errors.
WRG to it working on RAD Studio 12 – there are many thousands of articles most of which have code examples in them. As a result it’s not really practical or possible to go through each older article and update the code to work with new versions of the IDE. Most work (one of the benefits of Delphi is its backward compatibility), this one seems to need some work though. It’s a pretty elderly article from 2017! 🙂