4

In particular, I feel the need in TCharacter.IsLatin1 which is private.

type
  TCharacterHelper = class helper for TCharacter
  public
    class function IsLatin1(C: Char): Boolean; static; inline;
  end;

class function TCharacterHelper.IsLatin1(C: Char): Boolean;
begin
  Result := Ord(C) <= $FF;
end;

This one-liner method can be reimplemented in literally no time, but I'd better off leave the exact implementation details on the vendor's discretion.

Is there any way to "reintroduce" this method to public visibility?

Free Consulting
  • 4,300
  • 1
  • 29
  • 50

2 Answers2

8

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.
Community
  • 1
  • 1
LU RD
  • 34,438
  • 5
  • 88
  • 296
  • +1 Nicely done!! With a bit of extra work you can use the original method name. See my update. – David Heffernan Feb 07 '14 at 10:28
  • @DavidHeffernan, that was tricky! I'm not sure this is what the compiler architects expected, but the fact that this is possible proves that the language is modular, which is a good thing. – LU RD Feb 07 '14 at 10:35
  • No, I doubt this was intentional. FWIW, and I've not checked, but I believe that you can delete the `tmp` variable and write `Result := TCharacter.IsLatin1Cracker(aChar);` – David Heffernan Feb 07 '14 at 10:39
  • have you found any docs that cover the exact visibility rules for helpers. That all seems a little opaque to me. – David Heffernan Feb 07 '14 at 20:13
  • @DavidHeffernan, sorry, not more than whats written in docs (added to answer). – LU RD Feb 07 '14 at 23:18
  • @DavidHeffernan, class methods for records must be static, so I added a solution for this case as well. – LU RD Feb 08 '14 at 08:45
  • 1
    So the instance method is what it makes there. I think you've got it all covered now! – David Heffernan Feb 08 '14 at 08:51
6

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:

  1. Take the address of TCharacter.IsControl.
  2. Disassemble the code at that address until you found the first call instruction.
  3. Decode that 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.
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • I have not tested (I could not install it yet), but I believe it is no longer possible with Delphi Berlin, because class helpers no longer have access to private members – dipold May 10 '16 at 17:18