понедельник, 1 августа 2011 г.

Хак №11: получение GUID интерфейса по интерфейсной ссылке

Это перевод Hack#11: Get the GUID of an interface reference. Автор: Hallvard Vassbotn.

Недавно Randy Magruder обратился ко мне с одним своим очень интересным проектом, над которым он работает:
Что я пытаюсь сделать: добавлять и удалять поддерживаемые интерфейсы в класс в run-time и вызывать их. Я уже сделал достаточно, чтобы я мог использовать модель OTAServices model из Delphi для передачи дополнительных интерфейсов, добавления их во внутренний список, и заместил поведение QueryInterface, чтобы вызов возвращал интерфейс из списка, вместо стандартной таблицы объекта.
Это звучит как клёвый (и сложный) проект! Он продолжает:
Но если вызывающий объекта знает только GUID интерфейса, который ему нужен, и хочет получить подходящий интерфейс и вызвать метод в нём по имени, то что я могу с этим сделать? Мне использовать расширенный RTTI с IInvoker?
Ну, я не уверен, как это можно решить. AFAIK, не существует простого проецирования от объекта, реализующего интерфейс, к информации типа этого интерфейса. Одно из решений может заключаться в реализации проецирования GUID интерфейсов на информацию типа.
И ещё: ты случайно не знаешь способ извлечения GUID интерфейса в run-time, ...
Это должно быть возможным, хотя и немного сложновато.

Вы можете вспомнить код, который я написал для конвертации интерфейсной ссылки в реализующий объект. Вполне возможно изменить этот код в другой, который будет возвращать смещение в объекте, по которому расположена запись интерфейса в таблице интерфейсов объекта. Соединим это с вызовом TObject.GetInterfaceTable для получения PInterfaceTable, содержащей запись TInterfaceEntry реализуемого интерфейса. Сравнивая вычисленное смещение с полем IOffset, мы можем найти совпадение и узнать GUID из поля IID.

Из System.pas:
type
  PInterfaceEntry = ^TInterfaceEntry;
  TInterfaceEntry = packed record
    IID: TGUID;
    VTable: Pointer;
    IOffset: Integer;
    ImplGetter: Integer;
  end;

  PInterfaceTable = ^TInterfaceTable;
  TInterfaceTable = packed record
    EntryCount: Integer;
    Entries: array[0..9999] of TInterfaceEntry;
  end;

  TObject = class
    ...
    class function GetInterfaceTable: PInterfaceTable;
Но, конечно же, это будет работать только для интерфейсов, объявленных в режиме проектирования.

Randy продолжает:
...даже если он передаётся как IUnknown?
Да уж, ты не делаешь это проще, не так ли, Randy? ;) Конечно же, GUID IUnknown фиксирован (это {00000000-0000-0000-C000-000000000046}). И я не думаю, что существует хоть какой-то след интерфейса или GUID, из которого этот IUnknown был получен. OTOH, все интерфейсы наследуются от IUnknown, так что если ссылка на IUnknown, которая у вас есть, на самом деле является под-интерфейсом, то это может сработать (и сработает - как мы увидим чуть позже).

После ответа на e-mail Randy, я принял вызов по реализации моего предполагаемого решения - получения GUID по интерфейсной ссылке. Начальной точкой является код из статьи Хак №7: Interface в Object - который конвертирует интерфейсную ссылку обратно в объектную ссылку на объект, который реализует интерфейс (а теперь быстро скажите это 5 раз ;) ):
function GetImplementingObject(const I: IInterface): TObject; 
const 
  AddByte = $04244483;  
  AddLong = $04244481;  

type 
  PAdjustSelfThunk = ^TAdjustSelfThunk; 
  TAdjustSelfThunk = packed record 
    case AddInstruction: LongInt of 
      AddByte: (AdjustmentByte: ShortInt); 
      AddLong: (AdjustmentLong: LongInt); 
  end; 

  PInterfaceMT = ^TInterfaceMT; 
  TInterfaceMT = packed record 
    QueryInterfaceThunk: PAdjustSelfThunk; 
  end; 
  TInterfaceRef = ^PInterfaceMT; 

var 
  QueryInterfaceThunk: PAdjustSelfThunk; 
begin 
  Result := Pointer(I); 
  if Assigned(Result) then 
  try 
    QueryInterfaceThunk := TInterfaceRef(I)^. QueryInterfaceThunk; 
    case QueryInterfaceThunk.AddInstruction of 
      AddByte: Inc(PChar(Result), QueryInterfaceThunk.AdjustmentByte); 
      AddLong: Inc(PChar(Result), QueryInterfaceThunk.AdjustmentLong); 
    else     
      Result := nil; 
    end; 
  except 
    Result := nil; 
  end; 
end;
Хотя код выглядит как абракадабра, но большая его часть прозрачна, а объявления типов делают код само-документирующимся и очевидным (ага, конечно!). Код анализирует кодовые заглушки, генерируемые компилятором и конвертирующие интерфейсную ссылку (Self как IInterface) в объектную (Self как TObject).

В то время как объектная ссылка указывает на первый байт блока памяти, выделенной под объект, интерфейсная ссылка указывает куда-то в середину блока памяти объекта. Так что разница между двумя ссылками - это небольшое фиксированное смещение. Наша задача - узнать это смещение. Поскольку компилятору нужно выполнить конвертацию интерфейсного Self в объектный Self для вызова методов объекта, и он делает это добавлением отрицательного смещения к Self перед вызовом реализации метода - то код выше анализирует эту заглушку кода, выцепляет из неё смещение и использует его для конвертации ссылки.

Чтобы достичь нашей цели по получению GUID интерфейсной ссылки, нам нужно вернуть это смещение вместо готового результата (объектной ссылки). Давайте изменим функцию выше:
function GetPIMTOffset(const I: IInterface): integer;
// PIMT = Pointer to Interface Method Table
const
  AddByte = $04244483; // опкод для ADD DWORD PTR [ESP+4], Shortint
  AddLong = $04244481; // опкод для ADD DWORD PTR [ESP+4], Longint

type
  PAdjustSelfThunk = ^TAdjustSelfThunk;
  TAdjustSelfThunk = packed record
    case AddInstruction: LongInt of
      AddByte : (AdjustmentByte: ShortInt);
      AddLong : (AdjustmentLong: LongInt);
  end;

  PInterfaceMT = ^TInterfaceMT;
  TInterfaceMT = packed record
    QueryInterfaceThunk: PAdjustSelfThunk;
  end;
  TInterfaceRef = ^PInterfaceMT;

var
  QueryInterfaceThunk: PAdjustSelfThunk;
begin
  Result := -1;
  if Assigned(Pointer(I)) then
  try
    QueryInterfaceThunk := TInterfaceRef(I)^.QueryInterfaceThunk;
    case QueryInterfaceThunk.AddInstruction of
      AddByte: Result := -QueryInterfaceThunk.AdjustmentByte;
      AddLong: Result := -QueryInterfaceThunk.AdjustmentLong;
    end;
  except
    // Защита от не-Delphi интерфейсов и неверных ссылок
  end;
end;
Как вы можете видеть, это практически тот же самый код, что и выше, но возвращающий Integer вместо TObject. Имя функции немножко "гиковато" - PIMT это аббревиатура для указателя на таблицу методов интерфейса. Это специальное "поле", генерируемое компилятором, которое вставляется в экземпляр объекта, когда вы объявляете, что класс реализует интерфейс. Функция возвращает смещение этого поля. Заметим, что компилятор использует команду ADD для поправки параметра Self - но, фактически, добавляет отрицательное смещение. Вот почему в коде стоит минус перед возвращаемым значением.

Теперь мы можем переписать исходную функцию GetImplementingObject в терминах этой новой подпрограммы:
function GetImplementingObject(const I: IInterface): TObject;
var
  Offset: integer;
begin
  Offset := GetPIMTOffset(I);
  if Offset > 0 then
    Result := TObject(PAnsiChar(I) - Offset)
  else
    Result := nil;  
end;
Красивая маленькая функция - как было приятно убрать из неё дублирующийся код. Заметьте, что PAnsiChar - это единственный (в старых Delphi - прим.пер.) тип данных, допускающий арифметику указателей; мы используем его только для упрощения записи вычислений (прим.пер.: упражнение - что не так с типом PChar в этом контексте?).

Мы уже стоим в одном шаге от получения GUID интерфейса (или IID - что формально является правильным названием). Теперь мы можем получать смещение PIMT, но само по себе оно не очень полезно. Что делает его полезным - так это то, что мы можем использовать его для сравнения со смещениями, хранимых как часть записей InterfaceEntry, генерируемых компилятором для всех реализуемых им интерфейсов. Как указано выше, мы можем использовать класс TObject и его (классовую) функцию GetInterfaceTable для получения указателя на эту таблицу. С этим знанием мы можем написать функцию, которая пытается найти запись InterfaceEntry, соответствующую интерфейсной ссылке:
function GetInterfaceEntry(const I: IInterface): PInterfaceEntry;
var
  Offset: integer;
  Instance: TObject;
  InterfaceTable: PInterfaceTable;
  j: integer;
  CurrentClass: TClass;
begin
  Offset := GetPIMTOffset(I);
  Instance := GetImplementingObject(I);
  if (Offset >= 0) and Assigned(Instance) then
  begin
    CurrentClass := Instance.ClassType;
    while Assigned(CurrentClass) do
    begin
      InterfaceTable := CurrentClass.GetInterfaceTable;
      if Assigned(InterfaceTable) then
      begin
        for j := 0 to InterfaceTable.EntryCount - 1 do
        begin
          Result := @InterfaceTable.Entries[j];
          if Result.IOffset = Offset then
            Exit;
        end;  
      end;
      CurrentClass := CurrentClass.ClassParent;
    end;    
  end;
  Result := nil;  
end;
Во-первых, мы используем служебные подпрограммы выше для получения ссылки на объект и PIMT смещения. Затем мы проходим по классу и его предкам в поиске InterfaceEntry, имеющей то же смещение, что и PIMT поле. Когда мы нашли совпадение, мы возвращаем указатель на найденную запись. Эта запись содержит и PIMT смещение и IID. Давайте напишем простую функцию-обёртку, читающую IID:
function GetInterfaceIID(const I: IInterface; var IID: TGUID): boolean;
var
  InterfaceEntry: PInterfaceEntry;
begin
  InterfaceEntry := GetInterfaceEntry(I);
  Result := Assigned(InterfaceEntry);
  if Result then
    IID := InterfaceEntry.IID;
end;
Вот - это было очень просто. Итак, теперь у нас есть все функции и весь вспомогательный код - теперь нам нужно только протестировать, что это работает. Вот моё простое тестовое приложение:
program TestInterfaceGUID;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  HVInterfaceGUID in 'HVInterfaceGUID.pas';

type
  IMyInterface = interface
    ['{ABDA7685-DB67-43C1-947F-4B9535142355}']
    procedure Foo;
  end;

  TMyObject = class(TInterfacedObject, IMyInterface)
    procedure Foo;
  end;  
  
procedure TMyObject.Foo;
begin
end;

var
  MyInterface: IMyInterface;
  Unknown: IUnknown;
  Instance: TObject;
  IID: TGUID;
begin
  MyInterface := TMyObject.Create;
  Instance := GetImplementingObject(MyInterface);
  WriteLn(Instance.ClassName);
  if GetInterfaceIID(MyInterface, IID) then
    WriteLn('MyInterface IID = ', GUIDToString(IID));
  Unknown := MyInterface;  
  if GetInterfaceIID(Unknown, IID) then
    WriteLn('Dereived IUnknown IID = ', GUIDToString(IID));
  Unknown := TMyObject.Create;
  if GetInterfaceIID(Unknown, IID) then
    WriteLn('Pure IUnknown IID = ', GUIDToString(IID));
  ReadLn;
end.
Эта программа объявляет интерфейс с методом Foo и класс, его реализующий. Она создаёт экземпляр класса - присваивая его интерфейсной ссылке. Для начала мы тестируем функцию GetImplementingObject и выводим имя реализующего интерфейс класса. Затем мы вызываем GetInterfaceIID три раза и печатаем получающиеся GUID. В первом вызове мы используем нужный интерфейс напрямую - если наш код верен, это должно работать. Во втором вызове мы передаём интерфейсную ссылку в ссылку IUnknown. В зависимости от того, как компилятор реализует присваивание ссылок между совместимыми интерфейсами, это может работать, а может и не работать. Посмотрим. А пока, в третий раз, мы присваиваем ссылке на IUnknown новый экземпляр класса. В этом случае мы ожидаем получить (в смысле вывода на консоль) GUID IUnknown.

Когда мы запускам код, то получаем такой вывод:
TMyObject
MyInterface IID = {ABDA7685-DB67-43C1-947F-4B9535142355}
Derived IUnknown IID = {ABDA7685-DB67-43C1-947F-4B9535142355}
Pure IUnknown IID = {00000000-0000-0000-C000-000000000046}
Кажется, код работает ;) Мы получили ожидаемые результаты от печати имени класса и первого IID. Любопытно (и полезно), что второй "Derived IUnknown IID" возвращает IID исходного интерфейса. И, наконец, не удивительно, что третий IID получился IID IUnknown, определённый Microsoft. Причина, по которой сохранён второй IID, заключается в том, что ссылка IUnknown является простой копией ссылки MyInterface. Вот ассемблерный код, генерируемый при присваивании:
  Unknown := MyInterface;  
  mov eax,$0040a7a4
  mov edx,[MyInterface]
  call @IntfCopy
Этот код вызывает подпрограмму RTL для копирования интерфейсов - System._IntfCopy, которая обрабатывает учёт ссылок источника и назначения. Так что сама ссылка остаётся неизменной - меняются только счётчики ссылок. Вот почему мы получаем желаемый результат с IID IMyInterface вместо IID IUnknown во втором случае.

Если же интерфейс IUnknown присваивается через as, то получаются иные результаты:
  Unknown := MyInterface as IUnknown;  
  if GetInterfaceIID(Unknown, IID) then
    WriteLn('As IUnknown IID = ', GUIDToString(IID));
В этом случае мы получаем такой вывод:
  As IUnknown IID = {00000000-0000-0000-C000-000000000046} 
Мы получили IID IUnknown, а не IMyInterface. Причина в том, что компилятор генерирует as-cast так:
  Unknown := MyInterface as IUnknown;  
  mov eax,$0040a7a4
  mov edx,[MyInterface]
  mov ecx,$00408b0c
  call @IntfCast
Этот вариант кода использует вызов System._IntfCast, чтобы выполнить присваивание и преобразование - и смотря на его код, вызывающий QueryInterface, для выполнения конвертации, мы заключаем, что этот вызов вернёт другую интерфейсную ссылку (помните, поле PIMT) - которую TInterfacedObject добавил для интерфейса IUnknown (aka IInterface). Конечно же, этот интерфейс имеет свой собственный IID, а оригинальный IID, который Randy так хотел получить в своём вопросе, утерян навсегда.

Длинная строка в hex, являющаяся представлением IID, не очень-то "human readable". Некоторые интерфейсы (в частности - COM-интерфейсы) записывают их IID и "human readable" имена в реестр. К примеру, HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Interface\{00000000-0000-0000-C000-000000000046} = IUnknown. Можно просматривать регистрацию интерфейсов в реестре для получения читабельного имени интерфейса по его IID. Эта операция остаётся в качестве упражнения читателям ;)

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.