Продолжим изучать утилиту Make, которая, благодаря использованию сценариев, помогает очень эффективно автоматизировать процесс сборки файлов из исходных текстов. Процесс автоматизируется и нам не нужно вводить кучу команд.
Оказывается, Make может ещё много чего и на данном уроке мы в этом убедимся.
Make наряду с использованием переменных, к которым мы также вернёмся, умеет использовать также и функции, причём кроме того, что мы сами можем писать функции, а затем их использовать в сценарии, также существует ряд готовых функций, которые также помогают нам облегчить процесс сборки и настройки проекта.
Начнём с таких функций, которые мы можем писать сами в сценарии, а затем их использовать подобно тому, как мы их используем в языках программирования.
А прежде чем начнём, нам нужен будет проект, который мы сделаем из проекта прошлого урока с именем MYPROG33 и назовём его MYPROG34.
Откроем наш проект в Eclipse и откроем сразу же Makefile, так как работать на данном уроке мы будем только с ним.
Объявляются функции при помощи спецификатора define, затем со следующей строки идёт тело функции, окончанием которого служит спецификатор endef.
Также в функциях могут быть аргументы.
Вызываются функции с помощью ключевого слова call
$(call function, arg1, arg2, …)
А в теле функции аргументы уже используются с помощью использования переменных $1, $2 и т.д.
Давайте в нашем файле объявим функцию с простейшим телом после цели all
1 2 3 4 |
define build-obj-arith $(CC) -Iinc -O0 -g3 -Wall -c src/arith.c $(CC) -Iinc -masm=intel -g3 -Wall -c src/arith.c -S endef |
Телом функции является тело цели arith.o.
А в цели arith.o мы сначала удалим тело (вообще-то называется «команды», но я как-то так привык)
$(CC) -Iinc -O0 -g3 -Wall -c src/arith.c
$(CC) -Iinc -masm=intel -g3 -Wall -c src/arith.c -S
И вместо этого вызовем нашу функцию
1 2 |
arith.o: src/arith.c inc/arith.h $(call build-obj-arith) |
Проект наш по прежнему отлично соберётся, только желательно перед каждой такой сборкой его очищать, так как если зависимости не обновлены, то цель не будет собираться.
Аналогичные вещи проделаем также и с целями utils.o и student.o.
Сначала добавим функции после функции build-obj-arith
1 2 3 4 5 6 7 8 9 |
define build-obj-utils $(CC) -Iinc -O0 -g3 -Wall -c src/utils.c $(CC) -Iinc -masm=intel -g3 -Wall -c src/utils.c -S endef define build-obj-student $(CC) -Iinc -O0 -g3 -Wall -c src/student.c $(CC) -Iinc -masm=intel -g3 -Wall -c src/student.c -S endef |
А затем также удалим тела данных целей и вместо этого вызовем наши функции
1 2 |
utils.o: src/utils.c inc/utils.h $(call build-obj-utils) |
1 2 |
student.o: src/student.c inc/student.h $(call build-obj-student) |
Проект по-прежнему отлично собирается.
Также если мы хотим наш проект запустить, то нам для этого нужны аргументы командной строки, не забываем их добавить в настройках запуска, как мы делали в предыдущем занятии.
Также в сценариях мы можем использовать готовые функции, например, для обработки строк и для прочих нужных действий. Таких функций очень много и все мы их проходить не будем, лишь некоторые, которые сейчас нам могут облегчить жизнь.
Такие функции вызываются по-другому, без ключевого слова call, также перед первым аргументом запятая не ставится
$(function arg1, arg2, …)
Немного усложним задачу, так как использование функции без аргументов не совсем оправдано, так как наши 3 функции никакой, в принципе, экономии в коде не дали.
Удалим объявления всех наших трёх функций вместе с телами и вместо этого добавим одну общую, теперь мы будем имя цели передавать в аргументе (именно имя, без расширения)
1 2 3 4 |
define build-obj $(CC) -Iinc -O0 -g3 -Wall -c src/$(addsuffix .c,$1) $(CC) -Iinc -masm=intel -g3 -Wall -c src/$(addsuffix .c,$1) -S endef |
Как мы видим, здесь уже используется аргумент, причём данный аргумент сам является аргументом функции addsuffix, которая своим именем нам уже говорит о том, что к строковому значению аргумента мы добавляем ещё что-то, в нашем случае это расширение, которое и является первым аргументом, а вторым аргументом уже стоит аргумент нашей функции — имя цели.
Тела наших трёх целей мы теперь также изменим, там будет использоваться уже одна и та же функция, но в неё теперь будут переданы в качестве аргументов имена целей (или файлов) без расширений. Наши цели теперь будут выглядеть вот таким образом
1 2 3 4 5 6 7 8 |
arith.o: src/arith.c inc/arith.h $(call build-obj, arith) utils.o: src/utils.c inc/utils.h $(call build-obj, utils) student.o: src/student.c inc/student.h $(call build-obj, student) |
Проверим, что проект наш по прежнему отлично собирается.
Отдохнём немного от функций и займёмся переменными.
Оказывается, что помимо обычных переменных, make умеет работать с автоматическими переменными.
Таких переменных не так много, но их набор вполне достаточен. Их всего 4
- $@ — имя цели
- $< — имя первой зависимости
- $? — имена всех зависимостей, которые новее чем цель
- $^ — имена всех зависимостей цели
Перепишем теперь тело нашей функции с использованием автоматической переменной
1 2 3 4 |
define build-obj $(CC) -Iinc -O0 -g3 -Wall -c $< $(CC) -Iinc -masm=intel -g3 -Wall -c $< -S endef |
Мы используем здесь значение первой зависимости, а это файл с исходным кодом, который здесь и требуется.
Теперь аргументы в вызове функции нам будут не нужны, поэтому возвращаем наши цели к исходному состоянию, убрав аргументы
1 2 3 4 5 6 7 8 |
arith.o: src/arith.c inc/arith.h $(call build-obj) utils.o: src/utils.c inc/utils.h $(call build-obj) student.o: src/student.c inc/student.h $(call build-obj) |
Проверяем работу и движемся дальше.
Давайте имя конечного исполняемого файла вынесем в отдельную переменную
1 2 |
CC = gcc APP = myprog34.exe |
Заметим. что в основной цели all у нас в команде перечисляются все зависимости
all: main.o arith.o utils.o student.o
$(CC) -o myprog34.exe main.o arith.o utils.o student.o
А ведь для этого у нас также есть автоматическая переменная, поэтому применим её, а заодно и нашу переменную для имени конечного файла.
Теперь тело главной цели будет выглядеть вот так, думаю это красивее смотрится, чем раньше
1 2 |
all: main.o arith.o utils.o student.o $(CC) -o $(APP) $^ |
Проверяем, идём далее.
Давайте применим нашу функцию ещё и для цели создания файла main.o, в принципе нам все заголовочные файлы в ней отслеживать необязательно. Это будет нужно для дальнейшей автоматизации проекта. Цель примет теперь вот такой вид
1 2 |
main.o: src/main.c inc/main.h $(call build-obj) |
Идём дальше.
Давайте познакомимся с ещё некоторыми интересными функциями. Одна из них — wildcard. Данная функция в качестве аргумента использует файловый шаблон, который мы применяем в поиске файлов и каталогов.
Для примера давайте исследуем состав каталога src. Для этого объявим сначала соответствующую переменную
1 2 |
APP = myprog34.exe SRC_DIR = src/ |
Далее добавим переменную, в которую поместим с помощью вышеуказанной функции состав каталога
1 2 3 |
SRC_DIR = src/ src_files := $(wildcard $(SRC_DIR)*) |
И в главной цели выведем в консоль значение данной переменной
1 2 |
all: main.o arith.o utils.o student.o @echo $(src_files) |
Посмотрим, что у нас вывелось
src/utils.c src/main.c src/student.c src/arith.c
Это и есть все файлы нужного нам каталога, причём вывелись они вместе с именем самого каталога.
Отлично!
Есть ещё одна интересная функция — dir, с помощью которой мы можем получить имя каталога из строки
$(dir имена…)
Давайте в следующую переменную запишем каталоги наших найденных файлов с помощью данной функции
1 2 |
src_files := $(wildcard $(SRC_DIR)*) src_dir := $(dir $(src_files)) |
Выведем в теле основной цели также значение этой переменной в консоль
1 2 |
@echo $(src_files) @echo $(src_dir) |
В результате мы получим вот что
src/ src/ src/ src/
Не очень красиво, но тем не менее мы нашли в строке с файлом с путём каталог, в котором находится файл, вернее каталоги всех файлов, а так как они находятся в одном, то произошло вот такое вот учетверение.
Оказывается, есть ещё и функция, обратная по значению — notdir, которая, наоборот, отсекает имя каталога из строки, а оставляет только имя файла с расширением.
Исправим нашу строку с присвоением
files := $(notdir $(src_files))
Также изменим имя переменной в выводе в консоль
@echo $(files)
А вот и результат
utils.c main.c student.c arith.c
Неплохо.
Также есть интересная функция, которая оставит от файла только его базовое имя (без расширения). Эта функция носит имя basename. Применяется аналогичным образом
$(basename имена…)
Давайте с помощью данной функции уберём расширение от имени файла, добавив в нашу строку с присвоением использование такой функции
files := $(basename $(notdir $(src_files)))
Значение, полученное с помощью функции notdir, мы обработали здесь ещё функцией basename.
Вот результат
utils main student arith
Ещё одна интересная функция — это функция addsuffix. С помощью неё мы можем наоборот к имени файла добавить справа требуемое расширение
$(addsuffix суффикс,имена…)
Давайте теперь к полученным именам добавим другое расширения в той же строке с присвоением, заменив заодно и имя переменной
obj_files := $(addsuffix .o, $(basename $(notdir $(src_files))))
Как мы видим, у нас здесь используются сразу три функции, при помощи которых обрабатывается наша исходная переменная.
Прошу заметить, что данные функции работают не просто со строкой, а именно со списком строк, разделённых пробелами.
Также исправим имя переменной в выводе в консоль
@echo $(obj_files)
Проверим как работают наши функции
utils.o main.o student.o arith.o
Отлично! Мы получили список объектных файлов, которые нам надо собрать.
Теперь мы можем данный список спокойно использовать в нашей главной цели в качестве зависимостей
all: $(obj_files)
Далее самое интересное.
Существует поддержка шаблонов, которые обозначаются в виде знака процента — %. Используя шаблон, мы можем теперь автоматизировать наш процесс наших четырёх целей сборки объектных файлов, объединив их в одну цель.
Удалим эти четыре цели и добавим теперь только одну
1 2 3 |
%.o: src/%.c inc/%.h $(call build-obj) |
Красиво! Но дело не в красоте. Мы теперь не видим нигде в сценарии имён наших файлов. Это даёт нам возможность добавлять модули в наши каталоги inc и src, не как не исправляя Makefile, то есть он их подхватит теперь автоматически. Проверим ещё раз, как собирается наш проект и попробуем добавить теперь ещё один модуль.
Перед тем, как мы его добавим, вот эти строки мы можем смело удалить
@echo $(src_files)
@echo $(obj_files)
Давайте функции print_str, print_chars и print_uint32_arr объединим в отдельный модуль, чтобы убрать их из модуля main.
Создадим модуль с именем print_user, для этого сначала создадим в каталоге inc заголовочный файл с именем print_user.h следующего содержания
1 2 3 4 5 6 7 8 |
#ifndef PRINT_USER_H_ #define PRINT_USER_H_ //------------------------------------------------ #include <stdio.h> #include <string.h> //------------------------------------------------ //------------------------------------------------ #endif /* PRINT_USER_H_ */ |
Также, соответственно, в каталоге src создадим файл print_user.с следующего содержания
1 2 |
#include "print_user.h" //---------------------------------------------- |
Перенесём в данный файл из файла main.c вышеназванные функции вместе с телами, удалив их из main.c.
Создадим на данные функции в заголовочном файле прототипы и в файле main.c подключим наш новый модуль
1 2 |
#include "student.h" #include "print_user.h" |
В файле main.c также удалим прототипы данных функций
void print_str(const char *c_str);
void print_chars(const char *c_str);
void print_uint32_arr(const unsigned int *p_uint, unsigned int len);
В функции main() объявим локальный символьный массив
1 2 3 |
int main(int argc, char *argv[]) { char str1[30] = {}; |
В самом конце функции main() перед возвратом протестируем наши функции, которые мы перенесли в отдельный модуль
1 2 3 4 5 6 7 8 9 |
printf("Позиция не найдена\n"); } } strcpy(str1,"Hello, World!!!"); print_str(str1); print_chars(str1); unsigned int a[10] = {0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, \ 0x88888888, 0x99999999, 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC}; print_uint32_arr(a, 10); |
Очистим проект и заново его соберём, хотя очищать не обязательно.
Мы видим, что наш новый модуль в процессе сборки также полноправно участвует
gcc -Iinc -O0 -g3 -Wall -c src/print_user.c
gcc -Iinc -masm=intel -g3 -Wall -c src/print_user.c -S
Запустим наш проект и увидим, что все 3 функции прекрасно работают
Итак, на данном уроке мы расширили свой кругозор в области использования утилиты сборки проекта make, научились использовать автоматические переменные, готовые полезные функции, также научились писать и использовать свои функции, убедившись в том, что использование данных возможностей намного облегает процесс сборки проекта и также, что немаловажно, помогает его автоматизировать.
Всем спасибо за внимание!
Предыдущий урок Программирование на C Следующий урок
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Уважаемый АВТОР! Вы как то говорили, что может быть будете делать уроки по плюсам C++.
Или уже передумали? если же нет, то когда планируете запустить проект по C++?
К сожалению, я пока даже по Си не успеваю.
Просьба, если есть возможность сделать урок по конечным автоматам с AVR и на Си.
Спасибо.
Спасибо. Ничего подобного я не видел в инете. Вы научили меня любить Си. Жду с нетерпением продолжения.