Версия для печати


Переход на платформонезависимый стиль программирования
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1440

Cepгей Poщин
дата публикации 20-11-2011 10:34

С появлением Delphi XE2 мы получили возможность создавать приложения, работающие на разных платформах (win32, win64, MacOS). Можно делать приложения двух видов: VCL Form Application (VCL) и FireMonkey HD Application (FM). Первый вид это все существующие старые приложения для Windows (32- и 64-битные), использующие компоненты VCL. Второй вид это кроссплатформенные приложения, он не использует ни какие модули VCL, все визуальные компоненты унаследованы от TComponent.

В этой связи, мне захотелось первым делом адаптировать существующие компоненты для работы в новой версии. Не буду говорить, что это не вызовет сложностей, по моим оценкам переход будет не менее трудоёмким, чем переход с Dos на Windows, однако дорога в тысячу миль начинается с первого шага. Поделюсь небольшим, пока, опытом. Если статья вызовет интерес у жителей королевства при наличии свободного времени и вдохновения, буду публиковать продолжения. Речь пойдет о наборе моих самопальных компонент.

Для начала, введем определение (define) OldCode  для директив условной компиляции, с помощью которых будем проверять работу общих модулей с использованием старого и нового вариантов кода, т.е. будем компилировать приложения, добавляя/удаляя слово OldCode в Project Options/Delphi Compiler/Conditional Defines.

Conditional Defines
Можно бы и удалить весь старый код, но в данном случае более интересен вариант с «ручным» переключением. Упомяну также, что имеются стандартные определения MSWINDOWS, MACOS, CPUX64 и т.п., подробнее см.: Help.

Рассмотрим класс TInternalTimer из модуля InternalTimer.pas, унаследованный от TComponent. Это некоторое подобие стандартного компонента TTimer, сделать его универсальным не составит большого труда.

Создадим пару тестовых приложений InternalTimerFM.dproj и InternalTimerVCL.dproj, которые будут размещаться в папке Demo\InternalTimer. Первое, на что заругается компилятор это на обращение к модулю Windows, действительно, откуда на макинтоше возьмется API Windows? Нету там и «виндовых» сообщений, таким образом, про модуль Messages  тип TMessage и обработчики сообщений можем смело забыть. Вместо модулей Windows и Messages, будем использовать модуль FMX.Platform.

uses {$IFDEF OldCode}
     Windows,       // Этот код можно будет
     Messages,      // удалить, а пока используем для проверки
     {$ELSE}
     FMX.Platform// Этот код будет использоваться на всех платформах
     {$ENDIF}
     SysUtils,
     Classes;

В модуле FMX.Platform имеется глобальный объект Platform, в котором реализованы некоторые системные процедуры специфическим для каждой платформы образом. Для создания таймера вместо вызовов API функций SetTimer и KillTimer воспользуемся методами Platform.CreateTimer и Platform.DestroyTimer.

    {$IFDEF OldCode}
    // Создаём функцию обратного вызова из метода
    fInstanceProc := MakeObjectInstance(TimerProc);
    // Создаём таймер выполняющий функцию обратного вызова
    fHandle := SetTimer(0, Cardinal(Self), Interval, fInstanceProc);
    {$ELSE}
    // Создаём таймер. Функция обратного вызова нам не нужна
    fHandle := platform.CreateTimer(Interval, TimerProc)
    {$ENDIF}

Кроме создания и удаления таймера меняется формат метода, который используется в качестве функции обратного вызова  (в кроссплатформенном варианте обходимся без параметра):

    procedure TimerProc{$IFDEF OldCode}(var M: TMessage){$ENDIF};

Теперь пробуем собрать приложения обоих видов с OldCode и без, для трех платформ. Обращаю внимание, что после изменения Conditional Defines, необходимо обязательно делать полную сборку (Build) приложения. Такие результаты у меня получились.

Платформа Старый код Новый код
VCL FM VCL FM
Win-32 Ok Ok Ok Ok
Win-64 Ok Ok Ok Ok
OS X - Error - Ok

Кто не верит, пусть проверит.

Перейдем к более сложному модулю UMultiThread, который содержит компонент TMultiThreadFor. Этот компонент предназначен для выполнения циклических вычислений в нескольких нитях (параллельных потоках выполнения кода). Для синхронизации выполняемого кода использовалась критическая секция TRTLCriticalSection из модуля Windows. Вместо неё воспользуемся, объектом синхронизации TmultiReadExclusiveWriteSynchronizer из модуля System. SysUtils. Вот характерные участки, где используется этот объект.

interface
uses

  {$IFDEF OldCode}Windows,{$ELSE}System.SyncObjs,{$ENDIF} SysUtils, Classes,
  InternalTimer;
...

  TMultiThreadFor = class(TComponent)
  private
    ...
    {$IFDEF OldCode}
    FLock: TRTLCriticalSection;
    {$ELSE}
    FLock: TMultiReadExclusiveWriteSynchronizer;
    {$ENDIF}
...
  end;

...
      // Инициализация критической секции
      {$IFDEF OldCode}
      FillChar(FLock, SizeOf(FLock), 0);
      InitializeCriticalSection(FLock);
      {$ELSE}
      FLock := TMultiReadExclusiveWriteSynchronizer.Create;
      {$ENDIF}
...
      // Деинициализация критической секции
      {$IFDEF OldCode}
      DeleteCriticalSection(FLock);
      FillChar(FLock, SizeOf(FLock), 0);
      {$ELSE}
      FreeAndNil(FLock);
      {$ENDIF}
...
      // Вход в критическую секцию
      {$IFDEF OldCode}
      EnterCriticalSection(FLock);
      {$ELSE}
      FLock.BeginWrite;
      {$ENDIF}
...
      // Выход из критической секции
      {$IFDEF OldCode}
      LeaveCriticalSection(FLock);
      {$ELSE}
      FLock.EndWrite;
      {$ENDIF}

Поясню смысл данного объекта. Каждая нить периодически читает/записывает некоторые данные в общую область памяти, если другая нить в этот момент тоже запишит свои данные туда же, результат будет непредсказуемым, для правильной работы нужно дождаться пока операция чтения /записи будет окончена. В папке Demo\Lock есть небольшая демонстрационная программка в которой используются методы BeginWrite и EndWrite.

Далее, для определения количества ядер использовалась системная функция GetSystemInfo, вместо неё воспользуемся глобальной переменной CPUCount из модуля System.

      {$IFDEF OldCode}
      GetSystemInfo(Info);
      fThreadCount := Info.dwNumberOfProcessors;
      {$ELSE}
      fThreadCount := CPUCount;
      {$ENDIF}

Наибольшую сложность  вызвала функция WaitForMultipleObjects, прямого аналога для платформонезависимого кода я не обнаружил. В компоненте требовалось подождать окончания выполнения всех нитей но не более определенного времени (см. TMultiThreadFor.Wait). Поскольку теперь нить не имеет дескриптора, добавим в нить событие. Класс Tevent из модуля System.SyncObjs, в основном реализует всю логику работы аналогично «виндовым» событиям, которые создаются функцией CreateEvent.

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

      While not Flag do begin end;

Аналогичное действие, но без стопроцентной загрузки выполняет метод TEvent.WaitFor.

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

  TThreadItem = class(TThread)
  private
...
    {$IFNDEF OldCode}
    fIsTerminate: TEvent;
    {$ENDIF}
  public
    constructor Create(AOwner: TMultiThreadFor; AThreadIndex: Word);
    {$IFNDEF OldCode}
    destructor Destroy; override;
    {$ENDIF}
    procedure Execute; override;
  end;
...
constructor TThreadItem.Create(AOwner: TMultiThreadFor; AThreadIndex: Word);
begin
...
  {$IFNDEF OldCode}
  fIsTerminate := TEvent.Create(nil, True, False, 'ThreadItem' + inttostr(fThreadIndex));
  {$ENDIF}
end;
 
{$IFNDEF OldCode}
destructor TThreadItem.Destroy;
var SavedEvent: TEvent;
begin
  SavedEvent := fIsTerminate;
  inherited;
  FreeAndNil(SavedEvent);
end;
{$ENDIF}
 
procedure TThreadItem.Execute;
...
begin
  try
...
  finally
    {$IFDEF OldCode}
    fOwner.DoFinishThread(fThreadIndex);
    {$ELSE}
    try
      fOwner.DoFinishThread(fThreadIndex);
    finally
      fIsTerminate.SetEvent;
    end;
    {$ENDIF}
  end;
end;

...
function TMultiThreadFor.Wait(Delay: Cardinal = $FFFFFFFF): Cardinal;
...
begin
...
  {$IFDEF OldCode}
...
  {$ELSE}
    if Delay <> Cardinal($FFFFFFFF) then
    begin
      StartTick := Round(mSecInDay * Now);
      for I := 0 to fThreadCount - 1 do
        if (TThreadItem(fThreads[I]).fIsTerminate.WaitFor(Delay) in [wrTimeout, wrError])
           or
           ((Round(mSecInDay * Now) > StartTick + Delay) and (I < (fThreadCount - 1))) then
        begin
          result := WAIT_TIMEOUT;
          Exit;
        end;
    end;
    for I := 0 to fThreadCount - 1 do
      fThreads[I].WaitFor;
    Result := 0;
  {$ENDIF}
end;

Пакеты (dpk-файлы) различаются содержимым секции requires, которая может изменяться автоматически, поэтому использовать директиву условной компиляции не получится. Для пакета со старым кодом в свойстве проекта Conditional Defines всегда должно присутствовать слово OldCode, а для пакета с новым кодом оно должно всегда отсутствовать, иначе IDE будет умолять Вас вставить отсутствующий модуль в раздел requires.

Кроме того, рекомендую изменить свойство проекта Project Options/Delphi Compiler/Unit output directory на $(BDSLIB)\$(Platform)\$(Config) тогда откомпилированные модули будут попадать в ту папку, в которой находятся все стандартные библиотеки. Скорее всего это будет путь к папке находящейся в program files. Если Вы работаете в Windows 7, то вам придётся запускать IDE с правами администратора. Результаты компиляции у меня такие:

Платформа Старый код
VCL
Новый код
FM
Win-32 Ok Ok
Win-64 Ok Ok
OS X Error Ok

Для проверки работоспособности компонента используйте проект из модуля Demo\MultiThreadFor.

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



Специально для Королевства Delphi


К материалу прилагаются файлы: