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
Thanks for all the RTTI articles, they surely make the new RTTI stuff much more understandlable.
ReplyDeleteQ: 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?
I cann't asign left side of a TRect strcture, as Right or Left!
ReplyDeleteHow can I do it?
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
ReplyDeleteif OutputValue.isEmpty then doError
if not OutputValue.isArray then doError