Урок 14. Библиотеки
Привет! На этом уроке мы сделаем заключительный штрих в архитектуре нашего проекта: сделаем отдельную библиотеку математических функций и подключим её к нашему проекту.
Что такое библиотека?
Библиотека - это набор заранее написанных и готовых к использованию функций или других компонентов. Библиотеки используются для упрощения и ускорения процесса разработки программного обеспечения. Т.е. библиотека - это код, который кто-то написал и протестировал до тебя, ты можешь подключить этот код к своему проекту и тем самым сэкономить недели, месяцы, а иногда годы на написание и тестирование аналогичного кода.
Умение работать с библиотеками - важнейший навык современного разработчика: практически для любого функционала уже реализована библиотека, которая находится в открытом доступе, так называемый open source. За любым open source проектом стоит либо команда разработчиков, либо даже целая компания (например, репозиторий библиотеки gRPC по факту координирует Google). У любого open source проекта сотни или даже тысячи пользователей по всему миру, которые не только тестируют код библиотеки, но и занимаются его развитием.
Современным разработчикам остается лишь подобрать подходящую библиотеку и заниматься реализацией бизнес-логики продукта.
Какие бывают виды библиотек?
- Статическая библиотека. Заранее собранный код библиотеки добавляется в исполняемый файл.
- Динамическая библиотека. Загружается в память компьютера при запуске исполняемого файла.
Познакомимся с каждым из видов библиотек на практике.
Статическая библиотека Сначала я напишу краткий рецепт приготовления статической библиотеки:
- Создаем репозиторий для библиотеки на GitHub. Клонируем репозиторий и добавляем в него cpp файлы, содержащие логику, которую мы хотим добавить в библиотеку. В отдельной директории размещаем набор заголовочных файлов с объявлением функций, которые будут доступны пользователю нашей библиотеки.
- Делаем из cpp файлов объектные файлы. Те самые .o файлы из урока про этапы сборки проекта на C++
- Архивируем объектные файлы
А теперь давай выполним каждый шаг нашего рецепта.
- Создаём пустой репозиторий для нашей библиотеки на GitHub. Далее, клонируем репозиторий и создаем следующую структуру директорий
libmath/
├── src/ # Исходные файлы проекта
│ └── libmath.cpp # Реализация математических функций
├── include/
│ └── libmath.h # Заголовочный файл математических функций
├── .gitignore # Список файлов, игнорируемых Git
└── README.md # Описание проекта и инструкции по сборке
Файл libmath.cpp.
#include "libmath.h"
namespace libmath
{
int addition(int a, int b)
{
return a + b;
}
int subtraction(int a, int b)
{
return a - b;
}
int multiplication(int a, int b)
{
return a * b;
}
int division(int a, int b, int& c)
{
if (b == 0)
{
return -1;
}
c = a / b;
return 0;
}
int power(int a, int b)
{
int c = 1;
for (int i = 0; i < b; ++i)
{
c = c * a;
}
return c;
}
}
Файл libmath.h:
#pragma once
namespace libmath
{
int addition(int a, int b);
int subtraction(int a, int b);
int multiplication(int a, int b);
int division(int a, int b, int& c);
int power(int a, int b);
}
- Генерим объектный файл В корневой директории репозитория с библиотекой выполним команду:
g++ -c src/libmath.cpp -o libmath.o -I./include
С помощью опции -I указывается директория, где компилятор (точнее препроцессор) будет искать заголовочные файлы.
- Архивируем объектные файлы. Для этого выполняем команду:
ar rvs libmath.a libmath.o
ar - это архиватор rvs - набор параметров, которые управляют работой архиватора. На разных проектах используется разный набор ключей. Я традиционно использую следующие ключи:
r добавить файл в архив (с заменой)
s создать (обновить) индекс символов. Индекс используется линковщиком для быстрого поиска функций в архиве
v показать какие файлы добавляются/заменяются
После параметров ar указываем имя библиотеки и перечисляем файлы, которые будут добавлены в библиотеку.
Имя библиотеки для Linux традиционно записывается по следующему шаблону:
libИМЯ_БИБЛИОТЕКИ.a
a - сокращение от archive.
Библиотека готова! Осталось подключить её к нашему проекту. Перед подключением стоит учесть важный нюанс: библиотека libmath.a может использоваться в нескольких проектах, поэтому файл библиотеки libmath.a и заголовочный файл libmath.h должны располагаться в системных директориях, т.е. директориях, доступных всем пользователям ОС. Традиционно для размещения пользовательских библиотек используется директория /usr/local/lib, для заголовочных файлов - /usr/local/include. Скопируем нашу библиотеку в эти директории:
sudo cp libmath.a /usr/local/lib/
sudo cp include/libmath.h /usr/local/include/
Обрати внимание, для того, чтобы скопировать файл в системную директорию необходимо использовать права суперпользователя.
Наконец-то библиотеку можно подключать к нашему проекту. Текущая структура нашего проекта:
calculator/
├── src/ # Исходные файлы проекта
│ ├── app.cpp # Реализация бизнес-логики
│ ├── app.h # Заголовочный файл бизнес-логики
│ └── main.cpp # Точка входа в программу
├── .gitignore # Список файлов, игнорируемых Git
└── README.md # Описание проекта и инструкции по сборке
Как ты мог заметить, файлы libmath.h и libmath.cpp были перенесены из директории проекта в директорию библиотеки. Команда для сборки нашего проекта с библиотекой libmath.a:
g++ src/app.cpp src/main.cpp -I/usr/local/include -L/usr/local/lib -lmath -o calculator
С помощью опции -I указывается директория, где компилятор будет искать заголовочные файлы, с помощью -L - директория, где линковщик будет искать файлы библиотек.
На самом деле последнюю команду можно написать короче:
g++ src/app.cpp src/main.cpp -lmath -o calculator
Мы можем так сделать, потому как мы расположили библиотеку и заголовочный файл в директориях по умолчанию. Для того, чтобы узнать, в каких директориях препроцессор по умолчанию ищет заголовочные файлы, нужно выполнить команду:
cpp -v
Аналогично для линковщика:
ld --verbose | grep SEARCH_DIR
Отлично! У нас получилось собрать и подключить статическую библиотеку к нашему проекту.
Какие нюансы нужно помнить при работе со статической библиотекой:
- Придётся всё пересобирать. Использовать статическую библиотеку очень просто - подключил и забыл - код библиотеки будет добавлен в твой исполняемый файл. Но, если в библиотеку будет внесено изменение, например, исправлена ошибка, то придется пересобирать не только библиотеку, но и проект, где она была использована.
- Дублирование кода библиотеки. Код библиотеки добавляется в код твоего исполняемого файла. Важно: добавляется только тот код библиотеки, который ты используешь. Современные компиляторы достаточно умны, чтобы находить и удалять неиспользуемый код. Но, если в твоем проекте несколько бинарников, то код библиотеки будет добавляться в каждый из бинарников, что приведет к росту места, которое занимает проект.
- Внешние зависимости библиотеки придется доставлять отдельно. Как ты помнишь, в библиотеку попадают объектные файлы, а, значит, внешние зависимости в них неразрешены. Тебе придется изучить документацию к библиотеке и доставить внешние зависимости библиотеки отдельно. Ну или разбираться с кучей непонятных ошибок на этапе линковки твоего проекта;)
Перед тем, как переходить к изучению динамической библиотеки, удалим все артефакты сборки статической библиотеки:
sudo rm /usr/local/lib/libmath.a
sudo rm /usr/local/include/libmath.h
Динамические библиотеки
Для динамических библиотек я также сначала напишу краткий рецепт приготовления библиотеки:
- Создаем репозиторий для библиотеки на GitHub. Клонируем репозиторий и добавляем в него cpp файлы, содержащие логику, которую мы хотим добавить в библиотеку. В отдельной директории размещаем набор заголовочных файлов с объявлением функций, которые будут доступны пользователю нашей библиотеки.
- Делаем из cpp файлов объектные файлы. Те самые .o файлы из урока про этапы сборки проекта на C++
- Линкуем объектные файлы
Т.е. шаги 1 и 2 полностью идентичны для статических и динамических библиотек! Отличается лишь шаг 3: для создания статической библиотеки мы добавляем объектные файлы в архив, для создания динамической библиотеки - линкуем объектные файлы.
Выполнение шага 1 можешь посмотреть в разделе, посвященном статической библиотеке. А вот команда для создания объектных файлов для динамической библиотеки несколько отличается. В корневой директории репозитория с библиотекой выполним команду:
g++ -c -fPIC src/libmath.cpp -o libmath.o -I./include
Добавлен параметр -fPIC (Position Independent Code) — это опция компилятора g++, которая позволяет создавать позиционно-независимый код. Позиционно-независимый код более гибкий и удобный при создании динамических библиотек (shared libraries).
Собираем объектные файлы в библиотеку с помощью следующей команды:
g++ -shared libmath.o -o libmath.so
Для сборки динамической библиотеки используется g++, потому как сборка динамической библиотеки - это по сути сборка исполняемого файла, только без точки входа (т.е. без функции main). Чтобы g++ понимал, что он собирает динамическую библиотеку, ему необходимо передать ключ -shared
Скопируем нашу библиотеку в системные директории:
sudo cp libmath.so /usr/local/lib/
sudo cp ./include/libmath.h /usr/local/include/
После чего выполним команду:
sudo ldconfig
Это команда обновляет данные линковщика о доступных динамических библиотеках.
И, наконец, соберем наш проект:
g++ src/app.cpp src/main.cpp -lmath -o calculator
Команда для сборки проекта с динамической библиотекой идентична команде для сборки проекта со статической библиотекой.
Как я уже писал ранее, код, который содержится в динамической библиотеке, загружается в память компьютера при старте приложения. Значит, для каждого исполняемого файла мы можем узнать, какие библиотеки должны быть загружены при его запуске. Эту информацию можно узнать с помощью утилиты ldd:
ldd calculator
В списке библиотек можно найти нашу библиотеку математических функций:
libmath.so => /usr/local/lib/libmath.so
Отлично! У нас получилось собрать и подключить динамическую библиотеку к нашему проекту.
Какие нюансы нужно помнить при работе с динамической библиотекой:
- Динамическая библиотека включает все статические зависимости. Т.е. при подключении динамической библиотеки к нашему проекту, не нужно волноваться о появлении ошибок, связанных с её внешними зависимостями, на этапе линковки нашего проекта.
- Если в динамической библиотеке была найдена проблема, мы можем внести правки в библиотеку и пересобрать её - вносить правки в исполняемый модуль не нужно, при следующем старте наш исполняемый модуль будет автоматически использовать новую версию библиотеки.
- Одну и ту же динамическую библиотеку могут использовать несколько исполняемых файлов. Таким образом мы экономим место, занимаемое на диске системными файлами.
На этом, пожалуй, всё, что тебе нужно знать о библиотеках для успешного старта в профессии C++ разработчика. До встречи на следующем уроке!