9.2 Списки инициализации — munch

Предыдущая  Содержание  Следующая V*D*V

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

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

Эта проблема известна как вызовы статических конструкторов — объекты со временем жизни, равным основной программе, должны быть созданы как только начато выполнение main(). Нет никакой разницы между генерацией статических и динамических объектов. initPoint() и подобные функции упрощают соглашения о вызовах и разрешают вызовы в любом порядке, но фактическая работа в каждом случае выполняется new() и конструкторами.

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

Современные компоновщики, такие как GNU ld, разрешают компилятору составлять массив адресов, куда каждый объектный модуль может внести элементы, поскольку это компонуется в программу. В нашем случае мы могли бы собрать адреса всех функций init в такой массив и изменить main() для их вызова друг за другом. Однако, такая возможность доступна только производителям компилятора, но не пользователям.

Тем не менее, мы должны использовать подсказу. Определяем массив initializers[] и располагаем всё в main() следующим образом:

 

void (* initializers [])(void) = {

    0 };

 

int main () {

    extern void (* initializers [])(void);

    void (** init)(void) = initializers;

 

    while (* init)

        (** init ++)();

    ...

 

Всё что остаётся, это определить каждую функцию инициализации нашей программы как элемент initializers[]. Если есть утилита, подобная nm, которая может распечатать таблицу символов скомпонованной программы, то можно использовать следующий подход для генерации массива автоматически:

 

$ cc -o task object... libooc.a

$ nm -p task | munch > initializers.c

$ cc -o task object... initializers.c libooc.a

 

Мы предполагаем, что libooc.a это библиотека с модулем initializers.o, который определяет массив initializers[] как показано выше, содержащий только завершающий нулевой указатель. Модуль библиотеки используется компоновщиком только если данный массив не был определён в модуле, предшествующем libooc.a, в командной строке, вызывающей компилятор.

nm распечатывает таблицу символов task, полученную в результате первой компиляции. munch небольшая программа, генерирующая новый модуль initializers.c, который ссылается на все функции init в task. Во время второй компиляции чтобы определить надлежащие initializers[] для task компоновщик использует этот модуль, а не модуль по умолчанию от libooc.a.

Вместо массива munch мог бы генерировать функцию, вызывающую все функции инициализации. Однако, как мы увидим в главе 12, список классов в особенности может использоваться не только для инициализации, но направляться на другие использования.

То, что выводит nm обычно зависит от торговой марки используемого UNIX. К счастью, опция -p даёт команду nm в Berkeley распечатывать в порядке таблицы символов, а nm в System-V создать краткий выходной формат, который, как оказывается, почти аналогичен на выводу nm в Berkeley. Вот munch для обоих, реализованный с использованием awk:

 

NF != 3 || $2 != "T" || $1 !˜ /ˆ[0-9a-fA-F]+$/ {

    next

}

$3 ˜ /ˆ_?init[A-Z][A-Za-z]+$/ {

    sub(/ˆ_/, "", $3)

    names[$3] = 1

}

END {

    for (n in names)

        printf "extern void %s (void);\n", n

 

    print "\nvoid (* initializers [])(void) = {"

    for (n in names)

        printf "\t%s,\n", n

    print "0 };"

}

 

Первое условие быстро отбрасывает все записи таблицы символов за исключением таких как

 

00003ea8 T _initPoint

 

Предполагая, что имя, начинающееся с init и последовательности букв, начинающейся с заглавной буквы, относится к функции инициализации, необязательное начальное подчеркивание отбрасывается (одни компиляторы создают его, другие нет), а остальное сохраняется как индекс массива names[]. Как только все имена найдены, munch генерирует декларации функций и определяет initializers[].

Массив names[] используются из-за того, что каждое имя должно быть эмиттировано дважды. Имена сохраняются как индексы, а не значения элементов, чтобы избежать дублирования. (* Дублирование должно быть невозможным изначально, потому что глобальная функция не может быть определена дважды в одной и той же программе, но всегда лучше сделать безопасно, чем потом пожалеть.) munch может даже использоваться для генерации модуля по умолчанию для этой библиотеки:

 

$ munch < /dev/null > initializers.c

 

munch во многих отношениях нелеп: требуется два запуска компоновщика для правильной компоновки задачи; требуется вывод таблицы символов, подобный получаемому от nm, и разумный выходной формат; и, что худшее из всего, полагается на шаблон для выбора функций инициализации. Однако, munch обычно очень легко переносим, а шаблон выбора может быть адаптирован ко множеству проблем статических конструкторов. Не удивительно, что система AT&T C++ была реализована для некоторых машин с помощью (сложного) варианта munch.

 

Предыдущая  Содержание  Следующая