Calling a Private Method of Other Class
Sometimes it is necessary to call a private method of a class saved in another module. It contradicts the OOP principles used in Delphi, but one can try to execute such an operation. Let us consider as an example a case when it is necessary to load/save all the properties of TPersistent descendants, e.g. of an object of TFont class.
There exist standard Delphi classes TReader, TWriter designed to load/save properties of an object. The methods TWriter.WriteProperties(Instance: TPersistent) and TReader.ReadProperty(AInstance: TPersistent) are the most suitable for our purposes. The WriteProperties method allows for writing all properties of the TPersistent descendants into the stream. A call of the ReadProperty method within a loop allows the user to read all the previously saved properties from the stream.
Let us consider a procedure of saving the properties.
With Delphi5, it is evident: the declaration of the WriteProperties method is located in the protected section of the TWriter class, and it is easy to call it:
type THackWriter = class(TWriter); .... THackWriter(Writer).WriteProperties(Instance); //call WriteProperties ....
However, with Delphi4 the situation is more complicated: the WriteProperties method is located in the private section of the TWriter class, and a standard use of Delphi allows the user to call this method only from the module with the TWriter class, i.e. from classes.pas module. This problem seems to be insoluble because it is impossible to add a user code to classes.pas module or call the WriteProperties method from another module. But further in this article we are going to prove that this problem has a solution.
Please note that WriteProperties is a static method, i.e., its address is set during the compilation of the program. WriteProperties is called within the public method TWriter.WriteCollection.
To call WriteProperties, it is necessary to know its address. Let us try to determine it using the public method WriteCollection. One needs to develop a simplest project with a call of the WriteCollection method. This project must have a break point at the call of WriteCollection. Let us run this project: it will go up to the break point. Then we open the CPU window and enter the WriteCollection method by pressing the F7 key (Trace Info). Now we have reached the most interesting point of our technique: we need to find the WriteProperties call within the WriteCollection method and to calculate the offset (in bytes) of the “call TWriter.WriteProperties” command with respect to the starting point of the WriteCollection method. In the given case this offset is equal to $36 + 1 bytes. Thus, the code designed to determine the WriteProperties method address is the following:
var p: pointer; .... p := @TWriter.WriteCollection; Inc(PByte(p), $37); Inc(PByte(p), PInteger(p)^+4);
After a series of experiments we have encountered a problem. The code shown above became inoperative when the project was compiled with standard packages, which is logical, because the packages are absolutely identical to dll files, and the entry points in all the procedures of a package are grouped in the table. But there ain't such thing as an insoluble problem. Here is the new code:
var p: pointer; .... p := @TWriter.WriteCollection; if PByte(p)^=$FF then begin //skip jmp table (for packages) Inc(PByte(p), $2); p := Pointer(PInteger(p)^); end; Inc(PByte(p), $37); Inc(PByte(p), PInteger(p)^+4);
let us add few commands to increase the robust of this code:
var p: pointer; .... p := @TWriter.WriteCollection; if PByte(p)^=$FF then begin //skip jmp table (for packages) Inc(PByte(p), $2); p := Pointer(PInteger(p)^); p := Pointer(PInteger(p)^); end; Inc(PByte(p), $37); if PByte(PChar(p)-1)^<>$E8 then begin exit; end; Inc(PByte(p), PInteger(p)^+4); if PByte(p)^<>$55 then begin exit; end;
Now that we have determined the WriteProperties address, we only need to call it:
asm push eax push edx mov eax, Writer mov edx, Instance call p pop edx pop eax end;
The TReader.ReadProperty address can be determined similarly.
For Delphi3 and CBuilder3, 4, all the operations listed above will have to be performed once again. As a result, we have developed the code which can be used for saving/loading all the properties of the TPersistent descendants.
Where can it be used?
For instance, one can save TEdit.Font or TForm.Icon or TImage.Picture.
What are the advances of this technique?
We have developed a universal technique allowing the user to load/save all the properties of any TPersistent descendants. This technique has been implemented as a small code piece which can call private methods of another class.
What are the disadvantages of this technique?
“Bad programming style” contradicting the principles of OOP. In fact, our code is implicitly dependent on the classes.pas module, and any modification of this module, one of the objects TWriter, TReader, or methods TWriter.WriteCollection, TReader.ReadCollection can result in run-time errors in our code. The worst about it is that these errors will be detected only when the application is running, that is, the compiler is unable to find them. But how often do you change the classes.pas module? I think this operation is rather rare.
You can download the demonstration project and study the source code described above. You can use this code in your own applications, but I reject any responsibility for possible consequences of its use. On the other hand, we proved that ‘non-standard’ programming techniques can lead to interesting results like calling a private method of another class declared in another program module.
The problems described above arose during the development of the shareware project Storage Library, which uses this technique to load/save all the properties of the TPersistent object descendants, e.g. TEdit.Font, TForm.Icon or TImage.Picture. More information on the Storage Library project can be found on Web server of Deepsoftware.RU company at http://www.deepsoftware.ru/rsllib/
CallPrivate.zip - Demo project. Delphi3/4/5 CBuilder 3/4/5 versions.