Как написать свою библиотеку на Си
Если ты встал на путь С/С++ разработчика, то скорее всего (помимо использования стандартной библиотеки - libc) рано или поздно вам потребуется занятся разработкой собственных библиотек. Зачем??.. Причин может быть несколько. Например вы написали свою структуру данных или свой алгоритм, и хотите использовать его повторно или распространять. Так же возможно вы написали несколько утилит и все они используют один и тот же кусок кода (например, как часто это бывает, логгер), и будет логично вынести этот кусок кода в отдельный модуль. Поскольку сопровождать такой код будет проще.
Реализация
И так, давайте начнем с примера. Создадим заголовочный файл somecode.h, который будет содержать объявление некоторой функции. Пусть будет простая функция которая разбивает предложение на слова и печатает каждое слово в новой строке. Простой синтетический пример.
1
2
3
4
5
6
#ifndef __SOMECODE__
#define __SOMECODE__
void print_split(char* str);
#endif
И создадим файл somecode.c, в которой напишем реализацию нашей функции.
1
2
3
4
5
6
7
8
9
10
11
12
#include "somecode.h"
#include <string.h>
#include <stdio.h>
void print_split(char* str)
{
const char *word = strtok(str, " ");
while (word != NULL) {
printf("> %s\n", word);
word = strtok(NULL, " ");
}
}
Далее создадим файл main.c, где будем вызывать нашу функцию.
1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>
#include "somecode.h"
int main(int argc, char** argv)
{
if (argc > 1) {
print_split(argv[1]);
}
}
Давайте для начала скомпилируем это все самым обычным способом для проверки работоспособности.
- из исходных файлов получаем объектные файлы
- из объектных файлов получаем исполняемый файл
1
2
3
4
5
6
7
8
9
10
11
12
$ gcc -c -Wall -g -o somecode.o somecode.c
$ gcc -c -Wall -g -o main.o main.c
$ gcc -Wall -g -o a.out.1 main.o somecode.o
$ ls -l
$ ls -l
total 40
-rwxr-xr-x 1 ainr ainr 19816 Aug 9 21:22 a.out.1
-rw-r--r-- 1 ainr ainr 123 Aug 9 21:20 main.c
-rw-r--r-- 1 ainr ainr 3576 Aug 9 21:22 main.o
-rw-r--r-- 1 ainr ainr 213 Aug 9 21:21 somecode.c
-rw-r--r-- 1 ainr ainr 72 Aug 9 21:20 somecode.h
-rw-r--r-- 1 ainr ainr 6224 Aug 9 21:21 somecode.o
Запускаем исполняемый файл и видим, что все работает.
1
2
3
4
5
$ ./a.out.1 "hello my little pony"
> hello
> my
> little
> pony
А теперь рассмотрим пример получения библиотеки и линковки его к исполняемому файлу. Для компиляции используем вызов gcc со следующими опциями.
1
$ gcc -Wall -g -shared -fpic -o libsomecode.so somecode.c
Из файла с расширением .c мы получаем файл с расширением .so. И так же обратите внимание, что библиотека имеет префикс lib. Еще мы видим, что появились два дополнительных аргумента -shared и -fpic. С помощью опции -shared мы говорим компилятору, что хотим получить а выходе библиотеку. А опция -fpic говорит компилятору, что объектные файлы должны содержать позиционно-независимый код (position independent code), который рекомендуется использовать для динамических библиотек.
Теперь скомпилируем наш исполняемый файл подключив к нему нашу библиотеку. Для этого нужно указать название библиотеки через опцию -l.
1
2
3
$ gcc -Wall -g -o a.out.2 main.c -lsomecode
/usr/bin/ld: невозможно найти -lsomecode
collect2: error: ld returned 1 exit status
И опс.. мы получили ошибку… Линкер говорит нам, что он не знает где лежит наша библиотека. С помощью опции -L указываем текущую директорию, где лежит наша библиотека и компиляция проходит успешно.
1
$ gcc -Wall -g -o a.out.2 main.c -lsomecode -L.
Пытаемся запустить нашу и программу и ловим еще одну ошибку в котором говорится, что в процессе загрузки динамических библиотек отсуствует наша библиотека.
1
2
$ ./a.out.2 "hello my little pony"
./a.out.2: error while loading shared libraries: libsomecode.so: cannot open shared object file: No such file or directory
По умолчанию в операционной системе есть некоторое количество стандартных директорий, где должны располагатся библиотеки. Посмотреть этот список можно так.
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ld --verbose | grep SEARCH_DIR | sed "s/\;\ /\n/g"
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu")
SEARCH_DIR("=/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64")
SEARCH_DIR("=/usr/local/lib64")
SEARCH_DIR("=/lib64")
SEARCH_DIR("=/usr/lib64")
SEARCH_DIR("=/usr/local/lib")
SEARCH_DIR("=/lib")
SEARCH_DIR("=/usr/lib")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64")
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
Так же есть возможность задавать дополнительные директории с библиотеками с помощью переменной окружения LD_LIBRARY_PATH. С помощью утилиты ldd посмотрим от каких библиотек зависит наша программа.
1
2
3
4
5
$ ldd a.out.2
linux-vdso.so.1 (0x00007ffffe4e7000)
libsomecode.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcf3b680000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcf3b897000)
Добавим в LD_LIBRARY_PATH текущую директорию.
1
$ export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$PWD"
Видим, что наша библиотека подгрузилась.
1
2
3
4
5
ldd a.out.2
linux-vdso.so.1 (0x00007fffe2115000)
libsomecode.so (0x00007f4848a6c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4848860000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4848a76000)
И теперь программа запускается и работает.
1
2
3
4
5
$ ./a.out.2 "hello my little pony"
> hello
> my
> little
> pony
Если сравнить два исполняемых файла, то видим, что программа, которая использует динамическую библиотеку имеет меньший размер.
1
2
3
4
5
6
7
8
9
10
$ ls -l
total 92
-rwxr-xr-x 1 ainr ainr 19816 Aug 9 21:22 a.out.1
-rwxr-xr-x 1 ainr ainr 17864 Aug 9 21:25 a.out.2
-rwxr-xr-x 1 ainr ainr 18808 Aug 9 21:24 libsomecode.so
-rw-r--r-- 1 ainr ainr 123 Aug 9 21:20 main.c
-rw-r--r-- 1 ainr ainr 3576 Aug 9 21:23 main.o
-rw-r--r-- 1 ainr ainr 213 Aug 9 21:21 somecode.c
-rw-r--r-- 1 ainr ainr 72 Aug 9 21:20 somecode.h
-rw-r--r-- 1 ainr ainr 6224 Aug 9 21:23 somecode.o
Это произошло как раз за счет того, что реализация нашей функции теперь лежит за пределами нашего исполняемого файла. С помощью утилиты objdump можем глянуть внутрь бинарных файлов. И увидим, что во втором бинарном файле реализация функции отсутствует, но присутствует в библиотеке.
1
2
3
4
5
6
$ objdump -t a.out.1 | grep print_split
000000000000119c g F .text 0000000000000061 print_split
$ objdump -t a.out.2 | grep print_split
0000000000000000 F *UND* 0000000000000000 print_split
$ objdump -t libsomecode.so | grep print_split
0000000000001139 g F .text 0000000000000061 print_split
Разница в нашем случае может и маленькая, но в масштабах десятков и сотен файлов разница будет значительной.
Так что же мы сделали?
У нас есть исходные файлы, мы скомпилировали их, получив из них динамическую библиотеку. Далее эту библиотеку можем использовать повторно в других проектах.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-------------------------------- ---------------------------------
| Заголовочный файл | | Реализация |
| (somecode.h) | | (somecode.c) |
-------------------------------- ---------------------------------
| | | |
| void print_split(char* string);| | void print_split(char* string) |
| | | { |
| | | // ... |
| | | } |
| | | |
------------------------------- ---------------------------------
|
| Компилируем
\ / (gcc -shared -fpic -o liblog.so ....)
|
---------------------------------
| Динамическая библиотека |
| (liblog.so) |
---------------------------------
|
| (gcc ... -llog)
---------------------------------------------
| | |
----------------------- ----------------------- -----------------------
| Утилита1 | | Утилита2 | | Утилита3 |
----------------------- ----------------------- -----------------------
| | | | | |
| #include "somecode.h" | | #include "somecode.h" | | #include "somecode.h" |
| | | | | |
| print_split("..."); | | print_split("..."); | | print_split("..."); |
| | | | | |
----------------------- ----------------------- -----------------------
Примерно те же действия вы будете делать и под windows и под мак, будут немного другие компиляторы, но идея одна.