Недавно 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" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.