C/C++ a sdílené knihovny v Linuxu
1.12.2018
Sdílené knihovny obsahují binární kód, který může jedna nebo více aplikací použít. Pokud máte zkušenost s programováním v prostředí Windows, jde o obdobu tzv. dynamicky linkovaných knihoven (dll).
Vytvoření sdílené knihovny
Pro náš ukázkový příklad si nejprve vytvoříme a sestavíme jednoduchou sdílenou knihovnu. Celý kód bude v jednom zdrojovém souboru knihovna.cpp.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
extern "C" size_t vypis_text(const char* text)
{
if (!text)
{
printf("nezadal jsi žádný text!\n");
return 0;
}
else
{
printf("zadal jsi: %s\n", text);
return strlen(text);
}
}
extern "C" bool vrat_text(char* p_text, size_t delka_bufferu)
{
if (!p_text)
return false;
strncpy(p_text, "Nějaký vrácený text", delka_bufferu-1);
return true;
}
Překlad a sestavení sdílené knihovny s GCC
Pro sestavení sdílené knihovny si vytvoříme jednoduchý skript (zde nazvaný preloz_knihovnu.sh)
g++ -g -c -Wall -Werror -fpic knihovna.cpp g++ -shared -o knihovna.so knihovna.o
V prvním kroku vytvoříme objektový soubor (knihovna.o), a z něj v druhém kroku vlastní sdílenou knihovnu (knihovna.so). V tomto případě vytváříme knihovnu obsahující "debug informace" (to určujeme parametrem -g) protože jak si lze později vyzkoušet můžeme pak jednoduše krokovat kód knihovny. V praktickém použití bychom samozřejmě po odladění a před tím než aplikaci "dáme ven" při sestavení parametr -g nepoužili a místo toho naopak nastavili nějaký stupeň optimalizace (parametr -O3)
Aplikace využívající sdílenou knihovnu
Nyní si vytvoříme jednoduchou aplikaci využívající funkce z naší sdílené knihovny. Půjde o konzolovou aplikaci (soubor aplikace.cpp) do které musíme kromě standardních základních hlavičkových souborů vložit hlavičkový soubor dlfcn.h obsahující deklarace funkcí potřebných k načtení knihovny a získání adres požadovaných funkcí.
Načtení sdílené knihovny
Pro načení knihovny si vytvoříme globální proměnnou typu void* do které si uložíme handle načtené knihovny
void* _p_knihovna = nullptr;
V aplikaci (přímo v hlavní funkci main pak takto načteme sdílenou knihovnu:
_p_knihovna = dlopen("./knihovna.so", RTLD_LAZY);
if (!_p_knihovna)
{
printf("Chyba načtení knihovny: %s", dlerror());
return EXIT_FAILURE;
}
Knihovnu načteme pomocí funkce dlopen a v případě chyby získáme text chybové hlášky pomocí funkce dlerror(). Po použití někde na konci aplikace pak knihovnu uvolníme:
if (0 != dlclose(_p_knihovna))
{
printf("Chyba uvolnění knihovny: %s", dlerror());
return EXIT_FAILURE;
}
Získání a volání funkcí ze sdílené knihovny
Nyní si napíšeme funkci která získá adresu funkce ve sdílené knihovně a zavolá ji. Předesílám že v praxi pokud bychom v aplikaci funkci ze sdílené knihovny volali opakovaně, načetli bychom si ji jen jednou a uložili do globální proměnné pro opakované volání.
size_t vypis_text(const char* text) noexcept
{
typedef size_t (*vypis_text_t)(const char*);
vypis_text_t p_vypis_text = (vypis_text_t)dlsym(_p_knihovna, "vypis_text");
if (!p_vypis_text)
{
printf("Chyba získání funkce: %s", dlerror());
return 0;
}
return p_vypis_text(text);
}
Jak je vidět nejprve si musíme nadeklarovat typ odpovídající parametrům a návratovému typu funkce a do ukazatele na tento typ pak uložíme získanou adresu požadované funkce. Tu získáme pomocí funkce dlsym.
V případě druhé funkce které ve svém parametru vrací text jeho zkopírováním do zadaného bufferu pak kód vypadá takto:
bool vrat_text() noexcept
{
typedef bool (*vrat_text_t)(char*, size_t);
vrat_text_t p_vrat_text = (vrat_text_t)dlsym(_p_knihovna, "vrat_text");
if (!p_vrat_text)
{
printf("Chyba získání funkce: %s", dlerror());
return false;
}
char buff[255];
bool bret = p_vrat_text(buff, sizeof(buff));
printf("vrácený text: %s\n", buff);
return bret;
}
Hlavní funkce main obsahující volání těchto funkcí pak bude vypadat následovně:
int main (int argc, char *argv[])
{
_p_knihovna = dlopen("./knihovna.so", RTLD_LAZY);
if (!_p_knihovna)
{
printf("Chyba načtení knihovny: %s", dlerror());
return EXIT_FAILURE;
}
vypis_text("ahoj");
vrat_text();
if (0 != dlclose(_p_knihovna))
{
printf("Chyba uvolnění knihovny: %s", dlerror());
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Překlad a sestavení aplikace
Použité funkce pro dynamické načítání musíme přilinkovat do aplikace proto v parametrech překladu musí být -ldl. Překlad můžeme zavolat z konzole:
g++ aplikace.cpp -g -Wall -ldl -o aplikace
V případě použití Visual Studia Code si pak můžeme pro překlad následující tasks.json:
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "prekladac",
"type": "shell",
"command": "g++",
"args": [
"aplikace.cpp",
"-g",
"-Wall",
"-ldl", // pro funkci dlopen
"-oaplikace"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
Pro spuštění a ladění aplikace z VSCode pak následující launch.json
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/aplikace",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}