Содержание

Написание переносимых программ

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

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

Изолирование системно-зависимого кода

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

Рассмотри, например, задачу различения букв и остальных символов. Библиотека предоставляет функцию isalpha, которая получает символьный аргумент и возвращает не нуль, если это — буква, и — 0 в противном случае. Предположим, что программист пишущий компилятор языка FORTRAN, хочет узнать, начинается ли переменная с буквы в диапазоне от 'I' до 'N' для того, чтобы определить, будет ли она целой. Он может написать

upletter = toupper( name[0] );
if( upletter >= 'I'  &&  upletter <= 'N' ) {
    /* ... */
}

Если программа разрабатывается на машине, которая использует символьный набор ASCII, то всё будет отлично, так как буквы верхнего регистра имеют 26 последовательных значений. Но при переносе программы на машину, которая использует символьный набор EBCDIC, могут возникнуть проблемы, потому что между буквами 'I' и 'N' находятся 7 небукв, включая '}'. Поэтому имя }VAR будет считаться правильным именем целой переменной, что, естественно, не так. Для решения этой проблемы программист должен написать

if( isalpha( name[0] ) ) {
    upletter = toupper( name[0] );
    if( upletter >= 'I'  &&  upletter <= 'N' ) {
        /* ... */
    }
}

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

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

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

Другие области, которые могут быть системно-зависимыми, включают:

Остерегайтесь длинных имён с внешней связью

В соответствии со стандартом языка C, компилятор может ограничивать внешние имена (функции и глобальные объекты) шестью значащими символами. Это часто диктуется стадией «линкования» процесса разработки.

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

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

Обход реализационно-зависимого поведения

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

Важным свойством, которое различается в разных системах, является число символов, распознаваемых системой в именах внешних объектов и функций. Стандарт гарантирует, что система должна распознавать как минимум 6 символов, хотя в будущем стандарты могут снять или расширить это ограничение. Большинство систем допускают более, чем 6 символов, но некоторые распознают только 8. Для истинной переносимости функция или объект с внешней связью должны иметь уникальными первые шесть символов. Это может потребовать изобретательности в придумывании имён, но, разработав систему названий, можно много сделать и в этих границах. Цель, конечно, в том, чтобы имена объектов оставались понятны. Если все системы, которые будут использоваться, имеют меньшие ограничения, то программист может оставить в прошлом предел из шести символов. А, если перенос будет делаться на систему с шестисимвольным пределом, то придётся сделать некоторые изменения.

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

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

Диапазоны типов

Диапазон объекта типа int не указан в стандарте, сказано только, что он должен быть не менее чем от -32767 до 32767. Если объект должен содержать целое значение, то необходимо позаботится о том, будет ли этот диапазон доступен на всех системах. Если объект — счётчик, который никогда не выходит за границы 0 и 255, то диапазон будет достаточным. Однако для большего диапазона может понадобится long int.

То же самое относится к объектам типа float. Имеет смысл декларировать их как double.

Поведение при конвертировании чисел с плавающей точкой в целые, округление происходит по-разному на разных системах. Если программе важно знать как оно происходит, она должна обращаться к макро FLT_ROUNDS (определённого в заголовочном файле <float.h>), чьё значение описывает способ округления.

Специальные возможности

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

Использование препроцессора для облегчения переносимости

Препроцессор отчасти полезен для получения альтернативных последовательностей кода в целях переносимости. Условная компиляция при помощи директивы препроцессора #if позволяет вставлять различный код в зависимости от некоторых условий. Определение набора макро, который описывает различные системы, и макро, которое выбирает конкретную, делает простым добавление системно-зависимого кода.

Например, рассмотрим макро

#define OS_DOS    0
#define OS_CMS    1
#define OS_MVS    2
#define OS_OS2    3
#define OS_QNX    4

#define HW_IBMPC  0
#define HW_IBM370 1

#define PR_i8086  0
#define PR_370    1

Они описывают набор операционных систем (OS), аппаратного обеспечения (HW) и процессоров (PR), которые вместе описывают компьютер и его операционную систему. Если программа будет переносится на IBM 370 работающий под управлением операционной системы MVS, то ей придётся включить заголовочный файл, в котором определяются эти макро и декларировать следующие:

#define OPSYS     OS_MVS
#define HARDWARE  HW_IBM370
#define PROCESSOR PR_370

Следующий код будет включён только, если программа компилируется для IBM 370, работающий под MVS.

#if HARDWARE == HW_IBM370  &&  OPSYS == OS_MVS
    DoMVSStuff( x, y );
#endif

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

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