Чтобы не изобретать колесо и не тратить на это время, существует огромное количество уже готового функционала, который мы можем подключить и использовать в качестве библиотек. В системе находится очень много библиотек под разные нужды, в нашем случае мы будем использовать библиотеку math , в которой есть множество различных математических функций. Представьте, если бы вам пришлось писать с нуля извлечение квадратного корня... Зачем?
В этом проекте мы создадим программу, которая будет использовать библиотеку math, а также научимся компилировать наш код так, чтобы к нашей программе компоновались нужные нам библиотеки.
vector_distance_calculator
Принимает от пользователя, в качестве аргументов, два вектора формата x:y, высчитывает расстояние между ними и выводит на экран.
Пример выполнения
./ctor_distance_calculator 25.97:13.4 99.88:-105.34 Input a: (25.970000, 13.400000), b: (99.880000, -105.340000) Distance between them: 1764 ctor_distance_calculator.out 25.97:13.4 25.97:13.4 Input a: (25.970000, 13.400000), b: (25.970000, 13.400000) Distance between them: 0
- чтение аргументов программы.
- преобразование аргументов в векторы.
- вычисление расстояния между векторами.
- вывод результата на экран.
vector_distance_calculator.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>- stdio.h - библиотека ввода/вывода, используем функцию
printf()для вывода сообщений. - stdlib.h - стандартная библиотека, используем функцию функцию
strtod()для конвертации строки в вещественные числа. - math.h - библиотека математических функций, используем функцию
pow()для возведения числа в степень, функциюsqrtдля извлечения квадратного корня - две необходимые операции для вычисления расстояния между векторами.
Мы работаем с двумерными векторами, у вектора два числовых значения - x и y. Поэтому назовем структуру math_vector2.
Почему приставка 'math_' в имени?
В языке C++ есть такая структура данных, как динамический массив и он имеет название
vector. В языке C тоже пытаются такое реализовывать, но своими руками и также называют егоvector. По этим, непонятным мне причинам, простое имяvector2может вызвать путаницу у C/C++ разработчиков, поэтому я добавил префиксmath_к названию своей структуры.
Простая структура с двумя полями X и Y :
typedef struct
{
double x;
double y;
} math_vector2;math_vector2 str_to_vec2(char* str);
double calculate_distance(math_vector2 a, math_vector2 b);-
str_to_vec2()- эта функция будет считывать из строки вида"5.22:3.44"и создавать на их основе объектmath_vector2со значениямиx = 5.22иy = 3.44. -
calculate_distance()- здесь мы будем рассчитывать расстояние между двумя векторами.
char* vec_str_a = argv[1];
char* vec_str_b = argv[2];
math_vector2 vec_a = str_to_vec2(vec_str_a);
math_vector2 vec_b = str_to_vec2(vec_str_b);Начинается вызов нашей функции - str_to_vec2() по преобразованию строки в объект вектора.
math_vector2 str_to_vec(char* str)С помощью функции strtod, выделяем из строки 2 значения типа double- x и y:
char* endptr;
double x = strtod( str , &endptr );
if(*endptr != ':')
{
fprintf(stderr, "Format error: expected ':'\n");
exit(1);
}
double y = strtod( endptr + 1, NULL);Сохраняем эти значения в объекте math_vector2 и возвращаем его из функции:
math_vector2 output;
output.x = x;
output.y = y;
return output;Перед выходом из программы, нужно высчитать расстояние между векторами функцией calculate_distance():
double calculate_distance(double x, double y)Внутри себя она реализовывать формулу по нахождению расстояния между векторами.
- Делаем разницу X и Y между векторами:
double diff_x = b.x - a.x;
double diff_y = b.y - a.y;- Возводим их в степень 2:
double squared_diff_x = pow(diff_x, 2);
double squared_diff_y = pow(diff_y, 2);- Получаем из их суммы квадратный корень, это и есть расстояние:
double distance = sqrt(squared_diff_x + squared_diff_y);
return distance;В конце программы (функция main()) отобразим результат в выводе и сделаем успешный выход из программы через exit(0).
double distance = calculate_distance(vec_a, vec_b);
printf("Distance between them: %d\n", distance);
exit(0);При попытке скомпилировать программу:
gcc vector_distance_calculator.c -o vector_distance_calculator.outМы получим ошибки о undefined reference to XXX , где XXX - имя незнакомой компилятору функции.
Так как мы используем заголовок math.h, функции из этого заголовка находятся хоть и в стандартной библиотеке, но эта библиотека требует ручной компановки , то есть явного указания компилятору, что нам нужно скомпоновать программу с использованием этой библиотеки math.
Чтобы это сделать и компиляция прошла успешно, нужно добавить флаг -lm . Префикс l означает library после l идет название библиотеки, в нашем случае она называется просто m (libm.so).
gcc vector_distance_calculator.c -lm -o vector_distance_calculator.outИ программа скомпилировалась вместе с библиотекой math !
libpthread- библиотека для работы с потоками.libm- нужная намmath.- и так далее...
А как вообще понять, требуется ли явная компоновка для определенной функции из стандартной библиотеки или нет?
Хороший вопрос, четкого списка нет. Почему, например
unistd.hилиstdlib.hне требует явного указания компоновки , аmath.hтребует? Так исторически сложилось, не всем программам по умолчанию нуженmath.hилиpthreads.h.Если вы столкнулись с ошибкой, связанной с
undefined reference to XXX, гдеXXXимя функции из стандартной библиотеки, например:
sqrtизmath.hpthread_createизpthread.hОткройте документацию к этой функции через командуman, например для функцииsqrt:man sqrtИ вы увидете, что в первых строках описания этой функции указано из какой она библиотеки и как ее подключить при компиляции:
sqrt(3) NAME sqrt, sqrtf, sqrtl - square root function LIBRARY Math library (libm, -lm) . . . . . .строка
Math library (libm, -lm)дает имя библиотеки , где находится эта функция -
libmи аргумент компилятору, для подключения этой библиотеки --lm
Чтобы выделять блоки кода и к ним имели доступ другие программы, чтобы пользоваться уже созданной логикой, можно создавать библиотеки самому. Здесь мы создадим свою библиотеку custom_logger и клиентскую программу, которая будет с ней работать (вызывать функции из этой библиотеки) - client_custom_logger.
custom_logger- библиотека с тремя функциями, которые выводят сообщения на экран.client_custom_logger- исполняемая программа, которая вызывает функции из библиотекиcustom_logger.
Библиотека custom_logger имеет три функции:
void log_error(char* context, char* message);
void log_message(char* context, char* message);
void log_warning(char* context, char* message);которые выводят сообщения в стандартный вывод.
Программа client_custom_logger вызывает эти функции, передавая им параметры.
Библиотечные функции выводят сообщения, пример вывода:
ERROR -> context: MAIN_CTX > > > message: MY ERROR IS HERE.
MESSAGE -> context: MAIN_CTX > > > message: MY MESSAGE IS HERE.
WARNING -> context: MAIN_CTX > > > message: MY WARNING IS HERE.- Простейшие функции вывода сообщений у библиотеки.
- Сборка библиотеки и размещение ее в папке пользовательских библиотек.
- Простейшие вызовы этих функций в программе-клиенте.
- Сборка программы с компоновкой библиотеки.
Библиотека
custom_logger
code_sources/linux-c-system-programming-essentials/5-creating-custom-library/custom_logger/...
src- папка с кодом на Си.include- папка с заголовком функций.lib- папка со сборкой библиотеки.
Исполняемая программа
client_custom_logger
code_sources/linux-c-system-programming-essentials/5-creating-custom-library/client_custom_logger/...
client_custom_logger.c- код программы.include/custom_logger.h- заголовок из библиотеки, для подключения функций.
#include <stdio.h>- stdio.h - обработка ввода/вывода , используется функция
printf()для вывода.
Библиотека имеет всего три функции, которые выводят разные сообщения, с параметрами.
Объявление функций библиотеки происходит в файлах *.h:
include/custom_logger.h:
#ifndef CUSTOM_LOGGHER_H #define CUSTOM_LOGGER_H void log_error(char* context, char* message); void log_message(char* context, char* message); void log_warning(char* context, char* message); #endif
Реализация этих функций будет находится в файлах *.c:
src/custom_logger.c:
void log_error(char* context, char* message) { printf("ERROR -> context: %s > > > message: %s\n", context, message); } // ... и так далеее
Организация файлов для библиотек
- папка
./include/...для заголовков, где объявляются функции библиотеки. Они нужны для програм-пользователей этой библиотеки (им же нужно знать какие функции там объявлены).- папка
./src/...для кода на Си, где будут размещаться реализации функций из заголовков.- папка
./lib/...для сборок библиотеки.Итого, для распространения библиотеки нужны папки с их содержимым -
includeиlib, чтобы программы-клиенты могли их использовать.
Библиотека отличается от исполняемого файла, поэтому компилировать исходный код в библиотеку нужно другими аргументами в GCC.
Порядок сборки библиотеки:
- Сборка объектного файла
gcc -Wall -Wextra -pedantic -fPIC -c custom_logger.cПараметры для gcc:
-Wall- полезные предупреждения от компилятора о подзрительном коде (анализ).-Wextra- дополнительный анализ кода, для еще несколько полезных предупреждений.-pedantic- ругаться если что-то выходит из стандарта Си.-fPIC- генерация позиционно-независимого кода (PIC - Position-Independed Code). Это очень важно для подключения библиотек, этот параметр обязателен для генерации библиотеки.-cкомпиляция только объектного файла (oфайл), без линковки.
- Сборка библиотеки
gcc -shared -Wl,-soname,libcustomlogger.so -o libcustomlogger.so custom_logger.oПараметры для gcc:
-shared- указание что нам нужно собрать разделяемую библиотеку (so - shared library).-Wl,-soname,libcustomlogger.so- указание для компоновщика, чтобы он добавил в метаданные библиотеки ееSONAME. Это нужно для того, чтобы когда программа искала нужную библиотеку по имени, динамический загрузчик библиотек в системе быстро нашел ее по имениlibcustomlogger.so.-o libcustomlogger.so- указание имени выходного файла, который получится в результате.custom_logger.o- входной объект, из которого собирается библиотека (собирали в шаге 1).
Чтобы программы находили нашу библиотеку, ее нужно скопировать в одно из мест:
- В системную папку -
/usr/lib64/. - В папку с программой, которая от нее зависит и указывать при сборке программы через
-rpathотносительный путь к библиотеке. (Портируемый способ). - В какую либо другую папку и указать ее в переменной
LD_LIBRARY_PATH.
В итоге, какой бы способ не выбрали, там должнен лежать файл сборки нашей библиотеки - libcustomlogger.so.
Для этой статьи, мы выберем самый простой способ, копирование в системную папку - /lib/lib64.
Заметка - переменная
LD_LIBRARY_PATH
LD_LIBRARY_PATHсодержит в себе дополнительные папки, где искать библиотеки. Эта переменная нужна для системного динамического загрузчика библиотек -ld.so, в ней он дополнительно ищет, собственно библиотеки. Как и любую переменную, ее можно определить в текущем сеансе оболочки:export LD_LIBRARY_PATH="/opt/mylib/lib:$LD_LIBRARY_PATH"Это нужно чтобы перед сборкой программы, которая зависит от библиотеки, указать свой путь для библиотеки, если вы не хотите ее сразу размещать в системных папках вроде
/usr/lib64/....
Программа, которая хочет использовать библиотеку, должна иметь ее заголовочные файлы, где задекларированы функции из библиотеки:
#include "include/custom_logger.h"Далее, можно использовать эти функции в коде программы:
int main(void)
{
log_error("MAIN_CTX", "MY ERROR IS HERE.");
log_message("MAIN_CTX", "MY MESSAGE IS HERE.");
log_warning("MAIN_CTX", "MY WARNING IS HERE.");
return 0;
}При попытке собрать программу обычным способом:
gcc client_custom_logger.c -o client_custom_logger.outМы получим ошибки undefined reference с указанием на функции из библиотеки. Компилятор просто не знает откуда взять реализацию этих функций.
Чтобы указать нужную нам библиотеку, нужно проставить аргумент -l<имя_библиотеки>:
gcc client_custom_logger.c -lcustomlogger -o client_custom_logger.outОбратите внимание, что вместо полного имени libcustomlogger.so мы заменяем строку lib на -l.
После этого программа скомпилируется и будет работать как нужно:
./client_custom_logger.out
ERROR -> context: MAIN_CTX > > > message: MY ERROR IS HERE.
MESSAGE -> context: MAIN_CTX > > > message: MY MESSAGE IS HERE.
WARNING -> context: MAIN_CTX > > > message: MY WARNING IS HERE.Ранее был рассмотрен процесс сборки и подключения динамической библиотеки , теперь рассмотрим статические библиотеки.
Статическая библиотека является частью приложения, ее код (объектный файл) всегда включен в состав приложения. Таким образом ваше приложение (или библиотека) получает независимость от наличия каких-либо динамических библиотек, если вы их добавили как статические.
Плюсы:
[+] Простая сборка - нет сложных операций с компоновщиком, библиотека всего лишь является частью кода программы.
[+] Легко распространять вашу сборку - так как нет зависимости от наличия того или иного файла динамической библиотеки. Все необходимое вшито в вашу программу.
[+] Чистота кода - можно делить части программы на статические библиотеки, по принципу "разделяй и властвуй". Этот процесс легче чем с динамической библиотекой, так как - два плюса выше, упрощают процесс разделения логики на библиотеки.
Минусы:
[-] Жирнее размер сборки - так как статическая библиотека становится частью вашей программы, то и размер ее, увеличится.
[-] Необходимость пересборки - если вы внесли правку в библиотеку, то нужно пересобирать все приложение, ведь она часть приложения.
[-] Больше затрат времени на сборку - если внесли правку в приложение и хотите его пересобрать, то время увеличится, так как часть сборки уходит на статические библиотеки.
Во многом плюсы и минусы разных типов библиотек инверсивны , то есть если у статической библиотеки есть [-], то это [+] у динамической и так далее.
Плюсы:
[+] Меньше размер сборки - динамическая библиотека является независимым файлом, и не является частью вашей сборки, поэтому ваша сборка будет весить меньше, при наличии динамической библиотеки, относительно статической.
[+] Динамические обновления - если вы внесли правку в библиотеку, то не нужно пересобирать все приложение, так как динамическая библиотека независима.
Таким образом вы сможете обновлять логику в библиотеках, не пересобирая приложение, но пересобирая библиотеки.[+] Меньше затрат времени на сборку - вы собираете только то что есть в вашей программе, динамические библиотеки живут отдельной жизнью.
Минусы:
[-] Сложный процесс сборки библиотеки - (см. гайд по сборке выше) этапы сборки нетривиальны, нужно прокидывать разные флаги компилятору и компоновщику.
Далее нужно вшить зависимость от библиотеки в само приложение, тоже добавляя всякие флаги.[-] Меньше переносимость приложения - если пользователь захочет воспользоваться вашей программой, то ему нужно убедиться,
что динамические библиотеки у него имеются, если их нет - он получит ошибку и будет искать как запустить ваше приложения (если не забьет на него).
Однако, использование динамических библиотек намного более распространенно чем статических. Легкость обновлений библиотек, и отсутствие необходимости пересборки, наличие удобных сборщиков (Make) делают их более полезными для распространения ПО.
Статическая библиотека и программа которая ее использует:
Также имеется заголовок функций библиотеки mymathlib.h , который используется программой main.c .
Этапы сборки библиотеки:
- Сборка объектного файла С библиотеки нам нужен только объектный файл. Указываем для
gccфлаг-c, который
означает - проделать только компиляцию, без компоновщика.gcc -c mymathlib.c -o mymathlib.o
- Архивирование объектного файла
Статическая библиотека - это всего лишь архив, с расширением*.a, внутри которого объектные файлы библиотеки.ar rcs libmymathlib.a mymath.o
ar- утилита для создания архивов статических библиотек
rcs- набор флагов, гдеr= вставка объектных файлов,c= создание архива если его нет,s= установить
индекс для поиска.В итоге получается файл
libmymathlib.a.
Всего два шага и статическая библиотека готова.
Сборка клиента с библиотекой:
Указываем для gcc исходные файлы программы, и указываем подключение с библиотекой.
gcc main.c -L. -lmymathlib -o main-L.- устанавливаем поиск библиотеки в текущей папке.-lmymathlib- подключение файлаlibmymathlib.a.
mult_prog
Выводит результат умножения 25 на 4.
Пример выполнения
./mult_prog.out mult(25,4) = 100
- Использование внешнего файла с функцией
mult(). - Вывод результата
mult(25,4)на экран.
-
Исполняемая программа -
public/code_sources/linux-c-system-programming-essentials/6-full-compilation-process/mult_prog.c
Ссылка -
Реализация функции
mult()-public/code_sources/linux-c-system-programming-essentials/6-full-compilation-process/mult.c
Ссылка -
Заголовок с функцией
mult()-public/code_sources/linux-c-system-programming-essentials/6-full-compilation-process/mult.h
Ссылка
Программы могут иметь больше одного файла исходного кода .c . В данном примере, у нас есть два файла исходного кода:
mult.c- реализация функцииmult(), которая объявлена в заголовкеmult.h.mult_prog.c- использование функцииmult()из заголовкаmult.hв функцииmain(), то есть это наша исполняемая программа.
Соответственно, раз файлов несколько то и компилятору нужно их все указать.
Почему компилятору не передаются заголовки
.h?Заголовки автоматически встраиваются компилятором в исходный код
.cфайлов, на место где они используются, директивой препроцессораinclude "header.h". То есть на местоinclude "mult.h"вставится объявление функцииmult()из файла заголовка.
Указание файлов для компиляции идет просто, как и раньше, просто вместо одного файла передаем несколько: mult.c и mult_prog.c:
gcc -Wall -Wextra -pedantic mult.c mult-prog.c -o mult_prog.out Мы можем скомпилировать программу пошагово, чтобы увидеть весь процесс сборки программы.
Сборка программы состоит из четырех шагов:
- Препроцессор
- Компиляция
- Сборка
- Компоновка
Первым шагом идет препроцессор, именно поэтому конструкция #include и ```#define```` называется директивой препроцессора.
Препроцессор просто замещает директиву #include на содержимое файла из заголовка .h и для других директив работает точно также, наприер #define - подставляет использование макроса в коде на значение из #define, и так далее.
Так как файлов исходного кода у нас несколько, то и выполнить нужно несколько раз:
gcc -E -P mult_prog.c -o mult_prog.i
gcc -E -P mult.c -o mult.iВ итоге получится два файла , обработанные препроцессором, куда вставлены нужные значения из заголовков:
mult_prog.i и mult.i.
На этом этапе файлы, обработанные препроцессором, переводятся в язык ассемблера, более близкий к машинному коду язык. Итоговые файлы ассемблера отличаются на различных машинах и архитектурах, поэтому сборка программы должна происходить именно на той платформе, на какой она будет запускаться.
Чтобы собрать файлы ассемблера из файлов обработанных препроцессором, нужно выполнить следующее:
gcc -S mult_prog.i -o mult_prog.s
gcc -S mult.i -o mult.sВ итоге получится два файла с кодом ассемблера: mult_prog.s и mult.s.
Третий шаг называется сборкой (assembly) . На этом шаге файлы исходного кода ассемблера, созданные на предыдущем шаге, собираются в объектные файлы (object files) .
Итого нам нужно взять два файла ассемблера и создать из них два объектных файла:
gcc -c mult_prog.s -o mult_prog.o
gcc -c mult.s -o mult.oЭто уже двоичные файлы (binary files), и мы не можем их открыть в текстовом редакторе. Можно воспользоваться коммандой file чтобы увидеть метаинформацию про эти двоичные файлы:
file mult_prog.o
mult_prog.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not strippedПоследний шаг - объеденить все объектные файлы в единый файл. Этим занимается компоновщик (linker):
gcc mult.o mult_prog.o -o mult_prog.outВ итоге мы собрали из двух объектных файлов, один двоичный файл, который и является нашей исполняемой программой - mult_prog.out.
./mult_prog.out
mult(25,4) = 100Сборка программы занимает 4 шага, сборку программы mult_prog можно описать так:
-
Препроцессором транслируем файлы
mult_prog.c,mult.cв ->mult_prog.i,mult.i. -
Компилятором транслируем файлы
mult_prog.i,mult.iв язык ассемблера для целевой платформы ->mult_prog.s,mult.s. -
Сборщиком транслируем файлы ассемблера
mult_prog.s,mult.sв двоичные объектные файлы ->mult_prog.o,mult.o. -
Компоновщиком объединяем объектные файлы
mult_prog.o,mult.oв единый двоичный файл ->mult_prog.out.