Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

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
				}
			]
		}
	]
}