How to access private methods without helpers?
In Delphi 10 Seattle I could use the following code to work around overly strict visibility restrictions.
How do I get access to private variables?
type
TBase = class(TObject)
private
FMemberVar: integer;
end;
And how do I get access to plain or virtual private methods?
type
TBase2 = class(TObject)
private
procedure UsefullButHidden;
procedure VirtualHidden; virtual;
procedure PreviouslyProtected; override;
end;
Previously I would use a class helper to break open the base class.
type
TBaseHelper = class helper for TBase
function GetMemberVar: integer;
In Delphi 10.1 Berlin, class helpers no longer have access to private members of the subject class or record.
Is there an alternative way to access private members?
Solution 1:
If there is extended RTTI info generated for the class private members - fields and/or methods you can use it to gain access to them.
Of course, accessing through RTTI is way slower than it was through class helpers.
Accessing methods:
var
Base: TBase2;
Method: TRttiMethod;
Method := TRttiContext.Create.GetType(TBase2).GetMethod('UsefullButHidden');
Method.Invoke(Base, []);
Accessing variables:
var
Base: TBase;
v: TValue;
v := TRttiContext.Create.GetType(TBase).GetField('FMemberVar').GetValue(Base);
Default RTTI information generated for RTL/VCL/FMX classes is following
- Fields -
private
,protected
,public
,published
- Methods -
public
,published
- Properties -
public
,published
Unfortunately, that means accessing private methods via RTTI for core Delphi libraries is not available. @LU RD's answer covers hack that allows private method access for classes without extended RTTI.
Working with RTTI
Solution 2:
There is still a way to use class helpers
for access of private methods in Delphi 10.1 Berlin:
type
TBase2 = class(TObject)
private
procedure UsefullButHidden;
procedure VirtualHidden; virtual;
procedure PreviouslyProtected; override;
end;
TBase2Helper = class helper for TBase2
procedure OpenAccess;
end;
procedure TBase2Helper.OpenAccess;
var
P : procedure of object;
begin
TMethod(P).Code := @TBase2.UsefullButHidden;
TMethod(P).Data := Self;
P; // Call UsefullButHidden;
// etc
end;
Unfortunately there is no way to access strict private/private fields by class helpers with Delphi 10.1 Berlin. RTTI is an option, but can be considered slow if performance is critical.
Here is a way to define the offset to a field at startup using class helpers and RTTI:
type
TBase = class(TObject)
private // Or strict private
FMemberVar: integer;
end;
type
TBaseHelper = class helper for TBase
private
class var MemberVarOffset: Integer;
function GetMemberVar: Integer;
procedure SetMemberVar(value: Integer);
public
class constructor Create; // Executed at program start
property MemberVar : Integer read GetMemberVar write SetMemberVar;
end;
class constructor TBaseHelper.Create;
var
ctx: TRTTIContext;
begin
MemberVarOffset := ctx.GetType(TBase).GetField('FMemberVar').Offset;
end;
function TBaseHelper.GetMemberVar: Integer;
begin
Result := PInteger(Pointer(NativeInt(Self) + MemberVarOffset))^;
end;
procedure TBaseHelper.SetMemberVar(value: Integer);
begin
PInteger(Pointer(NativeInt(Self) + MemberVarOffset))^ := value;
end;
This will have the benefit that the slow RTTI part is only executed once.
Note: Using RTTI for access of protected/private methods
The RTL/VCL/FMX have not declared visibility for access of protected/private methods with RTTI. It must be set with the local directive {$RTTI}.
Using RTTI for access of private/protected methods in other code requires for example setting :
{$RTTI EXPLICIT METHODS([vcPublic, vcProtected, vcPrivate])}
Solution 3:
If you want a clean way that does not impact performance, you still can access private fields from a record helper using the with statement.
function TValueHelper.GetAsInteger: Integer;
begin
with Self do begin
Result := FData.FAsSLong;
end;
end;
I hope they keep this method open, because we have code with high performance demands.
Solution 4:
Assuming that extended RTTI is not available, then without resorting to what would be considered hacking, you cannot access private members from code in a different unit. Of course, if RTTI is available it can be used.
It is my understanding that the ability to crack private members using helpers was an unintentional accident. The intention is that private members only be visible from code in the same unit, and strict private members only be visible from code in the same class. This change corrects the accident.
Without the ability to have the compiler crack the class for you, you would need to resort to other ways to do so. For instance, you could re-declare enough of the TBase
class to be able to trick the compiler into telling you where a member lived.
type
THackBase = class(TObject)
private
FMemberVar: integer;
end;
Now you can write
var
obj: TBase;
....
MemberVar := THackBase(obj).FMemberVar;
But this is horrendously brittle and will break as soon as the layout of TBase
is changed.
That will work for data members, but for non-virtual methods, you'd probably need to use runtime disassembly techniques to find the location of the code. For virtual members this technique can be used to find the VMT offset.
Further reading:
- http://hallvards.blogspot.nl/2004/06/hack-5-access-to-private-fields.html
- https://bitbucket.org/NickHodges/delphi-unit-tests/wiki/Accessing%20Private%20Members