Linux - Biblioteki ładowane dynamicznie
fatalbomb
Tworząc pewne programy możemy chcieć, aby można było dokładać do nich nowe funkcje bez konieczności ponownej kompilacji całego kodu. Dobrym przykładem są różnego rodzaju wtyczki. Artykuł ten opisuje, jak zaimplementować taką funkcjonalność w naszej aplikacji.
Plik nagłówkowy dlfcn.h
Pierwsza rzecz, jaką musimy zrobić, to dołożyć nagłówek o nazwie dlfcn.h: ```cpp #include <dlfcn.h> ```Znajdziemy tam wszystkie potrzebne funkcje:
- void* dlopen(const char* uchwyt, int flaga);
- void* dlsym(void* uchwyt, const char* nazwa_funkcji);
- const char* dlerror();
- int dlclose(void* uchwyt)
Jak tego używać?
Królik doświadczalny
Pierwszą rzeczą, jaką musimy zrobić, jest stworzyć sobie jakąś przykładową bibliotekę, którą potem będziemy dołączać w trakcie wykonania programu. ```cpp #include <iostream>extern "C" void wypisz()
{
std::cout << "Tu mowi biblioteka dynamiczna!" << std::endl;
}
extern "C" int oblicz(int a, int b)
{
return 6a+2b;
}
Jeżeli piszemy kod w C++, nie możemy zapomnieć o poprzedzeniu definicji funkcji klauzulą "<b>extern "C"</b>". W przeciwnym razie kompilator zamiast funkcji o nazwie "oblicz" może wygenerować na przykład "oblicz_fii". W efekcie nasz program główny nie będzie mógł znaleźć naszych funkcji.
Gotowy kod kompilujemy następującym poleceniem:
<kbd>
g++ -shared biblioteka.cpp -o biblioteka.so
</kbd>
Opcja "-shared" mówi kompilatorowi, że chcemy wyprodukować bibliotekę dzieloną. W przeciwnym razie kompilator będzie próbował zlinkować całość do pliku wykonywalnego, co zakończy się błędem linkera, który nie będzie potrafił odnaleźć (nieistniejącej przecież) funkcji main.
Końcówka ".so" w nazwie pliku wynikowego nie jest obowiązkowa, wynika jedynie z pewnej konwencji. Równie dobrze możemy napisać "biblioteka.dll", to nie ma znaczenia.
<h2>Program główny</h2>
Mamy już gotową bibliotekę, teraz musimy zrobić z niej użytek. Zakładamy nowy plik (w tym przykładzie nazwiemy go <b>main.cpp</b>) i piszemy kod:
```cpp
#include <iostream>
#include <dlfcn.h>
int main(void)
{
std::cout << "Testujemy biblioteki dynamiczne...\n";
void* uchwyt = dlopen ("./biblioteka.so", RTLD_LAZY);
if (!uchwyt)
{
std::cout << "Nieudane: " << dlerror() << "\n";
return 1;
}
typedef void (*Funkcja1)();
typedef int (*Funkcja2)(int,int);
Funkcja1 wypisz = (Funkcja1)dlsym(uchwyt, "wypisz");
Funkcja2 oblicz = (Funkcja2)dlsym(uchwyt, "oblicz");
if ((!wypisz) || (!oblicz))
{
std::cout << "Nieudane: " << dlerror() << "\n";
return 2;
}
wypisz();
std::cout << "oblicz - wynik to " << oblicz(10, 33) << "\n";
dlclose (uchwyt);
return 0;
}
Wywołaniem funkcji dlopen otwieramy naszą bibliotekę. Jeżeli nie podaliśmy jawnie ścieżki dostępu w pierwszym parametrze, to system będzie
poszukiwał biblioteki w następujących miejscach:
- katalogi podane w zmiennej środowiskowej LD_LIBRARY_PATH
- katalog /lib
- katalog /usr/lib
int (*oblicz)(int, int) = (int(*)(int,int))dlsym (uchwyt, "oblicz");
Podobnie jak przy ładowaniu biblioteki, również przy ładowaniu funkcji może wystąpić błąd wynikający choćby z tego, że nie utworzyliśmy danej funkcji lub ma ona inną nazwę niż się spodziewaliśmy (może tak się zdarzyć jeżeli zapomnimy o extern „C” w kodzie biblioteki). W takim wypadku funkcja dlsym zwróci zero.
Na koniec zamykamy bibliotekę wywołując funkcję dlclose i podając uchwyt do biblioteki jako argument.
Kod ten kompilujemy poleceniem:
g++ main.cpp -o main -ldl
Opcja -ldl jest potrzebna, aby poinformować linker gdzie ma szukać kodu funkcji dlopen, dlsym, dlclose i dlerror.
Uruchamiamy nasz program i powinniśmy zobaczyć następujący rezultat:
Testujemy biblioteki dynamiczne...
Tu mowi biblioteka dynamiczna!
oblicz - wynik to 126
Najważniejsza wiadomość jest taka, że funkcję pokazywaną wskaźnikiem wywołujemy dokładnie tak samo jak zwykłą. Nie ma tutaj znaczenia, czy korzystamy z funkcji, czy z wskaźnika do niej.
W artykule opisano ładowanie funkcji, jako że do tego najczęściej się wykorzystuje biblioteki. Nic nie stoi na przeszkodzie jednak aby wiązać nasz program z należącymi do biblioteki zmiennymi czy wręcz obiektami klas. W każdym przypadku posługujemy się wskaźnikami według następującego schematu:
TypWskaznika* wsk = (TypWskaznika*)dlsym (uchwyt, "nazwa");
A co z Windows?
Co prawda artykuł dotyczy Linuxa, ale warto dla porządku wspomnieć o tym jak wykonywać podobne działania pod Windowsem. Różnice zresztą nie są wielkie. No ale po kolei:- Zamiast nagłówka dlfcn.h używamy windows.h
- Zamiast funkcji dlopen używamy LoadLibrary. Funkcja ta przyjmuje tylko jeden argument - nazwę biblioteki. Nie jest konieczne podawanie rozszerzenia pliku, ale sama biblioteka powinna mieć rozszerzenie .dll
- Uchwyt do biblioteki trzymamy w zmiennej typu HINSTANCE. Inna sprawa że typ HINSTANCE to wręcz przemianowany (typedefem lub #includem) void *.
- Do wyciągania funkcji i obiektów z biblioteki służy funkcja GetProcAddress, której używa się identycznie jak dlsym.
- Do zwalniania biblioteki używamy FreeLibrary, wywoływanej tak samo jak dlclose
- Przy kompilacji programu głównego nie dopisujemy -ldl. Przy kompilacji biblioteki nic się nie zmienia, poza tym że zamiast rozszerzenia .so używamy .dll. Ewentualnie możemy zostawić samą nazwę pliku, linker sam dopisze sobie to .dll.
- Przy deklarowaniu obiektów dorzucamy ciąg __declspec(dllexport) przed typem zwracanym przez funkcję. W takiej sytuacji nie musimy pisać extern "C", którego znaczenie jest podobne.