How to access private methods without helpers?
Asked Answered
H

6

33

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?

Hatchett answered 19/4, 2016 at 10:57 Comment(3)
Wow. This really sucks. I think I've only ever used class crackers in the past to either fix or extend broken or deficient RTL/VCL objects. It's never an elegant thing to do, but as a temporary workaround until Emba fixes their codebase they've been rather valuable. Seems insane that they would cripple a feature that helps us manage their variously unreliable framework libraries...Atalanti
@Hatchett This is the only question tagged delphi-10-berlin - just remove it and let it die.Atalanti
@Hatchett You need at least 5 score in the master tag to suggest a synonym. This is the first question for the master tag so nobody can make synonyms yet. Best to just delete the incorrect tag and deal with it if and when it becomes a problem. If nobody uses it then it's not a problem.Atalanti
M
25

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

Mahayana answered 19/4, 2016 at 12:7 Comment(6)
Try M := TRttiContext.Create.GetType(TCustomForm).GetMethod('SetWindowState'); where M is a TRttiMethod. You will find it is nil. Obviously not all VCL/RTL/FMX classes have extended RTTI generated.Greeting
It is not possible to access protected/private methods in RTL/FMX/VCL with RTTI. It is controlled by the {$RTTI} directive, which is local in scope.Hexachlorophene
@RudyVelthuis Thanks for correction. I don't know how on Earth I managed to miss it. Core Delphi libraries have private/protected RTTI generated only for fields and not for methods.Mahayana
@LURD Yes, RTTI directive is local in scope. It cannot be used to switch RTTI on and off without recompiling. It was my statement about full RTTI generation for RTL/VCL/FMX classes that was wrong. It is only generated for fields and not for methods.Mahayana
you can vote for change in Delphi in this ticket quality.embarcadero.com/browse/RSP-15273?filter=-2Tolliver
@Livius, the "?filter=2" is giving incorrect results. The URL is: quality.embarcadero.com/browse/RSP-15273Die
H
25

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])}
Hexachlorophene answered 11/6, 2016 at 8:55 Comment(4)
Nice find about the class helpers. But this shows they did not fully remove the bug yet. Of course this may well be intentional.Greeting
@RudyVelthuis, It is hard to understand why Emba thinks that a class helper should be restricted from having full access to the class. A class helper is per definition the class itself. Of course they can do what they want, but single out this feature when they could use their limited resources to something useful instead is beyond me. And the kudos for finding this loophole should go to Uwe Schuster.Hexachlorophene
It is not hard to understand at all. Encapsulation is one of the basic tenets of OOP. Something that breaks it easily undermines that and can be considered a bug. And a class helper is NOT per definition the class itself. On the contrary. It is merely a syntactic extension.Greeting
@RudyVelthuis, OOP is hierarchical, while class helpers are lateral. Nothing in the help disputes that a helper has another scope than the class itself.Hexachlorophene
I
17

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.

Iguana answered 21/3, 2017 at 19:54 Comment(1)
Class/Record helpers, fields and methods unlocked. So simple.Hexachlorophene
S
12

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:

Scoop answered 19/4, 2016 at 11:12 Comment(16)
I think emba did everyone a great disservice in closing this option, pushing people into not upgrading to the latest version, seems to me counter productive.Hatchett
@Johan: it was a bug, and it was fixed. This fix only did a disservice to those who want to have easy access to things to which they should not have access at all.Greeting
@RudyVelthuis There was always a way to access them through the hack in this answer. It may have been a bug but what purpose did it serve to close the class helper loophole? It was a clean solution. This change is preventing us from upgrading. The new features in Berlin aren't sufficient to justify the work in implementing this workaround. Why do we use it? We have code hooks to fix/change things in the VCL source. One to optimize TWriter and the other to replace the ReadState code in TCustomForm with our own high DPI code.Gannie
@GrayMatter: It was a loophole. I think that says it all. Using it was a hack, so certainly not a "clean" solution. At most, it was a convinient way to circumvent one of the basic tenets of OO. That should never be easy or convenient. It should always be recognizable as something you shouldn't do, as something dirty.Greeting
FWIW, the loophole was not closed entirely. You can still get at (the addresses of) private methods and private members. It is just a little more work. No need to refrain from upgrading.Greeting
@RudyVelthuis I am not saying that it should be convenient or easy but the only alternative is messy and brittle. There are times when you need to work around things. Code hooks wouldn't exist if that wasn't the case but even code hooks are cleaner than the present solution. What they should have done is included an define or some other way where you can get down and dirty without planting a bomb in your code. Also, it's only a matter of time before the close that loophole. Upgrading Delphi when you have a 1.5 million line program is not trivial so everything is taken into consideration.Gannie
It should be messy. It should simply not be avoided at all costs, and making it messy, dirty, etc. promotes the abstinence. The easier and (on the face of it) "cleaner" solution makes people do the wrong thing more often, because it is so simple. For some, it has become their standard modus operandi. Their design relies on it. That has come to a halt, and that is a Good Thing(tm).Greeting
@RudyVelthuis Messy <> Brittle. If they keep the address accesses in then it's fine but if we have to go back to copying the full implementation then that's a different story. I don't mind having to jump through hoops to do something that I shouldn't really be doing in the first place. What I do mind is that it shouldn't be fragile.Gannie
@DavidHeffernan I don't have 10.1 yet but do these work? PInteger(@Self.FPrivateIntegerField)^ := 200 and TMyClass((@Self.FPrivateObject)^).ClassNameGannie
Hacks are hacks, and they tend to be fragile. The higher the incentive to get rid of them.Greeting
Let us continue this discussion in chat.Gannie
No, sorry. I think what they did was right. I don't feel the need to discuss complaints about it, especially since they won't change anything.Greeting
@Gannie don't know because I am far away from my computer on holiday. Rudy is a dentist who doesn't develop software beyond personal and toy projects. Not much point arguing with him. Easy for him to be a purist.Scoop
I am in no way a "purist", but circumventing "private" goes against a lot, so it should be at least not too easy. Using class helpers made it too easy. And I don't buy the arguments of those who see themselves as "pragmatists", who rely on such hacks. Actually, writing solid code is a very pragmatist approach.Greeting
We only ever did it as a last resort. Honestly. Without practical experience why do you feel that you know best.Scoop
The issue with "private" is that the original developer thinks he knows better than you, and what you will use the class for (a psychic) He thinks he should make something private, but in fact the developers ending up needing more than just private. IMO this is why some people still use plain C and structs, because everything is public and modifiable, as dangerous as that is. These compiler changes are starting to tick me off a lot as I am never sure which compiler to use: it seems the best option is to buy the latest version of delphi, then use an old version that you get free. Or use recordsCowart
M
5

If you don't need ARM compiler support, you can find another solution here.

With inline asembler, you can access private field or method, easily.

I think David's answer is better in most case, but if you need a quick solution for a huge class, this method could be more useful.

Update(June 17): I've just noticed, I forgot to share his sample code for accessing private fields from his post. sorry.

unit UnitA;

type
  THoge = class
  private
    FPrivateValue: Integer;
    procedure PrivateMethod;
  end;
end.

unit UnitB;

type
  THogeHelper = class helper for THoge
  public
    function GetValue: Integer;
    procedure CallMethod;
  end;

function THogeHelper.GetValue: Integer;
asm
  MOV EAX,Self.FPrivateValue
end;

procedure THogeHelper.CallMethod;
asm
  CALL THoge.PrivateMethod
end;

Here is his sample code for calling private method.

type
  THoge = class
  private
    procedure PrivateMethod (Arg1, Arg2, Arg3 : Integer);
  end;

// Method 1
// Get only method pointer (if such there is a need to assign a method pointer to somewhere)
type
  THogePrivateProc = procedure (Self: THoge; Arg1, Arg2, Arg3: Integer);
  THogePrivateMethod = procedure (Arg1, Arg2, Arg3: Integer) of object;

function THogeHelper.GetMethodAddr: Pointer;
asm
  {$ifdef CPUX86}
  LEA EAX, THoge.PrivateMethod
  {$else}
  LEA RAX, THoge.PrivateMethod
  {$endif}
end;

var
  hoge: THoge;
  proc: THogePrivateProc;
  method: THogePrivateMethod;
begin
  // You can either in here of the way,
  proc := hoge.GetMethodAddr;
  proc (hoge, 1, 2, 3);
  // Even here of how good
  TMethod (method) .Code := hoge.GetMethodAddr;
  TMethod (method) .Data := hoge;
  method (1, 2, 3) ;
end;

// Method 2
// To jump (here is simple if you just simply call)
procedure THogeHelper.CallMethod (Arg1, Arg2, Arg3 : Integer);
asm
  JMP THoge.PrivateMethod
end;

unit UnitA;

type
  THoge = class
  private
    FPrivateValue: Integer;
    procedure PrivateMethod;
  end;
end.
Mancy answered 21/4, 2016 at 9:33 Comment(9)
yes, that will work in 10.1, however it's just an oversight and as such likely to be "fixed" (broken) in the next point release.Hatchett
Also this comes with the price of preventing this code from working in 64 bit Delphi.Coulee
@WarrenP a simple ifdef and some more x64 assembly will fix that, but ja not an improvement.Hatchett
I had to update it because it just such an arcane piece of hackery. Personally I'd be loath to go that route because it kills portability, but it does work.Hatchett
@Johan, Thanks for your 64bit support patch.Mancy
I think It's off topic, but the original author of above code makes many useful IDE plugins, if some of you are interested, please check his blog's "plugin" category. I really want you to know... translate.google.com/…Mancy
What's with all of the assembler for accessing the field? Why can't you just use PInteger(@Self.FPrivateValue)^ to get and set the value?Gannie
@Graymatter: because the whole problem is that the compiler will not allow access to private fields using normal syntax, such as grabbing the address of a private variable, so you have to resort to lower level hacks to get around the compiler's restrictions.Aurlie
@RemyLebeau I am aware of the compiler restrictions. We haven't upgraded to the latest version because of the restrictions as we use the "hack" in a few places. I haven't had the new version in front of me with it's restrictions so I couldn't test whether fix above worked because the address of the field was still available or whether asm bypassed the checks. After some more research I figured out what it was the assembler itself that bypassed the check.Gannie
M
3

Just use 'with' statement to access private fields !

See the sample code below, taken from this article I noticed today. (Thanks, Mr.DEKO as always !)

This hack is originally reported on QualityPortal in August 2019 as described on above aritcle. (login required)


before rewrite (using "asm" method)

function TPropertyEditorHelper.GetPropList: PInstPropList;
{$IF CompilerVersion < 31.0}
begin
  Result := Self.FPropList;
end;
{$ELSE}
// http://d.hatena.ne.jp/tales/20160420/1461081751
asm
  MOV EAX, Self.FPropList;
end;
{$IFEND}

rewrite using 'with'

function TPropertyEditorHelper.GetPropList: PInstPropList;
begin
  with Self do
    Result := FPropList;
end;

I was amazed it's so simple :-)

Mancy answered 28/11, 2019 at 10:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.