Программирование на основе классов и шаблонов

Системы сборки кроссплатформенного программного обеспечения из исходного кода

Аладин Дмитрий Владимирович

iu5edu.ru/wiki/cpp2

План лекции

  1. Этапы создания программы на C++.
  2. Системы сборки.
  3. Процедурная и объектная декомпозиция.
  4. Библиотеки.
  5. CMake.
  6. Современные конвейеры сборки программного обеспечивания.

Как создать программу на C++?

meme

Общий порядок создания программы на C++

1. Создание исходного кода программы

  • Создаем где-то файлы .cpp/.h
  • Нужен текстовый редактор. Круто если будет IDE (integrated development environment, интегрированная среда разработки).

Общий порядок создания программы на C++

2. Создание объектного кода программы

  • Перевод кода с языка высокого уровня в бинарный вид.
  • В итоге получаем файлы с расширением .o или .obj.
  • Нужен компилятор для создания объектного кода.

Общий порядок создания программы на C++

3. Создание исполняемого кода программы

  • Компилятор работает с каждым файлом исходного кода в отдельности. Игнорирует все связи и зависимости файлов между собой (обидно 😢).
  • Поэтому нужно к объектному коду добавить еще объектный код всех связанных элементов.
  • Процесс распознавания всех связей и объединения связанных объектных файлов в одно целое называется компоновкой (линковкой).
  • Получаем исполняемый код, который можем использовать и делиться.
  • Нужен компоновщик.

Общий порядок создания программы на C++

  1. Препроцессор обрабатывает все директивы препроцессора (например, директиву #include)
  2. Компилятор обрабатывает каждый файл с исходным кодом и создает из него объектный файл, который содержит машинный код.
  3. Компоновщик (он же линкер/линковщик) объединяет все объектные файлы в исполняемый файл. Данный процесс называется компоновкой/линковкой

Взято отсюда.

А как это на практике?

Давайте посмотрим как это работает на деле. По ссылке рассказано как это будем делать.

Знай свой компилятор в лицо!

Компилятор преобразует ваш код C++ в двоичные машинные инструкции и адреса, понятные вашему компьютеру.

Наиболее распространенными в зависимости от платформы являются:

  • Visual C++ (aka MSVC) для Windows
  • GNU Compiler Collection (aka GCC) для Linux
  • Clang для macOS

В примерах далее мы будем рассматривать только GCC.

GCC

GCC существует с 1987 года и является предпочтительным компилятором для многих дистрибутивов Unix / Linux. Он поставляется установленным во многих дистрибутивах, расположенных в usr/bin каталог, но если он у вас не установлен, выполните следующие команды:

$ sudo apt-get install gcc
$ sudo apt-get install g++
$ sudo apt-get install libc++abi-dev

Затем вы можете подтвердить, что он был установлен, используя следующую команду:

$ which gcc

Исходный код

#include <iostream>

#define GOODBYE std::cout << "Goodbye World!" << std::endl;

int main()
{
    std::cout << "Hello World!" << std::endl;
    GOODBYE;
#ifdef KIDDING
    std::cout << "Just kidding!" << std::endl;
#endif
}

1. Предварительная обработка

Предварительная обработка (aka Preprocessing) - это первый шаг в процессе компиляции.

Препроцессор - это, по сути, робот для вырезания и вставки, который добавляет, удаляет и заменяет исходный материал в каждом исходном файле. Это работает путем определения директив препроцессора, пронизанных вашим исходным текстом, например #include, #define и #ifdef и выполнение соответствующего преобразования.

1. Предварительная обработка

Наш main.cpp файл имеет несколько разных директив.

В первой строчке #include <iostream>. По данной директиве предпроцессор пойдет искать файл iostream и заменит директиву полным текстом этого файла!!!

Он делает это рекурсивно, поэтому один #include файл может сам #include несколько других файлов!!!

А можно посмотреть результат? x1

$ gcc main.cpp -E -D KIDDING
# 1 "main.cpp"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 414 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.cpp" 2
# 1 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream" 1 3
# 36 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/iostream" 3
# 1 "/Library/Developer/CommandL
...
$ gcc main.cpp -E -D KIDDING | wc -l  
45853

Предварительная обработка macOS привела к преобразованию исходного файла с более 45 853 строк!

2. Компиляция

Компиляция (aka Compilation) - на этом этапе ваш предварительно обработанный код преобразуется в серию инструкций по сборке. Эти инструкции определяются вашим процессором.

А можно посмотреть результат? x2

Сгенерировать только ассемблерный код:

$ gcc main.cpp -S

Посмотрим результат:

$ cat main.s | head  
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 13, 0	sdk_version 13, 1
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #32
	stp	x29, x30, [sp, #16]             ; 16-byte Folded Spill
	add	x29, sp, #16

3. Сборка

Сборка (aka Assembly) - это третий шаг в процессе компиляции. На всех платформах ассемблер берет код ассемблера, сгенерированный во время компиляции, и преобразует его в двоичный код.

А можно посмотреть результат? x3

Сгенерировать бинарный код:

$ gcc main.cpp -c

Посмотрим результат:

$ nm main.o | head 
0000000000000dcc s GCC_except_table11
0000000000000d68 s GCC_except_table4
0000000000000de0 s GCC_except_table40
0000000000000df4 s GCC_except_table45
0000000000000da8 s GCC_except_table7
                 U __Unwind_Resume
0000000000000a28 t __ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE13__get_pointerEv
0000000000000ab0 t __ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE18__get_long_pointerEv
0000000000000ad8 t __ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE19__get_short_pointerEv
0000000000000804 t __ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4dataEv

4. Компоновка

Компоновка (aka Linking) - компилятор объединяет объектные файлы в конечный пакет, который может быть либо исполняемым файлом, либо библиотекой.

P.S. В состав компилятора может входить компоновщик, как это сделано у g++.

  • Если объектный файл ссылается на функции или типы, которые в нем не определены, он просматривает другие объектные файлы, пытаясь найти эти определения.
  • Если определения найдены в другой библиотеке, вам нужно сообщить об этом компилятору (или, скорее, компоновщику).

А можно посмотреть результат? x4

Чтобы связать существующие объектные файлы:

$ gcc main.cpp -c -D KIDDING
$ g++ -o main main.o

Пробуем запустить полученный файл:

$ ./main
Hello World!
Goodbye World!
Just kidding!

Было страшно?

Не было страшно потому, что:

  1. Опустили информацию о том, что делается при компиляции (а там еще те пони 🐎 и единороги 🦄).
  2. Мы не обсудили еще системы сборок (это исправим).
  3. Еще не поговорили про компоновку и библиотеки (и это тоже исправим).

Утилита make

Утилита make предназначена для автоматизации преобразования файлов из одной формы в другую. По отметкам времени каждого из имеющихся объектных файлов (при их наличии) она может определить, требуется ли их пересборка.

Правила преобразования задаются в скрипте с именем Makefile, который должен находиться в корне рабочей директории проекта. Сам скрипт состоит из набора правил, которые в свою очередь описываются:

  1. целями (то, что данное правило делает);
  2. реквизитами (то, что необходимо для выполнения правила и получения целей);
  3. командами (выполняющими данные преобразования).

Makefile

В общем виде синтаксис Makefile можно представить так:

# Отступ (indent) делают только при помощи символов табуляции,
# каждой команде должен предшествовать отступ
<цели>: <реквизиты>
        <команда #1>
        ...
        <команда #n>

То есть, правило make это ответы на три вопроса:

{Из чего делаем? (реквизиты)} ---> [Как делаем? (команды)] ---> {Что делаем? (цели)}

Несложно заметить что процессы трансляции и компиляции очень красиво ложатся на эту схему:

{исходные файлы} ---> [трансляция] ---> {объектные файлы}

{объектные файлы} ---> [линковка] ---> {исполнимые файлы}

Пример Makefile

Для компиляции main.cpp достаточно очень простого Makefile:

main: main.cpp
	gcc main.cpp -c -D KIDDING
	g++ -o main main.o

Данный Makefile состоит из одного правила, которое в свою очередь состоит из цели — main, реквизита — main.cpp, и последовательности команд.

Теперь, для компиляции достаточно дать команду make в рабочем каталоге. По умолчанию make станет выполнять самое первое правило, если цель выполнения не была явно указана при вызове:

$ make <цель>

Makefile для модульной программы

program: program.o mylib.o
        g++ -o program program.o mylib.o

program.o: program.cpp mylib.hpp
        g++ -c program.cpp

mylib.o: mylib.cpp mylib.hpp
        g++ -c hylib.cpp

После запуска make попытается сразу получить цель program, но для ее создания необходимы файлы program.o и mylib.o, которых пока еще нет. Поэтому выполнение правила будет отложено и make станет искать правила, описывающие получение недостающих реквизитов. Как только все реквизиты будут получены, make вернется к выполнению отложенной цели. Отсюда следует, что make выполняет правила рекурсивно.

Про Makefile с уважением подрезано отсюда.

В чем проблема?

Наверное в названии текущей лекции? Где кроссплатформенность?

P.S. В Makefile торчат уши утилит, которые зависят от платформы.

Погружаемся глубже

Из статьи на Habr:

  • Make — [античность] мастодонт и заслуженный ветеран систем сборки, которого все никак не хотят отпустить на пенсию, а заставляют везти на себе все новые и новые проекты.
  • CMake — [средневековье] первая попытка уйти от низкоуровневых деталей make-а. Но, к сожалению, далеко уйти не удалось — движком здесь служит все тот же make для которого CMake генерирует огромные make-файлы на основе другого текстового файла с более выскоуровневым описанием билда.

Погружаемся глубже

  • Ant — [эпоха возрождения] своеобразный клон make для Java. <...> В качестве языка сценария (так же как и для Maven) здесь выбран XML — этот гнусный птичий язык 😆.
    Да еще, дебажить невозможно 🤪
  • SCons — [новые времена] самодостаточная, кросплатформенная билд система, написанная на Python. SCons одинаково хорошо справляется как с Java так и с C++ билдами. Язык сценария сборки — Python.
    Вот теперь рассказывайте о том, что тестирование программ C++ с помощью скриптов Python - это аморально 😃.

9 из 10 стоматологов предпочитают...

Судя по опросу, выбор очевидный!

Чуть позже рассмотрим "вторую" по популярности систему сборки CMake.

А сейчас рассмотрим причину, из-за которой нас системы сборки заинтересовали.

Процедурная и объектная декомпозиция

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

P.S. Спойлеры: что принесло с собой ООП для организации декомпозиции?

Что такое библиотека и какая она бывает?

Библиотека – это пакет кода, который предназначен для повторного использования многими программами. Обычно библиотека C++ состоит из двух частей:

  • заголовочный файл, который определяет функциональность, которую библиотека предоставляет (предлагает) программам, использующим ее;
  • предварительно скомпилированный двоичный файл, который содержит реализацию этой функциональности, предварительно скомпилированную в машинный код.

Существует два типа библиотек:

  • статические библиотеки;
  • динамические библиотеки.

Статическая библиотека

Статическая библиотека (иногда называемая archive, "архив") состоит из подпрограмм, которые скомпилированы и линкуются непосредственно с вашей программой.

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

В Windows статические библиотеки обычно имеют расширение .lib (library, библиотека),
а в Linux – расширение .a (archive, архив).

Динамическая библиотека

Динамическая библиотека (также называемая shared library, "общая библиотека") состоит из подпрограмм, которые загружаются в ваше приложение во время выполнения.

Когда вы компилируете программу, использующую динамическую библиотеку, библиотека не становится частью вашего исполняемого файла – она ​​остается отдельной единицей.

В Windows динамические библиотеки обычно имеют расширение .dll (dynamic link library, библиотека динамической компоновки),
а в Linux – расширение .so (shared object, общий объект).

Подробнее о динамических и статических библиотеках рассказано в лабораторной работе №1.

А причем тут системы сборки?

CMake

Кроссплатформенное программное средство автоматизации сборки программного обеспечения из исходного кода.

Не занимается непосредственно сборкой, а лишь генерирует файлы сборки из предварительно написанного файла сценария CMakeLists.txt и предоставляет простой единый интерфейс управления.

Помимо этого, способно автоматизировать процесс установки и сборки пакетов.

Упрощенный процесс работы CMake

В данной лекции мы рассмотрим только основы по синтаксису и модели программирования сценариев CMake. Основа для этого здесь.

В первой лабораторной работе вы освоите работу CMake для проектов C++.

Привет, мир!

Если вы создаете файл hello.txt со следующим содержимым:

message("Hello world!")         # A message to print

... вы можете запустить его из командной строки с помощью cmake -P hello.txt. -P Опция запускает данный скрипт, но не создает конвейер сборки.

$ cmake -P hello.txt
Hello world!

Все переменные являются строками

В CMake каждая переменная представляет собой строку. Вы можете заменить переменную внутри строкового литерала, окружив ее ${}.

Чтобы определить переменную внутри скрипта, используйте set команду. Первый аргумент - это имя присваиваемой переменной, а второй аргумент - ее значение.

$ cat hello.txt 
message("Hello ${NAME}!")       # Substitute a variable into the message
$ cmake -P hello.txt
Hello ! 
$ cat hello.txt
set(THING "funk")
message("We want the ${THING}!")
$ cmake -P hello.txt
We want the funk!

Имитация структуры данных, используя префиксы

$ cat hello.txt
set(JOHN_NAME "John Smith")
set(JOHN_ADDRESS "123 Fake St")
set(PERSON "JOHN")
message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")
$ cmake -P hello.txt
John Smith lives at 123 Fake St.

Каждое утверждение - это команда

В CMake каждый оператор представляет собой команду, которая принимает список строковых аргументов и не имеет возвращаемого значения. Аргументы разделяются пробелами (без кавычек). Как мы уже видели, set команда определяет переменную в области видимости файла (можно прокидывать переменные "выше" при сборке)

$ cat hello.txt     
math(EXPR MY_SUM "1 + 1")              # Evaluate 1 + 1; store result in MY_SUM
message("The sum is ${MY_SUM}.")
math(EXPR DOUBLE_SUM "${MY_SUM} * 2")  # Multiply by 2; store result in DOUBLE_SUM
message("Double that is ${DOUBLE_SUM}.")
$ cmake -P hello.txt
The sum is 2.
Double that is 4.

Команды управления потоком

Даже операторы управления потоком являются командами. Команды if/endif выполняют вложенные команды условно. Пробелы не имеют значения, но обычно для удобства чтения во вложенных командах делается отступ.

Ниже проверяется, установлена ли встроенная переменная CMake WIN32:

if(WIN32)
    message("You're running CMake on Windows.")
endif()

В CMake также есть while/endwhile команды,

Списки - это просто строки, разделенные точкой с запятой

В CMake есть специальное правило подстановки аргументов без кавычек.

  • Если весь аргумент представляет собой ссылку на переменную без кавычек, а значение переменной содержит точки с запятой, CMake разделит значение на точки с запятой и передаст несколько аргументов заключающей команде.

    set(ARGS "EXPR;T;1 + 1")
    math(${ARGS})      # Equivalent to calling math(EXPR T "1 + 1")
    message("${T}")    # Prints: 2;
    
  • С другой стороны, аргументы в кавычках никогда не разбиваются на несколько аргументов, даже после замены. CMake всегда передает строку в кавычках в качестве одного аргумента, оставляя точки с запятой нетронутыми:

    set(ARGS "EXPR;T;1 + 1")
    message("${ARGS}")  # Prints: EXPR;T;1 + 1
    

Непоказанная магия CMake

  • Функции и макросы с областями видимости
  • Включение других сценариев
  • Получение и настройка свойств

Все это вы узнаете из статьи и лабораторной работы №1.

А теперь страшно?

Современные конвейеры сборки программного обеспечивания

Современный софт собирают на конвейерах CI/CD.

Конвейер CI/CD — это система управления доставкой программного обеспечения для создания, тестирования и предоставления программного обеспечения как услуги.

Вопросы?

questions