Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Использование RTTI в приложениях на базе VCL

Тарасенко А.С. 2001
taralex@hotbox.ru

В статье рассматриваются механизм работы и использование механизма динамической идентификации типов (RTTI - runtime type identification) в приложениях, использующих объектную библиотеку ф. Borland VCL - Visual Component Library.

Содержание:

Почему не стоит читать эту статью?

RTTI используется ,как правило, во всех приложениях, созданных компилятором C++. Если компилятору указано не включать в объектный файл RTTI информацию, то не возможна будет динамическая идентификация типов и динамическое приведение типов. Обычно в состав RTTI входит следующая информация: имя типа (для идентификации), указатель на базовый тип (для приведения типов), указатель на конструктор копий. В RTTI поддерживающую VCL входит дополнительная информация, используемая IDE Builder C++, в первую очередь инспектором объектов. Однако структура и содержание этих дополнительных данных не документирована и вероятно может меняться в зависимости от версии Builder. Поэтому использование расширенной RTTI в приложениях возможно только при соблюдении ограничения: исходный код не может переноситься на другие версии Builder C++. Тем не менее, думаю приведенная информация будет интересна программистам в Builder C++, особенно при написании компонент. Все данные, приведенные в этой статье получены для Builder C++ 3.0.

Что можно узнать из RTTI?

Кроме имени типа, и указателя на базовый тип для классов со спецификатором DELPHICLASS в состав RTTI включается информация о всех свойствах, объявленных в секции __published. В том числе: имя свойства, его тип, указатели на методы установки, чтения и предикат хранения, а также значения индекса для методов установки/чтения, обслуживающих несколько свойств. А вот другие члены класса, объявленные в __published - переменные и методы в RTTI не попадают. Формат хранения информации о типе приведен в таблице №1.

Таблица №1

Поле Размер Описание
TypeID 1 байт Идентификатор типа,см табл.2
NameLen 1 байт Длина строки с именем типа
Different - Различается для разных TypeID

Таблица №2

Значение TypeID Описание
0x01 Целые типы short, int
0x02 Символный тип char
0x03 Перечислимые типы
0x04 Вещественные типы float, double, long double
0x05, 0x06 Не выяснено, скорее всего структуры и объединения
0x07 классы 
0x08 указатели на обработчики событий
0х10 строковые классы

Для классовпотомков TObject получить указатель на RTTI информацию можно посредством метода ClassInfo():

void* rtti = p_class-> ClassInfo();

Указатель rtti будет указывать на область памяти, с данными с информацией о типе объекта p_class.

Несколько слов о том, как самостоятельно исследовать информацию о типе. Для этого нужен какой-нибудь отладчик. В данном случае достаточно удобно пользоваться встроенным отладчиком (View|CPU). Получив значение указателя на RTTI класса, его содержимое просматривается в дампе памяти отладчика и имея описание класса (т.е. фактически зная информацию о его типе) можно делать логические заключения о структуре RTTI.

Работа со свойствами

Свойства (property) введены в Builder C++ специально для поддержки классов, описанных на Object Pascal. О том, как описать свойство, установить или получить его значение, написано десятки книг (для начинающих я порекомендовал бы книгу К.Сурков, Д.Сурков, А.Вальвачев "Программирование в среде DELPHI 2.0" Минск 1997). Тем не менее остановимся на некоторых моментах:

1. Доступ к значению свойства может быть напрямую или посредством функции:

__property int ParamA = {read = pаram_a};//доступ

к значению свойства напрямую

__property int ParamB = {read = GetParamB};//доступ

посредством функции метода GetParamB. В первом случае в RTTI будет приведено значение смещения переменной param_a в экземпляре класса, во втором - адрес функции, возвращающей значение свойства

2. Свойства бывают индексируемыми

__property int ParamA[int index] = {read= GetParamA};

К индексированному свойству доступ возможен только через функцию, которая принимает параметр index. Самое интересное - из RTTI нельзя узнать, индексируется свойство или нет.

3. Если несколько свойств обслуживаются одним методом, в описании свойства указывается целый индекс, передаваемый в метод:

__property int ParamA = {read = GetParamA,index = 0};

Значение этого индекса можно узнать из RTTI.

4. Для свойства можно указать, следует ли сохранять в файле формы его значение. Сохраняются значения свойств, отличные от значения по умолчанию, однако это можно запретить.

//значение свойства не сохраняется
__property int ParamA = {write = param_a, stored = false};
//значение свойства сохраняется
__property int ParamB = {write = pram_b, stored = true};
//сохранением свойства управляет
__property int ParamC = {write = pram_c, stored = StoredC);

предикат StoredC. Значение параметра stored можно узнать из RTTI.

5.Свойство может иметь значение по умолчанию (см. п 4)

__property int ParamA = {read = param_a, default = 100};

Отметим, что значение default не устанавливает начальное значение свойства - это работа конструктора.

Значение параметра default можно посмотреть в RTTI. Кстати, хотя везде написано, что параметру default должно присваиваться константное выражение, на самом деле можно присвоить только целое константное выражение из-за того, что как мы увидим далее для значения default отводится только 4 байта, а вещественные константы могут занимать 4,8 и 10 байт.

Получение информации о классе объекта

Как уже говорилось выше, информация об классе находится по адресу, возвращаемому методом ClassInfo. По этому адресу находится следующая информация:

  1. Идентификатор типа, равный для классов 7 (1 байт)
  2. Длина имени класса (1 байт)
  3. Строка с именем класса
  4. Указатель на метод (предположительно конструктор копий) (4 байта)
  5. Указатель на указатель на базовый тип (вот так вот сложно!) (4 байта)
  6. Количество опубликованных свойств, в том числе и в базовых классах (2 байта)
  7. Длина имени модуля, где объявлен класс (1 байт)
  8. Имя модуля, где объявлен класс
  9. Число свойств, опубликованных в самом классе (2 байта)
  10. Список свойств, опубликованных в классе (каждое 26 байт + имя свойства)
07 05 54 4D 65 6D 6F F0
3A 40 00 7C 3A 40 00 35
00 08 53 74 64 43 74 72
6C 73 2C 00 A8 62 41 00
48 00 00 FF 14 7D 41 00
01 00 00 00 00 00 00 80 
00 00 00 00 0A 00 05 41
6C 69 67 6E

Заголовок класса: 07 - идентификатор описания класса, имя класса, указатель на конструктор копий, указатель на базовый класс, количество опубликованных свойств, имя модуля, количество опубликованных в класск свойств. Перечисления свойств: Смещение переменной - прямой доступ для чтения, адрес метода установки значения, stored = true, index = -1 (нет индекса), default = 0, порядковый номер свойства, имя свойства (Align)

Формат RTTI для опубликованных свойств

Из выше приведенного дампа памяти можно уяснить себе, в каком формате храниться описание опубликованного свойства. Тем не менее приведем этот формат в явном виде:

  1. Указатель на указатель с информацией о типе свойства
  2. Доступ к значению свойства (чтение): адрес метода или смещение в объекта. Если указано смещение от начала класса, то в самом старшем байте записано 0xFF - чтобы отличить от метода.
  3. Доступ к значению свойства (запись): адрес метода или смещение в объекте
  4. Значение параметра stored: 0, 1, или адрес предиката.
  5. Значение параметра index: 4 байтное целое
  6. Значение параметра default: 4 байтное целое
  7. Порядковый номер свойства: 2 байтное целое, из чего ясно, что больше чем 65535 свойств не опубликовать, ди наверное и не надо.
  8. Имя свойства.

Для того чтобы прочитать значение свойства следует выполнить следующие действия:

  1. Получить RTTI для класса, содержащего желаемое свойство
  2. Получить указатель на описание желаемого свойства
  3. Выяснить его тип
  4. Если чтение осуществляется напрямую, вычислить адрес переменной - указатель на объект + смещение
  5. Если чтение выполняется через метод, следует привести указатель к указателю на метод и вызвать его

Установка значения свойства выполняется аналогично.

Как видно из выше сказанного, установка и чтение свойства невозможны без знания типа свойства. Рассмотрим кратко возможные форматы описания типов:

Формат целых типов:

  1. Идентификатор типа 0х01 (1 байт)
  2. Имя типа
  3. Варианты (1 байт) 2 - short, 3 - unsigned short, 4 - int, long

Формат символьного типа

  1. Идентификатор типа 0х02 (1 байт)
  2. Имя типа
  3. Варианты (1 байт) 0 - char, 1- unsigned char

Формат перечислений

  1. Идентификатор типа 0х03 (1 байт)
  2. Имя типа
  3. 9 байт
  4. Указатель либо на начало собственного описания, либо на указатель на описания типа, где фактически и описано содержимое перечисления (такая ситуация характерна для многих типов, пришедших из Pascalи видимо связана с особенностями компилятора этого языка) (4 байта)
  5. Если указатель (4) содержит адрес начала собственного описания, то вслед за ним идет список символьных констант, входящих в перечисление).
  6. Формат данных в перечислении 1,2 или 4 байта (1 байт)

Примечание: Если в описании перечисления указаны значения для символьных констант, например:

enum TSeason {winter = 100, spring = 200, summer = 300, autumn = 400};

то в RTTI такой тип трактуется как целый и вся символьная информация не включается в RTTI.

Формат вещественного типа

  1. Идентификатор типа 0х04 (1 байт)
  2. Имя типа
  3. Варианты (1 байт) 0 - float, 1 - double, 2 - long double

К вещественным типам относятся также типы для представления времени TDateTime (он объявлен тождественным типу double).

Формат классов

Формат описания классов рассмотрен выше

Формат обработчиков событий

  1. Идентификатор типа 0х08 (1 байт)
  2. Имя типа
  3. Количество параметров у метода-обработчика (1 байт)
  4. Описание параметров в формате: Имя параметра, Имя типа параметра (порядок, свойственный для Pascal).

Формат строковых типов

  1. Идентификатор 0х10 (1 байт)
  2. Имя типа

В Pascal для хранения строк существует специальный класс - String. В "чистом" С нет специальных типов для представления строковых данных, с ними работают как с массивами байтовых (или двухбайтовых) целых чисел. Для поддержки строк в формате String в Builder C++ объявлен специальный класс AnsiString.

Примеры доступа к опубликованным свойствам используя RTTI.

Хотел бы заметить, что в большинстве случаев, естественно, нет нужды организовывать доступ к свойством используя RTTI. Лишь в одном случае такой подход оправдан - когда на этапе компиляции программы неизвестен тип объектов, с которыми будет работать программа. Ситуация чем-то напоминающая использование OLE Автоматизации и диспетчерских интерфейсов. Кстати, использование упомянутой технологии в этом случае предпочтительно, но если объекты не предоставляют информацию о своем типе, и известно, что их код был скомпилирован в Builder C++ при использовании VCL, то работу с ними можно автоматизировать предлагаемым способом.

Работу со свойствами рассмотри на примере доступа к свойству целого типа:

//описание типа функции чтения свойства
typedef int __fastcall(* FINTPROPREAD)(void*);

Следует обратить внимание на две вещи: 1) соглашение о вызове функции должно совпадать с описанным в классе для доступа к значению функции; 2) поскольку метод- член класса неявно первым параметром получает указатель на экземпляр класса его вызвавший, следует описать этот параметр

int ReadIntProperty(void* p_obj, void* p_read) 
{ 
   //указатель на объект 
   char* p = (char*)p_obj; 
   //адрес памяти, где указан способ чтения свойства (см выше) 
   int* p4 = (int*)p_read; 
   // выяснение, как осуществляется доступ к значению свойства - 
   // напрямую или при помощи функции 
   if (((int)p4 & 0xFF000000) == 0xFF000000) 
   { //чтение напрямую 
        //вычисление адреса памяти, где хранится значение свойства 
        p += ((int)p4 & 0x00FFFFFF);
        p4 = (int*)p; 
        return *p4; 
    } 
    else 
   { //чтение через метод 
         FINTPROPREAD reader = (FINTPROPREAD)p4; 
          return reader(p_obj); 
    } 
} 

Установка значения свойства производится аналогично:

typedef void __fastcall(* FINTPROPSET)(void*, int); 

void SetIntProperty(void* p_obj, void* p_write, int value) 
{ 
     char* p = (char*)p_obj; 
     int* p4 = (int*)p_write; 
     if (((int)p4 & 0xFF000000) == 0xFF000000) 
    { 
         p += ((int)p4 & 0x00FFFFFF); 
         p4 = (int*)p; 
        *p4 = value; 
     } 
     else 
    { 
         FINTPROPSET writer = (FINTPROPSET)p4; 
        writer(p_obj, value); 
     } 
} 

Доступ к свойствам других типов осуществляется аналогично.

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

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 

Комментарии

1.
Аноним
Мне нравитсяМне не нравится
18 ноября 2005, 16:53:48
хотел разобраться - нихрена не ясно а можно как то по-проще объяснить либо представить полноценую прогу!!! Хотите то донести серьёзные вещи а получается что эта статейка для профессоров-кибернетиков из 2030года
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог