Please forgive the verbosity of the following code example. Using Delphi 2009, I created the two classes TOtherClass and TMyClass:
TOtherClass = class(TObject)
public
FData: string;
end;
TMyClass = class(TObject)
private
FIndxPropList: Array of TOtherClass;
function GetIndxProp(Index: Integer): TOtherClass;
procedure SetIndxProp(Index: Integer; Value: TOtherClass);
public
property IndxProp[Index: Integer]: TOtherClass read GetIndxProp write SetIndxProp;
end;
with access specifiers implemented as
function TMyClass.GetIndxProp(Index: Integer): TOtherClass;
begin
Result := self.FIndxPropList[Index];
end;
procedure TMyClass.SetIndxProp(Index: Integer; Value: TOtherClass);
begin
SetLength(self.FIndxPropList, Length(self.FIndxPropList) + 1);
self.FIndxPropList[Length(self.FIndxPropList) - 1] := Value;
end;
It's use can be illustrated as follows:
procedure Test();
var
MyClass: TMyClass;
begin
MyClass := TMyClass.Create;
MyClass.IndxProp[0] := TOtherClass.Create;
MyClass.IndxProp[0].FData := 'First instance.';
MyClass.IndxProp[1] := TOtherClass.Create;
MyClass.IndxProp[1].FData := 'Second instance.';
MessageDlg(MyClass.IndxProp[0].FData, mtInformation, [mbOk], 0);
MessageDlg(MyClass.IndxProp[1].FData, mtInformation, [mbOk], 0);
MyClass.IndxProp[0].Free;
MyClass.IndxProp[1].Free;
MyClass.Free;
end;
Never mind the obvious flaws of this "design". I realized that I'd like to be able to access the property IndxProp via RTTI, and subsequently moved the IndxProp to the published section. Much to my disappointment, I found that indexed properties are not allowed in the published section. As far as I understand (see Barry Kellys comment at How do I access Delphi Array Properties using RTTI), moving to D2010 won't enable me to do this.
On the other hand, the following is a quote from Robert Loves blog: "... properties and methods are now available via RTTI in both public and published sections, and Fields are available in all of the sections." (My italics.)
My question is this: if it's true that it is possible to get RTTI for public fields in D2010, shouldn't my original example (as shown above) work in D2010 (with RTTI)? Thanks in advance!
Yes, if all the property reader does is index into an array field or list-class field, then you can use RTTI to index into the field directly. This is kind of fragile, though, since it breaks your encapsulation, requiring you to write code to a specific implementation detail instead of a general principle, which is what RTTI is mainly good for. Your RTTI code has to match the exact structure of your class, and if it changes you have to change the code as well. That sort of defeats the purpose of using RTTI.
But, if there's no alternative available, since array properties have no RTTI for them, it may be the only way, for now at least.
EDIT: Updating this answer. Support for indexed properties was added to the extended RTTI system in XE2. (However, due to unrelated stability issues, you might want to wait for XE3...)
Related
I'd like to understand the principles of adding methods to RTTI (I mean the old one, which is supported by old Delphi versions (before Delphi 2010) or by FPC). As far as I know the RTTI is supposed to have information about published methods. But the following example doesn't work in my case:
{$M+}
TMyClass = class
published
procedure testfn(a,b,c: Integer);
end;
{$M-}
...
procedure TMyClass.testfn(a,b,c: Integer);
begin
ShowMessage('s');
end;
...
GetPropInfo(TMyClass, 'testfn'); // returns nil
I'd like to understand what I need to change to receive PPropInfo for the method.
I want to get the PTypeInfo for the method. In case of a property it can be retrieved via
PropInfo := GetPropInfo(...);
TypeInfo := PropInfo^.PropType;
TypeData := GetTypeData(TypeInfo);
I need something like that for methods.
Have a look at the mORMot Framework. It includes a whole bunch of additional RTTI helper functions including the very handy TMethodInfo object along with this handy function to populate it.
/// retrieve a method RTTI information for a specific class
function InternalMethodInfo(aClassType: TClass; const aMethodName: ShortString): PMethodInfo;
In the following type:
MyClass = class(TInterfacedPersistent)
private
FMyProperty: Integer;
published
procedure setMyProperty(Value: Integer); virtual;
property MyProperty: Integer read FMyProperty write setMyProperty;
I would like to know the name of the setter method of the "MyProperty" property via RTTI. I've tried the following:
procedure ShowSetterMethodsNames(pMyObject: TObject);
var
vPropList: TPropList;
vCount, I: Integer;
begin
vCount:= GetPropList(pMyObject.ClassInfo, tkProperties, #vPropList);
for I:= 0 to vCount -1 do
begin
if Assigned(vPropList[I]^.SetProc) then
ShowMessage(pMyObject.ClassType.MethodName(vPropList[I]^.SetProc));
end;
end;
Although the pointer is not nil, all I have is an empty message. Does anybody have some tip to me?
P.S.: I'm using Delphi XE4, and I know I should use extended RTTI instead of classic, but anyway, I can't do what I want in both features... So, any help will be appreciated. Thanks for the replies.
FINAL EDITION, problem solved:
Here is the code working, based in the (help of my friends and...) RTTI unit (DoSetValue method of TRTTIInstanceProperty class):
procedure ShowVirtualSettersNames(pObject: Pointer);
var
vSetter, vPointer: Pointer;
vPropList: TArray<TRttiProperty>;
vProp: TRttiProperty;
begin
vPropList:= RTTIUtils.ExtractProperties(TObject(pObject).ClassType); // Helper to get properties from a type, based in extended RTTI
for vProp in vPropList do
begin
vPointer:= TRttiInstanceProperty(vProp).PropInfo^.SetProc;
vPointer:= PPointer(PInteger(pObject)^ + Smallint(vPointer))^;
ShowMessage(TObject(pObject).ClassType.MethodName(vPointer));
end;
end;
This ONLY WORKS FOR VIRTUAL SETTERS, for statics the message is empty. Thanks everyone!
You can retrieve this method name, if
a) move the method to the published section (classic RTTI works with this section only (more accurately - compiled with {$M+} directive))
b) use right class specifier - MyClass.MethodName, because MethodName is class function
This code works on D7 and XE3:
MyClass = class(TInterfacedPersistent)
private
FMyProperty: Integer;
published
procedure setMyProperty(Value: Integer);
property MyProperty: Integer read FMyProperty write setMyProperty;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ppi: PPropInfo;
begin
ppi := GetPropInfo(MyClass, 'MyProperty');
ShowMessage(MyClass.MethodName(ppi.SetProc));
end;
P.S. What Delphi version are you using? What about Extended RTTI (since D2010)?
Read c:\rad studio\9.0\source\rtl\common\System.Rtti.pas
procedure TRttiInstanceProperty.DoSetValue
The setter of the property may be
a field (variable)
a static procedure
a virtual procedure (your case)
And those cases make PropInfo^.SetProc have different semantics of its value.
Direct address only applies to static procedures. For virtual methods you add a VMT offset and take the code address from that memory cell, as specified in that code i mentioned (but would not quote for copyright reasons).
Or you just could use TRttiProperty.SetValue and let Delphi do all those little under the hood details. See http://docwiki.embarcadero.com/Libraries/XE2/en/System.Rtti.TRttiProperty.SetValue
EDIT:
the code removed - it did not worked verbatim and the topic starter provided working version.
Regarding and I know I should use Extended RTTI instead of classic one - that is questionable claim. Extended RTTI is known to work noticeably slower than classic one. Dunno if someone did profiled it, but i suspect that is mostly due to the slow code of TValue. You can google and find that lot of people complained of slow TValue implementation and provided alternative ones with fixed efficiency. However since Extended RTTI only uses stock TValue it cannot benefit from those implementations and remains slower than classic one.
For reasons not really relevant to the question I want to use generics in my TRemotable classes. I've found that Soap.OPToSOAPDomConv.pas has some problems with this. It's using the old RTTI which I guess can't handle generics so the classes is not serialized into xml.
I've managed to change Soap.OPToSOAPDomConv.pas so that it works with generics. My main question is if it is considered ok to do changes in the Delphi source files?
If it isn't, is there a better way to do this? As long as it's just me using it I guess there is no big problems, but it's hard to distribute the source to others, and then there's also future changes in Delphi to consider.
The rest of this lenghty post is just details about what I'm actually doing :-)
I've changed this in Soap.OPToSOAPDomConv.pas (row 3759)
if SerializeProps then
begin
{ Serialized published properties }
Count := GetTypeData(Instance.ClassInfo)^.PropCount;
if Count > 0 then
begin
CheckedElemURI := False;
GetMem(PropList, Count * SizeOf(Pointer));
try
GetPropInfos(Instance.ClassInfo, PropList);
To: (not the prettiest implementation I guess)
New variables in the procedure:
Context: TRttiContext;
RttiProperty: TRttiProperty;
Row 3759:
if SerializeProps then
begin
{ Serialized published properties }
Count := 0;
for RttiProperty in Context.GetType(Instance.ClassInfo).GetProperties do
begin
if RttiProperty.Visibility = mvPublished then //The old method only read published
Count := Count + 1; //RTTI scoping [mvPublished] requires changes to
end; //soap.InvRegistry
begin
CheckedElemURI := False;
GetMem(PropList, Count * SizeOf(Pointer));
try
I := 0;
for RttiProperty in Context.GetType(Instance.ClassInfo).GetProperties do
if RttiProperty.Visibility = mvPublished then
begin
PropList[I] := TRttiInstanceProperty(RttiProperty).PropInfo;
I := I + 1;
end;
Some details as to what I am doing is probably helpful. The background is that the imported wsdl from the SOAP Web Service generates a huge unit, that consists of about 2000 classes and 300k rows of code. The web service design is out of my control. The WSDL Importer makes all of these classes visible in RTTI, which consumes the RTTI space and the unit won't compile.
I've refactored the code here and there and now have a working implementation. While refactoring I found that I could cut away some 50000 rows of redundant code by using generics. Since Delphi won't compile the imported wsdl "as is" anyway I will have to maintain the unit manually whenever new methods becomes available in the web service, so I want to make it as readable as possible.
Basically I'm changing according to below. The sample has very simplified classes, and the sample actually has more lines in the refactored code, but considering the original classes has a lot of procedures etc this method really makes the unit a lot more readable, and it's also easier to compose the classes.
TLCar = class(TRemotable)
private
FEngine: string;
FName: string;
published
property Name: string read FName write FName;
property Engine: string read FEngine write FEngine;
end;
TLBicycle = class(TRemotable)
private
FPedals: string;
FName: string;
published
property Name: string read FName write FName;
property Pedals: string read FPedals write FPedals;
end;
TListCarRequest = class(TRemotable)
private
FreturnedTags: TLCar;
published
property returnedTags: TLCar read FreturnedTags write FreturnedTags;
end;
TListBiCycleRequest = class(TRemotable)
private
FreturnedTags: TLBicycle;
published
property returnedTags: TLBicycle read FreturnedTags write FreturnedTags;
To:
TCommonReturnedTags = class(TRemotable)
private
FName: string;
published
property Name: string read FName write FName;
end;
TLCar = class(TCommonReturnedTags)
private
FEngine: string;
published
property Engine: string read FEngine write FEngine;
end;
TLBicycle = class(TCommonReturnedTags)
private
FPedals: string;
published
property Pedals: string read FPedals write FPedals;
end;
TGenericListRequest<T: TCommonReturnedTags, constructor> = class(TRemotable)
private
FreturnedTags: T;
published
property returnedTags: T read FreturnedTags write FreturnedTags;
end;
TListCarRequest = class(TGenericListRequest<TLCar>)
end;
TListBiCycleRequest = class(TGenericListRequest<TLBicycle>)
end;
Kind Regards,
Dan
There are two things to consider when making modifications such as this.
First of all, can the change have impact on existing functionality. As in this case I would say that it is safe because the functionality is new to this operation, so there shouldn't be any unexpected behavior.
Second part is the evolution development environment. Problem with the evolution of environment is that bindings between actions may change and that can lead to unexpected things. At this moment it is alright to assume that XE2 to has had it's share of updates. If it wouldn't, you would have to keep an eye when patching or updating. The second kind of change like the one from XE2 to XE3 can be handled better. Just put the following in top of Soap.OPToSOAPDomConv.pas:
{$IFNDEF VER230}
{$MESSAGE ERROR 'Intended to be used with XE2'}
{$ENDIF}
When get error while compiling you might remember vaguely that there was something about that file...
So in short, it is not bad things as far as try to evaluate impact and adapt to environment changes. Hopefully this was what you wanted to know.
I want to add a published property into TWinControl.
Is there someway to do this without the necessity of recompiling the base source code ?
If not, some way to recompile the base source code without too much troubles ?
Tks in advice...
EDIT 'CAUSE OF NEW IDEAS
Alright, What I'm thinking to do I'm trying to override the _GetMem from System.pas for classes
inherited from TWinControl.
Why ? 'Cause I'll alloc some extra space to the objects enough to an integer.
Why an integer ? 'Cause this way I can add any pointer to object.
So on the helper class to TWinControl I can make a Get an Set function to access this space of memory.
Good isn't it ? How to do this ?
Overrideing the GetMem procedure I can use the same strategy used on FastCode, create a jumper to the new procedure.
What I need now is understand how this memory alloc works InstanceSize to override this.
At all I'm studding how do Delphi do this... And to add this on DFM I will do the same way, I'll create a jumper to the filer.
Someone have some idea to add the new space in objects ? What method I need to override ? The jumper I know how to do.
Tks Again.
EDIT = Evolution
I think that I did the injection of memory.
I need to do more tests.
I've just did it, I'm not caring about optimizations at the moment, if some one would like to test it, here goes the code.
Just add the unit as the first unit of your project.
unit uMemInjection;
interface
uses
Controls;
type
THelperWinControl = class Helper for TWinControl
private
function RfInstanceSize: Longint;
function GetInteger: Integer;
procedure SetInteger(const Value: Integer);
public
property RfInteger: Integer read GetInteger write SetInteger;
end;
implementation
uses
Windows;
procedure SInstanceSize;
asm
call TWinControl.InstanceSize
end;
function THelperWinControl.GetInteger: Integer;
begin
Result := Integer(PInteger(Integer(Self) + (Self.InstanceSize - SizeOf(Integer)))^);
end;
function THelperWinControl.RfInstanceSize: Longint;
begin
Result := PInteger(Integer(Self) + vmtInstanceSize)^;
Result := Result + SizeOf(Integer);
end;
/////////////////////////////////////////////// FastCode ///////////////////////////////////////////////
type
PJump = ^TJump;
TJump = packed record
OpCode: Byte;
Distance: Pointer;
end;
function FastcodeGetAddress(AStub: Pointer): Pointer;
begin
if PBYTE(AStub)^ = $E8 then
begin
Inc(Integer(AStub));
Result := Pointer(Integer(AStub) + SizeOf(Pointer) + PInteger(AStub)^);
end
else
Result := nil;
end;
procedure FastcodeAddressPatch(const ASource, ADestination: Pointer);
const
Size = SizeOf(TJump);
var
NewJump: PJump;
OldProtect: Cardinal;
begin
if VirtualProtect(ASource, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
begin
NewJump := PJump(ASource);
NewJump.OpCode := $E9;
NewJump.Distance := Pointer(Integer(ADestination) - Integer(ASource) - 5);
FlushInstructionCache(GetCurrentProcess, ASource, SizeOf(TJump));
VirtualProtect(ASource, Size, OldProtect, #OldProtect);
end;
end;
/////////////////////////////////////////////// FastCode ///////////////////////////////////////////////
{ THelperWinControl }
procedure THelperWinControl.SetInteger(const Value: Integer);
begin
PInteger(Integer(Self) + (Self.InstanceSize - SizeOf(Integer)))^ := Value;
end;
initialization
FastcodeAddressPatch(FastcodeGetAddress(#SInstanceSize), #TWinControl.RfInstanceSize);
end.
Thanks to Smasher, I remembered how the Delphi team used class helpers and a designer trick to add properties to Delphi 2007 without breaking binary compatibility with Delphi 2006.
See this great article by Hallvard Vassbotn on how to do this.
I think it solves most, if not all, of your problems.
look for these things in the article:
TCustomFormHelper = class helper for TCustomForm
The FPixelsPerInch storage hack
Injecting design-time properties
Defining the streaming properties
You'll have to work your own way to do the streaming, though, as you hook from the outside world into TWinControl, but that might be possible too.
--jeroen
Delphi2007 and higher have "class helpers".
You can introduce new functions and properties, but no fields/variables. So you have to store the value of you new property in a extra object (via factory or whatever) or (very ugly) in the .Tag property...
Don't know if class helper also work in packages/design time?
If you are using this property only on the application level, you may use the following approaches:
composition: bundle a reference to TWinControl object with other properties into new class, and pass/operate objects this class in your calls
dictionary-like functions: GetMyPropertyFor( AWinControl: TWinControl): and SetMyPropertyFor( AWinControl: TWinControl: AValue: ), which internally maintain additional property for each called TWinControl object
ADDITION: Based on your additional comment, existing Tag property should play well for your needs. You can even define 'levels' by using different values there.
No, there is no way to modify TWinControl without recompiling the VCL. Also I don't recommend changing the VCL (since having a "custom" VCL can impact the portability of your project - at the very least between Delphi installations). I would aim at making another class that inherit from TWinControl and then add your published property to this new class.
If you still want to change the VCL see the following post:
http://www.delphigroups.info/2/6/744173.html
Note that "you will no longer be able to compile using runtime
packages"...
(I know the answer is a bit dense, comment on it what details you need more info about)
What you could do is what for instance TGridPanel does: it adds the Column, Row, ColumnSpan and RowSpan 'properties' to the object inspector for all components that are on the GridPanel.
That will solve your design-time support.
I thought I had a reference on how the TGridPanel does this (and TFlowPanel does similar things), but I can't find it right now. Probably Ray Konopka explained this during a conference, but that info might not be on-line.
For run-time support, you could go with class helpers.
When using class helpers, note that only the nearest visible one for a class will apply.
Another route you might follow is to use the Tag property (which is an Integer, but you can cast it to a Pointer or a TObject), but you might be bitten by others using that too.
You'd have to create your own design-time support for those tag properties though.
--jeroen
I'm trying to parse objects to XML in Delphi, so I read about calling the object's ClassInfo method to get its RTTI info.
The thing is, this apparently only works for TPersistent objects. Otherwise, I have to specifically add a compiler directive {$M+} to the source code for the compiler to generate RTTI info.
So I happily added the directive, only to find that, even if it did return something from the ClassInfo call (it used to return nil), now I cannot retrieve the class' properties, fields or methods from it. It's like it created the object empty.
Any idea what am I missing here? Thanks!
Did you put those properties and methods into the published section?
Besides that, 'classical' RTTI ($TYPEINFO ON) will only get you information on properties, not on methods. You need 'extended' RTTI ($METHODINFO ON) for those.
Good starting point for extended RTTI: David Glassborow on extended RTTI
(who would believe that just this minute I finished writing some code that uses extended RTTI and decided to browse the Stack Overflow a little:))
RTTI will only show you published properties,etc. - not just public ones.
Try your code with a TObject and see what happens - if that isn't working, post your code because not everyone is psychic.
Have you considered using the TXMLDocument component? It will look at your XML and then create a nice unit of Delphi classes that represents your XML file -- makes it really, really easy to read and write XML files.
As for the RttiType problem returning only nil, this probably occurs for one reason: in your test, you did not instantiate the class at any time. The compiler, because it never has a reference to this class (because it is not an instance at all), simply removes it from the information as a form of optimization. See the two examples below. The behavior is different when you have the class instantiated at some point in your code or not.
Suppose the following class:
type
TTest = class
public
procedure Test;
end;
and the following code below:
var
LContext: TRttiContext;
LType: TRttiType;
LTest: TTest;
begin
LContext := TRttiContext.Create;
for LType in LContext.GetTypes do
begin
if LType.IsInstance then
begin
WriteLn(LType.Name);
end;
end;
end;
so far, TTest class information is not available for use by RTTI. However, when we create at some point, within the application, then a reference is created for it within the compile, which makes this information available:
var
LContext: TRttiContext;
LType: TRttiType;
LTest: TTest;
begin
LTest := TTest.Create; //Here i´m using TTest.
//Could be in another part of the program
LContext := TRttiContext.Create;
for LType in LContext.GetTypes do
begin
if LType.IsInstance then
begin
WriteLn(LType.Name);
end;
end;
end;
At that point, if you use LContext.FindType ('TTest'), there will not be a nil return, because the compiler kept reference to the class. This explains the behavior you were having in your tests.