In this post I will try to explain how to create a “late-bound” dictionary based on invokable variants. Most of the techniques related to the creation of a custom Variant data type are already described in How To: Creating a custom Variant type.
To store the Key/Value pairs in my custom Variant, I will use TDictionary provided in Generics.Collections unit. Also note that the key of the dictionary is String since the we will use the late-bound properties to access specific values in the dictionary. The values are of type Variant, which enables me to store whatever information I want. The following code exemplifies the usage of the variant based dictionary:
var
Person: Variant;
begin
Person := VDict();
Person.FullName := 'John Smith';
Person.Age := 19;
Person.NetWorth := 9000;
WriteLn(Person.FullName);
{ Remove the Age property }
Person.RemoveAge();
{ Write all information stored in dictionary }
WriteLn(Person);
end.
And now let’s get to the actual coding process! The first thing is to create your class that will be held inside the TVarData structure. In my case, I use an already created class called TDictionary. Still, I declared an alias type for my String/Variant dictionary (makes life easier):
{ Declare the String/Variant dictionary that will
hold the real data }
TSVDictionary = TDictionary<String, Variant>;
The next step is the usual “declaring the TVarData record” that will map over our custom variants:
{ Mapping the TSVDictionary into TVarData structure }
TSVDictionaryVarData = packed record
{ Var type, will be assigned at runtime }
VType: TVarType;
{ Reserved stuff }
Reserved1, Reserved2, Reserved3: Word;
{ A reference to the enclosed dictionary }
FDictionary: TSVDictionary;
{ Reserved stuff }
Reserved4: LongWord;
end;
The FDictionary field in TSVDictionaryVarData record holds the dictionary instance used to hold the real key/value pairs. Now comes the important part: declaration of the proxy class that will manage the custom variant type I declared. To support late-binding capabilities in custom variants you have to descend from TInvokeableVariantType class. TInvokeableVariantType adds a few abstract methods which you have to override to intercept all calls made to methods/properties of your custom variant type. Note that TInvokeableVariantType derives from TCustomVariantType which means that you can add all the capabilities of a normal custom variant type on top of the dynamic ones.
The definition of the class I made follows:
{ Manager for our variant type }
TSVDictionaryVariantType = class(TInvokeableVariantType)
private
function DictionaryToString(const ADict: TSVDictionary): String;
public
procedure Clear(var V: TVarData); override;
procedure Copy(var Dest: TVarData; const Source: TVarData;
const Indirect: Boolean); override;
procedure CastTo(var Dest: TVarData; const Source: TVarData;
const AVarType: TVarType); override;
function GetProperty(var Dest: TVarData; const V: TVarData;
const Name: string): Boolean; override;
function SetProperty(const V: TVarData; const Name:
string; const Value: TVarData): Boolean; override;
function DoFunction(var Dest: TVarData; const V: TVarData;
const Name: string; const Arguments: TVarDataArray): Boolean; override;
function DoProcedure(const V: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean; override;
end;
As seen in the code above, I need the following capabilities for my variant based dictionary:
- Clearing. When the variant requires clearing, freeing the TSVDictionary instance is required to avoid memory leaks.
- Copying. Required for the assignments between variants. When copying from a variant to another, I do not copy the instance of the dictionary, rather I create a new one in the copy and copy all Key/Value pairs to it.
- Casting. My variant based dictionary supports casting to strings. Casting to other types is done through string.
- Property Get/Set Calls. Each late-bound property is actually a key in the dictionary. So I need to catch calls to get/set operations.
- Function and Procedure Calls. My dictionary supports three custom operations (one to get the count of pairs, one to remove a pair from a dictionary and one to clear the dictionary).
The next step is to create a function which will create instances of our variant type. The following code shows all the relevant code pieces that are used in registering the proxy class, and creating custom instances of the variant based dictionary.
Most of this code pattern should be already known if you read the previous “How-To”:
...
interface
{ Creates a new Variant Dictionary }
function VDict(): Variant;
implementation
...
var
{ Our singleton that manages our variant type }
SgtSVDictionaryVariantType: TSVDictionaryVariantType;
function VDict(): Variant;
begin
{ Clear out the result }
VarClear(Result);
with TSVDictionaryVarData(Result) do
begin
{ Assign the new variant the var type that was allocated for us }
VType := SgtSVDictionaryVariantType.VarType;
{ Create a new instance of the dictionary object }
FDictionary := TSVDictionary.Create();
end;
end;
...
initialization
{ Register our custom variant type }
SgtSVDictionaryVariantType := TSVDictionaryVariantType.Create();
finalization
{ Uregister our custom variant }
FreeAndNil(SgtSVDictionaryVariantType);
And now let’s see the most important pieces of code that make up the proxy class. Note that I will not present all the methods that make up that class. Most of them are pretty obvious, like CastTo, Clear and Copy.
The first victim is DoFunction, which I implemented to support a late-bound call that returns the count of items in the dictionary:
function TSVDictionaryVariantType.DoFunction(
var Dest: TVarData; const V: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean;
begin
Result := False;
{ We do not support arguments here }
if Length(Arguments) > 1 then
Exit;
{ Is the first parameter an error variant? }
if (Length(Arguments) = 1) and (Arguments[0].VType <> varError) then
Exit;
{ Requesting dictionary count? }
if Name = 'COUNT' then
begin
VariantInit(Dest);
Dest.VType := varInteger;
Dest.VInteger := TSVDictionaryVarData(V).FDictionary.Count;
Exit(true);
end;
{ Try to call the procedures }
Result := DoProcedure(V, Name, Arguments);
end;
As you can see, I check whether a call is made to the Count function. If that is true, I return the number of pairs contained within this dictionary. Otherwise I forward the call to another method of my proxy class called DoProcedure. I do that because DoProcedure supports two more custom calls which can be made as functions or procedures (I don’t care):
function TSVDictionaryVariantType.DoProcedure(
const V: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean;
var
Key: String;
begin
Result := False;
{ We do not support arguments here }
if Length(Arguments) > 1 then
Exit;
{ Is the first parameter an error variant? }
if (Length(Arguments) = 1) and (Arguments[0].VType <> varError) then
Exit;
{ Check if this is a removal call }
if Pos('REMOVE', Name) = 1 then
begin
{ Remve the prefix }
Key := System.Copy(Name, Length('REMOVE') + 1, Length(Name));
TSVDictionaryVarData(V).FDictionary.Remove(Key);
Exit(true);
end;
{ Is this a CLEAR call? }
if Name = 'NAME' then
begin
TSVDictionaryVarData(V).FDictionary.Clear();
Exit(true);
end;
end;
The previous code piece is a bit more interesting because I check whether a call is made to a function that starts with “REMOVE” and cosider the text that continues afterwards as being the name of the key to be removed. For example calls like “MyVar.RemoveAge()” will remove the key named “Age” from the dictionary. The second supported call is Clear which clears the enclosed dictionary.
The last important code piece that follows shows the implementation of property set/get processing methods:
function TSVDictionaryVariantType.GetProperty(
var Dest: TVarData; const V: TVarData;
const Name: string): Boolean;
begin
{ Type cast to our data type }
with TSVDictionaryVarData(V) do
begin
{ Try to read the value from the dictionary }
if not FDictionary.TryGetValue(Name, Variant(Dest)) then
Clear(Dest);
end;
{ Return true in any possible case }
Result := True;
end;
function TSVDictionaryVariantType.SetProperty(
const V: TVarData; const Name: string;
const Value: TVarData): Boolean;
begin
{ Type cast to our data type }
with TSVDictionaryVarData(V) do
{ Update the value in dictionary dictionary }
FDictionary.AddOrSetValue(Name, Variant(Value));
{ Return true in any possible case }
Result := True;
end;
A few things to note about the code:
- In DoProcedure and DoFunction methods, the Arguments array may contain one element even if no parameters are passed to the call. This happens because the compiler treats “MyVar.CallMe()” and “MyVar.CallMe” calls differently. In the first case it adds an argument of type varError, in the second it does not add any arguments.
- The proposed variant dictionary implementation is case-insensitive. That means that “MyVar.Age := 5” is equal to “MyVar.AGE := 5“. If you need to make it case-sensitive, override the FixupIdent method in the proxy class. FixupIdent as implemented in TInvokeableVariantType coverts all identifiers to upper case.
That is all for now. Unfortunatelly I could not find an interesting example to demonstrate the power of late-binding and dynamic dispatch. Everything that can be done dynamically can be done normally anyway…