Неделю тому назад я начал писать небольшую заметку, где в коде был доступ к protected методу класса. В результате два предложения о нем вылились в отдельную статью "Доступ к protected членам класса", комментарии к которой натолкнули меня на мысль продолжить тему. Сегодня я рассмотрю несколько способов доступа к private членам класса.
Создадим модуль uXyz и объявим в ней класс:
Первый способ, который приходит на ум – это использовать run-time type information (RTTI).
Много версий тому назад в Delphi появилась весьма интересная вещь - class and record helper. Это тип, который позволяет расширять существующие классы и записи новыми методами и свойствами без использования наследования (тем более, что у записей нет наследования). Вот он и является ключом для доступа к приватным членам класса в Delphi 10.3. Итак, опишем class helper для нашего класса. Этот раз не будем смущать окружающих словом hack, а назовем его скромно - TXyzHelper:
Это не единственный способ вызвать приватный метод из class helper. Например, можно воспользоваться типом System.Tmethod:
Таким образом, мы доказали, что при необходимости в Delphi модификаторы видимости членов класса private и strict 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т.е. в результате одного из улучшений компилятора в Delphi 10.1 Berlin разработчики лишились этого способа. Теперь Code Insight для Self. показывает только наш public метод, а компилятор без проблем позволяет его вызвать:
To enforce visibility semantics, class and record helpers cannot access private members of the classes or records that they extend.
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 также легко обходятся.
Ух ты. Через хелперы теперь можно универсальнее оформить свои хаки :)
ОтветитьУдалитьОпять в закладки.
ОтветитьУдалить