Добро пожаловать!

Я Дож, программирование — моё хобби. По мере того, как я осваиваю что-то новое, стараюсь об этом написать пост.
На текущий момент в блоге затронуты следующие темы: vim, free pascal, lisp, forth, m4
Занимаюсь разработкой своего языка под названием DEmbro, подбробней: wiki и svn
Для постов, не связанных с программированием, у меня есть отдельное жж.

апреля 15, 2010

RTTI

Тут я расскажу про некоторые возможности RTTI (run-time type information, информация о типах во время выполнения), которыми сам активно пользуюсь.

Самое простое и всем хорошо известное - это оператор is для проверки принадлежности класса определенному типа, и функция ClassName, возвращающая строку с именем класса.

Чуть по-сложнее - метаклассы:


type
  TMyClass = class
    constructor Create; virtual;
    class function GetValue: Integer;
  end;

  constructor TMyClass.Create;
  begin
  end;

  class function TMyClass.GetValue: Integer;
  begin
    Result := 5050;
  end;

type
  TMyClass2 = class(TMyClass)
  end;

  CMyClass = class of TMyClass;

var
  MyClass: CMyClass;

begin
  // присваиваем наш базовый класс
  MyClass := TMyClass;
  // можем обращаться ко всем классовым функциям
  Writeln(MyClass.GetValue);
  // можем создать экземпляр класса TMyClass
  Writeln(MyClass.Create.ClassName);
  // можем присвоить наследника
  MyClass := TMyClass2;
  // т.к. конструктор сделан виртуальным, следующий вызов приведет к созданию
  // экземпляра TMyClass2
  Writeln(MyClass.Create.ClassName);
end.


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

В классах помимо секций private, protected и public есть еще и четвертая - published. Все, что в ней содержится (методы и свойства), при компиляции сохраняется вместе с названиями. (Модуль для этого нужно компилировать с ключем $M+, иначе published не будет отличаться от public.) Для доступа к этой информации предусмотрено два метода (взято из док-ции на fpc):


public function TObject.MethodAddress(const name: shortstring): pointer;
public function TObject.MethodName(address: pointer): shortstring;


Указатель, который возвращает первая функция, нельзя сразу использовать, нужно сперва заполнить им структуру TMethod:


{$M+}
type
  TMyClass = class
   published
    function GetValue: Integer;
  end;

  TGetValue = function (): Integer of object;

function TMyClass.GetValue: Integer;
begin
  Result := 5050;
end;

var
  I: Integer;
  Obj: TMyClass;
  Method: TMethod;

begin
  Obj := TMyClass.Create;
  Method.Code := Obj.MethodAddress('GetValue');
  Method.Data := Obj;
  I := TGetValue(Method);
  Writeln(I);
  Obj.Free;
end.


Этих двух функций мне оказалось недостаточно. Представим себе, что нужно прочитать xml-файл и на каждый тэг мы прописали метод чтения, и хочется, чтобы эти методы регистрировались автоматически. Для этого нужно как-то извлечь массив со всеми published-методами и их названиями. После изучения модуля System, я написал следующий код


type
TMethodName = record
  Name: TString;
  Method: TMethod;
end;
TArrayOfMethodName = array of TMethodName;

{$IFDEF FLAG_FPC}
  // Версия для Free Pascal (2.2.4)
  // Отличается от Delphi только форматом хранения RTTI
  function GetMethodArray(Obj: TObject): TArrayOfMethodName;
    type
      tmethodnamerec = packed record
        name : pshortstring;
        addr : pointer;
      end;
      tmethodnametable = packed record
        count : Cardinal;
        entries : packed array[0..0] of tmethodnamerec;
      end;
      pmethodnametable =  ^tmethodnametable;
      var
        I: Integer;
        Table: PMethodNameTable;
  begin
    Table := pmethodnametable((Pointer(Obj.ClassType) + vmtMethodTable)^);
    SetLength(Result, Table^.Count);
    for I := 0 to High(Result) do begin
      Result[I].Name := Table^.entries[I].name^;
      Result[I].Method.Code := Table^.entries[I].addr;
      Result[I].Method.Data := Obj;
    end;
  end;
{$ELSE}
  // Версия для Delphi (2009)
  function GetMethodArray(Obj: TObject): TArrayOfMethodName;
    type
      TMethodtableEntry = packed Record
        len: Word;
        addr: Pointer;
        name: ShortString;
      end;
    var
      pp: ^Pointer;
      pMethodTable: Pointer;
      pMethodEntry: ^TMethodTableEntry;
      i, numEntries: Word;
  begin
    pp := Pointer(Integer(Obj.ClassType) + vmtMethodtable);
    pMethodTable := pp^;
    if pMethodTable = nil then begin
      SetLength(Result, 0);
      Exit;
    end;
    SetLength(Result, PWord(pMethodTable)^);
    pMethodEntry := Pointer(Integer( pMethodTable ) + 2);
    for I := 0 to High(Result) do begin
      Result[I].Name := pMethodEntry.name;
      Result[I].Method.Code := pMethodEntry.addr;
      Result[I].Method.Data := Obj;
      pMethodEntry := Pointer(Integer( pMethodEntry ) + pMethodEntry^.len);
    end;
  end;
{$ENDIF}


Функция GetMethodArray возвращает список из уже почти готовых для вызова методов и их названий. После этого можно, например, найти все те, которые содержат в начале слово XML и использовать их для обработки xml-файла.

TString - это псевдокод на обычную ascii-строку (не unicode).


Читать дальше......

апреля 11, 2010

Подписка и события (часть 1)

Недавно обдумывал систему сообщений в двух проектах, а именно как бы ее удобно реализовать возможностями паскаля. О том, к чему пришел, буду писать в цикле статей.

Зачем оно

Нужно это для отвязки кода. Если везде в программе напрямую вызывают методы других объектов, то это приводит к тому, что все ссылаются друг на друга. Это опасно тем, что любой может что-то не так изменять, от этого нужно для защиты что-то еще изменить - и очень быстро контроль за кодом теряется.

Все, наверно, сталкивались с чем-то вроде

Button.OnClick := MyClickProc;


Это один из простейших способов отвязать код того, что должна делать кнопка, от кода самой кнопки (рисования, обработки мыши и т.д.).

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

Итак, задача: придумать удобную, изящную и безопасную реализацию на паскале системы событий, решающую задачи в предыдущем абзаце.

Архитектура

Событие - это набор данных, описывающих то, что произошло. Подписчик - тот, кто зарегистрировался на получение определенного события.

Есть два архитектурных направления:

1) Глобальное. Есть глобальный класс, в котором зарегистрированы все подписчики и события программы. При отправке сообщения он фильтрует подписчиков, выбирая тех, кому может быть интересно это событие. В операционных системах используется глобальная система сообщений.
2) Локальное. На каждое событие в программе создается отдельный экземпляр специального класса, хранящий всех подписчиков на это событие. При этом этот класс никак не зависим от других, и не обращается ни к чему глобальному.

Сейчас я не буду говорить о достоинствах и недостатках этих методов - это я сделаю после того, как расскажу про свою реализацию. Но есть причины, по которым мне симпатизирует больше локальный подход. Поэтому дальше будет рассмотрение только его.

Проектируем

Ясно, что ни класс, который шлет сообщение, ни класс, который обрабатывает сообщение, не должны быть наследниками каких-то специальных классов-высылателей и классов-подписчиков. Ибо тогда может получиться, что чуть ли не каждый базовый класс в программе должен наследоваться от них, что весьма убого.

Таким образом, мы приходим к тому, что как минимум один класс должен жить своей жизнью - тот, кто хранит подписчиков и шлет им события. Назовем его TNotifier.

До сих пор под событием понималась некоторая абстракция. Что же может быть событием в паскале?
1) Наследники специального базового класса TEvent. К примеру, можно написать TEventButtonClick и задать в нем поля Name, X, Y, Button, в которых передавать параметры. Тогда все методы-обработчики нажатия на мышь будут иметь вид

procedure OnMyClick(Ev: TEventButtonClick);

2) Набор параметров функции. Например, в выражении

procedure OnMyClick(const Name: String; X, Y, Button: Integer);

событие описывают параметры функции.

Разница этих двух методов лишь в том, как объявляется описание события. И первый способ мне не нравится: для описания класса потребуется описать private-поля, public-свойства (чтобы никто не изменил поля и ничего не испортил), и реализовать конструктор. Это гораздо больше, чем

type
  TButtonClickHandler = procedure (
                              const Name: String;
                              X, Y, Button: Integer
                            ) of object;

а самое неприятное - содержит в себе много дублирований.

Итак, после всего этого, мы приходим к следующему варианту (псевдокод):


type
  // так описываем тип нотифера
  TClickNotifier: specialize TNotifier<procedure (
                                                 const Name: String;
                                                 Button, X, Y: Integer
                                               ) of object>;
 
TButton = class
 private
  // его необходимо создать в конструкторе и удалить в деструкторе
  FClickNotifier: TClickNotifier;
 public
  ...
  property ClickNotifier: TClickNotifier read FClickNotifier;
end;

...

procedure TButton.OnClick(Button: Integer);
begin
  // так отправляем
  FClickNotifier.Send(Self.Name, Button, Self.X + 5, Self.Y + 5);
end;

...

procedure TMyObject.SomeAction(const Name: String; Button, X, Y: Integer);
begin
  // тут обрабатываем нажатие на кнопку
end;

...

// так происходит регистрация подписчиков
Button.ClickNotifier.Subscribe(A.SomeAction);
Button.ClickNotifier.Subscribe(B.SomeAction);


Ну, а о том, какие у этого есть проблемы, я расскажу в следующей статье цикла.


Читать дальше......

Постоянные читатели

Обо мне

Моя фотография
Мой e-mail: vitek_03(at)mail(dot)ru