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.
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.
ReplyDeleteI 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.
ReplyDeleteI 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".
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.
DeleteSo 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.
DeleteEMBT fixed the issues where I needed that when they fixed the helper hack.
DeleteIn 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).
This comment has been removed by the author.
DeleteWhy 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.
DeleteWhat happens if the Method has parameters on Assembler example? (Even all)
ReplyDeleteYou 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.
DeleteHow can we get the private fields using the 3 ways here explained?
ReplyDeleteNot at all. See the last line: "I will discuss accessing private fields in a later post."
Delete