Delphi - access class string property by its value - delphi

I have a class defined which contains only strings as properties, and I need to get the property name based on its value as in the example below. In the example there are only 3 properties, in the real life class there are almost 1000. The problem is that this class is heavily used, and I want to know if I can get the property name by its value in a faster way.
unit Unit5;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,RTTI, StdCtrls, Diagnostics;
type
TConstDBElem = class
public
CCFN_1 : String;
CCFN_2 : String;
CCFN_3 : String;
constructor Create;
end;
TForm5 = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;
var
Form5: TForm5;
Obj: TConstDBElem;
implementation
{$R *.dfm}
procedure TForm5.Button1Click(Sender: TObject);
var iPos:Integer;
timer:TStopwatch;
Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
var
vrttiContext: TRttiContext;
vrttiField : TRttiField;
vType : TRttiType;
begin
vType := vrttiContext.GetType(TConstDBElem);
for vrttiField in vType.GetFields do
if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
begin
result := vrttiField.Name;
end;
end;
begin
timer := TStopwatch.Create;
timer.Start;
Memo1.Lines.Clear;
for iPos := 0 to 100000 do
GetName(Obj,'TEST3');
timer.Stop;
Memo1.Lines.Add(FloatToStr(timer.Elapsed.TotalSeconds));
end;
constructor TConstDBElem.Create;
begin
CCFN_1 := 'TEST1';
CCFN_2 := 'TEST2';
CCFN_3 := 'TEST3' ;
end;
initialization
Obj := TConstDBElem.Create;
finalization
Obj.Free;
end.
Yes,I know this is a very bad design, and this should not be done like this. Is there any option to make this search faster?

When GetName() finds a match, it is not stopping its loop, so it keeps searching for more matches. Assigning a function's Result does not exit the function, like you clearly think it does. As such, GetName() ends up returning the last match, not the first match. The loop should be calling Exit when it finds the first match:
Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
var
vrttiContext: TRttiContext;
vrttiField : TRttiField;
vType : TRttiType;
begin
vType := vrttiContext.GetType(TConstDBElem);
for vrttiField in vType.GetFields do
if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
begin
result := vrttiField.Name;
Exit; // <-- add this
end;
end;
Alternatively, use the version of Exit() that takes a parameter:
Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
var
vrttiContext: TRttiContext;
vrttiField : TRttiField;
vType : TRttiType;
begin
vType := vrttiContext.GetType(TConstDBElem);
for vrttiField in vType.GetFields do
if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
begin
Exit(vrttiField.Name); // <-- assigns Result and exits at the same time
end;
end;
In your simple example, the time wasted to search 3 fields is hardly noticeable, but when searching 1000 fields, it makes a difference.

You state in a comment that the values never change at runtime. In which case you can simply build a single dictionary at startup that has the property values as the dictionary key, and the property name as the dictionary value.
I'm assuming that all instances of the class have the same property values. If that's not the case then you'll need one dictionary per instance.

It's a "bad design" because someone wrote a class that they're treating like a C-style Struct. As has been said already, there are NO PROPERTIES defined in the class, just a bunch of PUBLIC DATA MEMBERS, aka, "fields".
There's no encapsulation, so anything you do to change the structure could have far-reaching implications on any unit that uses this. I agree that replacing the IMPLEMENTATION with a TStringList or a TDictionary would be smart, but ... there are no interfaces to adhere to! You have 1000-odd hard-wired references to public data members.
(The last time I saw something like this was some code written by a bunch of VB programmers who wrote classes as if they were C-style structs containing public data members, and then they wrote external functions to access the data, just as you'd do in C. Only they buried business logic inside of the accessor methods as well, which causes random pieces of the code to make direct references to the data members of the class.)
Off-hand, I'd say you're totally mis-using the RTTI code. Sure, the optimizations above will improve performance, but so what? It's the wrong solution!
If you really want to refactor this (and you certainly should!), first I'd look to see how widespread the use of this poor class is by changing the public to private and see how many errors you get.
Then I'd derive it from TStringList, delete all of the local fields, and move the GetName function inside of the class:
type
TConstDBElem = class( TStringList )
public
constructor Create;
function GetName( aName : string ) : string;
end;
Now, if I'm interpreting your example correctly, you want to do this to initialize the object:
constructor TConstDBElem.Create;
begin
Add( 'TEST1=CCFN_1' );
Add( 'TEST2=CCFN_2' );
Add( 'TEST3=CCFN_3' );
end;
Then replace all of the references in other units with a call to obj.GetName():
function TConstDBElem.GetName( aName : string ) : string;
begin
Result := Values[aName];
end;
You're replacing a reference to obj.CCFN_1 (?) or GetName(obj,'TEST1') with obj.GetName('TEST1').
(Maybe I'm totally off-base at this point. Sorry, but I just don't get how you're using this class from the example, and it doesn't make a whole lot of sense to me anyway. It would make more sense if you said what you're mapping between. I mean ... who needs to look up a local field name from a value associated with it? And what do you do with it once you've found it? Whomever wrote this had to go through some incredible contortions to make the code work because s/he sure didn't understand OOP when this was designed!)
At this point, you will have succeeded in decoupling the clients of this class (other units) from its internal implementation, and replacing those references with calls to an interface (a method) defined in the class instead.
Then you can do some testing to see what happens if you change the implementation, like from a TStringList to a TDictionary. But no matter how you slice it, I cannot imagine that either the TStringList or TDictionary will be slower than how you're abusing the RTTI system in your example. :)

Related

How to pass generic procedure TProc<T1,T2> as a parameter and invoke it?

I have a logging class, which links to many modules. The main method of this class is a class method:
type
TSeverity = (seInfo, seWarning, seError);
TLogger = class
class procedure Log(AMessage: String; ASeverity: TSeverity);
end;
Somewhere else I have a function DoSomething() which does some things that I would like to log. However, I do not want to link all the modules of the logger to the module in which 'DoSomething()' is declared to use the logger. Instead I would like to pass an arbitrary logging method as a DoSomething's parameter and call it from its body.
The problem is that TLogger.Log requires parameter of TSeverity type which is defined in logger class. So I can't define a type:
type
TLogProcedure = procedure(AMessage: String; ASverity: TSeverity) of Object;
because I would have to include an unit in which TSeverity is declared.
I was trying to come up with some solution based on generic procedure but I am stuck.
uses
System.SysUtils;
type
TTest = class
public
class function DoSomething<T1, T2>(const ALogProcedure: TProc<T1,T2>): Boolean; overload;
end;
implementation
class function TTest.DoSomething<T1, T2>(const ALogProcedure: TProc<T1, T2>): Boolean;
var
LMessage: String;
LSeverity: Integer;
begin
//Pseudocode here I would like to invoke logging procedure here.
ALogProcedure(T1(LMessage), T2(LSeverity));
end;
Somewehere else in the code I would like to use DoSomething
begin
TTest.DoSomething<String, TSeverity>(Log);
end;
Thanks for help.
Update
Maybe I didn't make myself clear.
unit uDoer;
interface
type
TLogProcedure = procedure(AMessage: String; AErrorLevel: Integer) of Object;
// TDoer knows nothing about logging mechanisms that are used but it allows to pass ALogProcedure as a parameter.
// I thoight that I can somehow generalize this procedure using generics.
type
TDoer = class
public
class function DoSomething(const ALogProcedure: TLogProcedure): Boolean;
end;
implementation
class function TDoer.DoSomething(const ALogProcedure: TLogProcedure): Boolean;
begin
ALogProcedure('test', 1);
Result := True;
end;
end.
Separate unit with one of the logging mechanisms.
unit uLogger;
interface
type
TSeverity = (seInfo, seWarning, seError);
// I know that I could solve my problem by introducing an overloaded method but I don't want to
// do it like this. I thought I can use generics somehow.
TLogger = class
class procedure Log(AMessage: String; ASeverity: TSeverity); {overload;}
{class procedure Log(AMessage: String; ASeverity: Integer); overload;}
end;
implementation
class procedure TLogger.Log(AMessage: String; ASeverity: TSeverity);
begin
//...logging here
end;
{class procedure TLogger.Log(AMessage: String; ASeverity: Integer);
begin
Log(AMessage, TSeverity(ASeverity));
end;}
end.
Sample usage of both units.
implementation
uses
uDoer, uLogger;
procedure TForm10.FormCreate(Sender: TObject);
begin
TDoer.DoSomething(TLogger.Log); //Incompatible types: Integer and TSeverity
end;
Introducing generics here does not help. The actual parameters that you have are not generic. They have fixed type, string and Integer. The function you are passing them to is not generic and receives parameters of type string and TSeverity. These types are mis-matched.
Generics won't help you here because your types are all known ahead of time. There is nothing generic here. What you need to do, somehow, is convert between Integer and TSeverity. Once you can do that then you can call your function.
In your case you should pass a procedure that accepts an Integer, since you don't have TSeverity available at the point where you call the procedure. Then in the implementation of that procedure, where you call the function that does accept a TSeverity, that's where you convert.
In scenarios involving generic procedural types, what you have encountered is quite common. You have a generic procedural type like this:
type
TMyGenericProcedure<T> = procedure(const Arg: T);
In order to call such a procedure you need an instance of T. If you are calling the procedure from a function that is generic on T, then your argument must also be generic. In your case that argument is not generic, it is fixed as Integer. At that point your attempt to use generics unravels.
Having said all of that, what you describe doesn't really hang together at all. How can you possibly come up with the severity argument if you don't know what TSeverity is at that point? That doesn't make any sense to me. How can you just conjure up an integer value and hope that it matches this enumerated type? Some mild re-design would enable you to do this quite simply without any type conversions.
As David Heffernan says, you cannot use generics in this way. Instead you should use a function to map the error level to a severity type, and use that to glue together the two. Based on your updated example, one could modify it like this:
unit uDoer;
interface
type
TLogProcedure = reference to procedure(const AMessage: String; AErrorLevel: Integer);
// TDoer knows nothing about logging mechanisms that are used but it allows to pass ALogProcedure as a parameter.
type
TDoer = class
public
class function DoSomething(const ALogProcedure: TLogProcedure): Boolean;
end;
implementation
class function TDoer.DoSomething(const ALogProcedure: TLogProcedure): Boolean;
begin
ALogProcedure('test', 1);
Result := True;
end;
end.
You can then provide the glue procedure which converts the error level to a severity:
implementation
uses
uDoer, uLogger;
function SeverityFromErrorLevel(const AErrorLevel: Integer): TSeverity;
begin
if (AErrorLevel <= 0) then
result := seInfo
else if (AErrorLevel = 1) then
result := seWarning
else
result := seError;
end;
procedure LogProc(const AMessage: String; AErrorLevel: Integer);
var
severity: TSeverity;
begin
severity := SeverityFromErrorLevel(AErrorLevel);
TLogger.Log(AMessage, severity);
end;
procedure TForm10.FormCreate(Sender: TObject);
begin
TDoer.DoSomething(LogProc);
end;
Note I didn't compile this, but the essence is there. I used a procedure reference (reference to procedure) as they're a lot more flexible, which may come in handy later.

How to provide a derived instance with an ancestor field declaration?

I am having some trouble figuring out this case of inheritance.
In my class TBalans, I have a routine Initialiseer that takes a TBalPar object as parameter. TBalPar is the ancestor class of TNewBalPar that has additional fields. Now I would like to reach the additional fields from within my TBalans class. I still can feed a TNewBalPar object to the Initialiseer routine, but how do I get to the data of the descendant class?
What I tried is the following: I derived TBalans too into TNieuweBalans, gave it the new additional fields, and assign them in the routine:
type
TBalPar = class
//some vars
end;
TNewBalPar = class(TBalPar)
ExtraVar: TValue;
end;
TBalans = class
MyBalPar: TBalPar;
function Initialiseer(ABalPar: TBalPar): Boolean; virtual;
end;
TNieuweBalans = class(TBalans)
MyBalPar: TNewBalpar; //declared again so I don't need to cast it when using it
MyExtraVar: TValue;
function Initialiseer(ABalPar: TBalPar): Boolean; override;
end;
function TBalans.Initialiseer(ABalPar: TBalPar): Boolean;
begin
MyBalPar := ABalPar;
end;
function TNieuweBalans.Initialiseer(ABalPar: TBalPar): Boolean;
begin
inherited;
MyBalPar := TNewBalPar(ABalPar);
MyExtraVar := MyBalPar.ExtraVar; //instead of casting TNewBalPar(MyBalPar).ExtraVar
end;
This code works, but it feels wrong: I declare the MyBalPar field twice. I would like to improve on it.
Note that I am not looking for a way how to expose ExtraVar to the outside world, but how to use it conveniently within TNieuweBalans.
How can I eliminate the double MyBalPar field but still prevent frequent typecasting?
Current design
The need for a convenient designated derived field type for an ancestral field is not forbidden, nor uncommon for that matter. But your implementation, like you sense already, has some problems:
the doubled fields require unnecessary memory,
you need to synchronize changes to TBalans.MyBalPar and TNieuweBalans.MyBalPar,
you need to synchronize changes to TNieuweBalans.MyBalPar.ExtraVar and TNieuweBalans.MyExtraVar,
you do not enforce the derived class type: feeding a TBalPar object to TNieuweBalans.Initialiseer results in an access violation because MyBalPar.ExtraVar does not exist.
There are multiple ways to overcome each of these problems.
The most elementary solution to prevent extra fields is to provide properties for them with getters that extract the values from the inherited class (I renamed some of your types and variables for comprehensibility):
type
TBalPar = class(TObject)
// some variables
end;
TBalParEx = class(TBalPar)
private
FExtra: TValue;
public
property Extra: TValue read FExtra write FExtra;
end;
TBalance = class(TObject)
private
FBalPar: TBalPar;
public
function Initialize(ABalPar: TBalPar): Boolean; virtual;
property BalPar: TBalPar read FBalPar;
end;
TBalanceEx = class(TBalance)
private
function GetExtra: TValue;
procedure SetExtra(Value: TValue);
public
function BalPar: TBalParEx;
function Initialize(ABalPar: TBalPar): Boolean; override;
property Extra: TValue read GetExtra write SetExtra;
end;
function TBalanceEx.BalPar: TBalParEx;
begin
Result := TBalParEx(inherited BalPar);
end;
function TBalanceEx.GetExtra: TValue;
begin
Result := BalPar.Extra;
end;
procedure TBalanceEx.SetExtra(Value: TValue);
begin
BalPar.Extra := Value;
end;
With this approach, there is only one typecast needed and it does not require additional storage.
To enforce TBalanceEx.BalPar to be of type TBalParEx, you could raise an exception in the Initialize routine:
function TBalance.Initialize(ABalPar: TBalPar): Boolean;
begin
FBalPar := ABalPar;
Result := True;
end;
function TBalanceEx.Initialize(ABalPar: TBalPar): Boolean;
begin
if ABalPar is TBalParEx then
Result := inherited Initialize(ABalPar)
else
raise Exception.Create('Wrong BalPar type');
end;
Of course, this places the sole responsibility of a correct class functioning on the requirement to always call the Initialize routine before any other usage of the other class members. Since that is what initialization obviously is intended for, you could ignore that, but protection against misuse could be added like:
TBalance = class(TObject)
protected
function HasBalPar: Boolean; virtual;
...
TBalanceEx = class(TBalance)
protected
function HasBalPar: Boolean; override;
...
function TBalance.HasBalPar: Boolean;
begin
Result := FBalPar is TBalPar;
end;
function TBalance.Initialize(ABalPar: TBalPar): Boolean;
begin
FBalPar := ABalPar;
Result := HasPalBar;
end;
function TBalanceEx.GetExtra: TValue;
begin
if HasBalPar then
Result := BalPar.Extra
else
Result := nil;
end;
function TBalanceEx.HasBalPar: Boolean;
begin
Result := BalPar is TBalParEx;
end;
function TBalanceEx.Initialize(ABalPar: TBalPar): Boolean;
begin
Result := inherited Initialize(ABalPar);
if Result = False then
raise Exception.Create('Initialization went wrong');
end;
procedure TBalanceEx.SetExtra(Value: TValue);
begin
if HasBalPar then
BalPar.Extra := Value;
end;
In turn, this requires not to forget to implement HasBalPar for each derived class. You could 'protect' against that with:
TBalance = class(TObject)
strict private
function HasBalPar: Boolean;
private
...
TBalanceEx = class(TBalance)
strict private
function HasBalPar: Boolean;
private
...
Design considerations
All in all, making this a robust design requires some work. And your current approach raises the question why you would want to have the Extra field in the TBalanceEx class too. Even why to have a TBalanceEx class at all.
From the naming of your classes, I assume you have the following equivalent: A structure which has structural parameters like build date, owner, location, and you have a specialized structure, say a castle, with additional parameters like the number of towers and whether it has a moat:
TStructureData: Location, BuildDate
TCastleData: Location, BuildDate, TowerCount, HasMoat
TStructure: StructureData
TCastle: StructureData, CastleData
The question you need to answer is whether a structure needs to know if it is a castle, or a palace, a warehouse, a biological or chemical structure. Assume your program evolves to being able to handle all different kinds of structures, then you are always bound to add two classes to your program, resulting in a more and more complex and improvised design which in the end will get you in trouble, if not already. The challenge is to make this a more generalized and abstract design.
For example:
must TStructureData and TStructure be separate classes?
could calculations, analysations, or presentational requests on the specific data be 'outsourced' to the specific class? E.g.: if you add a GetFeatures routine to the TStructureData class, then the TStructure class can request the features of a TCastle without knowing it being a Castle.
...
Think big.

Delphi - how to enum string to type

I got strings in database like 'TGroupBox' or 'TEdit' ... now I need to check element against them... how do I enumerate string to type?
I mean something like this:
mystr := 'TGroupBox';
If (page.Controls[0] is mystr) then ...
Of course it won't work, as error appears:
E2015 Operator not applicable to this operand type
How do I do that correctly?
You can verify that
page.Controls[0].ClassName = mystr
using the ClassName property.
But notice that this doesn't do exactly the same thing as the is operator. To see the difference, suppose you have a class TFruit and a subclass TApple. If myFruit is an instance of a TApple, then both myFruit is TApple and myFruit is TFruit will yield true. But of course, the ClassName will still only be TApple.
If you need the full functionality of the is operator, you can make use of the ClassParent property, as suggested by hvd:
function IsDerivedFrom(AClass: TClass; const AClassName: string): boolean;
begin
if not Assigned(AClass) then Exit(false);
result := SameText(AClass.ClassName, AClassName) or
IsDerivedFrom(AClass.ClassParent, AClassName);
end;
To get the class of an object, use the ClassType property:
IsDerivedFrom(page.Controls[0].ClassType, mystr);
The function you are looking for is GetClass located in System.Classes. Be aware that the class has to be registered.
System.Classes.GetClass
For the specific scenario in the question body the answer by Andreas Rejbrand (with assistance from hvd) is a good one. However, for the broader problem implied by the question title - how to I convert a string containing a class name to a class reference? - you can utilise extended RTTI in a new(ish) version of Delphi:
unit ClassLookupUtils;
interface
uses
System.SysUtils, System.Generics.Collections, System.Rtti;
type
RttiClassLookup = record
strict private
class var FMap: TDictionary<string, TClass>;
class destructor Destroy;
public
class function Find(const ClassName: string): TClass; static;
end;
implementation
class destructor RttiClassLookup.Destroy;
begin
FMap.Free;
end;
class function RttiClassLookup.Find(const ClassName: string): TClass;
var
RttiType: TRttiType;
RttiContext: TRttiContext;
begin
if FMap = nil then
begin
FMap := TDictionary<string, TClass>.Create;
for RttiType in RttiContext.GetTypes do
if RttiType is TRttiInstanceType then
FMap.AddOrSetValue(RttiType.Name.ToLowerInvariant, (RttiType as TRttiInstanceType).MetaclassType);
end;
if not FMap.TryGetValue(ClassName.ToLowerInvariant, Result) then
Result := nil;
end;
end.
In use:
var
MyStr: string;
MyStrClass: TClass;
begin
//...
MyStrClass := RttiClassLookup.Find(MyStr);
if MyStrClass <> nil then
for I := 0 to Page.ControlCount - 1 do
if Page.Controls[I].InheritsFrom(MyStrClass) then
begin
//...
end;
The background here is that SomeObj is SomeClass is implemented as (SomeObj <> nil) and SomeObj.InheritsFrom(SomeClass).
You have a good answer from #UweRaabe usingRTTIto getClassName.
A simple (and not very robust) hack without using RTTI would be to use the TComponent.Name property, which is a string, like this - without the is operator:
If (pos('GroupBox', page.Controls[0].name)>0 ) then ...
By default, a control gets the same name as the instance variable, so GroupBox1.name='GroupBox1'. You can either change your database entries to use the substr 'groupbox' or extract 'groupbox' from the type name string in your database.
That being said, if you've inherited this design approach of persisting type names as strings in a database and then using them at runtime to check the types of different components, then you're stuck with it, and so be it. But Delphi is a strongly typed, compiled language, so persisting type names as strings in a database and reading them at runtime and decoding them into Delphi types just doesn't "smell right" IMO. I would re-think this design if possible. Consider doing it all in Delphi using classOf type, enumerations, etc.

Delphi - Using TStringList in Class Definition (very new)

I'm doing a simple class definition in Delphi and I wanted to use a TStringList in the class & it's constructor (so everytime you create an object, you pass it a StringList and it does some magic stuff to the StringList data, copying the string list to it's own internal string list). The problem I get is that when I try to declare what it "uses" before the class definition (so it knows how to handle the TStringList), it fails on compile. But without that, it doesn't know what a TStringList is. So it seems to be a scoping issue.
Below is a (very simplified) class definition, similar to what I'm trying to do. Can someone suggest how I can make this work and get the scoping right?
I tried adding the uses statements at the project level as well, but it still fails. I wonder what I need to do to get this right.
unit Unit_ListManager;
interface
type
TListManager = Class
private
lmList : TStringList;
procedure SetList;
published
constructor Create(AList : TStringList);
end;
implementation
uses
SysUtils,
StrUtils,
Vcl.Dialogs;
constructor TBOMManager.Create(AList : TStringList);
begin
lmList := TStringList.Create;
lmList := AListList;
end;
procedure SetPartsList(AList : TStringList);
begin
lmList := AListList;
ShowMessage('Woo hoo, got here...');
end;
end.
Kind Regards
You didn't show where exactly you were adding the unit reference, but I'm betting it was the wrong place. Take note of the additional code between interface and type.
I've also corrected your definition of the constructor, which you had placed in published instead of public. Only property items belong in the published section.
unit Unit_ListManager;
interface
uses
Classes,
SysUtils,
StrUtils,
Vcl.Dialogs;
type
TListManager = Class
private
lmList : TStringList;
procedure SetList;
public
constructor Create(AList : TStringList);
end;
implementation
constructor TListManager.Create(AList : TStringList);
begin
inherited Create; // This way, if the parent class changes, we're covered!
// lmList := TStringList.Create; This would produce a memory leak!
lmList := AListList;
end;
procedure TListManager.SetList;
begin
// You never provided an implementation for this method
end;
end.

Can one get/set class-level members using Enhanced RTTI in Delphi?

Preface, it seems i failed to say it clear. I want to enumerate, read and set, all the class var or class properties of a given TClass variable.
There is no problem in finding TClass - it is passed.
There is no problem in filtering by presence of given attribute.
There problem is that RTTI just misses the way to enumerate class fields rather than instance fields.
I wanted to make a declarative DLL Loader. Since the DLLs are process-global in windows (you cannot load the same DLL twice) it corresponds to class-level fields or properties rather than to instant-level ones.
So i thought something like LoadDLL(filename:string; funtions: TClass) and use attributes to specify which entry points to fetch.
Then i run into the wall: while there is TRttiClassRefType in Delphi - one just cannot get it by given instance or whatever: TRTTIContext.GetType(Class) returns context for instances, rather than for metatypes.
Enumerating all the metatypes until the needed one got by name is... ugly. And fragile. And slow. Though it seems to be the only real code snippet containing TRttiClassRefType that i can find.
So - is there a way i can get and set class-level variables or properties, finding them, by some custom attribute attached ?
The following test program only finds IV and IP, and skips CV and CP...
program Project19;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, RTTI;
type
DLL_Info = class (TCustomAttribute) end;
{$M+} {$RTTI EXPLICIT FIELDS ([vcPublic]) PROPERTIES ([vcPublic])}
DLL_Functions = class
public
[DLL_Info] var IV: pointer; // type is just stub
[DLL_Info] class var CV: pointer;
[DLL_Info] property IP: pointer read IV;
[DLL_Info] class property CP: pointer read CV;
end;
{$M-}
var df: DLL_Functions;
procedure SetDLLFunctions;
var
LContext: TRttiContext;
LType: TRttiType;
LVar: TRttiField;
LProp: TRttiProperty;
LAttr: TCustomAttribute;
DLL_fn_Entry, DLL_fn_Optional: boolean;
DLL_fn_Name: string;
fn_ptr: Pointer;
begin
LContext := TRttiContext.Create;
try
LType := LContext.GetType(DLL_Functions);
for LVar in LType.GetFields do begin
Writeln(Lvar.Name);
end;
for LProp in LType.GetProperties do begin
Writeln(LProp.Name);
end;
finally
LContext.Free;
end;
end;
begin
try
df := DLL_Functions.Create;
df.IV := #SetDLLFunctions; // imitating DLL quering by GetProcAddress
df.CV := #SetDLLFunctions;
SetDLLFunctions;
df.Destroy;
ReadLn;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Some possible workarounds:
Using record to enlist entrypoints would require passing TWO pointers: the record itself and TypeInfo. Which is redundant and can potentially get incoherent.
Creating class instances just to hold static pointers would be one extra level of indirection when calling, and would be a redundant entity to keep track of.

Resources