Чтобы поддерживать базовые механизмы SOAP архитектуры, Delphi добавила поддержку расширенной RTTI информации для интерфейсов начиная с версии 7. Как мы видели в предыдущей статье, все интерфейсы поддерживают базовую информацию RTTI: имя интерфейса, его GUID, имя модуля с объявлением, родительский интерфейс и число методов.
Чтобы добавить к интерфейсу расширенную RTTI информацию, его нужно собрать в режиме
{$M+}/{$TYPINFO ON} или {$METHODINFO ON}. Альтернативно, вы можете просто унаследовать свой интерфейс от IInvokable (определённого в модуле System с $M+). Это расширит генерируемую RTTI для интерфейса информацией по сигнатуре каждого метода.Код поддержки как клиентской, так и серверной стороны SOAP в Delphi используют эту расширенную RTTI информацию. Некоторые из самых базовых подпрограмм можно найти в модуле
IntfInfo (исходный код которого доступен в Delphi 2005 и выше, но отсутствует в предыдущих версиях Delphi) - к примеру, посмотрите на подпрограммы FillMethodArray и GetIntfMetaData.Есть также код генерации WSDL (Web Service Description Language - язык описания web-сервисов) по списку зарегистрированных интерфейсов в web сервисе (см.
WSDLPub.pas), а также код динамической генерации интерфейсов (таблицы методов интерфейса - interface method table, IVT) по WSDL описанию интерфейса с методами-заглушками, которые вызывают TRIO.Generic. Эти методы-заглушки ответственны за упаковку параметров вызова на клиентской стороне в XML-форматированное SOAP сообщение, отправку его для выполнения серверу, ожидание ответа, декодирования возвращённого SOAP ответа (снова XML) и обновление out и var параметров, включая Result (в методе TOPToSoapDomConvert.ProcessSuccess из OPToSOAPDomConv). Весьма "неслабый" код! Заметьте, что TRIO не поддерживает соглашение вызова register – рекомендуется использовать stdcall.Это была некоторая вводная информация о том, как используется расширенная RTTI информация для интерфейсов и где можно найти код, работающий с ней. Хотя в модуле
IntfInfo есть низкоуровневые подпрограммы доступа (экспортируемые, если определена директива условной компиляции DEVELOPERS), мы бы хотели запачкать свои руки и реализовать их самостоятельно.Как обычно, RTTI структуры для методов интерфейсов содержат множество упакованных
ShortString – что означает невозможность написать прямые объявления подобных структур данных на Pascal. Копаясь в расширенной RTTI интерфейсов Borland-а, пошагово проходясь по коду SOAP в отладчике, дампя raw данные RTTI и логически выводя (динамические) размеры полей, мне удалось воссоздать исходные структуры и написать псевдо-Pascal объявления для проецирования на RTTI структуры. К примеру, вот ASCII-дамп, который я сделал вручную при исследовании одного интерфейса:
{ MethodCount:1; HasMethodRTTI:1;
Test:(
Name: #3, 'F', 'o', 'o',
Kind: #0,
CallConv: #0,
ParamCount: #3,
Flags: #8,
ParamName: #4, 'S', 'e', 'l', 'f',
TypeName: #14, 'I', 'M', 'y', 'M', 'P', 'I', 'n', 't', 'e', 'r', 'f', 'a', 'c', 'e',
TypeInfo: #24, 'T', 'O', #0,
Flags: #0,
Name: #1, 'A',
TypeName: #7, 'I', 'n', 't', 'e', 'g', 'e', 'r', }
На внешнем уровне мы начинаем с записи, которая следует за полем IntfUnit блока tkInterface в вариантной части записи TTypeData из модуля TypInfo:
PExtraInterfaceData = ^TExtraInterfaceData;
TExtraInterfaceData = packed record
MethodCount: Word; // число методов
HasMethodRTTI: Word; // $FFFF, если нет RTTI для методов,
// и снова число методов, если есть RTTI
Methods: packed array[0..High(Word) - 1] of TInterfaceMethodRTTI;
end;
Для всех интерфейсов поле MethodCount содержит число методов интерфейса. Для "обычных" интерфейсов (компилируемых с $METHODINFO OFF) поле HasMethodRTTI будет равно $FFFF - указывая, что для интерфейса нет дополнительной RTTI. Расширенная RTTI интерфейсов (для интерфейсов, собранных с $METHODINFO ON) поле HasMethodRTTI будет равно полю MethodCount, а за ним будет упакованный массив информации о каждом методе:
PInterfaceMethodRTTI = ^TInterfaceMethodRTTI;
TInterfaceMethodRTTI = packed record
Name: TPackedShortString;
Kind: TMethodKind; // mkProcedure или mkFunction
CallConv: TCallConv;
ParamCount: Byte; // включая Self
Parameters: packed array[0..High(Byte) - 1] of TInterfaceParameterRTTI;
case TMethodKind of
mkFunction:
(Result: TInterfaceResultRTTI);
end;
RTTI одного метода интерфейса содержит имя метода, вид метода (procedure или function), соглашение вызова, число параметров (включая неявный параметр Self) и упакованный массив информации о каждом параметре. Если метод является функцией, то после массива есть ещё одно дополнительное поле о результате функции.
PInterfaceParameterRTTI = ^TInterfaceParameterRTTI;
TInterfaceParameterRTTI = packed record
Flags: TParamFlags;
ParamName: TPackedShortString;
TypeName: TPackedShortString;
TypeInfo: PPTypeInfo;
end;
Объявление записи для параметров содержит флаги (указывая на вид параметра - var, const, out или обычный, параметр-массив или ссылка), имя параметра, имя типа параметра и указатель на RTTI информацию о типе (если тип имеет RTTI).Наконец, у нас есть запись для результата функции:
PInterfaceResultRTTI = ^TInterfaceResultRTTI;
TInterfaceResultRTTI = packed record
Name: TPackedShortString;
TypeInfo: PPTypeInfo;
end;
И снова у нас есть имя типа и указатель на его RTTI.Запись
TExtraInterfaceData выше показывает приблизительную раскладку генерируемой компилятором RTTI информации в памяти. Для внешнего кода нам бы хотелось трансформировать это во что-то более удобное для использования. Заметьте, что это будет очень похоже на то, что мы делали с published методами. И методы интерфейсов и published методы имеют сигнатуры с информацией о параметрах и возвращаемом типе. Так что я решил переделать связанные с сигнатурами определения из модуля HVPublishedMethodParams в отдельный модуль HVMethodSignature:
unit HVMethodSignature;
interface
uses
Classes, SysUtils, TypInfo, HVVMT;
type
TCallConv = (ccReg, ccCdecl, ccPascal, ccStdCall, ccSafeCall);
PMethodParam = ^TMethodParam;
TMethodParam = record
Flags: TParamFlags;
ParamName: PShortString;
TypeName: PShortString;
TypeInfo: PTypeInfo;
end;
TMethodParamList = array of TMethodParam;
PMethodSignature = ^TMethodSignature;
TMethodSignature = record
Name: PShortString;
MethodKind: TMethodKind;
CallConv: TCallConv;
ParamCount: Byte;
Parameters: TMethodParamList;
ResultTypeName: PShortString;
ResultTypeInfo: PTypeInfo;
end;
function MethodKindString(MethodKind: TMethodKind): String;
function MethodParamString(const MethodParam: TMethodParam; ExcoticFlags: Boolean = False): String;
function MethodParametesString(const MethodSignature: TMethodSignature; SkipSelf: Boolean = True): String;
function MethodSignatureToString(const Name: String; const MethodSignature: TMethodSignature): String; overload;
function MethodSignatureToString(const MethodSignature: TMethodSignature): String; overload;
implementation
function MethodKindString(MethodKind: TMethodKind): String;
begin
case MethodKind of
mkSafeProcedure,
mkProcedure : Result := 'procedure';
mkSafeFunction,
mkFunction : Result := 'function';
mkConstructor : Result := 'constructor';
mkDestructor : Result := 'destructor';
mkClassProcedure: Result := 'class procedure';
mkClassFunction : Result := 'class function';
end;
end;
function MethodParamString(const MethodParam: TMethodParam; ExcoticFlags: Boolean = False): String;
begin
if pfVar in MethodParam.Flags then Result := 'var '
else if pfConst in MethodParam.Flags then Result := 'const '
else if pfOut in MethodParam.Flags then Result := 'out '
else Result := '';
if ExcoticFlags then
begin
if pfAddress in MethodParam.Flags then
Result := '{addr} ' + Result;
if pfReference in MethodParam.Flags then
Result := '{ref} ' + Result;
end;
Result := Result + MethodParam.ParamName^ + ': ';
if pfArray in MethodParam.Flags then
Result := Result + 'array of ';
Result := Result + MethodParam.TypeName^;
if Assigned(MethodParam.TypeInfo) then
Result := Result + ' {' + MethodParam.TypeInfo.Name + '} ';
end;
function MethodParametesString(const MethodSignature: TMethodSignature; SkipSelf: Boolean = True): String;
var
i: integer;
MethodParam: PMethodParam;
begin
Result := '';
for i := 0 to MethodSignature.ParamCount - 1 do
begin
MethodParam := @MethodSignature.Parameters[i];
// Пропускаем неявный параметр Self для методов классов и интерфейсов
// Заметьте, что Self не включается в типы событий
if SkipSelf and
(i = 0) and
(MethodParam.Flags = [pfAddress]) and
(MethodParam.ParamName^ = 'Self') and
(MethodParam.TypeInfo.Kind in [tkInterface, tkClass]) then
Continue;
Result := Result + MethodParamString(MethodParam^);
if i < MethodSignature.ParamCount - 1 then
Result := Result + '; ';
end;
end;
function CallingConventionToString(CallConv: TCallConv): String;
begin
case CallConv of
ccReg : Result := 'register';
ccCdecl : Result := 'cdecl';
ccPascal : Result := 'pascal';
ccStdCall : Result := 'stdcall';
ccSafeCall: Result := 'safecall';
else Result := 'TCallConv('+IntToStr(Ord(CallConv))+')';
end;
end;
function MethodSignatureToString(const Name: String; const MethodSignature: TMethodSignature): String; overload;
begin
Result := Format('%s %s(%s)',
[MethodKindString(MethodSignature.MethodKind),
Name,
MethodParametesString(MethodSignature)]);
if MethodSignature.MethodKind = mkFunction then
begin
Result := Result + ': ' + MethodSignature.ResultTypeName^;
if Assigned(MethodSignature.ResultTypeInfo) then
Result := Result + ' {' + MethodSignature.ResultTypeInfo.Name + '} ';
end;
Result := Result + ';' ;
if MethodSignature.CallConv <> ccReg then
Result := Result + ' ' +
CallingConventionToString(MethodSignature.CallConv) + ';';
end;
function MethodSignatureToString(const MethodSignature: TMethodSignature): String; overload;
begin
Result := MethodSignatureToString(MethodSignature.Name^,
MethodSignature);
end;
end.
Этот код является простым расширением кода, который мы видели в статье про хак получения параметров published методов через их ассоциацию с событиями. Расширенная RTTI интерфейсов содержит более детализированную информацию, чем сигнатура события, так что мы добавили обработку PTypeInfo параметров и возвращаемых типов, а также соглашения вызова и имени метода. Эти подпрограммы являются достаточно прямолинейной трактовкой информации из структур RTTI в строковое представление. Мы говорили про эти моменты в предыдущих статьях.Единственный пропущенный кусок - это код из середины цепочки трансляции, который переводит внутреннюю raw информацию RTTI в наши более удобные структуры:
unit HVInterfaceMethods;
interface
uses
TypInfo, HVMethodSignature;
type
// Просто-используемые записи фиксированного размера
PInterfaceInfo = ^TInterfaceInfo;
TInterfaceInfo = record
UnitName: String;
Name: String;
Flags: TIntfFlags;
ParentInterface: PTypeInfo;
Guid: TGUID;
MethodCount: Word;
HasMethodRTTI: Boolean;
Methods: array of TMethodSignature;
end;
procedure GetInterfaceInfo(InterfaceTypeInfo: PTypeInfo; var InterfaceInfo: TInterfaceInfo);
implementation
type
// … сюда вставляем определение TExtraInterfaceData …
function Skip(Value: PShortString): Pointer; overload;
begin
Result := Value;
Inc(PChar(Result), SizeOf(Value^[0]) + Length(Value^));
end;
function Skip(Value: PPackedShortString; var NextField{: Pointer}): PShortString; overload;
begin
Result := PShortString(Value);
Inc(PChar(NextField), SizeOf(Char) + Length(Result^) - SizeOf(TPackedShortString));
end;
function Skip(CurrField: Pointer; FieldSize: Integer): Pointer; overload;
begin
Result := PChar(Currfield) + FieldSize;
end;
function Dereference(P: PPTypeInfo): PTypeInfo;
begin
if Assigned(P) then
Result := P^
else
Result := nil;
end;
procedure GetInterfaceInfo(InterfaceTypeInfo: PTypeInfo; var InterfaceInfo: TInterfaceInfo);
// Конвертирует из raw структур RTTI в наши user-friendly структуры
var
TypeData: PTypeData;
ExtraData: PExtraInterfaceData;
i, j: integer;
MethodInfo: PMethodSignature;
MethodRTTI: PInterfaceMethodRTTI;
ParameterInfo: PMethodParam;
ParameterRTTI: PInterfaceParameterRTTI;
InterfaceResultRTTI: PInterfaceResultRTTI;
begin
Assert(Assigned(InterfaceTypeInfo));
Assert(InterfaceTypeInfo.Kind = tkInterface);
TypeData := GetTypeData(InterfaceTypeInfo);
ExtraData := Skip(@TypeData.IntfUnit);
// Интерфейс
InterfaceInfo.UnitName := TypeData.IntfUnit;
InterfaceInfo.Name := InterfaceTypeInfo.Name;
InterfaceInfo.Flags := TypeData.IntfFlags;
InterfaceInfo.ParentInterface := Dereference(TypeData.IntfParent);
InterfaceInfo.Guid := TypeData.Guid;
InterfaceInfo.MethodCount := ExtraData.MethodCount;
InterfaceInfo.HasMethodRTTI := (ExtraData.HasMethodRTTI = ExtraData.MethodCount);
if InterfaceInfo.HasMethodRTTI then
SetLength(InterfaceInfo.Methods, InterfaceInfo.MethodCount)
else
SetLength(InterfaceInfo.Methods, 0);
// Методы
MethodRTTI := @ExtraData.Methods[0];
for i := Low(InterfaceInfo.Methods) to High(InterfaceInfo.Methods) do
begin
MethodInfo := @InterfaceInfo.Methods[i];
MethodInfo.Name := Skip(@MethodRTTI.Name, MethodRTTI);
MethodInfo.MethodKind := MethodRTTI.Kind;
MethodInfo.CallConv := MethodRTTI.CallConv;
MethodInfo.ParamCount := MethodRTTI.ParamCount;
SetLength(MethodInfo.Parameters, MethodInfo.ParamCount);
// Параметры
ParameterRTTI := @MethodRTTI.Parameters;
for j := Low(MethodInfo.Parameters) to High(MethodInfo.Parameters) do
begin
ParameterInfo := @MethodInfo.Parameters[j];
ParameterInfo.Flags := ParameterRTTI.Flags;
ParameterInfo.ParamName := Skip(@ParameterRTTI.ParamName, ParameterRTTI);
ParameterInfo.TypeName := Skip(@ParameterRTTI.TypeName, ParameterRTTI);
ParameterInfo.TypeInfo := Dereference(ParameterRTTI.TypeInfo);
ParameterRTTI := Skip(@ParameterRTTI.TypeInfo, SizeOf(ParameterRTTI.TypeInfo));
end;
// Результат функции
if MethodInfo.MethodKind = mkFunction then
begin
InterfaceResultRTTI := Pointer(ParameterRTTI);
MethodInfo.ResultTypeName := Skip(@InterfaceResultRTTI.Name, InterfaceResultRTTI);
MethodInfo.ResultTypeInfo := Dereference(InterfaceResultRTTI.TypeInfo);
MethodRTTI := Skip(@InterfaceResultRTTI.TypeInfo, SizeOf(InterfaceResultRTTI.TypeInfo));
end
else
MethodRTTI := Pointer(ParameterRTTI);
end;
end;
end.
Код получился немножко хитрым и сложным для чтения из-за необходимости пропускать строковые поля переменной длины. Низкоуровневый код модуля IntfInfo использует иной подход - модель с ReadString, ReadByte, ReadWord, ReadLong. Но мне нравится само-документирующий аспект псевдо-записей, и я хотел использовать их и в коде доступа. Заметьте, что в некоторых точках, только одно поле записи находится на своём месте и может быть прочитано.Имея такую основательную базу кода, теперь мы можем написать небольшую подпрограмму дампа интерфейса, которая будет печатать псевдо-определение интерфейса:
procedure DumpInterface(InterfaceTypeInfo: PTypeInfo);
var
InterfaceInfo: TInterfaceInfo;
i: integer;
begin
GetInterfaceInfo(InterfaceTypeInfo, InterfaceInfo);
WriteLn('unit ', InterfaceInfo.UnitName, ';');
WriteLn('type');
Write(' ', InterfaceInfo.Name, ' = ');
if not (ifDispInterface in InterfaceInfo.Flags) then
begin
Write('interface');
if Assigned(InterfaceInfo.ParentInterface) then
Write(' (', InterfaceInfo.ParentInterface.Name, ')');
WriteLn;
end
else
WriteLn('dispinterface');
if ifHasGuid in InterfaceInfo.Flags then
WriteLn(' [''', GuidToString(InterfaceInfo.Guid), ''']');
if InterfaceInfo.HasMethodRTTI then
for i := Low(InterfaceInfo.Methods) to High(InterfaceInfo.Methods) do
WriteLn(' ', MethodSignatureToString(InterfaceInfo.Methods[i]))
else
for i := 1 to InterfaceInfo.MethodCount do
WriteLn(' procedure UnknownName',i,';');
WriteLn(' end;');
WriteLn;
end;
И, наконец, нам нужен код для теста:
type
TNumber = Integer;
TNewNumber = type Integer;
TIntegerArray = array of Integer;
TNormalClass = class
end;
TPersistentClass = class(TPersistent)
end;
TSetOfByte = set of byte;
TEnum = (enOne, enTwo, enThree);
type
{.$M+} {.$TYPEINFO ON}
// В отношении RTTI интерфейсов, METHODINFO имеет тот же эффект, что и $M/$TYPEINFO
{$METHODINFO ON}
IMyMPInterface = interface
['{AA503475-0187-4108-8E27-41475F4EF818}']
procedure TestRegister(A: Integer; var B: String); register;
procedure TestStdCall(LongParaName: TObject; const B: String; var C: Integer; out D: Byte); stdcall;
procedure TestSafeCall(out R: Integer); safecall;
function Number: TNumber; cdecl;
function NewNumber: TNewNumber; cdecl;
function AsString: String; pascal;
function AsString2: String; safecall;
// Поддерживаемые возвращаемые типы
procedure A2(const A: TIntegerArray);
procedure OkParam1(Value: TSetOfByte);
procedure OkParam2(Value: TSetOfByte);
procedure OkParam3(Value: Variant);
procedure OkParam4(Value: TNormalClass);
function OkReturn1: ShortString;
function OkReturn2: TObject;
function OkReturn3: IInterface;
function OkReturn4: TSetOfByte;
function OkReturn5: TNormalClass;
function OkReturn6: TEnum;
function OkReturn7: TClass;
function OkReturn8: Pointer;
function OkReturn9: PChar;
function OkReturn10: TIntegerArray;
end;
{$M-}
{$WARN SYMBOL_PLATFORM OFF}
procedure Test;
begin
DumpInterface(TypeInfo(IMyMPInterface));
end;
begin
try
Test;
except
on E: Exception do
WriteLn(E.Message);
end;
ReadLn;
end.
И получаемый вывод:
unit TestExtendedInterfaceRTTI;
type
IMyMPInterface = interface (IInterface)
['{AA503475-0187-4108-8E27-41475F4EF818}']
procedure TestRegister(A: Integer {Integer} ; var B: String {String} );
procedure TestStdCall(LongParaName: TObject {TObject} ; const B: String {String} ; var C: Integer {Integer} ; out D: Byte {Byte} ); stdcall;
procedure TestSafeCall(out R: Integer {Integer} ); safecall;
function Number(): Integer {Integer} ; cdecl;
function NewNumber(): TNewNumber {TNewNumber} ; cdecl;
function AsString(): String {String} ; pascal;
function AsString2(): String {String} ; safecall;
procedure A2(const A: TIntegerArray {TIntegerArray} );
procedure OkParam1(Value: TSetOfByte {TSetOfByte} );
procedure OkParam2(Value: TSetOfByte {TSetOfByte} );
procedure OkParam3(Value: Variant {Variant} );
procedure OkParam4(Value: TNormalClass {TNormalClass} );
function OkReturn1(): ShortString {ShortString} ;
function OkReturn2(): TObject {TObject} ;
function OkReturn3(): IInterface {IInterface} ;
function OkReturn4(): TSetOfByte {TSetOfByte} ;
function OkReturn5(): TNormalClass {TNormalClass} ;
function OkReturn6(): TEnum {TEnum} ;
function OkReturn7(): TClass;
function OkReturn8(): Pointer;
function OkReturn9(): PAnsiChar;
function OkReturn10(): TIntegerArray {TIntegerArray} ;
end;
Код, включающий вспомогательные модуля и тестовый код этого примера, а также других моих статей по RTTI, можно найти на CodeCentral здесь. Тестовый код также содержит некоторый дополнительный код по тестированию поддержки дополнительных типов параметров. Мои эксперименты показали, что следующие типы параметров не поддерживаются в расширенном RTTI методов интерфейсов (прим.пер.: Hallvard проверял в Delphi 7-Delphi 2006):
- Все типы указатели (прим.пер.: поддерживается Delphi XE)
- Открытые массивы (
array of Type), динамические массивы - OK (прим.пер.: поддерживается Delphi XE) - Классовые ссылки (вроде
TClass) (прим.пер.: поддерживается Delphi XE) - Записи (вроде
TRect) (прим.пер.: поддерживается Delphi XE) - Нетипизированные
varиout
Из-за моего летнего отдыха и общей вялости эта статья была представлена вам немного позже, чем планировалось изначально. Надеюсь, я смогу довести до конца следующую статью о расширенной RTTI информации для public и published методов класса в разумные сроки.
P.S. Директива
$METHODINFO впервые появилась в Delphi 7, а не Delphi 6.
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.