В статье излагается конкретный опыт коллектива разработчиков геоинформационной системы «Панорама» по переносу этой системы с платформы Windows на Linux. Первая версия системы «Панорама» была создана специалистами Топографической службы Вооруженных Сил РФ в 1991 году. Программы были написаны на языке Си с применением встроенного ассемблера для системы MS-DOS.
Разработка оказалась достаточной удачной и при простом интерфейсе имела высокую скорость отображения растровой и векторной графики, а также профессиональный набор средств векторизации отсканированных карт местности. Это обеспечило относительное долголетие системы, которая широко применяется до сегодняшнего дня в целях создания электронных карт. Благодаря компактности системы на ее основе создана космонавигационная программа для станции «Мир». Одним из основных условий разработки данной программы было требование размещения загрузочного кода вместе с картой мира масштаба 1 : 40 млн. на одной дискете емкостью 1,2 Мбайт. При этом, программа должна еще показывать текущую орбиту, перемещать в реальном времени подспутниковую точку с учетом параметров орбиты, определять зоны дня и ночи, показывать зоны радиосвязи и выполнять другие необходимые расчеты.
С появлением Windows 95 ядро системы было переписано на языке С++ и расширено для создания ГИС, способной решать различные прикладные задачи (связь, навигация, экология, земельный кадастр и др.). Участие в разработке программ принимали Национальная картографическая корпорация, компании «Геоспектрум» и «Эпсилон Технологии». В результате на сегодняшний день создана ГИС «Карта 2000», инструментальные средства GIS Tool Kit, система земельного кадастра «Земля и право» и ряд других систем.
При разработке программ для Windows мы были уверены, что это не последняя платформа для работы их задач, поэтому разработка велась так, чтобы ядро системы не пришлось переписывать третий раз.
Исходные тексты программ системы имеют размер несколько десятков мегабайт на языке C++. Программное обеспечение работает с векторной и растровой графикой и выполняет большой объем специальных вычислений. В качестве платформ поддерживаются: Linux, QNX, OC-РВ, Windows CE, Windows 98/NT, Intel, MIPS и Sparc. При компиляции использовались трансляторы Borland C++, Visual C++, Watcom C++, GNU C++.
Некоторые правила
Прежде всего, необходимо так спроектировать систему или внести изменения в готовую, чтобы графический интерфейс был реализован отдельными подпрограммами. Даже если это программа графического редактирования, трехмерного моделирования и т.п., она должна быть разделена на уровне исходных текстов. Программы с графическим интерфейсом можно разделить на два типа: задачи потоковой обработки данных и задачи, выполняющиеся в интерактивном режиме.
Сценарий задач потоковой обработки данных может иметь следующий вид:
- ввод исходных данных;
- обработка данных с выдачей сообщений о ходе процесса;
- выдача сообщений о результате выполнения процесса.
Графическая часть такой задачи может быть представлена в виде диалога, отображающего введенные данные и ход выполнения процесса. Сам процесс может быть реализован в виде отдельной библиотеки с определенной точкой входа. В качестве дополнительного параметра процессу передается идентификатор диалога, который может быть идентификатором окна в Windows или адресом функции обратного вызова, что позволяет организовать обратную связь. Такой подход облегчает смену инструментальных средств: при переносе переписывается только диалог по имеющемуся образцу. Кроме того, возможна специализация программистов на разработке диалогов и решении прикладных задач, что повышает уровень разработки и сокращает сроки.
Выполнение интерактивных задач (например, графического редактирования), основано на обработке событий, связанных с устройствами ввода/вывода: мышь, клавиатура, экран, таймер и т.п. Процесс обработки событий может быть реализован в виде отдельной библиотеки с несколькими универсальными точками входа либо число точек входа может соответствовать числу обрабатываемых событий. Обратная связь может выполняться через вспомогательный параметр, как для задач первого типа. Реализация графических функций может быть выполнена двумя способами.
Первый способ состоит в использовании области памяти, содержащей образ окна программы. Изображение строится в памяти и затем отображается на экране на основе минимального набора графических функций, доступных в операционной системе, например, BitBlt в Windows или XPutImage в подсистеме X Window в среде Linux. Функции, выполняющие отображение в память, должны быть независимы от размера палитры. Этого можно достичь путем применения макроопределений и вспомогательных переменных, описывающих текущие характеристики области памяти и палитры. Например, размер точки в байтах, ширина строки в байтах и т.д. Для упрощения логики задачи и ускорения работы можно применять 4 байта на точку, но это потребует дополнительных затрат оперативной памяти.
Второй способ - разработка своей собственной библиотеки графических функций, скрывающих особенности графической подсистемы на применяемой платформе. Здесь необходимо выполнить определение вспомогательных структур, констант и идентификаторов, скрывающих применяемые в конкретной графической подсистеме объекты. Например, координаты точки, координаты прямоугольной области, идентификатор окна, идентификатор палитры, элементы описания цвета точки и так далее.
Для написания пользовательского интерфейса могут применяться средства языка Java, что упрощает поддержку нескольких платформ - взаимодействие с библиотекой подпрограмм на языке C++ достаточно просто реализуется интерфейсом Jini.
При выполнении доступа к данным применяются различные функции операционной системы по работе с файлами и оперативной памятью. Такие функции обычно имеют посредников в стандартных библиотеках Cи или C++, однако, применение функций-посредников не всегда допустимо. Например, прямой вызов функции CreateFile() позволяет открыть файл с отключенной буферизацией на запись данных. Такая возможность может понадобиться при обработке некоторых категорий данных (журнал транзакций, файлы отката и т. п.), однако, вызов функции open() не обеспечивает такого режима записи.
При работе с оперативной памятью более гибкой является функция VirtualAlloc(), чем malloc() или new(). Кроме того, применение функций malloc() или new() может привести к ошибкам, когда в одном проекте применяются библиотеки, собранные разными трансляторами. В результате, возникает необходимость применения системно-зависимых функций.
Проблему переноса соответствующих исходных текстов можно решать двумя путями. Первый - применять функции стандарта POSIX, как наиболее распространенные. Второй - описывать макроопределения для требуемых функций, чтобы основной текст выглядел одинаково для разных платформ. Макроопределения функций и необходимых констант могут располагаться в заголовочном файле. Если компактность кода важнее производительности, то могут создаваться функции-посредники в виде отдельного набора исходных текстов.
Другая проблема, которая возникает при переносе программ на разные платформы, - учет требований процессоров по выравниванию данных и интерпретации числовых значений. Например, процессор Intel допускает обращение к числовым переменным, расположенным по адресу, не кратному длине операнда - порядок байт в слове: от младшего разряда к старшему. Компиляторы С/С++ для платформы Intel интерпретируют битовые структуры в порядке от младшего битового поля к старшему и такой же порядок байт в структуре. Процессоры MIPS и Sparc требуют выравнивания адреса переменной кратно ее длине (short - кратно 2, long - 4, double - 8), а порядок байт в слове: от старшего байта к младшему. Битовые поля в структуре располагаются от старшего поля к младшему в пределах байта, а байты от младшего к старшему (то есть обратно порядку битовых полей).
Так как выравнивание адресов переменных и размеров структур может быть обеспечено на любой платформе и повышает скорость выполнения программы (за счет уменьшения времени выборки данных процессором), то такой подход должен быть взят за правило при любой разработке.
Для корректной работы с двоичными данными, хранящимися в файле или базе данных, необходимо предусмотреть признак порядка расположения байт в структуре данных. Этот признак может учитываться при считывании данных в память, где может быть выполнен разворот байт. Для оптимизации многократного доступа к данным результаты разворота могут сохраняться в файле с соответствующим изменением признака. Для правильной интерпретации бит в пределах байта целесообразно в структурах применять макрокоманды, изменяющие порядок битовых полей в описании структур в соответствии с целевой платформой. Это упростит порядок преобразования данных.
Для хранения текстовых данных наиболее часто применяется кодировка OEM, ANSI, КОИ-8 и UNICODE. Функции по работе с символьными строками в разных операционных системах требуют разной кодировки. Для постоянного хранения данных целесообразно использовать одну кодировку для всех текстовых данных. Перед выводом текста на экран он может перекодироваться в соответствии с требуемой текущей кодировкой. Функция перекодировки может быть написана с применением макрокоманд. Для оптимизации многократного доступа к данным целесообразно завести в структуре данных признак применяемой кодировки. При смене кодировки данных признак соответственно обновляется.
Как сохранить надежность?
Поддержка в исходном тексте различных платформ усложняет программу, что может сказаться на ее надежности, поэтому для повышения качества работы программы необходимо руководствоваться рядом правил.
При выделении памяти под структуру или класс необходимо выполнить инициализацию значений каждой переменной. В самом простом случае можно применить функцию memset() для обнуления всех значений - не следует надеяться на опции транслятора по автоматической очистке памяти нулями. Особое внимание следует уделять вещественным переменным. В больших текстах подпрограмм рекомендуется устанавливать начальные значения и для локальных переменных. При ошибочном начальном значении переменной сбой может принимать самые разные формы. Но, при выполнении программы под управлением отладчика, память, как правило, очищается, что затрудняет локализацию ошибки.
Большая группа ошибок связана с применением указателей при программировании на языках Cи или C++. При передаче указателей в качестве параметров функций целесообразно предусмотреть передачу и размера области, на которую ссылается указатель. Если передается указатель на структуру, то в ней целесообразно предусмотреть поле размера структуры, которое должно заполняться до вызова функции. При получении параметра - указателя функция первым делом должна проверить, что указатель не равен нулю и размер соответствующей области имеет допустимое значение.
С учетом превышения темпов развития процессоров над темпами развития программного обеспечения излишняя осторожность такого подхода оправданна - фактор надежности гораздо важнее быстродействия.
Ошибочным действием является возврат из функции указателя на локальную переменную - такой код может долго работать правильно, но при переносе на другую платформу или использовании другого компилятора приведет к ошибке из-за различий в организации стека. Данные ошибки нужно исключать еще на этапе анализа исходного текста - либо функция должна возвращать значение локальной переменной, либо заполнять область данных, которая задана указателем и размером области.
Одна и та же область памяти может выделяться в программе несколько раз. При этом важно следить за тем чтобы эта память освобождалась, а указатель принудительно устанавливался в ноль. Анализ указателя при завершении программы и при перераспределении памяти поможет избежать потери ресурсов. Такой же подход целесообразен и при работе с идентификаторами файлов. Например:
if (pointer) { FreeTheMemory (pointer); pointer = 0;} pointer = AllocateTheMemory(newsize);
В надежной программе результат операции выделения памяти всегда должен анализироваться. Ошибка при выделении памяти может приводить к исключительной ситуации или возврату нулевого значения. Это зависит от применяемого компилятора, его параметров и платформы. Для однозначной интерпретации может применяться следующий подход:
try() { pointer = AllocateTheMemory (newsize); } catch(...) { pointer = 0; } if (pointer = 0) { // Обработка ошибки ... } else { // Удачное выполнение ...}
Операторы try и catch поддерживаются не на всех платформах, поэтому они могут быть заменены на макрокоманды, например:
#ifdef DISABLE_EXEPTION #define TRY if (1) #define CATCH() else #else #define TRY try #define CATCH() catch(...) #endif
Чтобы упростить сопровождение и модернизацию программного обеспечения, необходимо проектировать реентерабельные подпрограммы. Одно из условий этого - минимизация применения глобальных переменных: применять их для хранения констант или включать в текст подпрограмм критические разделы, семафоры и т.п. В большинстве случаев можно обойтись применением локальных переменных и передачей параметров. Но в этом случае может возникнуть ошибка, связанная с переполнением стека.
Для снижения нагрузки на стек целесообразно выделять в отдельные подпрограммы участки текста, требующие дополнительной памяти для выполнения операции. В этом случае можно использовать в качестве локальных переменных небольшие массивы, что эффективнее динамического выделения и освобождения памяти в программе.
Различные трансляторы Си/C++ используют свои умолчания при сокращенных объявлениях переменных. Они могут оказаться знаковыми либо нет, длинными или короткими целыми. Это же относится и к константам в программах. Все это может стать источником неожиданных ошибок при переносе программ на другие платформы. Чтобы избежать этого следует минимизировать разнотипность переменных. Например, исключить из применения для локальных переменных и параметров тип short int, а для функций внешнего интерфейса использовать параметры типа: signed long int, signed char *, double.
Навстречу новым платформам
Таким образом, поставив перед собой задачу разрабатывать многоплатформное программное обеспечение, можно заодно добиться повышения качества создаваемых текстов, надежности работы программ и более четкой организации работ. Время, потраченное на внедрение и поддержку соответствующих правил разработки вполне компенсируется на этапе сопровождения и модернизации сложных программных комплексов, а процесс переноса кода на новую платформу существенно упрощается.
Об авторе
Олег Беленков — руководитель проекта «Панорама», Топослужба ВС РФ. С ним можно связаться по электронной почте по адресу: panorama@lvl.ru