Особенности портирования сложного модульного ПО написанного на Delphi под ОС Linux

Данное повествование не предназначено для разработчиков, которые не знают что такое Delphi и не умеют на нём программировать. Просьба людей, не имеющих дела с данными средствами разработки, не комментировать статью и не травмировать и без того расшатанные нервы дедушек, упорно продолжающих поддерживать многолетние разработки, написанные с применением данных средств разработки. Предложения переписать всё с нуля на что-то более модное не приветствуются.

Введение

Меня зовут Тимофеев Константин, мне 40 лет и я являюсь ведущим программистом компании 3В Сервис в подразделении, занимающемся системами автоматизации динамических расчётом (САДР). Нашим основными программным продуктом является среда моделирования динамических систем SimInTech (в девичестве ПК Моделирование В Технических Устройствах — 4, сокращённо ПК «МВТУ»).

Исторически данный программный продукт начал развиваться на кафедре «Ядерные реакторы и энергетические установки» (Э7) МГТУ им. Баумана. Первые версии ПК «МВТУ» были разработаны на кафедре под руководством к.т.н. Козлова Олега Степановича. Программный комплекс предназначался для структурного моделирования динамики ядерных энергоустановок. Функционал ПК «МВТУ» заключался в составлении модели системы в виде блок-схемы с последующим автоматическим формированием системы дифференциальных уравнений динамики системы и их численном решении и выводе результатов расчёта на графики. В дальнейшем развитии этой темы к разработке в 2003 году подключился я и на базе ПК «МВТУ-3» мы начали создавать промышленную версию программного комплекса — ПК «МВТУ-4», получившую в дальнейшем название SimInTech. Разработка нового варианта программы потребовала очень серьёзной переработки архитектуры программы и позволила создать комплекс, позволяющий строить полномасштабные модели динамики ядерных энергостановок, а также систем в других отраслях промышленности, и являющийся достойным конкурентом систем Simulink, AmeSim, VisSim.

В качестве среды разработки для ПК «МВТУ-3» мы использовали Delphi, начиная с версий 3 (и заканчивая версией 5). Выбор данного средства разработки был обусловлен простотой создания графического интерфейса программы и большим количеством компонентов, скоростью разработки по сравнению с разработкой системы на других средствах того времени. Применение этой среды позволило создать достаточно большой по объёму пакет в очень сжатые сроки командой включавшей в себя трёх разработчиков (из которых по факту основным был один).

При разработке следующей версии программы в 2003 году, учитывая достаточно серьёзный объём ранее разработанного кода я также использовал в качестве средства разработки Delphi версии 6. В настоящее время мы используем как основное средство разработки под ОС Windows Delphi 10.3.3. Также надо понимать что хоть основная часть кода написана на Delphi, также в составе комплекса присутствуют модули и на С\С++ , а также на FORTRAN. При этом сам наш программный продукт с тех пор значительно усложнился, оброс большим количеством библиотек и модулей и прошёл значительную эволюцию под влиянием его применения на предприятиях атомной отрасли.

Глава 1:   Что мы имеем — архитектура и особенности программного комплекса

Главной составной частью ПК SimInTech является графическая оболочка. Графическая оболочка предназначена для того, чтобы пользователи могли визуально составлять блок-схемы технологических систем моделируемых объектов. В её основе лежит система векторной графики собственной разработки, которая включает в себя набор объектов — базовых графических примитивов как то: линии, полигоны, эллипсы, текстовые вставки, блоки, группы примитивов, линии связи, системные контролы (чекбокс, комбо-бокс, однострочный текстовый редактор). Поверх объектов-примитивов лежат групповые объекты — контейнеры графических примитивов (страницы). Контейнер — это то, что обеспечивает отрисовку схемы целиком и её редактирование — т.е. передачу примитивам событий клавиатуры и мыши). Также каждый контейнер включает в себя и объект — скриптовую машину для обеспечения динамизации изображения (изменения цвета положения, текста в зависимости от расчётных параметров схемы). Встроенная скриптовая машина позволяет делать как техническую анимацию, так и проводить расчёты, по сути она представляет собой встроенный язык программирования общего назначения.

Графическая оболочка предоставляет пользователю следующие сервисы:

— составление схемы из блоков, создание видеокадров из примитивов.

— работу с библиотеками блоков.

— вывод параметров расчёта на графики, в текстовые таблицы, в файл, в OPC, в shared memory.

— управление расчётом.

— синхронизация различных моделей и совместный расчёт.

— загрузка плагинов для блоков\схем\слоёв.

— работа со звуком.

— редактирование текстов скриптов с подсветкой кода.

— внешнее управление программой через встроенный COM-сервер (это использовалось для автоматизации расчётов в связке с другими программами).

Оболочка является наиболее сложной частью системы в точки зрения пользовательского интерфейса и объёма кода. Оболочка была написана на Delphi с применением достаточно разнообразных компонентов, из которых кроме стандартных контролов (кнопок полей ввода, комбо и чекбоксов) необходимо выделить следующие:

— для вывода рассчитанных значений на графики — Tee Chart Pro.

— для редактирования текстов с подсветкой — первоначально SynEdit, впоследствии TBCEditor.

— для работы со звуком — DSPack.

— для вывода деревьев и построения редакторов списков сигналов\объектов\свойств блоков\параметров расчёта\дерева библиотеки\дерева структуры проекта — VirtualTreeView.

— для работы с базами данных для отдельных модулей системы — zeosdbo.

— для трехмерной технической анимации — GLScenes.

— для разного — JEDI VCL + JCL -отдельные контролы, некоторые библиотеки например расчёт хешей и контрольных сумм.

Кроме того, в программе как DLL подключается несколько библиотек на C и FORTRAN, например модуль шифрования RSA и расчёта MD5 хеша. Для рендеринга графики схем в данный момент в Windows-версии программы используется Direct2D, который позволяет сделать достаточно плавную и производительную техническую анимацию (в том числе и с большими по размеру растровыми текстурами).

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

Система плагинов построена на основе наследования их от виртуальных базовых классов. То есть есть общий модуль где описываются базовые классы. В оболочке от него ничего не наследуется, а в подгружаемых DLL от базовых классов наследуются те или иные блоки расширения, в которых мы делаем override базовым методам. Для создания плагинов в каждой из библиотек реализована фабрика классов, которая по заданному имени внутри модуля создаёт заданный объект расширения. Такой подход позволяет сделать очень гибкую структуру плагинов и иметь там не только методы (как в классических интерфейсах), но и объекты\ссылки на системные структуры\и так далее. Минусом является то, что ограничивается возможность использования других компиляторов — т.е. при реализации плагина должен использоваться тот же компилятор что и оболочки (ну или совместимый по генерируемому коду). Кроме того, внутри плагинов могут находится и окошки — т.е. вспомогательный интерфейс.

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

Общий объем исходного кода только графической оболочки составляет приблизительно 215073 строк кода (это чистый код самого проекта, без компонентов и без файлов конфигурации форм dfm). С плагинами объем исходного кода на Delphi в проекте составляет 2858375 строк кода, что уже является весьма серьёзным числом.

Общая архитектура комплекса приведена на рисунке ниже.

63df33b28b25954a1289fda760e739b6.png

Глава 2: Импортозамещение или заказчики захотели извращений

В общем жили мы жили, не тужили и баблос рубили спокойно делая программу под ОС Windows, и тут раз и вторая смена ©. Первые звонки с тем, что недурно бы поддержать и другие операционные системы начались не с отечественного заказчика как ни странно, а с немецкого института GRS для которого мы сделали систему проектирования моделей для их кода на нашей платформе. Немцы предложили нам BDSM –, а хорошо бы нам для отчёта перед нашим посконным немецким начальством поддержать отечественную (ну в смысле исконно немецкую) операционную систему Suse Linux, а то непатриотично мол как-то, да и денег платить за неё заставляют. Прельстившись шуршанием не очень большого количества евровалюты, мы начали думать, как таки заставить программу на этом поделии светлого и благородного сумрачного немецко-финского гения. Первое что пришло при этом в голову — завести программу под Wine и таки действительно СТАРАЯ версия нашего софта под ним пошла, но только 32 битная и без Direct2D рендера, ну и соответственно без красивостей в виде прозрачностей и быстрых градиентов. Ну и кроме того, это было весьма скромно по производительности, особенно в многопоточном режиме. Возможно, после утечек исходного кода от майкрософта этот слой API в линуксе будет доведён до работающего состояния пригодного для полноценной работы нашего софта, но пока это не всегда так. Но захотелось иметь что-то чуть более приличное и по скорости и максимально реализующее функционал Windows-версии.

Java мы отмели сразу, ибо проблемы со стыковкой к ней библиотек на FORTRAN и откровенно отстойная скорость работы с более-менее сложными скриптами и графикой показались недостаточным аргументом для переписывания всего объёма кода на неё. Кроме того, она дико многословная по сравнению с Delphi да и нам нужен был именно нативный код, в первую очередь из-за необходимости стыковки с некоторым количеством стороннего расчётного софта (как немецкого так и нашего). Поэтому Java пошла в топку (на моей прошлой работе часть системы на ней всё-таки переписали, из опыта эксплуатации получилось очень сильно хуже исходной версии), наступать на старые грабли не очень хотелось. Туда же пошёл и C#, из-за плохой поддержки разработки на нём GUI именно в Linux и ненативного кода с проблемами стыковки со сторонними расчётными библиотеками.

Из вариантов сделать кроссплатформенный GUI остались 2:

1.       Qt и C++ — это замечательный путь, но переписывание всего кода системы не впечатлило, кроме того под основную систему это на самом деле скорее всего означало было ухудшение отзывчивости и качества пользовательского интерфейса — VCL контролы использует родные системные всё таки. Далее встал вопрос наличия необходимой для формирования достаточно сложного пользовательского интерфейса базы компонентов, которые были перечислены выше.  Посмотрев некоторые программы, выполненные при помощи Qt (qucs например как достаточно близкий по функциям) я осознал, что этот путь всё таки будет погибелен скорее всего.

2.       Free Pascal + Lazarus. Самый большой плюс этой среды разработки и компилятора был в практически полной (но не совсем) переносимости текста исходной программы, что позволяло дословно сохранить логику работы. Но были большие сомнения в том, что те компоненты и особенности которые были в коде программы корректно переварятся этим компилятором.

Итак в силу ограничений в человеческих ресурсах  было решено попробовать максимально использовать имеющуюся кодовую базу, оставив её на Delphi и дополнив код IFDEF-ами для сборки программы под другую ОС в среде разработке Lazarus. Немалую роль также сыграло то, что это позволяло не прерывать работу над основной версией SimInTech для Windows и параллельно вносить изменения сразу в 2 варианта сборки.  Ибо срок работы был небольшой, а перенести надо было много и ещё так чтобы оно заработало.

Анализ встроенных в Lazarus штатно компонентов показал, что маловато будет, и было решено поискать что-то, в чём этого добра насыпано поболее, особенно в части отрисовки 2D-графики. Сначала был попробован рендер AggPas в штатном Lazarus-е, но он показался сильно медленным и не устраивающим по некоторым особенностям совместимости кода с Direct2D библиотекой. Поэтому было принято искать что-то поинтереснее. Поиск по интернету вывел меня на нестандартную сборку Lazarus-а под названием CodeTyphon Studio и первые эксперименты с этой средой привели к выводу что вот это в принципе то, что позволит собирать программу на Linux с максимальным использованием существующего кода. Свою роль сыграло то что:

1 — там много компонентов по умолчанию, функционально и по использованию совместимых с теми, которые я использовал в Delphi-версии. То есть требовалась фактически минимальная переработка кода. Из всего что мне было нужно отсутствовали BCEditor (но был SynEdit, который у меня использовался в редакторе скриптов в более старой версии), DSPack (но были аналоги, и на самом деле наличие звука пока было не критично), TeeChart Pro (но штатно был аналог TAChart, а вот построение графиков как раз было очень критично).

2 — в наличии оказался неплохой компонент для рендеринга графики — ORCA 2D, структура которого была очень похожа на вызовы Direct2D и который был принципиально пригоден к использованию для отрисовки сложных изображений моделей и видеокадров.

Глава 3: Начинаем портировать

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

1 — необходимо было иметь общий менеджер памяти для головного модуля и плагинов, выполненных в виде динамических shared object — библиотек.

2 — должна была быть возможность загрузки сторонних динамических библиотек на C и FORTRAN.

3 — было необходимо, чтобы некоторые формы находились в динамически загружаемых библиотеках, т.к. за счёт них выполнялось расширение интерфейса.

4 — должны быть аналоги большинства компонентов с идентичным или похожим пользовательским и программным интерфейсом.

5 — файлы форм dfm должны без лишних проблем конвертироваться (дальше как показала практика целесообразно иметь 2 раздельных набора файлов описания форм для Windows и Linux из за некоторой разницы в дизайне системных виджетов.

Самые большие сомнения на этот счёт были касательно пунктов 1 и 3, потому что используемая в Lazarus библиотека LCL хоть и является практически точным клоном VCL, но различается по ряду особенностей.

Первым делом была развернута виртуальная машина под VirtualBox с доступом к нашему репозиторию, где мы ведём разработку. В качестве операционной системы на тот момент была выбрана Open Suse, немножко позднее была сделана виртуальная машина на Alt Linux 8.3 и потом Alt Linux 9. Далее с сайта https://pilotlogic.com/ был выкачан архив с дистрибутивом среды разработки. Развертывание среды разработки было произведено по инструкции к ней — распаковать архив и выполнить su ./install.sh  , но в процессе развертывания на Alt Linux встала проблема, что этой ОС в установочном скрипте в списке не было — пришлось добавить самому и написать письмо разработчику чтобы включил в код, сейчас эта разновидность линукса поддерживается.  Если что не так — то в дистрибутиве можно дополнить файлик  CodeTyphonIns\installbin\ScriptsLin\ln_All_Functions.sh  вписав в него особенности установки.

Для Alt Linux туда была добавлен текст, рядом с тем местом, где определяется установка для Ubuntu:

#------------ 100 Alt (apt-get compitible)----------
 elif [ -f /etc/altlinux-release ] ; then
   vOSVerNum=100
   vOSDistribution="Alt Linux (apt-get compatible)"
   vMultiArchDirPlan=200

В AstraLinux среда разработки ставится без лишних телодвижений на данный момент (с sudo). Бинарные файлы скомпилированные под AltLinux 9 запускаются и под Astra Linux Orel.

У Astra Linux на данный момент есть одна проблема, не связанная непосредственно с данной разработкой — штатно на нём стоит в настройках стабильный репозиторий в котором git версии 2.11, а для последнего SmartGIT-а нужно не ниже 2.16. Проблема решается путём переключения на экспериментальный репозиторий и обновления пакета git из него. 

В итоге среда разработки была успешно установлена на тестовую виртуальную машину с Linux.

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

Анализ кода, поставляемого с компилятором Free Pascal, показал, что в составе RTL-библиотек имеется модуль cmem, который позволяет использовать менеджер памяти из libc. Он также как и для Delphi ставится первым модулем в dpr-файле и подключает внешний менеджер памяти (в Delphi для этого есть  SimpleShareMem ну или fastmm4 \ fastmm5). На этом пункте можно было с уверенностью поставить галочку. Но при дальнейшем тестировании было выяснено что производительность этого менеджера памяти достаточно отстойная, поэтому поиски были продолжены и в результате был найден неплохой менеджер памяти fpcx64mm из пакета  Synopse mORMot framework 2.  Но он был не распределённый, поэтому для того, чтобы его использовать он был подключен через небольшой промежуточный модуль simmm.pas следующего содержания:

unit simmm;
  //  Прокладка для использования общего менеджера памяти
{$IFNDEF DCAD}
{$DEFINE IS_DLL_UNIT}
{$ENDIF}

{$IFDEF FPC}
 {$MODE Delphi}{$H+} 
{$ENDIF} 

interface

{$IFDEF UNIX}

  {$IFDEF IS_DLL_UNIT}
  uses cthreads, dl;
  {$ELSE}
  uses cthreads, fpcx64mm;
  {$ENDIF}

{$ELSE}
uses  FastMM5;
{$ENDIF}

implementation

{$IFDEF UNIX}
{$IFDEF IS_DLL_UNIT}
  var
     NewMM,
     OldMM:      TMemoryManager;
     MainHandle: Pointer;
     GetCommonMemoryManager: procedure(var aMemMgr: TMemoryManager);
{$ENDIF}
{$ENDIF}

initialization

  {$IFDEF UNIX}
  {$IFDEF IS_DLL_UNIT}

    MainHandle:=dlopen(nil, RTLD_LAZY);
    GetCommonMemoryManager:=dlsym(MainHandle,'GetMemoryManager');
    GetCommonMemoryManager(NewMM);
    GetMemoryManager(OldMM);
    SetMemoryManager(NewMM);

  {$ENDIF}
  {$ELSE}

  //Расшариваем менеджер памяти
  if IsLibrary then
     FastMM_AttemptToUseSharedMemoryManager
  else
     FastMM_ShareMemoryManager;
  {$ENDIF}

  {$IFDEF UNIX}
  {$IFDEF IS_DLL_UNIT}
finalization
    SetMemoryManager(OldMM);
  {$ENDIF}
  {$ENDIF}
end.

Как видно из данного кода ключ DCAD определяется только в головном модуле, при этом мы в нём определяем как экспортируемую функцию GetMemoryManager. А в dll\so мы получаем адрес этой функции  путём вызова dlopen (nil, RTLD_LAZY) — если эта функция вызывается с нулевым первым аргументом, она возвращает хендл на главный исполняемый файл и дальше мы получаем оттуда адрес функции для получения центрального менеджера памяти и используем в плагинах его. Соответственно во всех плагинах первым модулем мы прописываем simmm для обеспечения общего выделения памяти.

Вторая проблема — это вызов форм, находящихся внутри dll\so модулей. В Delphi по факту эта проблем не возникала. Т.е. если мы делаем там TForm.Create, то форма создается нормально. В Lazarus на линуксе оказалось всё несколько интереснее, т.к. для того, чтобы форма создалась, необходимо в so-библиотеке прописать в списке модулей:

  …..
  Classes,
  {$IFDEF FPC}
  Interfaces,
  {$ENDIF}
  Forms,  

и определить инициализацию Application в секции initialization и завершение в finalization:

{$IFDEF FPC}
initialization
  Application.Initialize;
finalization
  Application.Terminate;
end.
{$ENDIF}

Выполнив такие операции, мы в дальнейшем можем  спокойно создавать формы внутри so.

Наше приложение является многопоточным, поэтому сразу возник также вопрос с тем как это будет поддерживаться в Lazarus. В результате чтения документации и экспериментов было выяснено, что на UNIX-системах чтобы внутри программы или so создавать поток, в списке модулей проекта должен быть прописать модуль cthreads:

  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}

Таким образом все dpr-файлы были модифицированы типовым образом для того, чтобы в них заработал нужный нам функционал, в качестве примера привожу исходник dpr‑файла для плагина расчёта систем управления:

{$IFDEF FPC}
  {$MODE Delphi}   //Это определение нужно, чтобы компилятор Free Pascal включил режим
{$ENDIF}                  //совместимости с Delphi.
                               
library mbtylib;
uses
  simmm,
  {$IFDEF UNIX}
  cthreads, //сейчас этот модуль включен в simm.pas чтобы он был подключен для всех библотек сразу
  {$ENDIF}  
  Classes,
  {$IFDEF FPC}
  Interfaces,
  {$ENDIF}
  Forms,  
  MBTYTools in 'MBTYTools.pas',
  MBTYObjts in 'MBTYObjts.pas',
  uMBTYThread in 'uMBTYThread.pas',
  SpecBlocks in 'SpecBlocks.pas',
  InfoUnit in 'InfoUnit.pas' {MBTYInfoForm},
  MBTYtranslate in 'MBTYtranslate.pas',
  uDebugBlockForm in 'uDebugBlockForm.pas' {DebugBlockForm};

{$R *.res}

//Тут мы храним картинки, которые у нас пойдут во вспомогательные кнопку на тулбарах
{$R mbtybuttons.res}

  //Эта функция возвращает адрес структуры DllInfo
function  GetEntry:Pointer;
begin
  Result:=@DllInfo;
end;

exports
  GetEntry name 'GetEntry',         //Функция получения адреса структуры DllInfo
  CreateObject name 'CreateObject'; //Функция создания объекта

{$IFDEF FPC}
initialization
  Application.Initialize;
finalization
  Application.Terminate;
end.
{$ENDIF}
begin
end.

Таким образом 2 базовые для текущей архитектуры программного комплекса проблемы получили своё решение и на Linux. Дальше оставалось проанализировать насколько корректно будут передаваться в DLL строки и ссылки на объекты — с этим при подключении общего менеджера памяти всё оказалось в порядке. С загрузкой сишных и фотрановских .so также всё оказалось хорошо, но необходимо помнить, что intel fortran и gfortran по умолчанию по разному передают в функции аргументы, поэтому там где это было нужно, пришлось видоизменить декларацию вызова некоторых подпрограмм из сторонних DLL\SO. Как пример можно привести следующий фрагмент кода, подключающего процедуру на FORTRANе:

Вариант для Windows — INTEL FORTRAN, конвенция stdcall:

  TSbros = procedure(
    Nuzl,Nu,Ngran,Nmat,Nko,idmdt0,iadiab0: integer;       // +++ 31.03.2014 (iadiab0)
    var El,Iel,Uzel,Iuzel,
      NYZL,G,Nnas,Nzad,Nelu,
      Gran,Granu,Ngu,Ngut,Propm,Nelm,
      Zadv,Pump,Nzadp;
    Dtau,DtauG,Htau,TauE: TPPRealType;
…………………..
    );stdcall;

Вариант  для Linux gfortran с конвенцией cdecl:

   //Версия для стыковки с версией для под GFortran	
  TSbros = procedure(
    var Nuzl,Nu,Ngran,Nmat,Nko,idmdt0,iadiab0: integer;       // +++ 31.03.2014 (iadiab0)
    var El,Iel,Uzel,Iuzel,
      NYZL,G,Nnas,Nzad,Nelu,
      Gran,Granu,Ngu,Ngut,Propm,Nelm,
      Zadv,Pump,Nzadp;
    var Dtau,DtauG,Htau,TauE: TPPRealType;
………………….
    );cdecl;

Из приведённого кода видно, что в Intel FORTRAN целочисленные параметры функции по умолчанию передаются в стек напрямую, а массивы и вещественные числа передаются через указатели, а в gfortran все параметры передаются через указатели. При этом по умолчанию конвенция вызовов в Intel FORRAN для Windows — stdcall, в gfortran, что для Windows что для Linux — cdecl.

Кроме этого также следует обратить внимание на именование экспортируемых функций при загрузке их из DLL: если в Intel Fortran для Windows имя экспортируемой функции остаётся неизменным и соответствует имени функции, то в gfortran добавляется суффикс _  :

{$IFDEF Windows}'wwww'{$ELSE}'wwww_'{$ENDIF}

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

gcc -shared -fPIC -o …/…/bin/libcommonclibs.so -s nn.c prime.c r_keygen.c rc4c.c rsa.c md5c.c -fpack-struct=1 -Wconversion

При компиляции модулей на Си следует обратить внимание на выравнивание структур, для того, чтобы таковое совпадало с кодом на Pascal.

Далее был предпринят анализ используемых компонентов и поиск их аналогов в CodeTyphon Studio, чтобы понять, насколько будет модифицироваться код программы. Ниже приведена таблица, показывающая что где есть.

Компонент

Delphi

CodeTyphon Studio\ Lazarus

TButton

+

+

Tedit

+

+

TjvComponentPanel

+

+

TactionList

+

+

TjvFormMagnet

+

-

TcontrolBar

+

+

TtoolBar

+

+

TjvOfficeColorPanel

+

— есть аналог ThexaColorPicker

TcheckBox

+

+

TcomboBox

+

+

TjvListBox

+

— заменяем на TListBox

TeeChart Pro

+

— штатно нет

TjvSpinEdit

+

— заменяем на TspinEdit

TjvFontComboBox

+

— заменяем на TplFontComboBox

TBCEditor

+

-заменяем на TsynEdit

TvirtualStringTree

+

+

TPageControl

+

+

TTabControl

+

+

TPanel

+

+

TBitBtn

+

+

TJvButton

+

—          заменяем на TBitBtn

TScrollBar

+

+

TFrame

+

+

TJvListView

+

—          заменяем на TListView

TSpeedButton

+

+

Компоненты системных диалогов

+

+

GLScenes

+

+

DSPack

+

—          есть другие компоненты для звука

Indy

+

+

TPngImage

+

+ заменяем на TPortableNetworkGraphic

Вообще на текущий момент компоненты JVCL в этой сборке портированы практически все, поэтому часть замен можно и не делать. Анализ показал, что всё что надо программе, чтобы обеспечить сборку на Linux там имеется, кроме TeeChart Pro, о котором будет описано чуть ниже, потому что его удалось в итоге заставить там работать и это сняло сразу большую головную боль.

После успешно проведённой проверки среды CodeTyphon Studio на тестовом приложении был начат непосредственно процесс портирования и формирования единой кодовой базы для Windows и Linux версий программы. Для этого сначала на виртуальной машине был настроен git и установлена для удобства работы программы SmartGit, была выполнена загрузка рабочей копии из репозитория и создана отдельная ветка altlinux.

Первое что необходимо сделать для портирования программы на Lazarus\CodeTyphon это пересохранить ВСЕ файлы исходного текста (pas, dpr и разные include-ы) в формат UTF8 BOM! Это строго необходимо, если у вас в сообщениях есть хотя бы одна русская буква! Это делается в Delphi или в любом другом текстовом редакторе.

7723b90dfcdc6089a7a6273c8af7ea11.png

Второе  — это с формирования файлов проектов (ctpr) под данную среду разработки. Первоначально это было сделано при помощи функции

d90d3e3c50084aeb29c97491adc698dc.png

В дальнейшем ctpr-файлы, проектов для CodeTyphon Studio просто копировались для разных модулей с изменением внутри этого xml-файла имени проекта и названия самого файла и модулей в проекте. Также можно просто сделать там пустой проект и дальше размножить из него ctpr файлы на все компоненты программы.

Процесс портирования естественно был начат с самого проблемного с этой точки зрения места — то есть графической оболочки программы, где сосредоточен практически весь пользовательский интерфейс. После того, как мы получили ctpr-файл, открываем его в в Code Typhon, прописываем все пути поиска исходников, если проект был разложен по директориям и первым делом вносим модификации в dpr-файл проекта графической оболочки (головного исполняемого модуля программы) — добавляем с условной компиляцией модули:

  simmm,
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF }
  {$IFDEF FPC}
  Interfaces,
  {$ENDIF}

Делаем экспорт функции получения менеджера памяти головного модуля:

{$IFDEF FPC}
exports
  GetMemoryManager;  // это надо чтобы общий менеджер памяти увидели SO-шки
{$ENDIF}

и убираем в условную компиляцию модуль подключения COM-сервера и функций для работы с Excel и если есть что-то ещё системно-зависимое.

Дальше была нажата кнопка «Скомпилировать» в среде разработке и пошло исправление выдаваемых компилятором ошибок. Первое что пришлось проделать во всех модулях — это добавить в заголовок юнита вот такое определение, чтобы включался режим совместимости:

{$IFDEF FPC}
 {$MODE Delphi}{$H+}
{$ELSE}
 {$DEFINE Windows}
{$ENDIF}

Я в курсе что это также включается в настройках проекте в CodeTyphon Studio, но некоторые модули в такой режим не переводились.

Потом были обвязаны условной компиляцией все системно-зависимые модули в uses — в первую очередь Windows и Messages:

{$IFDEF FPC} FileUtil,{$ELSE} Windows,{$ENDIF}

и введены в базовые модули программы (DataTypes.pas) недостающие элементарные типы:

  {$IFDEF FPC}
  PRect = ^TRect;
  {$ENDIF}
TWindowsPointArray = array of {$IFNDEF FPC}Windows.{$ENDIF}TPoint;

Дальше оказалось, что для обеспечения бинарной совместимости при сохранении и чтении файлов необходимо учитывать то, что размеры перечислимых и множественных типов данных в Delphi и Free Pascal разные:

В Delphi — 1 байт, в FreePascal — 4, что касалось перечисление, например, стилей линий или заливок:

  //Размеры некоторых типов данных
 SizeOfTColor      = 4;
 SizeOfTPenStyle   = 1;    //SizeOf(TPenStyle);     //В FPC размеры enum 4 байта !
 SizeOfTBrushStyle = 1;   //SizeOf(TBrushStyle);
SizeOfTDataMode   = 1;  //SizeOf(TDataMode);

Поэтому везде в коде там где размеры типов в компиляторах различались было закомментировано использование SizeOf и размер задан числом фиксированно !

Также были определены недостающие функции, которые присутствовали в модуле Windows:

{$IfDef FPC}
procedure ZeroMemory(const Data: pointer; Size: integer); inline;
begin
  FillChar(Data^,Size,0);
end;
{$EndIf}

Ну и соответственно, те места, которые вызывали функции из Windows были заменены эквивалентами из кросс-платформенных модулей:

{$IfDef fpc}CreateDir(newdir){$else}CreateDirectory(PChar(newdir),nil) and (GetLastError <> ERROR_ALREADY_EXISTS){$endif}

Также были переделаны функции сохранения и считывания строк из потока для обеспечения возможности загрузки данных из Windows версии, т.к. формат строк в Lazarus\CodeTyphon — UTF8 по умолчанию, а в современном Delphi — UTF16:

{$IFDEF FPC}
procedure   SaveStr(const S:string;Stream:TStream);
  var N,c: cardinal;
      UniString: UnicodeString;
begin
 UniString:=UTF8Decode(S);
 N:=Length(UniString);
 with Stream do begin
   c:=N or $80000000;
   Write(c,SizeOfInt);
   if N > 0 then begin
      Write(Pointer(UniString)^,N*2);
   end;
 end
end;

procedure   LoadStr(var   S:string;Stream:TStream);
  var N: cardinal;
      UniString: UnicodeString;
  procedure DoLoadAsAnsi;
   var tmps: ansistring;
  begin
     //старая версия - ansi
     SetLength(tmps,N);
     if N>0 then Stream.Read(Pointer(tmps)^, N);
     S:=tmps;
  end;
begin
 with Stream do begin
   Read(N,SizeOfInt);
   if (N and $80000000) <> 0 then begin
     //юникод
     N:=N and (not $80000000);
     SetLength(S,N);
     SetLength(UniString,N);
     if N > 0 then begin
        Stream.Read(Pointer(UniString)^, N*2);
        S:=UTF8Encode(UniString);
     end;
   end
   else
     DoLoadAsAnsi;
 end
end;
{$ELSE}
.......

При этом надо обратить внимание, что эта функция эволюционировала ещё с не-юникодной версии Delphi, и соответственно, когда программа была переделана на юникод то для идентификации формата строки был задействован старший бит в 4-х байтном числе перед данными строки. Если он 1 то значит строка кодирована 2-байтными символами в UTF16, а если 0 — значит это старая ANSI строка.

В большинстве модулей где была голая математика не потребовалось вообще никаких изменений в коде, кроме как указать в начале модуля режим совместимости {$MODE Delphi}.

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

В самый верх модуля, как и у всех вставляем:

{$IFDEF FPC}
 {$MODE Delphi}{$H+}
{$ENDIF}

В uses обвязываем системно-зависимые модули условной компиляцией:

{$IFDEF FPC} LCLType, LCLIntf, LMessages, GraphType,{$ELSE}Windows, Messages,{$ENDIF}

Модули:

SysUtils, Classes, Graphics, Controls, Forms, Dialogs, и другие свои

оставляем как есть, но если там префикс VCL.  — то его надо убрать !

В секции implementation там где линкуется dfm-файл, вместо   {$R *.dfm} вставляем условную компиляцию, чтобы для CodeTyphon у нас грузился файл формы с расширением frm (в данном случае там сделан также выбор и по языку интерфейса):

{$IFDEF ENG}
{$IFnDEF FPC}
{$R *.eng.dfm}
{$ELSE}
{$R *.eng.frm}
{$ENDIF}
{$ELSE}
{$IFnDEF FPC}
  {$R *.dfm}
{$ELSE}
  {$R *.frm}
{$ENDIF}
{$ENDIF}

Дальше необходимо перенести файл конфигурации формы из Delphi (dfm) в другой формат CodeTyphon (frm). Для этого в Lazarus\Code Typhon существует инструмент конвертации dfm-файлов, который прекрасно понимает их в текстовом формате (хоть и пишет при этом ошибку).

Поэтому выбираем следующий пункт меню, не обращая никакого внимания на слово двоичный — с текстовым форматом dfm-файлов эта утилита также прекрасно работает:

f2f349c6e26e79c3a17df18a1640e30a.png

и выбираем dfm файл конвертируемого модуля. После этого получаем сообщением об «ошибке» и не обращаем на него никакого внимания.

Если в frm -файле никакой экзотики которой нет в данной сборке нет, то мы можем сразу нажать кнопку «Переключить форму\модуль (F12)» которая сделает попытку загрузить frm-файл. Далее возможны 2 варианта: все компоненты есть — тогда мы сообщения о том что каких-то свойств нет просто игнорирует или же каких-то компонентов у нас нет — тогда мы открываем в текстовом редакторе frm-файл и меняем там имена классов компонентов на аналоги имеющиеся под линуксовой средой разработки и пробуем сделать «Переключить форму\модуль (F12)» ещё раз пока всё не станет хорошо и не появится дизайн формы:

39ba416b4b3b7b3ad3ca8ec1901b1bc1.png

В некоторых случаях, чтобы упростить задачу и не искать аналог описание компонента просто вырезалось из frm-файла в текстовом редакторе и компонент создавался уже динамически при FormCreate, где также была для этого сделана условная компиляция. Также надо обратить внимание, что размеры кнопок\полей ввода\чекбоксов в штатной линуксовой GTK2 которая используется там для Lazarus\Code Typhon как системная библиотека виджетов не совпадают с виндовыми и зачастую надо немножко повозить мышкой по формочке, чтобы привести её в более благородный вид.

Там где имена классов компонентов не совпадают — в исходном тексте класса формы делаем условную компиляцию, например так:

    cProgressBar: {$IFDEF FPC}TProgressBar{$ELSE}TJvGradi
    
            

© Habrahabr.ru