In particular, I feel the need in TCharacter.IsLatin1
which is private
.
type
TCharacterHelper = class helper for TCharacter
pu
See update below
As is widely known, helpers do crack private visibility. So, private members are visible from a class helper. However, this behaviour does not extend to static members, so TCharacter.IsLatin1
is inaccessible (by fair means) outside the unit in which it is declared.
What about unfair means? Well, some public methods of TCharacter
do call IsLatin1
. And even though IsLatin1
is declared inline
, it seems that these methods are compiled with call statements rather than the code inlined. Perhaps that's because they calls occur in the same unit, or the same type, and the inline engine is not capable of inlining.
Anyway, where I am going with this is that you could, at runtime, disassemble one of these calls. For sake of argument, let's consider IsControl
:
class function TCharacter.IsControl(C: Char): Boolean;
begin
if IsLatin1(C) then
Result := InternalGetLatin1Category(C) = TUnicodeCategory.ucControl
else
Result := InternalGetUnicodeCategory(UCS4Char(C)) = TUnicodeCategory.ucControl;
end;
Its first act is to call IsLatin1
. The compiled code looks like this:
System.Character.pas.517: 00411135 C3 ret 00411136 8BC0 mov eax,eax TCharacter.IsControl: 00411138 53 push ebx 00411139 8BD8 mov ebx,eax System.Character.pas.533: 0041113B 8BC3 mov eax,ebx 0041113D E852FFFFFF call TCharacter.IsLatin1 00411142 84C0 test al,al 00411144 740F jz $00411155
So, you could do the following:
TCharacter.IsControl
.call
instruction.call
instruction to find the target address, and that's where IsLatin1
can be found.I'm not remotely advocating this for IsLatin1
. It's such a simple function, and not subject to change, that it's surely better to re-implement it. But for more complex situations, this method can be used.
And I'm also not claiming originality. I learnt this technique from the madExcept source code.
OK, @LU RD resourcefully found a way to prove me wrong. Congratulations for that. What I said about static
methods is accurate, however, @LU RD used a very adept trick of introducing a non-static class method and by that way crack the private members.
I'd like to take his answer a bit further by showing how to use two helpers to expose the functionality using the original name:
unit CharacterCracker;
interface
uses
System.Character;
type
TCharacterHelper = class helper for TCharacter
public
class function IsLatin1(C: Char): Boolean; static; inline;
end;
implementation
type
TCharacterCracker = class helper for TCharacter
public
class function IsLatin1Cracker(C: Char): Boolean; inline;
end;
class function TCharacterCracker.IsLatin1Cracker(C: Char): Boolean;
begin
Result := TCharacter.IsLatin1(C); // resolves to the original method
end;
class function TCharacterHelper.IsLatin1(C: Char): Boolean;
begin
Result := TCharacter.IsLatin1Cracker(C);
end;
end.
You can use this unit, and the only helper that is active outside the unit is the one declared in the interface section. Which means you can write code like this:
{$APPTYPE CONSOLE}
uses
System.Character,
CharacterCracker in 'CharacterCracker.pas';
var
c: Char;
begin
c := #42;
Writeln(TCharacter.IsLatin1(c));
c := #666;
Writeln(TCharacter.IsLatin1(c));
Readln;
end.
Is there any way to "reintroduce" this method to public visibility?
Yes. By introducing a non-static function call through a new class function.
The trick here is to use the helper ability to access all members through Self
. See Access a strict protected property of a Delphi class? and How do I use class helpers to access strict private members of a class?.
This is done by calling a private helper non-static function from the new class function, where Self
can be resolved.
Type
TCharacterHelper = class helper for TCharacter
private
class function IsLatin1Cracker(aChar: Char): Boolean; inline;
public
// Introduce a new public static class function
class function IsLatinOne(aChar: Char): Boolean; static; inline;
end;
class function TCharacterHelper.IsLatinOne(aChar: Char): Boolean;
begin
Result := IsLatin1Cracker(aChar);
end;
class function TCharacterHelper.IsLatin1Cracker(aChar: Char): Boolean;
begin
Result := Self.IsLatin1(aChar); // Here Self can access base class
end;
You cannot use the original method name though, but still the original class function can be called this way.
Oops, David showed a way to expand this idea to use the original name. That can be a versatile trick to have in the toolbox.
Just to refer to what documentation has to say about this:
Ordinary Class Methods:
You can use Self to call constructors and other class methods, or to access class properties and class fields.
Class Static Methods:
Unlike ordinary class methods, class static methods have no Self parameter at all.
Note: Records can only have static class methods, unlike classes.
Class and Record Helpers:
You can use the helper any place where you can legally use the extended class or record. The compiler's resolution scope then becomes the original type, plus the helper.
...
The visibility scope rules and memberList syntax are identical to that of ordinary class and record types.
You can define and associate multiple helpers with a single type. However, only zero or one helper applies in any specific location in source code. The helper defined in the nearest scope will apply. Class or record helper scope is determined in the normal Delphi fashion (for example, right to left in the unit's uses clause).
As you noted above, records can only have static class methods. So if you wanted to "reintroduce" a private class method in a record, here is a solution (based on David's technique):
Suppose we have:
Type
TTestRec = record
private
class Function IsLatin1(C: Char): Boolean; static; inline;
end;
And add a helper in a new unit:
unit HelperUnitForTTestRec;
interface
Type
TTestRecHelper = record helper for TTestRec
public
class function IsLatin1(c:Char): Boolean; static; //inline; !! Inlining not possible
end;
implementation
Type
TTestRecCracker = record helper for TTestRec
private
function IsLatinOne(C:Char): Boolean; inline;
public
class function IsLatin1Cracker(c:Char): Boolean; static; inline;
end;
function TTestRecCracker.IsLatinOne(c: Char): Boolean;
begin
Result := Self.IsLatin1(C); // <-- Here is Self resolved
end;
class function TTestRecCracker.IsLatin1Cracker(c: Char): Boolean;
var
tmp: TTestRec;
begin
Result := tmp.IsLatinOne(C); // <-- Must use a call to ordinary method
end;
class function TTestRecHelper.IsLatin1(c: Char): Boolean;
begin
Result := IsLatin1Cracker(C);
end;
end.