
LINUX / KOZLOV2_1
.pdfПервая пара файлов есть интерфейс и реализация метода, выводящего в консоль некоторые сообщения.
1 // file: lib1.h
2
3#ifndef LIB1_H
4#define LIB1_H
6void present_lib_1();
8#endif // LIB1_H
1:комментарий, сообщающий имя файла;
3, 8: так как в языке С++ существует правило одного определения, необходимо защититься от множественного включения данного файла. Множественное включение может произойти, если с помощью директивы #include данный файл будет подключен более одного раза в каком-то другом файле. Встретив директиву #ifndef, препроцессор включит исходный код, заключенный между строками #ifndef LIB1_H/#endif, только в том случае, если символьная метка LIB1_H не была определена;
4: если препроцессор дошел до этой строки, то файл включается первый раз и это необходимо отметить во избежание повторного включения. Директива #define определяет символьную метку LIB1_H;
6: объявление функции present_lib_1. Функция принимает нуль аргументов и ничего не возвращает.
Рассмотрим пример: 1 // file: lib1.cpp
2
3#include «src/module1/lib1.h»
4#include <iostream>
5
6void present_lib_1()
7{
8std::cout << «I'm library number 1\n»;
9#ifdef MOM
10std::cout << «Library number 1, MOM-macro was defined\n»;
11#endif
12}
181
PDF created with pdfFactory Pro trial version www.pdffactory.com
1: комментарий, сообщающий имя файла. Данный файл содержит реализацию описанной в файле lib1.h функции present_lib_1;
3:подключаем объявление реализуемой функции;
4:так как реализация функции present_lib_1 использует вывод в стандартный поток cout (консоль), то по стандарту необходимо подключить соответствующий файл стандартной библиотеки;
8:вывод (<<) в консоль (std::cout) строки «I'm library number 1»;
9, 11: если данный файл будет скомпилирован с макросом MOM, то все, что заключено между #ifdef MOM/#endif, попадет в исполняемый файл, иначе – нет;
10:вывод (<<) в консоль (std::cout) строки «Library number 1, MOM-macro was defined».
В функции main производятся вызовы функций present_lib_1 и present_lib_2: 1 // file: main.cpp
2
3#include «src/module1/lib1.h»
4#include «src/module2/lib2.h»
6int main()
7{
8present_lib_1();
9present_lib_2();
10
11 return 0;
12 }
1:комментарий, сообщающий имя файла;
3:так как в теле main производится вызов функции present_lib_1, то по стандарту нужно подключить заголовочный файл, в котором она объявлена;
4:аналогично строке 3, но для функции present_lib_2;
6: одна из стандартных сигнатур функции main (принимает нуль аргументов, возвращает целое число);
8:вызов present_lib_1;
9:вызов present_lib_2;
11: возврат 0 (успешное завершение).
Второй модуль имеет аналогичное содержание с точностью до переименования 1 в 2, MOM в MTM и отсутствия main:
182
PDF created with pdfFactory Pro trial version www.pdffactory.com
1 // file: lib2.h
2
3#ifndef LIB2_H
4#define LIB2_H
6void present_lib_2();
8#endif // LIB2_H
1// file: lib2.cpp
3#include «src/module2/lib2.h»
4#include <iostream>
6void present_lib_2()
7{
8std::cout << «I'm library number 2\n»;
9#ifdef MTM
10std::cout << «Library number 2, MTM-macro defined\n»;
11#endif
12}
Как отмечалось ранее, утилита make использует файлы, в которых описаны зависимости. Для того чтобы явно указать make-файл, имеется опция -f. Кроме тог, разработчиками предусмотрены зарезервированные имена make-файлов, которые будут рассматриваться, если не использована опция -f. Эти имена в порядке приоритета:
GNUmakefile, makefile и Makefile. Поэтому, если мы введем, находясь в папке build-system,
в командной строке make, утилита использует файл Makefile. В этом файле заданы имя проекта, имена модулей, из которых он состоит, подключаются описания модулей, а также правила генерации объектного и исполняемого кода:
1ifeq ($(Root),)
2Root := $(shell cd ../;pwd)
3endif
4
5 Project_Name := proj
6
7 Modules := \
183
PDF created with pdfFactory Pro trial version www.pdffactory.com
8Module1 \
9Module2
10
11 include $(Root)/build-system/tools.mk
12
13 $(foreach m,$(Modules),$(eval include $(Root)/build-system/$(m).mk)) 14
15 include $(Root)/build-system/linking.mk
16 include $(Root)/build-system/targets.mk
Встроке 1 проверяется, задана ли переменная Root (путь до корневого каталога, в котором находятся папки src и build-system). Для получения значения переменной используется оператор $(...). Значение неопределенной переменной – это пустая строка.
Встроке 2 переменной Root присваивается значение, полученное от функции shell, которая позволяет выполнять команды оболочки прямо из make-файлов.
Встроке 5 задается имя проекта.
Встроке 7 вводится переменная Modules, которой присваиваются имена входящих в проект модулей. Символ «\» предписывает make рассматривать строки 8 и 9 как продолжение строки 7. Переменная Modules фактически является массивом, в котором первый элемент равен Module1, второй – Module2.
Встроке 11 выполняется директива include. Make заменяет директиву на полный код файла tools.mk, прерывает обработку текущего файла, переходит к обработке кода файла tools.mk и всех его «вложений». Важно понимать, что все переменные, определенные и переопределенные в подключаемом файле, будут доступны по завершении «встраивания» в файле-инициаторе.
Встроке 13 выполняется цикл. Переменная m последовательно принимает значения, равные элементам «массива» Modules, затем вызывается функция eval. В данном случае работа связки foreach-eval, с учетом значения переменной Modules эквивалентна паре строк:
include $(Root)/build-system/Module1.mk include $(Root)/build-system/Module2.mk
Подключаемые файлы содержат правила для компиляции отдельных модулей проекта. Если понадобится добавить новый модуль, будет достаточно создать файл с описанием нового модуля и добавить это имя к «массиву» Modules.
Встроках 15 и 16 подключается еще пара файлов.
Теперь перейдем к содержимому подключаемых файлов.
184
PDF created with pdfFactory Pro trial version www.pdffactory.com
$(Root)/build-system/tools.mk - файл с именами и опциями используемых утилит:
1CXX := g++ -c
2LD := g++
3DEPTOOL := g++ -MM
5Output_Flag := -o
6Include_Flag := -I
Вэтом файле определяются переменные, которые задают имена утилит компиляции (строка 1), компоновки (строка 2), построения зависимостей (строка 3) и их опций. Использование переменных позволяет легко «менять» утилиты. $(Root)/build-system/Module1.mk - описание первого модуля:
1Module_Name := Module1
2
3Sources := \
4src/module1/lib1.cpp \
5src/module1/main.cpp
7CXXFLAGS := -DMOM
9include $(Root)/build-system/compilation.mk
$(Root)/build-system/Module2.mk - описание второго модуля: 1 Module_Name := Module2
2
3Sources := \
4src/module2/lib2.cpp
6CXXFLAGS := -DMTM
8include $(Root)/build-system/compilation.mk
Для каждого модуля задается имя, перечисляются исходные файлы, его составляющие, задаются флаги компиляции и подключается в обоих случаях один и тот же файл. Идея проста: в файлах задано только описание содержимого модулей с помощью заранее определенных переменных.
В файле $(Root)/build-system/compilation.mk на основе этих переменных строятся правила компиляции:
185
PDF created with pdfFactory Pro trial version www.pdffactory.com
1Objs := $(addprefix $(Root)/build/,$(Sources:.cpp=.o))
2IncludePath := $(addprefix $(Include_Flag),$(Root))
4$(foreach s,$(Objs),$(eval $(s)_cxxflags := \
5$(CXXFLAGS) $(IncludePath)))
7$(Objs): $(Root)/build/%.o: $(Root)/%.cpp
8@$(shell mkdir -p $(@D))
9@$(eval _curObjAbs := $(subst /,\/,$@))
10@$(eval _curObjNm := $(@F))
11@$(eval _curDepAbs := $(_curObjAbs:.o=.d))
12@$(DEPTOOL) $($@_cxxflags) $< | \
13sed 's/$(_curObjNm):/$(_curObjAbs):/g' > \
14$(_curDepAbs:.d=.t)
15@sed -e 's/$(_curObjAbs)://g' -e 's/\\//g' \
16-e 's/ //g' -e 's/[^ ].*/&: /g' \
17$(_curDepAbs:.d=.t) > $(_curDepAbs)
18@cat $(_curDepAbs:.d=.t) >> $(_curDepAbs)
19@rm -f $(_curDepAbs:.d=.t)
20$(CXX) $($@_cxxflags) $< $(Output_Flag) $@
22Deps := $(Objs:.o=.d)
23-include $(Deps)
25$(Module_Name)_mo := $(Objs)
27Interm := $(Interm) $(Deps) $(Objs)
Встроке 1 переменной Objs присваивается значение, возвращаемое функцией addprefix, которая добавляет приставку $(Root)/build/ к каждому элементу «массива» Sources, в котором предварительно заменено каждое вхождение подстроки .cpp на .o. Таким образом, Objs - это «массив» содержащий абсолютные имена объектных файлов.
Встроке 2 абсолютный путь до корня сборки определяется соответствующей опцией компилятора и запоминается в переменную IncludePath. С помощью этой переменной мы укажем компилятору, где искать написанные нами заголовочные файлы (lib1.h и lib2.h).
186
PDF created with pdfFactory Pro trial version www.pdffactory.com
Как отмечалось ранее, make обрабатывает файлы в два прохода (построение графа зависимостей/выполнение правил). Рассмотрим пример:
variable := first target: prerequisites
@echo $(variable) variable := second
В данном случае на экран будет выведено: second.
Этот небольшой пример призван проиллюстрировать проблему, связанную с флагами компилятора, т.е. со значением переменной CXXFLAGS. В описании каждого модуля ей присваиваются свои значения, но использоваться она будет только при втором проходе, поэтому будет иметь значение, определенное в последнем подключенном файле.
Встроках 4 и 5 проблема решается путем создания дополнительных переменных с уникальными именами и значением CXXFLAGS данного модуля. Для каждого объектного файла текущего модуля заводится уникальная переменная с именем {абсолютный путь объектного файла + суффикс}. Например $(Root)/src/module1/src1.o_cxxflags.
Встроке 7 используется идиома Static Pattern Rules. К каждому элемента «массива» Objs применяется шаблон $(Root)/build/%.o, затем найденная часть «%» подставляется в $(Root)/%.cpp. Получаются правила вида:
$(Root)/build/%.o: $(Root)/%.cpp
Набор команд для этих правил строится по шаблону, определенному в строках 8 – 20. Строка 8 начинается с символа «@», который предписывает make не выводить команду, указанную в этой строке, на экран. Сама команда создает средствами оболочки папку, в которую будет помещен объектный файл. Так как объектный файл есть цель этого правила, то к имени папки, в которой он будет создан, можно обратиться с помощью специальной переменной «@D».
Для генерации зависимости объектного файла от соответствующих ему исходного и заголовочных файлов, используемых в исходном, привлекается компилятор g++.
Для того чтобы перевести компилятор в режим генерации зависимостей, используется опция -MM. g++ генерирует зависимости в виде make-правила без команд. Например, если в файле main.cpp подключены lib1.h и lib2.h, результат будет следующим:
main.o: main.cpp lib1.h lib2.h
Если же передать g++ (опция -I) полные пути до папок, в которых находятся файлы с исходным кодом, то будет построено правило с использованием полных путей до этих файлов. К сожалению, нам неизвестно, как заставить g++ использовать полный путь до объектного файла. Поэтому придется проделать постобработку результатов работы g++.
187
PDF created with pdfFactory Pro trial version www.pdffactory.com
Встроке 9 создается временная переменная, содержащая абсолютное имя объектного файла. Так как абсолютное имя файла – это цель данного правила, то к нему можно обратиться с помощью специальной переменной «@». Для постобработки мы будем использовать потоковый редактор sed. У редактора sed символ «/», используемый для разделения папок в абсолютном имени, имеет особое значение, поэтому мы его экранируем. Функция subst заменяет каждое вхождение «/» на экранированный вариант
«\/».
Встроке 10 создается временная переменная, содержащая имя объектного файла
(используется автоматическая переменная «@F»). Ясно, что, когда объектного файла не существует, нам неважно, изменялись или нет файлы, на основе которых он создавался, поэтому учет зависимостей в данной ситуации ничего не даст. Компилятор в любом случае придется вызвать. А вот если объектный файл существует, то надо учитывать зависимости. Таким образом, зависимости необходимо обновлять не для текущего акта сборки проекта, а для следующего.
Встроке 11 создается временная переменная, содержащая абсолютное имя файла, содержащего правило, созданное генератором зависимостей.
Встроке 12 вызывается генератор зависимостей, ему передаются восстановленные флаги компиляции $($@_cxxflags) и имя файла с исходным кодом (так как этот файл – первая предпосылка в правиле, то к нему можно обратиться посредством переменной «<»). Вывод генератора обрабатывается редактором sed, который дополняет имя объектного файла до абсолютного и записывает результат во временный файл.
Так как мы используем специальные файлы с описанием зависимостей, то может возникнуть специфическая ситуация, когда эти зависимости утратили свою актуальность. Например, в период с последней генерации файла с зависимостями было исключено использование в соответствующем файле с исходным кодом какого-либо интерфейсного файла, причем этот файл был либо перемещен, либо удален. При анализе правила make проверяет существование файлов-предпосылок и, если не может их найти в указанном месте, завершает работу с ошибкой. Это ограничение нужно обойти.
Вслучае, если в правиле указан несуществующий файл как цель и нет ни предпосылок, ни команд, make рассматривает все цели, зависящие от этого файла, как требующие обновления. Значит, нужно создать набор таких правил, в котором в качестве целей будут все файлы, от которых зависит объектный файл.
Встроках 15 – 17 создается такой набор правил. Мы подаем на вход sed описание зависимостей, сформированное парой строк выше. Удаляем из него упоминание об объектном файле 's/$(_curObjAbs)://g', стираем символы продолжения строки 's/\\//g',
188
PDF created with pdfFactory Pro trial version www.pdffactory.com
убираем все пробелы 's/ //g' и формируем правила указанного вида 's/[^ ].*/&: /g', записываем эти правила в выходной файл с зависимостями.
Встроке 18 объединяется вывод генератора зависимостей с набором правил, сформированных в ходе выполнения строк 15 – 17.
Встроке 19 удаляется временный файл.
Встроке 20 вызывается компилятор с флагами $($@_cxxflags), подается на его вход файл с исходным кодом $<, указывается $(Output_Flag) имя выходного файла $@.
Встроке 22 запоминаются имена файлов с сгенерированными зависимостями.
Встроке 23 подключаются файлы с зависимостями. Знак «–» перед директивой include указывает на то, что существование этих файлов не обязательно.
Встроке 25 сохраняются абсолютные имена объектных файлов в специальную переменную, имя которой зависит от имени модуля.
Встроке 27 запоминаются имена файлов, которые будут созданы в ходе выполнения правил этого файла.
Теперь перейдем к файлу $(Root)/build-system/linking.mk – правила для компоновки объектных файлов, входящих в проект:
1 ModulesObjs :=
2$(foreach m,$(Modules),$(eval ModulesObjs := $(ModulesObjs) \
3$($(m)_mo)))
4
5 $(Root)/build/$(Project_Name): $(ModulesObjs)
6@$(shell mkdir -p $(@D))
7$(LD) $(LDFLAGS) $^ $(Output_Flag) $@
9Interm := $(Interm) $(Root)/build/$(Project_Name)
Встроках 1 – 3 извлекаются абсолютные имена объектных файлов, из которых состоит проект.
Встроке 5 определяется правило, согласно которому исполняемый файл проекта зависит от объектных файлов модулей.
Встроках 6 и 7 задаются команды для компоновки проекта. Сначала создается выходная директория, затем вызывается компоновщик. Переменная с именем «^» содержит все предпосылки, перечисленные через пробел.
Встроке 9 запоминаются имена создаваемых в этом файле выходных файлов.
189
PDF created with pdfFactory Pro trial version www.pdffactory.com
Итак, осталось неосвещенным содержимое только одного файла. Утилите make необходимо как-то сказать, что именно мы хотим получить из того многообразия целей, что описали выше.
$(Root)/build-system/targets.mk:
1.DEFAULT_GOAL := all
2.PHONY: all
3
4 all: $(Root)/build/$(Project_Name)
5
6.PHONY: clean
7clean:
8rm -rf $(Interm)
Встроке 1 указывается на то, что целью по умолчанию является all.
Встроке 2 говорится, что цель all не является каким-либо файлом, создаваемым на диске.
Встроке 4 цель all зависит от исполняемого файла проекта, таким образом, указывая all целью, мы призываем make собрать проект.
Аналогично цель clean описывает процесс удаления всех файлов, создаваемых при сборке проекта. Именно для этого мы запоминали в переменную Interm все создаваемые в процессе сборки файлы.
Соберем проект (находясь в папке build-system): $ make
g++ -c -DMOM -I/home/diss/Book/Build_system/example /home/diss/Book/Build_system/example/src/module1/lib1.cpp -o /home/diss/Book/Build_system/example/build/src/module1/lib1.o g++ -c -DMOM -I/home/diss/Book/Build_system/example /home/diss/Book/Build_system/example/src/module1/main.cpp -o /home/diss/Book/Build_system/example/build/src/module1/main.o g++ -c -DMTM -I/home/diss/Book/Build_system/example /home/diss/Book/Build_system/example/src/module2/lib2.cpp -o /home/diss/Book/Build_system/example/build/src/module2/lib2.o g++ /home/diss/Book/Build_system/example/build/src/module1/lib1.o /home/diss/Book/Build_system/example/build/src/module1/main.o /home/diss/Book/Build_system/example/build/src/module2/lib2.o -o /home/diss/Book/Build_system/example/build/proj
Вывод свидетельствует о том, что проект собрался успешно.
190
PDF created with pdfFactory Pro trial version www.pdffactory.com