Денис Злобин

Урок 14. Библиотеки

Привет! На этом уроке мы сделаем заключительный штрих в архитектуре нашего проекта: сделаем отдельную библиотеку математических функций и подключим её к нашему проекту.

Что такое библиотека?

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

Умение работать с библиотеками - важнейший навык современного разработчика: практически для любого функционала уже реализована библиотека, которая находится в открытом доступе, так называемый open source. За любым open source проектом стоит либо команда разработчиков, либо даже целая компания (например, репозиторий библиотеки gRPC по факту координирует Google). У любого open source проекта сотни или даже тысячи пользователей по всему миру, которые не только тестируют код библиотеки, но и занимаются его развитием.

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

Какие бывают виды библиотек?

  1. Статическая библиотека. Заранее собранный код библиотеки добавляется в исполняемый файл.
  2. Динамическая библиотека. Загружается в память компьютера при запуске исполняемого файла.

Познакомимся с каждым из видов библиотек на практике.

Статическая библиотека Сначала я напишу краткий рецепт приготовления статической библиотеки:

  1. Создаем репозиторий для библиотеки на GitHub. Клонируем репозиторий и добавляем в него cpp файлы, содержащие логику, которую мы хотим добавить в библиотеку. В отдельной директории размещаем набор заголовочных файлов с объявлением функций, которые будут доступны пользователю нашей библиотеки.
  2. Делаем из cpp файлов объектные файлы. Те самые .o файлы из урока про этапы сборки проекта на C++
  3. Архивируем объектные файлы

А теперь давай выполним каждый шаг нашего рецепта.

  1. Создаём пустой репозиторий для нашей библиотеки на 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);
}
  1. Генерим объектный файл В корневой директории репозитория с библиотекой выполним команду:
g++ -c src/libmath.cpp -o libmath.o -I./include

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

  1. Архивируем объектные файлы. Для этого выполняем команду:
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

Отлично! У нас получилось собрать и подключить статическую библиотеку к нашему проекту.

Какие нюансы нужно помнить при работе со статической библиотекой:

  1. Придётся всё пересобирать. Использовать статическую библиотеку очень просто - подключил и забыл - код библиотеки будет добавлен в твой исполняемый файл. Но, если в библиотеку будет внесено изменение, например, исправлена ошибка, то придется пересобирать не только библиотеку, но и проект, где она была использована.
  2. Дублирование кода библиотеки. Код библиотеки добавляется в код твоего исполняемого файла. Важно: добавляется только тот код библиотеки, который ты используешь. Современные компиляторы достаточно умны, чтобы находить и удалять неиспользуемый код. Но, если в твоем проекте несколько бинарников, то код библиотеки будет добавляться в каждый из бинарников, что приведет к росту места, которое занимает проект.
  3. Внешние зависимости библиотеки придется доставлять отдельно. Как ты помнишь, в библиотеку попадают объектные файлы, а, значит, внешние зависимости в них неразрешены. Тебе придется изучить документацию к библиотеке и доставить внешние зависимости библиотеки отдельно. Ну или разбираться с кучей непонятных ошибок на этапе линковки твоего проекта;)

Перед тем, как переходить к изучению динамической библиотеки, удалим все артефакты сборки статической библиотеки:

sudo rm /usr/local/lib/libmath.a
sudo rm /usr/local/include/libmath.h

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

Для динамических библиотек я также сначала напишу краткий рецепт приготовления библиотеки:

  1. Создаем репозиторий для библиотеки на GitHub. Клонируем репозиторий и добавляем в него cpp файлы, содержащие логику, которую мы хотим добавить в библиотеку. В отдельной директории размещаем набор заголовочных файлов с объявлением функций, которые будут доступны пользователю нашей библиотеки.
  2. Делаем из cpp файлов объектные файлы. Те самые .o файлы из урока про этапы сборки проекта на C++
  3. Линкуем объектные файлы

Т.е. шаги 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

Отлично! У нас получилось собрать и подключить динамическую библиотеку к нашему проекту.

Какие нюансы нужно помнить при работе с динамической библиотекой:

  1. Динамическая библиотека включает все статические зависимости. Т.е. при подключении динамической библиотеки к нашему проекту, не нужно волноваться о появлении ошибок, связанных с её внешними зависимостями, на этапе линковки нашего проекта.
  2. Если в динамической библиотеке была найдена проблема, мы можем внести правки в библиотеку и пересобрать её - вносить правки в исполняемый модуль не нужно, при следующем старте наш исполняемый модуль будет автоматически использовать новую версию библиотеки.
  3. Одну и ту же динамическую библиотеку могут использовать несколько исполняемых файлов. Таким образом мы экономим место, занимаемое на диске системными файлами.

На этом, пожалуй, всё, что тебе нужно знать о библиотеках для успешного старта в профессии C++ разработчика. До встречи на следующем уроке!