Skip to content

Instantly share code, notes, and snippets.

@jpluimers
Last active February 4, 2026 11:52
Show Gist options
  • Select an option

  • Save jpluimers/2c07b0979245f18706490237b2265f57 to your computer and use it in GitHub Desktop.

Select an option

Save jpluimers/2c07b0979245f18706490237b2265f57 to your computer and use it in GitHub Desktop.
Archived version of "Code snippet: IDE debug visualizer plugin for generic and template types"

Remark

Since one never knows when the Embarcadero sites are up, or when they tear down useful resources like sites for documentation, blogs, communities and forums, below is an important article I saved as it is one of the very few of their own public examples on writing debug visualisers.

All internal links point relative to https://web.archive.org/web/20260204110835/https://blogs.embarcadero.com/code-snippet-ide-debug-visualizer-plugin-for-generic-and-template-types/

I removed the leading . from all code snippets, so there is at least a chance this will compile.

Oh, and I put copies of the source examples into separate files.

Content

Code snippet: IDE debug visualizer plugin for generic and template types

By David Millington 

November 27, 2017

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

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:

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!

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.
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;
int _tmain(int argc, _TCHAR* argv[])
{
std::vector<int> myVec;
std::map<int, std::string> myMap;
return 0;    // BP here
}
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment