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
Drugi parametr powoduje, że program będzie rozwiązywał wszystkie symbole podczas wyciągania adresu funkcji z danej biblioteki (przy wywołaniu funkcji dlsym). Jeżeli zamiast RTLD_LAZY napiszemy RTLD_NOW, nastąpi to od razu jeszcze w trakcie wywoływania funkcji dlopen. Funkcja zwraca uchwyt do naszej biblioteki, który potem wykorzystujemy operując funkcjami dlsym i dlclose. Jeżeli wystąpi błąd w czasie ładowania biblioteki (choćby brak pliku lub błędny format wewnętrzny), funkcja zwróci zero. Dlatego też sprawdzamy od razu czy to, co dostaliśmy, to na pewno jakiś uchwyt. W przeciwnym razie program wyrzuca błąd i kończy się. Funkcja dlerror w razie błędu zwraca ciąg tekstowy zawierający komunikat o błędzie. Dopóki program jest w fazie testów warto sobie wyświetlać tekst zwrócony przez tą funkcję, ponieważ jest bardzo pomocny w razie kłopotów – zawiera szczegółowe informacje na temat błędu. Jeżeli program dał radę wciągnąć nasz twór, to możemy zająć się wyciąganiem z biblioteki tego, co nas interesuje. Funkcja dlsym zwraca wskaźnik do poszukiwanej przez nas danej lub funkcji. Jej pierwszy argument to uchwyt do biblioteki, a drugi to nazwa szukanego elementu. Funkcja dlsym zwraca jednakże wskaźnik typu niezdefiniowanego (void*). Aby przerobić go na wskaźnik do funkcji, musimy posłużyć się rzutowaniem. W tym celu najlepiej zdefiniować sobie "na boku" typ wskaźnika pokazującego na określony typ funkcji. Jest to rozwiązanie najwygodniejsze, gdyż w przeciwnym wypadku musielibyśmy napisać taki oto bełkot:
   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:
  1. Zamiast nagłówka dlfcn.h używamy windows.h
  2. 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
  3. Uchwyt do biblioteki trzymamy w zmiennej typu HINSTANCE. Inna sprawa że typ HINSTANCE to wręcz przemianowany (typedefem lub #includem) void *.
  4. Do wyciągania funkcji i obiektów z biblioteki służy funkcja GetProcAddress, której używa się identycznie jak dlsym.
  5. Do zwalniania biblioteki używamy FreeLibrary, wywoływanej tak samo jak dlclose
  6. 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.
  7. 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.
Jakie to ma konsekwencje? Otóż można w łatwy sposób stworzyć kod który będzie można bez żadnych modyfikacji przenosić między Windowsem a Linuxem (oczywiście trzeba będzie osobno je przekompilować pod każdym systemem). Najprostszym sposobem jest stworzyć plik nagłówkowy włączany przez wszystkie pliki projektu, a wyglądający mniej więcej tak: ```c #if OS==1 /* Linux */ #include <dlfcn.h> /* Zostawiamy to HINSTANCE żeby nie wymyślać jeszcze jednej nazwy */ #define HINSTANCE void * #define SHARED_EXPORT extern "C" #define LOAD_LIB(name) dlopen(name, RTLD_LAZY) #define LOAD_SYM(lib,name) dlsym(lib,name) /* ... */ #elif OS==2 /* Windows */ #include <windows.h> /* HINSTANCE jest już zdefiniowany w windows.h */ #define SHARED_EXPORT __declspec(dllexport) #define LOAD_LIB(name) LoadLibrary(name) #define LOAD_SYM(lib,name) GetProcAddress(lib,name) /* ... */ #else #error Nieprawidlowa wartosc stalej OS: musi byc 1 (Linux) lub 2 (Windows) #endif ``` Należy tylko pamiętać przy kompilacji o zdefiniowaniu stałej OS: robimy to opcją -DOS=xxx.

1 komentarz

  1. a nie jest tak ze __declspec(dllexport) to po prostu makro ktore zawiera tez extern "C" ?