Wednesday, June 02, 2004

Hack #5: Access to private fields

As your mama taught you, you should protect your private parts, and leave other peoples private parts alone. The same goes for programming, of course. Encapsulation and information hiding is two of the main pillars of object oriented programming, so we better not be messing with those, right? Right. But once in a blue moon, we just have the urge to behave badly and reach into the private fields of another class, defined in another unit.

When possible, the previously described hack of accessing a private field through a published property's RTTI is preferred to the technique presented in this article. However, sometimes that hack is not very practical or even possible. Using RTTI is relatively slow (although the RTTI access to retrieve field offsets could be cached) and the private field you need access to may not be exposed as a property. In addition, if you need access to all or most of a class's internal private fields, the solution described here may be preferable.

As mentioned earlier, Delphi's concept of private (in contrast with D8/D2006-2007's strict private) still allows other code in the same unit full access, while excluding access to external units. So all classes and routines in the same unit are intimate friends of each other.

Have you noticed how promiscuous some of the VCL classes are with each other? For instance, the abstract base class of the component streaming system, TFiler, defines a number of private fields. The actual worker classes TReader and TWriter (both descending from TFiler) are both defined in the same unit, and blatantly access the private fields defined in TFiler (for instance the all-important FStream field).

If you should wish to write your own application-level streaming system (with the ability to stream and auto-create non-TComponent classes) that still participates in the TPersistent DefineProperties system, you would have to extend TReader and TWriter. It looks like these classes have been designed to be extended, considering the virtual protected methods they introduce. Your own TMyReader and TMyWriter classes would not have access to the TFiler private fields, nor to the private fields defined in TReader and TWriter. This could(*) severely limit what your descendant classes could do.

Ok, enough talk already - on to the meat. To get access to the private fields of TFiler, declare a shadow class that exactly matches the private declaration from Classes.pas:

type   
TFilerHack = class(TObject)
private
FStream: TStream;
// other private fields here
end;

Given a TFiler instance, you can now simply cast to TFilerHack to get access to the fields:

procedure Foo(Filer: TFiler); 
var
Stream: TStream;
begin
Stream := TFilerHack(Filer).FStream;
end;

Notice that I still use the private visibility directive for the field - this still gives access to all code in the same unit. Normally, this kind of cracker classes should be declared in the implementation section of the unit. This hack is of course extremely version sensitive to changes in the cracked class. Compared to other solutions of using magic field offsets it has the benefit of having the compiler computing the offset for you and from being protected against changes in the base class (TObject in this case). Still, if you ever use this hack, make sure you {$IFDEF} or {$IF} protect the code against changes in compiler and RTL version (or 3rd party component set version).


(*) A few years ago (using D5), I actually implemented an application-level streaming system by writing my own TReader and TWriter classes and using hacks similar to the ones described in this article. Since then TFiler, TReader and TWriter have been improved with regards to extensibility - providing protected read-only properties to get to some of the private fields, for instance (kudos to Borland CodeGear on that one). It may well be that a functional application-level streaming system could be built without using this hack in D7, but I wouldn't count on it. I still use TFiler as an illustrative example.

10 comments:

  1. Yo Hallvard, another nice hack!

    BTW, saw your appearance on my chat server yesterday, when I was away from the server, in another country. By the time I came back, you're gone. What's up?

    Chee Wee

    ReplyDelete
  2. Is that possible to access a private class var field:

    type
    TTest = class
    strict private
    class var FValue: integer;
    ...
    end;

    say I want to access TTest.FValue .

    ReplyDelete
  3. > Is that possible to access a private class var field

    Hm. I don't think there is a clean way to do this, no.

    AFAIK, class vars are implemented using scope-controlled global variables. The only way I can think of is to find the address of the class var by peeking into the machine code generated for a method that you know access the class var.

    Not a nice one.

    ReplyDelete
  4. Luckily I design and deploy my application with runtime packages. I can retrieve the address of the private class var via the exported symbol in runtime packages.

    ReplyDelete
  5. I wish to change value of a variable declaerd in unit Graphics.pas:

    var
    ScreenLogPixels: Integer;

    Is there possible to do so with hacking methods you introduced here?

    ReplyDelete
  6. Seems this hack no longer works in the lastest Delphi.

    ReplyDelete
  7. Hm. Are you sure? Do you have a sample? What happens

    ReplyDelete
  8. This comment has been removed by a blog administrator.

    ReplyDelete
  9. I doesn't work in Delphi Xe 6

    Here is a simple test

    type
    TFoo = class
    strict private
    Field1: Integer;
    public
    constructor Create(aFieldValue : Integer);
    property Field : Integer read Field1;
    end;

    THackFoo = class
    private
    Field1: Integer;
    end;
    procedure TForm1.FormCreate(Sender: TObject);
    var
    Foo : TFoo;
    begin
    Foo := TFoo.Create(7);
    THackFoo(Foo).Field1 := 8;
    Caption := IntToStr(Foo.Field);

    end;

    { TFoo }

    constructor TFoo.Create(aFieldValue: Integer);
    begin
    inherited Create;
    Field1 := aFieldValue;
    end;


    The private fiels doesn't get updated

    ReplyDelete

Comments are moderated - spam and non-relevant links to will be deleted.