17 мая 2020

Доступ к private членам класса

    Неделю тому назад я начал писать небольшую заметку, где в коде был доступ к protected методу класса. В результате два предложения о нем вылились в отдельную статью "Доступ к protected членам класса", комментарии к которой натолкнули меня на мысль продолжить тему. Сегодня я рассмотрю несколько способов доступа к private членам класса.

Создадим модуль uXyz и объявим в ней класс:
unit uXyz;

interface

type
  TXyz = class
  strict private
    procedure StrictPrivateProc;
  strict protected
    procedure StrictProtectedProc;
  public
    procedure PublicProc;
  end;

implementation

{ TXyz }

procedure TXyz.StrictProtectedProc;
begin
  Writeln('StrictProtectedProc');
end;

procedure TXyz.PublicProc;
begin
  writeln('PublicProc')
end;

procedure TXyz.StrictPrivateProc;
begin
  Writeln('StrictPrivateProc');
end;

end.
Для вызова приватного метода способы, которые я описал в статье о вызове protected методов, не сработают. Но не стоит отчаиваться. Инкапсуляция в Delphi удовлетворяет принципу "если нельзя, но очень хочется, то можно".

    Первый способ, который приходит на ум – это использовать run-time type information (RTTI).
program TestXyz;

{$APPTYPE CONSOLE}

uses
  System.Rtti,
  uXyz in 'uXyz.pas';

procedure Test;
var
  xyz: TXyz;
  Method: TRttiMethod;
begin
  xyz := TXyz.Create;
  Method := TRttiContext.Create.GetType(TXyz).GetMethod('StrictPrivateProc');
  Method.Invoke(xyz, []);
  xyz.Free;
end;
Выполнение этого кода приведет к ошибке:
First chance exception at $004CB066. Exception class $C0000005 with message 'access violation at 0x004cb066: read of address 0x00000000'. Process TestXyz.exe (8780)
Причина в том, что функция GetMethod вернула в переменную Method пустой указатель. Может так нельзя вызывать метод класса? Успешный вызов public метода показывает, что можно:
procedure Test;
var
  xyz: TXyz;
  Method: TRttiMethod;
begin
  xyz := TXyz.Create;
  Method := TRttiContext.Create.GetType(TXyz).GetMethod('PublicProc');
  Method.Invoke(xyz, []);
  xyz.Free;
end;
Оказывается, чтобы вызвать приватный метод таким способом, нужно, что бы автор класса выдал на это разрешение. В модуле с классом его автор должен указать директиву $RTTI, которая используется для управления количеством информации предоставляемой классом. Т.е. немного изменим модуль uXyz:
unit uXyz;

interface
{$RTTI EXPLICIT METHODS([vcPublic, vcProtected, vcPrivate])}

type
  TXyz = class
и терерь вызов TRttiContext.Create.GetType(TXyz).GetMethod('StrictPrivateProc') вернет нам указатель на приватный метод. Но, что делать если у нас нет исходных кодов модуля с классом?

    Много версий тому назад в Delphi появилась весьма интересная вещь - class and record helper. Это тип, который позволяет расширять существующие классы и записи новыми методами и свойствами без использования наследования (тем более, что у записей нет наследования). Вот он и является ключом для доступа к приватным членам класса в Delphi 10.3. Итак, опишем class helper для нашего класса. Этот раз не будем смущать окружающих словом hack, а назовем его скромно - TXyzHelper:
program TestXyz;

{$APPTYPE CONSOLE}

uses
  uXyz in 'uXyz.pas';

type

  TXyzHelper = class helper for TXyz
    procedure CallPrivate;
  end;

{ TXyzHelper }

procedure TXyzHelper.CallPrivate;
begin
  Self.StrictPrivateProc;
end;

procedure Test;
var
  xyz: TXyz;
begin
  xyz := TXyz.Create;
  xyz.CallPrivate;
  xyz.Free;
end;
Компиляция программы приводит к ошибке:
[dcc32 Error] TestXyz.dpr(19): E2361 Cannot access private symbol TXyz.StrictPrivateProc
С прискорбием сообщаю, что этот способ работал много лет, пока это не заметили разработчики Delphi и не решили, что он нарушает правила инкапсуляции. Согласно What's New Delphi 10.1 Berlin:
Other Delphi Compiler Improvements
To enforce visibility semantics, class and record helpers cannot access private members of the classes or records that they extend.
т.е. в результате одного из улучшений компилятора в Delphi 10.1 Berlin разработчики лишились этого способа. Теперь Code Insight для Self. показывает только наш public метод, а компилятор без проблем позволяет его вызвать:
procedure TXyzHelper.CallPrivate;
begin
  Self.PublicProc
end;
Но несмотря на то, что подсказка Code Insight не показывает protected метод, его тоже можно вызвать:
procedure TXyzHelper.CallPrivate;
begin
  Self.StrictProtectedProc
end;
А, что же делать с private методом? Благодаря тому, что разработчики Delphi без косяков ничего не пишут, слова "class and record helpers cannot access private members" не совсем правда. На этот раз эти разработчики забыли о операторе WITH. Стоит немного изменить процедуру:
procedure TXyzHelper.CallPrivate;
begin
  with Self do
    StrictPrivateProc
end;
и все - "class and record helpers can access private members".

    Это не единственный способ вызвать приватный метод из class helper. Например, можно воспользоваться типом System.Tmethod:
procedure TXyzHelper.CallPrivate;
var
  proc: procedure of object;
begin
  TMethod(proc).Code := @TXyz.StrictPrivateProc;
  TMethod(proc).Data := Self;
  proc;
end;
    Еще несколько способов вызова приватного метода можно подсмотреть у японского блогера. Он предлагает для этого использовать встроенный ассемблер. Из всех предложенных им вариантов мне понравился своей простотой вот этот:
procedure TXyzHelper.CallPrivate;
asm
  JMP TXyz.StrictPrivateProc
end;
Его вариант с CALL TXyz.PrivateProc не стоит рассматривать, т.к. подобный вызов метода с параметрами может привести к глюкам в программе. А на 64-х битной программе сразу вызывает ошибку:
Project TestXyz.exe raised exception class $C0000005 with message 'c0000005 ACCESS_VIOLATION'
Еще два его варианта вызова приватного метода реализуются с помощью одного class helper'а, который определяет адрес приватного метода:
type

  TXyzHelper = class helper for TXyz
    function GetMethodAddr: Pointer;
  end;

{ TXyzHelper }

function TXyzHelper.GetMethodAddr: Pointer;
asm
  LEA EAX, TXyz.StrictPrivateProc
end;
Этот class helper позволяет вызвать приватный метод по его адресу:
procedure Test;
var
  xyz: TXyz;
  proc: procedure (xyz: TXyz{еще параметры});
begin
  xyz := TXyz.Create;
  @proc := xyz.GetMethodAddr;
  proc(xyz{еще параметры});
  xyz.Free;
end;
или же снова воспользовавшись типом System.Tmethod:
procedure Test;
var
  xyz: TXyz;
  proc: procedure of object;
begin
  xyz := TXyz.Create;
  TMethod(proc).Code := xyz.GetMethodAddr;
  TMethod(proc).Data := xyz;
  proc;
  xyz.Free;
end;
Недостатком методов нашего японского коллеги является то, что они ограничены архитектурой x86/x64.

    Таким образом, мы доказали, что при необходимости в Delphi модификаторы видимости членов класса private и strict private также легко обходятся.

2 комментария:

  1. Ух ты. Через хелперы теперь можно универсальнее оформить свои хаки :)

    ОтветитьУдалить