Искусство программирования для Unix.

17.5. Программирование, обеспечивающее переносимость.

Переносимость программ обычно рассматривается в квази-пространственных понятиях: возможно ли перенести данный код на существующее аппаратно-программное обеспечение, отличное от того, для которого код создавался? Однако десятки лет опыта Unix подсказывают, что долговечность во времени важна в той же степени, если не в большей. Если бы можно было подробно предсказать будущее программного обеспечения, то оно, вероятно, было бы настоящим. Тем не менее, в программировании, обеспечивающем переносимость, следует пытаться обдумывать варианты построения программ на основе тех функций окружающей системы, которые, вероятнее всего, сохранятся, и избегать технологий, которые, скорее всего, в обозримом будущем прекратят свое существование.

Что касается Unix, два десятка лет изучения вопросов определения переносимых API-интерфейсов позволили полностью решить данную проблему. Средства, описанные в Единой спецификации Unix, вероятно, будут присутствовать во всех современных Unix-платформах и вряд ли не будут поддерживаться в будущем.

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

17.5.1. Переносимость и выбор языка.

Первым вопросом в программировании, обеспечивающем переносимость, является выбор языка реализации. Все основные языки, рассмотренные в главе 14, являются высоко переносимыми в том смысле, что совместимые реализации доступны на всех современных Unix-системах, а для большинства из них также доступны реализации для Windows и MacOS. Проблемы переносимости часто возникают не в основных языках, а в библиотеках поддержки и степени интеграции с локальным окружением (особенно в IPC-методах и управлении параллельными процессами, включая инфраструктуру для GUI-интерфейсов).

17.5.1.1. Переносимость С.

Базовый язык С в высшей степени переносим. Его стандартной реализацией в Unix является GNU C-компилятор, который повсеместно распространен не только Unix-системах с открытым исходным кодом, но и современных коммерческих вариантах операционной системы. GNU С был перенесен на Windows и классическую MacOS, но ни в одной из этих сред не стал широко распространенным, поскольку испытывает недостаток в переносимых привязках к собственным GUI-интерфейсам данных систем.

Стандартная библиотека ввода/вывода, математические подпрограммы и поддержка интернационализации переносимы во всех реализациях С. Файловый ввод/вывод, сигналы и управление процессами являются переносимыми между вариантами Unix, при условии, что используются только современные API-интерфейсы, описанные в Единой спецификации Unix; более старый C-код часто содержит нагромождения условных операторов препроцессора, направленных на достижение переносимости, но они поддерживают интерфейсы, появившиеся до стандарта POSIX, из старых коммерческих Unix-систем, которые по состоянию на 2003 год устарели или близки к этому.

Более серьезные проблемы с переносимостью С начинаются в области IPC, параллельных процессов и GUI-интерфейсов. Вопросы переносимости IPC и параллельных процессов обсуждались в главе 7. Реальную практическую проблему создают GUI-инструментарии. Большое количество GUI-инструментариев с открытым исходным кодом универсально переносятся между современными вариантами Unix, а также в Windows и классическую MacOS — Tk, wxWindows, GTK и Qt — четыре хорошо известных вида инструментария с открытым исходным кодом и документацией, которую нетрудной найти в Web. Однако ни один из них не распространяется со всеми платформами, и (по причинам более юридическим, чем техническим) ни один из них не предоставляет на всех платформах естественный вид и восприятие GUI-интерфейса. В главе 15 даны некоторые рекомендации, позволяющие справиться с данной проблемой.

По теме разработки переносимого C-кода написаны целые тома. Эта книга не входит в их число, и авторы рекомендуют внимательно изучить "Recommended С Style and Coding Standards" [11], а также главу о переносимости из книги "The Practice of Programming" [40].

17.5.1.2. Переносимость С++

Для С++ на уровне операционной системы характерны те же проблемы переносимости, что и для С, а также ряд собственных. Одной из дополнительных проблем является то, что GNU компилятор с открытым исходным кодом для С++ значительно отстает от коммерческих реализаций; поэтому к середине 2003 года не существовало универсально внедряемого эквивалента GNU С, на котором можно основывать стандарт де-факто. Более того, ни один из компиляторов С++ не реализовывает полностью ISO-стандарт C++99, хотя GNU С++ подошел к этому очень близко.

17.5.1.3. Переносимость shell.

Переносимость shell-сценариев, к сожалению, является низкой. Проблема заключается не в самой оболочке; bash(1) (Bourne Again shell с открытым исходным кодом) распространена достаточно широко, для того чтобы малоразвитые shell-сценарии могли выполняться почти в любой среде. Проблема заключается в том, что в большинстве shell-сценариев интенсивно используются другие команды и фильтры, которые являются менее переносимыми, и их присутствие на какой-либо определенной целевой машине никоим образом не гарантируется.

Данную проблему можно преодолеть героическими усилиями, как в инструментах autoconf(1). Однако это действительно достаточно трудно, и большинство сложнейших случаев программирования, которые обычно реализовались в shell, переместились к языкам сценариев второго поколения, таким как Perl, Python и Tcl.

17.5.1.4. Переносимость Perl.

Perl отличается хорошей переносимостью. В стандартном варианте языка даже предоставляется переносимый набор привязок к Tk-инструментарию, который поддерживает переносимые GUI-интерфейсы в Unix, MacOS и Windows. Однако этому мешает одна проблема. Perl-сценарии часто требуют добавочных библиотек из архива CPAN (Comprehensive Perl Archive Network — полный сетевой архив Perl), которые не обязательно присутствуют в любой реализации Perl.

17.5.1.5. Переносимость Python.

Python имеет превосходную переносимость. Как и Perl, стандартный вариант Python предоставляет переносимый набор привязок к Tk-инструментарию, который поддерживает переносимые GUI-интерфейсы в Unix, MacOS и Windows.

Стандартный Python обладает более развитой стандартной библиотекой, чем Perl и не имеет эквивалента архиву CPAN, на который могли бы полагаться программисты. Вместо этого важные модули расширения регулярно встраиваются в стандартный дистрибутив Python в ходе выпуска второстепенных версий. Это заменяет пространственную проблему временной — Python менее подвержен влиянию эффекта недостающих модулей. За это приходится расплачиваться тем, что второстепенные версии Python отчасти более важны, чем главные версии Perl. На практике этот компромисс благоприятствует Python.

17.5.1.6. Переносимость Tcl.

Tcl демонстрирует хорошую переносимость в целом, но она сильно различается в зависимости от сложности проекта. Tk-инструментарий для кроссплатформенного GUI-программирования является естественным для Tcl. Как и в случае с Python, развитие основного языка проходит относительно гладко с немногими проблемами перекоса версий. К сожалению, Tcl даже более, чем Perl, зависит от средств расширения, которые не обязательно поставляются с каждой реализацией языка, а кроме того, не существует эквивалента CPAN-архива для централизованного их распространения.

Таким образом, для мелких проектов, не зависящих от расширений, переносимость Tcl превосходна. Однако крупные проекты часто сильно зависят как от расширений, так и (как в случае с shell-программированием) от вызываемых внешних команд, которые могут отсутствовать на целевой машине; часто они имеют слабую переносимость.

Парадоксально, но Tcl может страдать от простоты добавления в него расширений. К тому моменту, когда определенное расширение становится интересным в качестве части стандартного дистрибутива, существует, как правило, несколько различных его версий. На симпозиуме разработчиков Tcl/Tk (Tcl/Tk Workshop 1995 года) Джон Аустерхоут (John Ousterhout) объяснил, почему в стандартном дистрибутиве Tcl отсутствует поддержка ОО-средств, так:

Представьте себе пять мулл, сидящих неподалеку, каждый из которых говорит: "Убей его, он неверный". Если бы я внедрил специфическую ОО-схему в ядро, то один из них сказал бы: "Благословляю тебя, сын мой, ты можешь поцеловать мой перстень", а остальные четыре сказали бы "Убей его, он неверный".

17.5.1.7. Переносимость Java.

Переносимость Java превосходна — в конце концов, основной целью создания языка был девиз "написанное однажды работает везде". Вместе с тем переносимость Java не идеальна. Трудности в основном связаны с проблемами перекоса версий между JDK 1.1 и более старым GUI-инструментарием AWT (с одной стороны) и JDK 1.2 и более новым Swing. Это обусловлено несколькими важными причинами.

• Конструкция AWT Sun была настолько неадекватной, что ее необходимо было заменить инструментарием Swing.

• Отказ Microsoft от поддержки Java-разработки на Windows и попытка заменить данный язык С#.

• Решение Microsoft удерживать поддержку аплетов в Internet Explorer на уровне JDK 1.1.

• Лицензионные положения Sun, которые делают невозможной реализацию JDK 1.2 с открытым исходным кодом, замедляя внедрение пакета (особенно в мире Linux).

Разрабатывая программы, в которых задействованы GUI-интерфейсы, Java-разработчики, ищущие переносимости, в обозримом будущем окажутся перед лицом выбора: для сохранения максимальной переносимости (включая Microsoft Windows) остановиться на JDK1.1/AWT со слабо спроектированным инструментарием или получить лучший инструментарий и средства JDK 1.2, жертвуя некоторой переносимостью.

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

17.5.1.8. Переносимость Emacs Lisp.

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

Кроме того, также весьма стабилен набор примитивов Emacs. Он достиг завершенности для задач, которые в течение многих лет возлагались на редактор (манипуляция буферами, обработка текста). Только введение системы X нарушило эту картину, и очень немногие режимы Emacs должны иметь сведения об X. Проблемы переносимости обычно являются проявлением капризов средств операционной системы на C-уровне привязок; управление подчиненными процессами в таких режимах в качестве почтовых агентов — почти единственная область, где такие проблемы проявляются с непредсказуемой частотой.

17.5.2. Обход системных зависимостей.

После выбора языка и библиотек поддержки следующим вопросом переносимости обычно является расположение ключевых системных файлов и каталогов: почтовых спулов, каталогов журнальных файлов и т.д. Прообразом данного типа проблем является то, где расположен каталог почтового спула — /var/spool/mail или /var/mail.

Часто можно избежать этого вида зависимости, отступив и воссоздав проблему. Зачем, так или иначе, открывать файл в каталоге почтового спула? Если запись в него необходима, то, возможно, лучшим решением будет просто вызов локального почтового транспортного клиента, с тем чтобы блокировка файла была выполнена верно. Если выполняется чтение из почтового каталога, то, вероятно, лучше будет обратиться к нему посредством POP3- или IМАР-сервера.

Подобные вопросы применимы и к другим системным файлам. Не лучше ли, например, вместо ручного открытия журнальных файлов воспользоваться средствами syslog(3). Интерфейсы вызова функций через библиотеку С стандартизированы лучше, чем расположение системных файлов, и этим следует пользоваться.

Если параметры расположения системных файлов должны присутствовать в коде, то наилучшая альтернатива зависит от способа распространения дистрибутива: исходный код или бинарная форма. Если распространяется исходный код, то могут помочь инструменты autoconf, которые рассматриваются в следующем разделе. В случае распространения бинарных файлов хорошей практикой будет заставить программу получать начальные параметры системы и выяснять, возможно ли автоматически настроиться на локальные условия, например, путем проверки существования каталогов /var/mail и /var/spool/mail.

17.5.3. Инструменты, обеспечивающие переносимость.

Часто для разрешения вопросов переносимости, обследования системной конфигурации и настройки make-файлов можно использовать GNU-утилиту с открытым исходным кодом autoconf(1), которая рассматривалась в главе 15. Пользователи, которые сегодня предпочитают компилировать программы из исходного кода, рассчитывают на возможность ввести команды configure; make; make install и получить чистую сборку. На странице <http://seul.org/docs/autotut/> публикуется хорошее учебное пособие по использованию данных инструментов. Даже если программа распространяется в бинарном виде, инструменты autoconf(1) способны помочь автоматизировать решение проблемы подстройки кода под условия различных платформ.

Существуют другие инструменты, которые решают данную проблему. Двумя из наиболее широко известных средств являются утилита Imake(1), связанная с системой X Window и инструмент Configure, созданный Ларри Уоллом (Larry Wall, создавшим позднее язык Perl) и приспособленный для многих различных проектов. Все они, как минимум, также сложны как набор autoconf, и используются в настоящее время не чаще. Они не охватывают такой же широкий диапазон целевых систем как autoconf.