Monday 22 January 2018

Accessing private methods of another class

In versions of Delphi before 10.1 Berlin, it was possible to access private members and private methods of a class simply by defining a class helper for it and accessing these private items in its methods.

But that it was so easy to access private members undermined the basic OO principles of information hiding and encapsulation. That is why this loophole (which could be considered a bug) was closed in Delphi 10.1 Berlin. But there was a lot of howling about this, because it was so easy and so convenient to have access to everything, even to other classes' "private parts".

There has been a lot of discussing going on about this. I will not repeat the arguments made there, but will state that I agree with the closing of the loophole. Accessing private members or methods is a hack, and a hack should never be easy. This hack was far too easy. Instead of using the hack, people should rather have tried to find other ways to achieve their goals.

But there is one argument of the pro-helper people that actually made sense: sometimes a class is not designed well, and you must resort to hacks.

Three hacks

They have all been mentioned before, in several blogs and StackOverflow answers, but I want to recap them here, in one post. All of these still require helper classes, as only there, you can still access private methods or members.

Accessing private methods

Say we have a class, let's call it THasPrivateMethods, that has at least one private method, let's say procedure PrivateMethod, that you want to call. The following three ways to call the private method have all been verified in Delphi 10.2.2 Tokyo. They should also work in previous versions.

Using assembler

The first way to get at private methods is to define a helper class with a new method. Here, I'll call the helper THelper and the first method CallingPrivateMethodUsingAsm. This method must be an assembler method:

type
  THelper = class helper for THasPrivateMethods
  public
    procedure CallingPrivateMethodUsingAsm;
  end;

The implementation is extremely simple:

{$STACKFRAMES OFF}
procedure THelper.CallingPrivateMethodUsingAsm;
asm
       JMP     THasPrivateMethods.PrivateMethod
end;

In other words, in assembler, helper methods can still access private methods.

Using a method pointer

For those who are not very good with assembler, there is another, more tedious way, using a method pointer:

    procedure CallingPrivateMethodUsingTMethod; inline;

The implementation takes the address of the private method (which can apparently still be queried), puts it in a method pointer and then calls the method pointer:

type
  TPrivateMethod = procedure of object;

...
  
procedure THelper.CallingPrivateMethodUsingTMethod;
var
  M: TMethod;
begin
  M.Code := @THasPrivateMethods.PrivateMethod;
  M.Data := Self;
  TPrivateMethod(M);
end;

Using with

Inside a method of a class helper, you can't call a private method directly anymore, but you can use a simple trick (probably forgotten when they closed the loophole), using "with Self do". I am no fan of "with", but in this special case, it works as expected:

procedure THelper.CallingPrivateMethodUsingWith;
begin
  with Self do PrivateMethod;
end;

Yes, it is that easy. But you must use "with Self do" to call the private method. Now, if you inline this method, there is absolutely no overhead. It will work as if you had called the private method in your own routine:

procedure NeedsAccessToPrivateMethod;
var
  H: THasPrivateMethods;
begin
  H := THasPrivateMethods.Create;
  try
    // ...
    H.CallingPrivateMethodUsingWith;
    // ...
  finally
    H.Free;
  end;
 end;
 

Again, if CallingPrivateMethodUsingWith is inlined, the resulting code is the same as if you had written:

  try
    // ...
    H.PrivateMethod;
    // ...
  finally

If you can read assembler, you can verify this in the CPU view of the IDE.

I will discuss accessing private fields in a later post.

11 comments:

  1. Many thanks, this is very nice and clever. Assembler gives me the creeps but I never knew about method pointers or the "with self do". This makes me consider updating our projects from Seattle to the most recent version.

    ReplyDelete
  2. I argue that all three hacks are a very likely a leftover of some incomplete fix to close the loophole and eventually will also be fixed.

    I guess the people at Embarcadero did not even know otherwise they would have mentioned these as alternative when everyone and their cat ranted against the "Self fix".

    ReplyDelete
    Replies
    1. I assume they are aware of these leftover loopholes by now (they don't live in a void, after all) and decided not to fix them yet. So for now, they work.

      Delete
    2. So how do you access private methods these days? I seem to remember you needed access to some of them and used helpers to access them before the loophole was closed.

      Delete
    3. EMBT fixed the issues where I needed that when they fixed the helper hack.

      In one case I need to override virtual methods that are private so I use a class with the same VMT layout to achieve that (see TRttiInvokableMethodHack in Spring.pas).

      Delete
    4. This comment has been removed by the author.

      Delete
    5. Why on earth are private methods made virtual? That doesn't make much sense. OK, found one: if you implement several classes that derive from an abstract class in the same unit (e.g. TPolicy). But even then, I would make them protected.

      Delete
  3. What happens if the Method has parameters on Assembler example? (Even all)

    ReplyDelete
    Replies
    1. You will have to pass them. But note that if they are not on the stack (only three parameters, including the hidden Self parameter), you can simply call the method like I showed. The registers will not be modified, so EAX, EDX and ECX, the first three parameters, will remain the same for the call to the actual private method. If there are more parameters, you will have to do some stack fiddling.

      Delete
  4. How can we get the private fields using the 3 ways here explained?

    ReplyDelete
    Replies
    1. Not at all. See the last line: "I will discuss accessing private fields in a later post."

      Delete