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.


extern "C" __declspec(dllexport) BOOL IsMyPlugin()
{
return TRUE;
}

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)();

//ładujemy bibliotekę
hModule = LoadLibrary(path);

//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.

Krótki przykład:


#define MYDLL_API __declspec(dllexport)

//funkcja zwracająca ilość interfejsów
extern "C" MYDLL_API DWORD DLLGetClassNum()
{
return 2;
}
//funkcja zwracająca dane o interfejsie
extern "C" MYDLL_API void DLLGetClassDesc(DWORD cls,
LPDLLCLASSDESC pDesc)
{
if(cls == 0)
{
ZeroMemory(pDesc,sizeof(DLLClassDesc));
pDesc->ClassID = CLASS_THRESOURCE;
pDesc->ClassROOT = CLASS_THRESOURCE;
pDesc->CreateFunc = CTHResource_Create;
pDesc->Flags = DLLFLAGS_GAME|DLLFLAGS_EDITOR;
strcpy(pDesc->Name,"CTHResource");
strcpy(pDesc->Desc,"ple ple ple.");
}
if(cls == 1)
{
ZeroMemory(pDesc,sizeof(DLLClassDesc));
pDesc->ClassID = CLASS_THTEXT;
pDesc->ClassROOT = CLASS_THTEXT;
pDesc->CreateFunc = CTHText_Create;
pDesc->Flags = DLLFLAGS_GAME|DLLFLAGS_EDITOR;
strcpy(pDesc->Name,"CTHText");
strcpy(pDesc->Desc,"bla bla bla.");
}
}

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();

ID64 ID;
ID32 ClassID;
ID32 ClassROOT;

DWORD Counter;
CTHBuffer RAW_Data;

virtual THRESULT GetHeader(LPTHRESOURCE_H pHeader);


virtual THRESULT Load(LPTHFILE pStream);
virtual THRESULT Capture();
virtual THRESULT Release();

virtual THRESULT RAW_LoadTo(LPTHFILE pFile);
virtual THRESULT RAW_SaveFrom(LPTHFILE pFile);
virtual THRESULT RAW_GetObjectSize();
};
typedef CTHResource* LPTHRESOURCE;

// 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ę:


DLLGetClassNum_type pGetClassNum = NULL;
DLLGetClassDesc_type pGetClassDesc = NULL;

FindFirstFile(filename)
do
{
//należy zapamiętać gdzieś HANDLE do bibliotek,
//aby dało się je zwolnić przed zakończeniem programu

hModule[i] = LoadLibrary(filename);
DWORD clsnum = 0;

pGetClassNum =
(GetClassNum_Type)GetProcAddress(hModule[i],"DLLGetClassNum");

pGetClassDesc =
(GetClassDesc_Type)GetProcAddress(hModule[i],"DLLGetClassDesc");

if(pGetClassNum == NULL || pGetClassDesc == NULL)
continue;
clsnum = pGetClassNum();

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


Skocz do: Wybierz forumWarsztat - Programowanie
gier komputerowych|--Szkółka| |--Szkółka -
języki| |--Szkółka - grafika| |--Szkółka -
inne|--Programowanie gier| |--Ogólnie|
|--Dźwięk| |--Sztuczna Inteligencja|
|--Inne|--Programowanie grafiki|
|--Programowanie grafiki| |--OpenGL|
|--DirectX|--Produkcja| |--Pomysły|
|--Projektowanie| |--Projekty| |--Grafika|
|--Ogłoszenia|--O czym innym| |--Konferencje,
spotkania| |--Warsztat| |--Aktualności|
|--Artykuły| |--Wykłady| |--Compo|
|--Lepperlandia|--Śmietnik| |--Z odrzutu



Powered by Knowledge Base, wGEric (C) 2002 PHPBB.com MOD
This script (Knowledge Base - MX Addon v. 1.03e) is modified
by Haplo





W1.5b (C) 2004
[admin: ayufan, g[R]eK, Goliatus, mikael_, Regedit]
Wszystkie czasy w strefie CET (Europa)

Powered by phpBB2 Plus 1.52 based on phpBB 2.0.10 © 2001, 2002 phpBB
Group :: FI Theme :: Mody i Podziękowania












Wyszukiwarka