4.10. Сборка выполняемой программы
В традиционной общецелевой операционной среде превращение подготовленных текстов алгоритмов в выполняемую программу происходит, как правило, в два этапа. Сначала производится трансляция, в результате которой получают объектные модули, а затем компоновка, связывающая эти модули между собой. Если же операционная среда предполагает обслуживать расширяемый программный фонд, и в частности поддерживать многовариантность, то аппарат сборки несколько усложняется.
Необходимость привлечения дополнительных системных средств объясняется тем, что в наших построениях используется более свободная трактовка понятия «модуль» (см. разд. 2.1). В традиционной среде на стадии сборки программы в качестве модулей выступают обычно единицы алгоритмического языка, допускающие автономную трансляцию. Границы же модулей в многовариантных программах обычно не совпадают с границами автономно транслируемых единиц. Модуль многовариантности может включать в себя несколько таких единиц, или же может представлять собой фрагмент текста на алгоритмическом языке, вставляемый в формируемый текст модуля трансляции.
Использование в качестве модулей вставляемых фрагментов приводит к тому, что для подготовки трансляции требуется, вообще говоря, выполнять препроцессирование, т. е. строить из текстов модулей многовариантности текст модуля трансляции. Таким образом, сборка выполняемой программы в многовариантном случае включает не два, а три этапа: препроцессирование, трансляцию и компоновку.
Однако отличия в обслуживании многовариантных программ не сводятся к подключению препроцессирования. Здесь придется немного скорректировать всю совокупность представлений об организации сборки, о чем и пойдет речь в настоящем разделе.
4.10.1. Препроцессирование, трансляция и компоновка. Итак, описание требуемой конкретной конфигурации составлено. Что же происходит дальше? Конечно, было бы по меньшей мере непоследовательно сначала вооружить разработчика многовариантной программы развитыми средствами поддержки составления описания конфигурации, а затем, на завершающем этапе, оставить его один на один с теми скудными возможностями для организации препроцессирования, трансляции и компоновки, которые предоставляет обычно общецелевая операционная среда.
С одной стороны, совокупность материалов, используемых для построения многовариантной программы, довольно обширна и сложна, что сильно затрудняет ручное выполнение многих рутинных операций. (Этим операциям в традиционной среде не уделяется должного внимания, поскольку они не требуют сколько-нибудь заметных усилий при работе с программами небольшого объема.) Поясним, например, из-за чего возникают трудности в области экономии трансляции. Автономно транслируемая программная единица (модуль трансляции) может содержать несколько вариантных гнезд. Каждая комбинация сменных модулей, заполняющих (при препроцессировании) эти гнезда, требует самостоятельной трансляции. Хранение и учет множества объектных модулей, появляющихся в результате таких трансляций, превращаются для многовариантных программ в самостоятельную, весьма трудоемкую проблему.
С другой стороны, как было видно из предшествующего изложения, обеспечение среды для эффективного задания конкретных конфигураций многовариантной программы включает в себя хранение в программном фонде разнообразной информации, касающейся связей между отдельными модулями. Эта информация составляет необходимый базис, благодаря которому средства системной поддержки могли бы, получив от пользователя описание конфигурации, практически полностью избавить его от подавляющего большинства забот по препроцессированию, трансляции и компоновке. Точнее говоря, от пользователя потребуется только заполнить все гнезда ввода в среде подготовки расчета затем он нажмет функциональную клавишу Выполни и, не задумываясь о произведенной средствами поддержки рутинной работе, по завершении сборки заданной программы и ее выполнения приступит к анализу полученных результатов.
Разумеется, совсем забыть о существовании препроцессирования, трансляции и компоновки пользователю не удастся. Но вспоминать о них он будет только при обнаружении каких-либо ошибок, появление которых, к сожалению, неизбежно при достаточно энергичной работе по развитию программного фонда.
С исправлением ошибок связан еще один важный технологический момент. Редактирование исходного текста программы в традиционной среде должно повлечь за собой уничтожение объектного модуля, порожденного ранее в результате трансляции этого текста. В многовариантной среде текст (точнее, модуль многовариантности), как правило, так или иначе участвует в порождении уже не одного, а нескольких объектных модулей, причем проследить вручную все имеющиеся связи «исходный текст объектный модуль» оказывается далеко не просто. В задачу средств системной поддержки входит выявление и уничтожение всех устаревших объектных модулей, порожденных с участием отредактированного исходного текста.
В традиционной среде иногда для подобных манипуляций применяются штатные универсальные средства, подобные make в системе UNIX. Обращаться к ним для организации сборки версий многовариантной программы, по-видимому, нецелесообразно. Прежде всего, подобным средствам обычно недоступны некоторые функции, выполняемые конфигурационным препроцессором.
Кроме того, связи между компонентами многовариантной программы довольно сложны и о всех этих связях надо поставить в известность подключаемое средство. Поэтому соответствующее описание оказалось бы излишне громоздким и, главное, дублирующим уже имеющуюся в программном фонде информацию. В то же время для специализированных средств поддержки многовариантности отслеживание соответствия между исходными текстами и порожденными объектными модулями не составит особого труда. Немаловажно и то, что многие из задач, стоящих перед средствами поддержки, решаются существенно проще, если к манипуляциям над программным фондом не допускаются инородные программные системы.
4.10.2. Распространенные схемы компоновки. Несмотря на отмеченные выше препятствия, в цепочке «препроцессирование трансляция компоновка», осуществляющей сборку многовариантных программ, первые два звена реализуются относительно легко. Конфигурационный препроцессор напоминает макропроцессор: в роли макровызовов выступают гнезда каркаса, а в роли библиотеки макроопределений программный фонд, содержащий сменные модули. Главное отличие заключается в том, что помимо исходного текста и сменных модулей в работе препроцессора на равных правах участвует еще и третья сторона описание конкретной конфигурации, назначающее содержимое гнезд. Второе звено сборки трансляция выглядит практически так же, как и в общецелевой среде.
Основные сложности связаны с организацией компоновки, к изучению которой мы и переходим. Прежде всего рассмотрим, какие схемы компоновки используются в традиционных операционных средах. Наиболее широко распространены перечислительная, корневая и комбинированная схемы.
При перечислительной схеме в описании конкретной конфигурации явно перечисляются все составляющие собираемую программу автономно транслируемые программные единицы модули трансляции. (И сейчас еще попадаются операционные среды, где перечисляются не первичные объекты с исходными текстами, а вторичные объектные модули. Но, как было показано в разд. 2.2, явное указание вторичных объектов влечет за собой множество неудобств, и поэтому подобные решения мы здесь и в дальнейшем рассматривать не будем.) В ходе компоновки разрешаются внешние связи, но они никак не влияют на состав включаемых в собираемую программу модулей.
При корневой схеме в описании конфигурации явно указывается только головной (корневой) модуль трансляции, а все остальные включаются в собираемую программу в ходе разрешения внешних связей (запросов импорта). Считается, что в распоряжении компоновщика (называемого также редактором связей) находится вся совокупность модулей программного фонда и для каждого модуля известны все его входные точки и внешние ссылки. Компоновщик, разрешая очередную внешнюю ссылку, находит в программном фонде модуль с соответствующей входной точкой и включает этот модуль в собираемую программу. Тем самым, с одной стороны, возможно, разрешается еще несколько внешних ссылок, но, с другой стороны, могут добавиться новые неразрешенные ссылки из числа внешних ссылок подключаемого модуля.
Наконец, третья, комбинированная схема обладает отдельными чертами перечислительной и корневой. Подобно перечислительной схеме тут задается перечень имен головной группы модулей, которые безусловно включаются в собираемую программу. Кроме того, в компоновке участвуют одна или несколько библиотек. Хранящиеся в них модули подключаются к собираемой программе уже избирательно, по корневой схеме разрешая оставшиеся внешние ссылки головной группы. Комбинированная схема, сочетающая в себе достоинства (о которых речь пойдет ниже) корневой и перечислительной, является сейчас, по-видимому, наиболее популярной.
Три упомянутые схемы компоновки встречаются в самых различных модификациях. Кроме того, существуют и несколько иные подходы. В частности, самостоятельную ветвь образует инкрементальная сборка программ (см. разд. 2.3.2), позволяющая при внесении изменений не повторять весь процесс компоновки заново, а ограничиться коррекцией небольших участков загружаемого кода. Но широкого распространения подобные решения пока не получили, и мы не будем на них останавливаться.
Рассмотрим теперь достоинства и недостатки указанных схем. Попытаемся разобраться в том, какие из этих схем могут служить в качестве основы при сборке многовариантных программ.
4.10.3. Перечислительная компоновка. На первый взгляд, перечислительная схема безнадежно проигрывает корневой. Действительно, с какой стати программиста принуждают явно составлять какой-то перечень модулей трансляции, когда всю эту работу может с успехом проделать за него компоновщик? Или, скажем, пусть создатель многомодульной программы на очередном этапе решил расчленить на два модуля некоторый периферийный модуль трансляции. Почему при этом он обязан (подобно перечислительной сборке см. разд. 3.4.3) вручную отредактировать стоящий на вершине программной иерархии первичный объект, содержащий перечень модулей?
Неужели разработчики штатного обеспечения не заметили этих очевидных слабостей или поленились реализовать более рациональную схему? Конечно же, видеть причину распространенности перечислительной схемы всего лишь в нерадивости разработчиков штатного обеспечения было бы неверно. Если принять версию нерадивости, то в самой популярной, комбинированной схеме ее перечислительный компонент будет восприниматься как чисто избыточный, не обязательный для реализации. Но разработчики тем не менее почему-то упорно не пытаются сэкономить здесь свои усилия.
Интерес к перечислительной схеме не ослабевает, по-видимому, не в последнюю очередь из-за потребностей вариантного программирования. В частности, именно перечислительная схема компоновки требуется для реализации одного из описанных в первой главе (см. разд. 1.3) простейших способов оформления варианта размножения окрестности вариантного фрагмента.
Напомним постановку задачи и основные шаги этого способа. Пусть неожиданно потребовалось наряду с имеющимся алгоритмом получить его модификацию, причем все отличия между двумя версиями алгоритма заключены в четко очерченном фрагменте исходного текста. И пусть имеющаяся операционная среда не располагает специализированными средствами поддержки оформления варианта. Для определенности предположим кроме того, что вариантный фрагмент содержится непосредственно в тексте модуля трансляции.
Тогда оформление варианта может быть выполнено следующим образом. Сначала создается (под другим именем) копия исходного текста модуля трансляции. Затем в ней с помощью обычного редактора старый вариантный фрагмент заменяется новым. И наконец, при формировании задания на компоновку в перечень модулей трансляции включается, в зависимости от потребности, старый или новый модуль.
Легко заметить, что для данного способа оформления варианта жизненно необходимо ручное формирование перечня модулей трансляции. В нем теперь можно даже усмотреть творческий момент: требуется не просто механически перечислить модули, разрешающие транзитивное замыкание внешних ссылок корневого модуля, а на каком-то этапе сознательно выбрать одну из двух возможных модификаций модуля, содержащего вариантный фрагмент.
В условиях отсутствия специализированных средств поддержки вариантности отказываться от перечислительной схемы, по-видимому, нецелесообразно. Ведь при корневой схеме компоновки осуществить подобный способ оформления варианта можно было бы, только до предела расширив размножаемую окрестность. Дублировать придется не единичный модуль трансляции, а весь программный фонд. Или, по крайней мере, надо будет образовать новый программный фонд, включив в него все материалы, используемые при сборке данной конкретной конфигурации программы. Столь масштабные мероприятия необходимы, поскольку здесь указание корневого модуля полностью задает весь последующий ход компоновки. Следовательно, если содержащий вариант модуль не корневой, применить размножение окрестности как способ оформления варианта в пределах одного программного фонда не удается.
Теперь становится понятным, из-за чего библиотеки модулей (в особенности библиотеки общего назначения) подключаются при комбинированной компоновке по корневой схеме. Дело в том, что в существующих библиотеках, как правило, отсутствует вариантность, а без нее в явном перечислении модулей просто нет необходимости. Тут уже преимущества корневой схемы (в первую очередь компактность описания конфигурации) становятся неоспоримыми, благодаря чему она решительно вытесняет перечислительную.
Итак, мы определили место перечислительной схемы в традиционном «маловариантном» программировании. Вернемся к многовариантным программам. Способна ли работать эта схема компоновки в контексте многовариантности?
Применение перечислительной схемы сильно облегчило бы жизнь разработчикам средств системной поддержки многовариантности, избавив их от многих забот по поиску модулей трансляции для разрешения внешних ссылок. На их долю осталась бы только сравнительно несложная работа препроцессирование отдельных модулей трансляции, т. е. заполнение вариантных гнезд сменными модулями в соответствии с имеющимся описанием конкретной конфигурации.
Допустимость перечислительной компоновки (подобно допустимости перечислительного механизма сборки см. разд. 3.4.3) существенно зависит от того, насколько динамично меняется состав модулей, задаваемый перечнем. На практике встречаются задачи вычислительного эксперимента, где состав модулей трансляции, включаемых в конкретные конфигурации многовариантной программы, относительно стабилен. В таком случае применение перечислительной компоновки не так уж катастрофично, поскольку пользователь составляет требующийся перечень модулей один раз и затем лишь изредка его корректирует.
Чаще, однако, изменения состава модулей трансляции носят регулярный характер. Здесь нередки ситуации, где нетехнологичность перечислительной компоновки явно бросается в глаза. Пусть, например, для одного из вариантных гнезд созданы два сменных модуля, один из которых содержит обращение к некоторому вспомогательному модулю трансляции, а второй нет. Тогда каждый раз при переназначении этих сменных модулей надо будет, вообще говоря, редактировать перечень составляющих компонуемую программу модулей трансляции, нарушая таким образом безболезненность переключения сменных модулей. Дело осложняется еще и тем, что злополучный вспомогательный модуль трансляции нельзя механически выбросить из перечня при переключении на второй сменный модуль, так как к этому вспомогательному модулю могут быть обращения из других (в свою очередь, возможно, вариантных) частей компонуемой программы.
Иногда из положения удается выйти, составив перечень компоновки «с запасом», т. е. включив в него на всякий случай все потенциально полезные модули трансляции. Но чаще такое решение не проходит, в частности, из-за соображений компактности выполняемого кода.
В итоге попытку применения перечислительной схемы компоновки к многовариантным программам, по-видимому, придется признать неудачной. Посмотрим теперь, что может дать здесь корневая схема.
4.10.4. Корневая компоновка. В многовариантной среде корневая компоновка протекает следующим образом. Прежде всего препроцессор формирует текст корневого модуля трансляции. Точнее, сначала выясняется, не транслировался ли ранее корневой модуль в той же конфигурации, т. е. при том же составе включаемых в него сменных модулей. Если транслировался, то соответствующий объектный модуль уже готов и он просто извлекается из программного фонда. Если заданная комбинация сменных модулей встретилась впервые, то формируется полный текст модуля и выполняется трансляция.
Так или иначе, в результате мы имеем объектный модуль для корневого модуля трансляции. Атрибутами объектного модуля являются его внешние связи, из которых выясняются необходимые для разрешения этих связей модули трансляции. Каждый из затребованных таким образом модулей включается в собираемую программу, с ним производятся те же действия: сначала либо извлечение имеющегося объектного модуля, либо (иначе) препроцессирование и трансляция, а затем выяснение из внешних ссылок необходимых модулей трансляции. Новые модули трансляции в свою очередь пополняют собой собираемую программу. Сборка завершается, когда таким образом обработаны все потребовавшиеся модули трансляции.
Описанный алгоритм сборки подкупает своей простотой и очевидностью. Хотя из-за некоторых особенностей распространенных языков программирования реальная сборка может оказаться несколько тяжеловеснее, тем не менее никаких принципиальных сложностей здесь не возникает.
Существует единственное ограничение, накладываемое на структуру программы для упрощения корневой сборки, и заключается оно в следующем. Входные точки модуля трансляции не должны определяться в сменных модулях, подставляемых прямо или косвенно в его гнезда. Только приняв это ограничение, можно считать, что любая внешняя связь однозначно определяет модуль трансляции, содержащий отвечающую ей входную точку. Неоднозначность определения модуля трансляции привела бы к весьма значительному усложнению алгоритма сборки. Но, к счастью, сформулированное ограничение выглядит достаточно естественным и на практике никак не стесняет разработчика.
4.10.5. Модуль трансляции и сменный модуль. На этих страницах постоянно сталкиваются два понятия: модуль трансляции и сменный модуль. Эти типы модулей заметно отличаются друг от друга.
Модуль трансляции общепризнанный объект операционной среды. Он должен обладать достаточной степенью самостоятельности, позволяющей ему автономно транслироваться и затем участвовать в компоновке. Самостоятельность модуля трансляции обеспечивается прежде всего посредством включения в его текст спецификаций интерфейса. Присутствие спецификаций облегчает, кроме того, независимое изучение текста модуля.
Сменный модуль объект специализированной операционной среды, поддерживающей многовариантность. Все его собратья, принадлежащие одному и тому же вариантному гнезду, имеют идентичный интерфейс. Очевидно, нет необходимости размножать спецификации этого интерфейса в тексте каждого из «братских» сменных модулей их следует «вынести за скобки», записав в единственном экземпляре, например, в окружающем гнездо тексте. При изучении текста сменного модуля средства поддержки многовариантности покажут его в контексте окружения гнезда, так что разработчик сможет увидеть записанные там спецификации.
Итак, модуль трансляции обязан иметь спецификации интерфейса иначе он окажется изолированным от остальной программы. Напротив, сменный модуль, или, иначе, модуль изменяемости, обязан вынести эти спецификации вовне. Отсюда следует, что границы этих модулей не совпадают.
Что лучше: модуль трансляции или сменный модуль? Вопрос, вообще говоря, некорректный каждый хорош на своем месте. Однако отсутствие или слабость средств поддержки многовариантности нередко вынуждают разработчика превращать сменные модули в модули трансляции, дублируя их спецификации. И это весьма огорчительно. Беда здесь не только в потенциальном источнике нарушения целостности фонда из-за неизбежных рассогласований при синхронном редактировании нескольких копий, но и в том, что идентичность интерфейсов модулей трансляции может быть кем-то впоследствии воспринята не как функциональное родство, а всего лишь как случайное совпадение.
Любопытно, что проблема объявления принадлежности модуля определенному однородному семейству эффективно решается на глобальном уровне. Для многократно используемых модулей каждый интерфейс вправе централизовано получить уникальный (среди всех программистов Земли) идентификатор в эпоху повсеместного распространения Интернета не составляет труда смастерить общедоступный генератор идентификаторов интерфейсов. И действительно, такие генераторы реализованы и активно используются.
Но получение глобального идентификатора для каждого вариантного гнезда многовариантной программы представляется нелепым расточительством. К тому же такой подход решил бы лишь незначительную часть проблем поддержки многовариантного программирования, к обсуждению которых мы возвращаемся в следующем разделе.
4.10.6. Предупреждение повторения сборочных работ. Заканчивая разговор о возможных схемах сборки многовариантных программ, вновь подчеркнем, что при любой схеме имеет смысл предусмотреть средства предупреждения повторных препроцессирования, трансляции и компоновки.
Каждый раз перед препроцессированием и трансляцией надо проверить, не формировался и не транслировался ли раньше данный модуль в той же конфигурации, т. е. с тем же составом вставляемых сменных модулей. И если модуль когда-либо обрабатывался, то, конечно, сэкономить время: ни препроцессирования, ни трансляции выполнять не нужно, а следует извлечь из программного фонда ранее полученный объектный модуль.
Те же соображения применимы и к загрузочному модулю. Если поступает запрос на сборку программы с не изменившейся после проведенной ранее сборки конкретной конфигурацией, то теперь уже все этапы сборки (т. е. препроцессирование, трансляция и компоновка) не повторяются, а из программного фонда извлекается готовый загрузочный модуль.
При создании системных средств для предупреждения повторения сборочных работ должны быть так или иначе учтены следующие два аспекта сопутствующего обслуживания вторичных объектов.
Во-первых, при внесении изменений в первичный объект надо выявить и исключить из программного фонда все зависящие от него вторичные объекты, т. е. все объектные и загрузочные модули, порожденные в свое время с участием данного исходного текста.
Во-вторых, имеет смысл предусмотреть какой-либо алгоритм отсева части хранящихся в программном фонде вторичных объектов, и автоматически или вручную запускать его, если этих объектов расплодилось слишком много. Проще всего в этом алгоритме удалять из фонда вторичные объекты, к которым долго не было обращений.
|