Wszystko, czego mama nie powiedziała Wam o plug in'ach
Warsztat - Programowanie gier komputerowych :: Wszystko, czego mama nie powiedziała Wam o plug-in'ach
Strona główna Forum Szukaj Lista użytkowników Grupy Zarejestruj Zaloguj
Artykuły Kliknij na kategorię żeby dodać artykuł Szukaj Programowanie gier » Artykuły » Ogólnie o programowaniu [Wersja do drukowania] Wszystko, czego mama nie powiedziała Wam o plug-in'ach Opis Pisanie wtyczek za pomocą bibliotek DLL Autor Regedit Data Sob 04 Gru, 2004 2:39 pm Typ ** Słowa klucze Kategoria Ogólnie o programowaniu Odsłony 691
Wszystko, czego mama nie powiedziała Wam o plug-in'ach Pisanie wtyczek za pomocą bibliotek DLL
1. Wstęp Wyobraźmy sobie sytuację: Napisaliśmy program (nieważne jaki) obsługujący kilka rodzajów plików. Nagle ktoś mądry napisał inny program ze swoim typem pliku, a my uznając, że ten typ jest godny używania postanowiliśmy rozbudować nasz program o możliwość obsługi nowego pliku. Jeżeli nasz program nie przewidywał ewentualnej rozbudowy to pojawiają się schody. W najlepszym przypadku musimy dopisać wewnątrz programu nowe funkcje obsługi plików, całość skompilować, a następnie rozesłać do, niezwykle uszczęśliwionych tym faktem, użytkowników. A gdyby nasza aplikacja uwzględniała rozszerzenia, to wystarczyłoby tylko napisać Plug-in obsługujący nowe pliki i umożliwić jego ściągnięcie. Zero ingerencji w kod źródłowy właściwego programu. 2. O DLL'ach słów kilka Ten rozdział przeznaczony jest dla opornych na wiedzę o DLL'ach.
DLL ( DynamicLinkLibrary ) jest to plik zawierający jakieś fragmenty programu. Może być podłączony do aplikacji dynamicznie lub (na przekór nazwie) statycznie.
Łączenie statyczne - nie polega na władowaniu całego kodu do programu, tylko na związaniu naszej aplikacji z konkretną biblioteką. Potrzebne jest do tego (w czasie kompilacji) plik nagłówkowy oraz plik "*.lib" potrzebny dla linkera aby potwierdzić zawartość DLL'a. Jeżeli już skompilujemy, a następnie uruchomimy nasz program, a nie będzie biblioteki do której się podłączyliśmy to system wypluje jakieś komunikaty i nie uruchomi aplikacji. Przykładem takiego łączenia jest dodawanie API DirectX.
Łączenie dynamiczne - poprzedni sposób łączenia był łatwy i przyjemny, ale trochę ciężko jest się statycznie podłączyć do biblioteki, o której istnieniu nawet nie wiemy. Tu z pomocą przychodzi trudniejsza wersja: dynamiczna. Odbywa się poprzez znajdowanie konkretnych funkcji używając ich nazw (najczęściej). Tutaj pojawia się wiele problemów: po pierwsze nazwy w C++ są wikłane (tzn. funkcja DoSth() zapisana jest jako DoSth_#@%FRE$#R@).
Umożliwia to przeładowanie funkcji, ale praktycznie uniemożliwia odwołanie się do niej poprzez nazwę. Ten problem można rozwiązać stosując wyrażenie extern "C" przy deklaracji funkcji. Wymusza brak wikłania nazw. Drugi problem ma naturę bardziej merytoryczną. Otóż nie jesteśmy w stanie sprawdzić czy funkcja, którą otrzymaliśmy z dll'a, jest typu void (func) (int x, int y) czy int (func) (void). Funkcje otrzymujemy przez nazwę, a tam (przy wyłączonym wikłaniu) nie jest nic zapisane o argumentach czy wartości zwracanej. Jedynym rozwiązaniem jest stworzenie pewnego szablonu, do którego pasować będą wszystkie ładowane dynamicznie biblioteki. A od tego już tylko mały krok do Plug-in'a. 3. Prosty Plug-in z DLL'a Ustalmy parę funkcji które nasza biblioteka będzie eksportować.
Po pierwsze standardowa funkcja bibliotek:
BOOL APIENTRY DllMain( HANDLE hModule, DWORD Reason, LPVOID lpReserved ) { switch (Reason) { case DLL_PROCESS_ATTACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return TRUE; }
Jest uruchamiana za każdym razem gdy (p)odpinamy proces lub wątek.
Następnie zdefiniujmy jakąś funkcje po której zorientujemy się, że dana biblioteka jest naszym plugiem.
Funkcje wygląda trywialnie, nie chodzi tu głównie o jej działanie, ale o sam fakt istnienia w danej bibliotece. Jeżeli jej nie ma to na pewno nie jest to nasz Plug-in.
Teraz wystarczy już tylko zdefiniować kilka funkcji (tak jak poprzednią), o określonych nazwach i argumentach, i mamy już gotową wtyczkę.
Jeszcze tylko krótki kurs jak to obsłużyć w programie: (na przykładzie IsMyPlugin()) Najpierw ładujemy bibliotekę; ścieżkę możemy pobrać np. przeszukując konkretny katalog.
//definicja typu wskaźnika do funkcji typedef BOOL (*IsMyPlugin_type)();
//pobieramy wskaźnik do funkcji IsMyPlugin_type pFunction = NULL; pFunction = (IsMyPlugin_type)GetProcAddress(hModule,"IsMyPlugin"); if(pFunction == NULL) { //to nie nasz plugin return; } BOOL ret = pFunction(); . . . //przed zakończeniem programu FreeLibrary(hModule);
Jeszcze tylko kwestia wskaźników do funkcji; funkcje wywołuje się tak samo jak zwykłą, bez żadnych operatorów wyłuskania. 4. Export interfejsu Czasami człowiek chciałby jednak w jednej bibliotece zawrzeć kilka rozszerzeń, co wtedy? Można zdefiniować bądź ile funkcji i jakoś je numerować ale prościej zapakować to wszystko do interfejsów.
Interfejs jest to nic innego jak klasa z virtualnymi metodami (i destruktorem). Owa virtualność metod jest dla tego ważna, że takie metody zapisywane są w klasie jako konkretne adresy (łączone runtime a nie w czasie kompilacji) , a o to nam przecież chodzi przy dynamicznym łączeniu biblioteki.
Oprócz samych interfejsów będą nam jeszcze potrzebne standardowe (dla każdej naszej wtyczki) funkcje zarządzające.
W tym przykładzie ukazałem jeszcze jedną ważną rzecz. Otóż, informacji o typie itp., musimy przekazać wskaźnik do funkcji tworzącej dany interfejs. Nie możemy po prostu znaleźć konstruktora, gdyż jest nievirtualny i do tego ma wikłaną nazwę.
Przykład interfejsu oraz ww. funkcji:
class MYDLL_API CTHResource { public: CTHResource(void* pData); virtual ~CTHResource();
// nazwa w tym przypadku może być wikłana // bo nie będziemy od strony programu // odwoływać się bezpośrednio, tylko przez // funkcję "DLLGetClassDesc" MYDLL_API void* CTHResource_Create(void* pData);
//definicja void* CTHResource_Create(void* pData) { return new CTHResource(pData); } 5. Fabryka Klas Mamy już całą masę wtyczek, teraz pozostało już tylko zmusić program aby zechciał coś z nimi zrobić. Oczywiście plug'i nie mogą egzystować w całkowitym oderwaniu od aplikacji, byłyby wtedy całkowicie bezużyteczne gdyż program nie wiedziałby co z nimi zrobić. Można tego uniknąć w bardzo prosty sposób: definiujemy kilka klas podstawowych np.
class CTHTexture : public CTHResource {...}; class CTHMesh : public CTHResource {...};
Są to klasy w jakiejś statycznie łączonej bibliotece. Cały czas są dostępne od strony kodu źródłowego aplikacji i nie ma problemów z ich użyciem. Następnie, w celu poszerzenia możliwości naszego programu, dorabiamy wtyczkę z klasami:
class CTHCubeTexture : public CTHTexture class CTHVolumeTexture : public CTHTexture class CTHSkinnedMesh : public CTHMesh
W naszym programie powyższe klasy dalej widziane i obsługiwane są jako ich klasy podstawowe, co nie przeszkadza w fakcie, że w chwili wykonania pObject->Load(...) załaduje nam się np. tekstura3d a nie zwykła 2d.
Podsumowując na wtyczkach operujemy poprzez ich interfejsy podstawowe. W tym celu zostało również użyte dwuczłonowe nazewnictwo klas (patrz definicja funkcji DLLGetClassDesc(...)) ClassID oznacza typ wtyczki natomiast ClassROOT oznacza typ interfejsu podstawowego.
Na koniec jeszcze kilka zdań o tej całej fabryce. Jest to klasa która rejestruje i tworzy inne klasy/interfejsy. Najprostsza implementacja ma postać:
class CTHClassFactory { public: // lista dwukierunkowa elementów LPTHCLASSFACTORYELEMENT First; LPTHCLASSFACTORYELEMENT Last; public: CTHClassFactory(); ~CTHClassFactory();
//rejestruje klasę z plug-in'a THRESULT RegisterClass(LPDLLCLASSDESC pDesc); THRESULT UnregisterClass(ID32 ClassID, ID32 ClassROOT); // tworzy obiekt o zadanym typie THRESULT CreateObject(ID32 ClassID, ID32 ClassROOT, void** ppObject, void* params); THRESULT GetClassDesc(ID32 ClassID, ID32 ClassROOT, LPDLLCLASSDESC pDesc); };
Oto krótki algorytm, pokazujący jak to wszystko poskładać, aby zarejestrować każdą klasę:
for(DWORD i = 0; i < clsnum; i++) { pGetClassDesc(i,&desc); ClassFactory.RegisterClass(&desc); } }while(FindNextFile(filename);
I to wszystko (albo przynajmniej większość). 6. Podsumowanie Z wielkim bólem dobrnąłem wreszcie do końca. Pewnie o kilku rzeczach zapomniałem napisać (jak zwykle), ale mam nadzieje, że ten artykuł rzucił jakieś światło (niekoniecznie nowe) na kwestie wtyczek. Pomimo tego, że starałem się używać pseudokodu, to nie zawsze mi to wychodziło; po prostu łatwiej jest coś wkleić z gotowego projektu niż bawić się z tym od nowa. Proszę tylko nie traktować tego jako kompletnego kodu - za nic w świecie nie będzie działał.
Parę, co bardziej spostrzegawczych, osób zauważyło pewnie pokrewieństwa między definicją klasy CTHResource a typem pliku omawianym w poprzednim artykule ("Uniwersalny typ pliku"). Spostrzeżenie jak najbardziej słuszne. Sprawą połączenia tego formatu pliku oraz systemu wtyczek postaram się zająć w następnym artykule.
Autor: HANS (B.E.H.C.) E-mail: behc@_USUN_@space.pl Data: 2 Luty 2002