Зростання популярності ОС Linux і поява попиту на підтримку різних операційних платформ зажадав від розробників освоєння технології створення многоплатформного програмного забезпечення. У статті викладається конкретний досвід колективу розробників геоінформаційної системи «Панорама» щодо перенесення цієї системи з платформи 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++, C++GNU.
Деякі правила

Насамперед, необхідно спроектувати систему або внести зміни в готову, щоб графічний інтерфейс був реалізований окремими подпрограммами. Навіть якщо це програма графічного редагування, тривимірного моделювання і т. п., вона повинна бути розділена на рівні вихідних текстів. Програми з графічним інтерфейсом можна розділити на два типи: задачі потокової обробки даних і завдання, які працюють в інтерактивному режимі.

Сценарій завдань потокової обробки даних може мати наступний вигляд:
введення вихідних даних;
обробка даних з видачею повідомлень про перебіг процесу;
видача повідомлень про результат виконання процесу.

Графічна частина такої задачі може бути представлена у вигляді діалогу, що відображає дані про хід виконання процесу. Сам процес може бути реалізований у вигляді окремої бібліотеки з певною точкою входу. В якості додаткового параметра процесу передається ідентифікатор діалогу, який може бути ідентифікатором вікна в Windows або адресою функції зворотного виклику, що дозволяє організувати зворотний зв’язок. Такий підхід полегшує зміну інструментальних засобів: при перенесенні переписується тільки діалог за наявним зразком. Крім того, можлива спеціалізація програмістів на розробці діалогів і вирішенні прикладних завдань, що підвищує рівень розробки і скорочує терміни.

Виконання інтерактивних завдань (наприклад, графічного редагування), засноване на обробці подій, пов’язаних з пристроями вводу/виводу: миша, клавіатура, екран, таймер і т. п. Процес обробки подій може бути реалізований у вигляді окремої бібліотеки з кількома універсальними точками входу або число точок входу може відповідати кількості оброблюваних подій. Зворотній зв’язок може виконуватися через допоміжний параметр, як для завдань першого типу. Реалізація графічних функцій може бути виконана двома способами.

Перший спосіб полягає у використанні області пам’яті, яка містить образ вікна програми. Зображення будується в пам’яті і потім відображається на екрані на основі мінімального набору графічних функцій, доступних в операційній системі, наприклад, BitBlt Windows або XPutImage в підсистемі X Window в середовищі Linux. Функції, що виконують відображення в пам’ять, повинні бути незалежні від розміру панелі. Цього можна досягти шляхом застосування макровизначень і допоміжних змінних, що описують поточні характеристики області пам’яті і палітри. Наприклад, розмір точки в байтах, ширина рядка в байтах і т. д. Для спрощення завдання логіки і прискорення роботи можна застосовувати 4 байти на точку, але це зажадає додаткових витрат оперативної пам’яті.

Другий спосіб розробка своєї власної бібліотеки графічних функцій, що приховують особливості графічної підсистеми на застосовуваної платформі. Тут необхідно виконати визначення допоміжних структур, констант та ідентифікаторів, які приховують застосовуються в конкретній графічній підсистемі об’єкти. Наприклад, координати точки, координати прямокутної області, ідентифікатор вікна, ідентифікатор панелі, елементи опису кольору крапки і так далі.

Для написання інтерфейсу можуть застосовуватися засоби мови Java, що спрощує підтримку декількох платформ взаємодія з бібліотекою підпрограм мовою C++ досить просто реалізується інтерфейсом Jini.

При виконанні доступу до даних застосовуються різні функції операційної системи по роботі з файлами і оперативною пам’яттю. Такі функції зазвичай мають посередників в стандартних бібліотеках Сі або 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++. При передачі покажчиків в якості параметрів функцій доцільно передбачити передачу і розміру області, на яку посилається вказівник. Якщо передається покажчик на структуру, то в ній доцільно передбачити поле розміру структури, яке має заповнюватися до виклику функції. При отриманні параметра покажчика функція насамперед повинна перевірити, що вказівник не дорівнює нулю і розмір відповідної області має допустиме значення.

З урахуванням перевищення темпів розвитку процесорів над темпами розвитку програмного забезпечення зайва обережність такого підходу виправдана фактор надійності набагато важливіше швидкодії.

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

Одна і та ж область пам’яті може виділятися в програмі кілька разів. При цьому важливо стежити за тим щоб ця пам’ять звільнялася, а покажчик примусово встановлювався в нуль. Аналіз покажчика при завершенні програми та при перерозподілі пам’яті допоможе уникнути втрати ресурсів. Такий же підхід доцільний і при роботі з ідентифікаторами файлів. Наприклад:

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.
Назустріч новим платформам

Таким чином, поставивши перед собою завдання розробляти многоплатформное програмне забезпечення, можна заодно домогтися підвищення якості створюваних текстів, надійності роботи програм і більш чіткої організації робіт. Час, витрачений на впровадження і підтримку відповідних правил розробки цілком компенсується на етапі супроводження та модернізації складних програмних комплексів, а процес перенесення коду на нову платформу істотно спрощується.