Toruń 2005 Programowanie Zespołowe 2004/2005 Zespół VIII Portowalne biblioteki dynamiczne Autor: Lessnau Adam WPROWADZENIE Biblioteka to plik zawierający skompilowany kod i dane, które mogą zostać dołączone do programów. Biblioteki pozwalają na większą elastyczność kodu programów, przyspieszają procesy rekompilacji oraz ułatwiają wprowadzanie poprawek. Generalnie wyróżniamy trzy rodzaje bibliotek: statyczne (static library) współdzielone (shared library) dynamicznie ładowane (dynamically loaded DL) Warto zauważyć, że niektórzy ludzie używają terminu biblioteka łączona dynamicznie (DLL) do określenia biblioteki dzielonej, inni natomiast mianem biblioteki DLL opisują dowolną bibliotekę, którą można wykorzystać jako bibliotekę DL (ładowaną dynamicznie). Są też tacy, dla których termin DLL jest równoznaczny z obydwoma określeniami. BIBLIOTEKI STATYCZNE Biblioteki statyczne są po prostu zbiorami najzwyklejszych plików obiektowych. Tradycyjnie, pod systemami unix'owymi, używa się w ich przypadku rozszerzenia '.a'. Zbiory te tworzone są przy użyciu programu ar (archiver). Obecnie nie są tak często używane jak dawniej. Związane jest to z zaletami, jakie posiadają (opisane poniżej) biblioteki dzielone. Niemniej jednak biblioteki statyczne wciąż są czasami tworzone. Biblioteki statyczne umożliwiają użytkownikom łączenie (konsolidowanie) z programami bez konieczności rekompilowania ich kodu, co stanowi dużą oszczędność czasu kompilacji. Biblioteki statyczne są często użyteczne dla twórców, którzy pragną by programiści konsolidowali swoje programy z ich bibliotekami, ale równocześnie nie chcą udostępniać kodu źródłowego. Teoretycznie, kod ze statycznej biblioteki, która została skonsolidowana do pliku wykonywalnego, powinien wykonywać się trochę szybciej (ok. 1-5%) niż kod wykorzystujący biblioteki
dzielone lub dynamicznie ładowane, jednak w praktyce w związku z innymi czynnikami wpływającymi na działanie aplikacji rzadko da się to potwierdzić. BIBLIOTEKI WSPÓŁDZIELONE Biblioteki dzielone są bibliotekami ładowanymi przez programy w czasie ich uruchamiania. Gdy biblioteka dzielona zostanie prawidłowo zainstalowana w systemie, wszystkie nowo uruchamiane programy automatycznie z niej korzystają. W rzeczywistości obsługa bibliotek dzielonych w nowoczesnych systemach, jak np. Linux, jest jeszcze bardziej elastyczna i zaawansowana gdyż pozwala na: aktualizacje bibliotek przy zachowaniu obsługi programów, które chcą korzystać ze starszych, niezgodnych wstecz wersji tych bibliotek; zastępowanie specyficznych bibliotek, czy też nawet specyficznych funkcji w bibliotece podczas wykonywania określonych programów; dokonywanie tego wszystkiego, podczas gdy w systemie działają programy korzystające z zainstalowanych bibliotek. W Linuksie system nazewnictwa shared libraries jest dość skomplikowany. Istnieją bowiem trzy różne nazwy: tzw. 'soname', 'real name' i 'linker name'. W dużym uproszczeniu można powiedzieć, iż nazwa pliku powinna mieć przedrostek 'lib' i rozszerzenie '.so'. Zainteresowancych odsyłam pod adres: http://www.tldp.org/howto/program-library-howto/shared-libraries.html. BIBLIOTEKI DL Biblioteki ładowane dynamicznie (DL) są bibliotekami ładowanymi po uruchomieniu programu. Ponieważ możliwe jest wstrzymanie procesu załadowania takich bibliotek do momentu, gdy będą one potrzebne, są one szczególnie użyteczne przy tworzeniu różnego rodzaju wtyczek i modułów. Biblioteki ładowane dynamicznie w systemie Linux nie wyróżniają się z punktu widzenia swojego formatu. Zbudowane są one tak jak omawiane uprzednio standardowe biblioteki dzielone, czy też pliki obiektowe. Podstawową różnicą jest to, że nie są one ładowane automatycznie podczas uruchamiania bądź konsolidowania programu. Zamiast tego, do operowania na funkcjach biblioteki, dostępu do jej obiektów, obsługi błędów oraz jej otwierania i zamykania, wykorzystywany jest interfejs API. Linux generalnie używa takiego samego interfejsu API jak system Solaris - nazywać go będziemy interfejsem dlopen(). Jednak nie wszystkie platformy korzystają z tego mechanizmu. Na przykład HP-UX posługuje się metodą shl_load(), BeOS load_add_on(), a platforma Windows używa systemu bibliotek DLL z całkowicie innym interfejsem LoadLibrary(). Jeżeli chcielibyśmy zbudować aplikację portowalną na wiele systemów, do ładowania i wykorzystywania bibliotek warto rozważyć zastosowanie pakietu 'GNU libtool'.
Pakiet 'GNU libtool' jest zbiorem skryptów wspomagających operacje na bibliotekach. Dostarcza on przenaszalnego interfejsu do takich operacji jak tworzenie plików obiektowych, linkowania bibliotek (statycznych i dzielonych), konsolidowania i debugowania programów wykonywalnych oraz instalowanie zarówno programów jak i bibliotek. LibTool zawiera również bibliotekę libltdl, która to jest przenośnym wraperem służącym do dynamicznego ładowania programów. Poniżej omówię przykładowe wykorzystanie biblioteki 'libltdl' do ładowania plugin'ów. W celu poznania pozostałych możliwości 'GNU libtool' odsyłam do manuala owego pakietu. BIBLIOTEKA LIBLTDL W celu użycia 'libltdl' w swoich programach należy dołączyć nagłówek `ltdl.h': #include <ltdl.h> W poniższych przykładzie korzystam z następujących typów zdefiniowanych w `ltdl.h': Typ: lt_ptr lt_ptr jest zwykłym wskaźnikiem. Typ: lt_dlhandle lt_dlhandle jest uchwytem do modułów. Każdy moduł otwarty 'lt_dlopened' jest skojarzony z lt_dlhandle. Oraz z następujących funkcji zdefiniwanych w `ltdl.h': Funkcja: int lt_dlinit (void) Inicjuje 'libltdl'. Musi zostać wywołana przed użyciem 'libldtl' i może być wywoływana kilka razy. Jeśli zakończy się sukcesem to zwraca 0, w przeciwnym razie zwraca numer błędu. Funkcja: int lt_dlexit (void) Zamyka 'libltdl' wraz z wszystkimi otwartymi modułami. Podobnie jak 'lt_dlinit' może być wywoływana kilka razy i zwraca 0 w przypadku sukcesu. W przeciwnym razie zwraca numer błędu. Funkcja: lt_dlhandle lt_dlopen (const char *filename) Otwiera moduł z pliku o nazwie 'filename' i zwraca uchwyt na niego. lt_dlopen jest zdolna otwierać dynamiczne moduły, przeładowalne statyczne moduły, sam program oraz natywne biblioteki dynamiczne. Wywoływana kilkukrotnie na tym samym module zwraca za każdym razem ten sam uchwyt. Jeśli libltdl nie może znaleźć modułu w katalogu z programem funkcja przeszukuje dodatkowo pozostałe katalogi z modułami (ścieżki poszukiwań) w następującej kolejności: 1. ścieżki zdefiniowane przez użytkownika - owe ścieżki mogą być zmieniane przez funkcje lt_dlsetsearchpath, lt_dladdsearchdir and lt_dlinsertsearchdir. 2. ścieżki znajdujące się w zmiennej środowiskowej LTDL_LIBRARY_PATH 3. systemowe katalogi z bibliotekami - np. pod Linuksem umieszczone są w zmiennej LD_LIBRARY_PATH Funkcja: lt_dlhandle lt_dlopenext (const char *filename)
Działa identycznie jak 'lt_dlopen' z tą jednak różnicą, że jeśli nie znajdzie pliku o nazwie 'filename' to próbuje znaleźć plik o takiej samej trzonowej nazwie ale innym rozszerzeniu (np.: '.la', '.so', '.sl'). Funkcja: int lt_dlclose (lt_dlhandle handle) Zmnejsza wskaźnik referencji do modułu. Jeśli wskaźnik osiągnie zero oraz żaden inny moduł nie jest zależny od powyższego modułu, wtedy moduł zostaje zwolniony. Zwraca 0 w przypadku sukcesu. Funkcja: lt_ptr lt_dlsym (lt_dlhandle handle, const char *name) Zwraca adres funkcji o nazwie 'name' w module 'handle'. Jeśli dana funkcja nie istnieje w module zwracane jest NULL. Funkcja: const char * lt_dlerror (void) Zwraca napis opisujący ostatni błąd spowodowany przez funkcje 'libltdl'. Zwraca NULL w przypadku gdy nie wystąpił żaden błąd od momentu inicjalizacji lub od ostatniego wywołania owej funkcji. Funkcja: int lt_dlsetsearchpath (const char *search_path) Nadpisuje istniejącą ścieżkę poszukiwań użytkownika wartością 'search_path', która to musi być listą bezwzględnych ścieżek oddzielonych przecinkiem. Zwraca 0 w przypadku sukcesu. Funkcja: int lt_dlforeachfile (const char *search_path, int (*func) (const char *filename, lt_ptr data), lt_ptr data) Dla każdego modułu znajdującego się w katalogu 'search_path' wywołuje funcję 'func' przekazując jej nazwę modułu oraz dane 'data'. W przypadku, gdy funkcja 'func' zwróci wartość niezerową funkcja 'lt_dlforeachfile' kończy działanie. Jeśli 'search_path' wynosi NULL to funckja przeszukuje wszystkie standardowe lokacje zgodnie z kolejności opisaną powyżej. 'lt_dlforeachfile' zwraca wartość zwróconą przez ostatnio wywałaną funkcję 'func'. PRZYKŁAD W C++ Główny program wygląda następując: #include <iostream> #include <vector> #include <ltdl.h> #include <dirent.h> #include "plugin.h" using namespace std; /* * klada Plugin - odpowiada za każdy załadowany moduł */ class Plugin public: Plugin (DefaultPlugin* defaultplugin, DestroyPlugin destroyplugin) plugin = defaultplugin; destroy = destroyplugin; ~Plugin ()
destroy (plugin); plugin = NULL; ; DefaultPlugin* plugin; DestroyPlugin destroy; /* * klada Application - zarządza modułami, tzn. umożliwia ich załadowanie, zwolnienie oraz wywoływanie funkcji w nich zawartych */ class Application vector<plugin*> plugins; vector<lt_dlhandle> handle; friend int loadplugintmp (const char *filename, lt_ptr data); int loadplugin (const char *filename); public: void init (); void loadplugins (const char* folder); void run (); void close (); ; // pomocnicza funkcja, wraper dla metody load_plugin int loadplugintmp (const char *filename, lt_ptr data) ((Application*)data)->loadPlugin (filename); // inicjuje bibliotekę libltdl void Application::init () lt_dlinit (); // laduje plugin o nazwie filename do pamieci int Application::loadPlugin (const char *filename) lt_dlhandle handletmp; CreatePlugin create; DestroyPlugin destroy; DefaultPlugin *plugin; try handletmp = lt_dlopenext (filename); if (handletmp == NULL) throw int(1); handle.push_back (handletmp);
create = (CreatePlugin) lt_dlsym (handletmp, "create"); destroy = (DestroyPlugin) lt_dlsym (handletmp, "destroy"); if ((create == NULL) (destroy == NULL)) throw int(2); plugins.push_back (new Plugin (create (), destroy)); cout << " - OK" << endl; catch (int i) switch (i) case 1: cout << " - FAIL (it's not plugin)" << endl; break; case 2: cout << " - FAIL (function 'create' does't exist)" << endl; break; default: throw; cout << "\t" << lt_dlerror () << endl; catch (...) cout << " - FAIL" << endl; ; return 0; // dla kazdego modulu w katalogu folder wywołuje funkcje loadplugin void Application::loadPlugins (const char* folder) lt_dlforeachfile (folder, loadplugintmp, this); // dla kazdego moduly wykonuje funkcje talk void Application::run () cout << endl << endl << "Zaczynamy gadac : " << endl; for (int i = 0; i < plugins.size(); i++) plugins[i]->plugin->talk (); cout << endl << endl; // zwalnia wszystkie moduly i zamyka ltdl void Application::close () for (int i = 0; i < plugins.size(); i++) delete plugins[i]; plugins.clear (); lt_dlexit ();
Application app; // glowne funkcja programu int main () app.init (); app.loadplugins ("./plugins"); app.run (); app.close (); return 0; Oprócz niego należy stworzyć również pluginy. Każdy plugin oparty jest na pewnym szablonie. W naszym przykładzie szablon wygląda następująco: #ifndef PLUGIN_H #define PLUGIN_H #include <iostream> using namespace std; class DefaultPlugin public: ~DefaultPlugin () cerr << "DefaultPlugin was deleted." << endl; ; virtual void talk () cout << "Wiem, ale nie powiam." << endl; typedef DefaultPlugin* (*CreatePlugin) (); typedef void (*DestroyPlugin) (DefaultPlugin*); #endif Stwórzmy jeszcze dwa pluginy (pluing_1 oraz plugin_2). Oba są prawie identyczne, dlategoteż zamieszcze kod źródłowy tylko jednego z nich. #include <iostream> #include "plugin.h" using namespace std; class Plugin1 : public DefaultPlugin public:
~Plugin1 () cerr << "Plugin1 was deleted." << endl; ; void talk (); void Plugin1::talk () cout << "Hehe, to ja pierwszy plugin!!!" << endl; extern "C" DefaultPlugin* create () return new Plugin1; extern "C" void destroy (Plugin1 *plugin) delete plugin; Plik Makefile potrzebny do skompilowowania powyższych źródeł wygląda następująco: CC=g++ #LDFLAGS=-lmy obj=plugin_1.o plugin_2.o dll=plugin_1.dll plugin_2.dll so=plugin_1.so plugin_2.so ROOT_FOLDER=../ PLUGINS_FOLDER=plugins/ linux: $(so) $(CC) main.cc -o $(ROOT_FOLDER)main -lltdl strip $(ROOT_FOLDER)main windows: $(dll) $(CC) main.cc -o $(ROOT_FOLDER)main -lltdl strip $(ROOT_FOLDER)main.exe %.o : %.cc $(CC) -fpic -c $< -o $@ %.so : %.o $(CC) -fpic -shared -o $(ROOT_FOLDER)$(PLUGINS_FOLDER)$@ $< strip $(ROOT_FOLDER)$(PLUGINS_FOLDER)$@ %.dll : %.o dlltool --export-all --output-def $<.def $< dllwrap --driver-name g++ --def $<.def -o $(ROOT_FOLDER)$(PLUGINS_FOLDER)$@ $< strip $(ROOT_FOLDER)$(PLUGINS_FOLDER)$@ clean: rm -f $(obj) *.def
Dla leniwych dołączam jeszcze plik z wszystkimi źródłami potrzebymi do stworzenia powyższego przykładu. BIBLIOGRAFIA Manual do GNU libtool - http://www.gnu.org/software/libtool/manual.html HowTo na temat bibliotek - http://www.tldp.org/howto/program-library-howto/