Tuesday, September 22, 2009

TValue in Depth

TValue is one of the key Types in the new RTTI System. We already covered some of the basics in the introduction to TValue. It's now time to pull back the covers and explorer how it was designed so you can exploit the entire power of TValue.


Before we get too far, lets take a look at the field in the interface of TValue.


TValue = record
...
private
FData: TValueData;
end;



Since TValue can store data from any type. I was interested in how this accomplished, it proved useful
in determining how to store data from various unknown types I knew I would be throwing at it in the future.

TValueData is defined as:

TValueData = record
FTypeInfo: PTypeInfo;
// If interface, then a hard-cast of interface to IInterface.
// If heap data (such as string, managed record, array, etc.) then IValueData
// hard-cast to IInterface.
// If this is nil, then the value hasn't been initialized and is empty.
FHeapData: IInterface;
case Integer of
0: (FAsUByte: Byte);
1: (FAsUWord: Word);
2: (FAsULong: LongWord);
3: (FAsObject: TObject);
4: (FAsClass: TClass);
5: (FAsSByte: Shortint);
6: (FAsSWord: Smallint);
7: (FAsSLong: Longint);
8: (FAsSingle: Single);
9: (FAsDouble: Double);
10: (FAsExtended: Extended);
11: (FAsComp: Comp);
12: (FAsCurr: Currency);
13: (FAsUInt64: UInt64);
14: (FAsSInt64: Int64);
15: (FAsMethod: TMethod);
end;


It's just a variant record, that takes 24 bytes of memory.

The key parts are FTypeInfo, and then either FHeapData or one of the variant parts
would be use to store the data.

Knowing this how this is stored helps in understanding the low level routines to set and access
the data stored in a TValue.


TValue = record
...
public
...
// Low-level in
class procedure Make(ABuffer: Pointer; ATypeInfo: PTypeInfo; out Result: TValue); overload; static;
class procedure MakeWithoutCopy(ABuffer: Pointer; ATypeInfo: PTypeInfo; out Result: TValue); overload; static;
class procedure Make(AValue: NativeInt; ATypeInfo: PTypeInfo; out Result: TValue); overload; static;

// Low-level out
property DataSize: Integer read GetDataSize;
procedure ExtractRawData(ABuffer: Pointer);
// If internal data is something with lifetime management, this copies a
// reference out *without* updating the reference count.
procedure ExtractRawDataNoCopy(ABuffer: Pointer);
function GetReferenceToRawData: Pointer;
function GetReferenceToRawArrayElement(Index: Integer): Pointer;
...
end;


Basically, you can use Make() to place any data with type information into a TValue.

Here is an example that placed an Integer and TRect in


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
IntData : Integer;
IntValue : TValue;

RecData : TRect;
RecValue : TValue;

begin
IntData := 1234;
//Granted it's easier to call IntValue := IntData; but this is an example.
TValue.Make(@IntData,TypeInfo(Integer),IntValue);
Writeln(IntValue.ToString);
RecData.Left := 10;
RecData.Right := 20;
TValue.Make(@RecData,TypeInfo(TRect),RecValue);
Writeln(RecValue.ToString);
readln;
end.

Output:

1234
(record)


When dealing with Deserialization issues I realized I had to recreate record structures when I did not know anything but the TypeInfo. This can be done by calling:


TValue.Make(nil,TypeInfoVar,OutputTValue);


Extracting data can also be done using the low level routines, here is an example of using ExtractRawData.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
RecData : TRect;
RecDataOut : TRect;
RecValue : TValue;

begin
RecData.Left := 10;
RecData.Right := 20;
TValue.Make(@RecData,TypeInfo(TRect),RecValue);

RecValue.ExtractRawData(@RecDataOut);
Writeln(RecDataOut.Left);
Writeln(RecDataOut.Right);

readln;
end.

Output:

10
20


You can use things like GetReferenceToRawData() with the SetValue and GetValue on records.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
RecData : TRect;
RecValue : TValue;
Ctx : TRttiContext;

begin
Ctx := TRttiContext.Create;
// Create empty record structure
TValue.Make(nil,TypeInfo(TRect),RecValue);
// Set the Left and Right Members, using the pointer to the Record
Ctx.GetType(TypeInfo(TRect)).GetField('Left').SetValue(RecValue.GetReferenceToRawData,10);
Ctx.GetType(TypeInfo(TRect)).GetField('Right').SetValue(RecValue.GetReferenceToRawData,20);
// Extract the record to report the results.
RecValue.ExtractRawData(@RecData);
Writeln(RecData.Left);
Writeln(RecData.Right);
readln;
Ctx.Free;
end.

Output:

10
20


These little examples show how to deal with types that you don't know about at compile time. However, sometimes you do know the type you will be working with at compile time. When this is know it becomes much easier to to work with using a the Generic/Parametrized Type functions that TValue Provides:


class function From<T>(const Value: T): TValue; static;
function AsType<T>: T;
function TryAsType<T>(out AResult: T): Boolean;
function Cast<T>: TValue; overload;


The following example shows From<T>() IsType<T>() and AsType<T>() in use.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
RecData : TRect;
RecDataOut : TRect;
RecValue : TValue;
begin
RecData.Left := 10;
RecData.Right := 20;

RecValue := TValue.From<TRect>(RecData);

Writeln(RecValue.IsType<TRect>);

RecDataOut := RecValue.AsType<TRect>;

Writeln(RecDataOut.Left);
Writeln(RecDataOut.Right);
readln;
end.

Output:

TRUE
10
20


There are also other function on TValue that help you with Array Types.


function GetArrayLength: Integer;
function GetArrayElement(Index: Integer): TValue;
procedure SetArrayElement(Index: Integer; const AValue: TValue);


It should be noted that dynamic arrays that are declared like this are currently not
supported:


Var
IntArray : Array of Integer;


But if you can your code to be like this they work fine.


type
TIntArray = Array of Integer;

var
I : TIntArray;
// or
I : TArray<Integer>; {defined in System.pas as: TArray<T> = array of T;}


Another small gotcha is that TValue.FromVariant() is misleading.

I thought it meant that I would be taking a Variant and stuffing it into the TValue, however you will find that it really is not doing that, it's storing the data using originating type, the following code shows how it behaves.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, TypInfo,Rtti;

var
vExample : Variant;
Value : TValue;
begin
vExample := 'Hello World';
Value := TValue.FromVariant(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.ToString);

vExample := 1234;
Value := TValue.FromVariant(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.ToString);

readln;
end.

Output:

tkUString
Hello World
tkInteger
1234


If you want to store a Variant into a TValue you can use this method

program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, TypInfo,Rtti;

var
vExample : Variant;
Value : TValue;
begin
vExample := 'Hello World';
Value := TValue.From(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.AsType);

vExample := 1234;
Value := TValue.From(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.AsType);

readln;
end.

Output:

tkVariant
Hello World
tkVariant
1234


There are also other ways to work with TValue that I just don't have the time to cover. I recommend opening up Rtti.pas and exploring the interface to see everything that is available. The items I failed to cover are fairly straight forward.

I hope this give's you a good taste of how TValue works.

RTTI Article List

3 comments:

  1. Thanks for all the RTTI articles, they surely make the new RTTI stuff much more understandlable.

    Q: how does one cast a string into its enumarted value (correctly typed? NOT its Ordinal value. i.e :

    TMyEnum=(my_One,my_Two,my_three);
    res:=StrToEnum(TypeInfo(TMyEnum),'my_Two');
    i need res to be a TValue of type TMyEnum, NOT typed as an integer.

    Is this a good solution? :

    function StrToEnum(aString:String,aPTypeInfo:PTypeInfo) :TValue;
    begin
    OrdVal:=GetEnumValue(aPTypeInfo,aString);
    TValue.Make(OrdVal,aPTypeInfo,result);
    end;

    result should be TValue typed as TMyEnum?

    ReplyDelete
  2. I cann't asign left side of a TRect strcture, as Right or Left!
    How can I do it?

    ReplyDelete
  3. I use TValue.Make(nil,TypeInfoVar,OutputTValue); a lot on dynamic arrays. In Delphi xe2 the result OutputTvalue.isEmpty=false. I recently tried Delphi 10 Berlin. Here the resulting value is OutputTvalue.isEmpty=true. So I had to change my tests from
    if OutputValue.isEmpty then doError
    if not OutputValue.isArray then doError

    ReplyDelete