Can I call static private class method with class helper?

前端 未结 2 819
猫巷女王i
猫巷女王i 2021-01-06 10:34

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

type
  TCharacterHelper = class helper for TCharacter
  pu         


        
相关标签:
2条回答
  • 2021-01-06 11:18

    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.
    
    0 讨论(0)
  • 2021-01-06 11:29

    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.
    
    0 讨论(0)
提交回复
热议问题