Cele i zasady tworzenia klas pochodnych

From MorphOS Library

Revision as of 11:49, 24 January 2011 by Krashan (talk | contribs) (Dispatcher: Translated.)

Grzegorz Kraszewski


Ten artykuł w innych językach: angielski

Wprowadzenie

Tworzenie klas pochodnych to jedna z podstawowych technik programowania obiektowego. W MUI najczęściej używamy klas pochodnych do następujących celów:

  • Umieszczenie całej funkcjonalności programu w zestawie metod, które mogą być potem wykonywane w notyfikajach. Do tego celu najczęściej służy klasa pochodna klasy Application.
  • Dostosowywanie klas standardowych do potrzeb programu przez przeciążanie specjalnie do tego przeznaczonych metod. Typowym przypadkiem są klasy pochodne od List, Numericbutton czy Popstring.
  • Tworzenie gadżetów i obszarów z własnymi procedurami rysowania się oraz obsługiwania zdarzeń zewnętrznych (mysz, klawiatura). W tym celu tworzy się klasy pochodne od klasy Area.

Niezależnie od powodu tworzenia klas pochodnych, robi się to zawsze w ten sam sposób. Programista musi napisać nowe metody lub przeciążyć istniejące, stworzyć funkcję dispatchera, zdefiniować strukturę danych obiektu (bywa, że jest ona pusta) i w końcu stworzyć klasę. Warto zauważyć, że klasy pochodne w MUI robi się tak samo jak w BOOPSI. Jedyną różnicą jest to, że MUI posiada własne funkcje do tworzenia i usuwania klas.

Dane obiektu

Dane każdego obiektu są przechowywane w obszarze automatycznie przydzielanym przez system. Obszaru tego używa się do przechowywania aktualnych wartości atrubutów, oraz do zmiennych i buforów wewnętrznych, repezentujących stan obiektu. Jest on zwykle definiowany jako struktura. Rozmiar danych obiektu jest podawany jako argument funkcji MUI_CreateCustomClass(). W hierarchii klas, każda klasa może dodać swoją część do obszaru danych obiektu. Jednakże w przeciwieństwie do C++, każda klasa ma dostęp jedynie do swojej części danych. Kod klasy nie może sięgnąć do danych klasy nadrzędnej (W C++ jest to możliwe, jeżeli pole danych ma deklarację protected, albo public). Dane klas nadrzędnych są dostępne wyłącznie poprzez atrybuty i metody zdefiniowane w tych klasach.

Z powodu wewnętrznych ograniczeń BOOPSI, rozmiar obszaru danych obiektu dla danej klasy nie może przekraczać 64 kB. Duże bufory i tablice powinny być w razie potrzeby alokowane dynamicznie w konstruktorze i zwalniane w destruktorze. Obszar danych obiektu jest zawsze kasowany przez BOOPSI zerami przed przekazaniem obiektowi do użytku. Jeżeli klasa nie potrzebuje danych obiektu, może jako rozmiar bloku danych podać zero funkcji MUI_CreateCustomClass().

Pisanie metod

Metoda MUI jest zwykłą funkcją w C, jednakże zestaw argumentów i ich typów jest częściowo stały.

IPTR NazwaMetody(Class *cl, Object *obj, TypStruktury *msg);

Rezultat metody może być albo liczbą całkowitą, albo wskaźnikiem na cokolwiek. Dlatego rezultat ten jest typu IPTR, który jest zdefiniowany jako "liczba całkowita wystarczająco duża, aby zmieścił się w niej wskaźnik". W aktualnej wersji MorphOS-a jest to po prostu liczba 32-bitowa (tak samo jak LONG). Jeżeli metoda nie ma nic sensownego do zwrócenia jako wynik, najlepiej po prostu zwrócić zero. Dwa pierwsze, stałe argumenty to: wskaźnik na klasę obiektu, oraz wskaźnik na sam obiekt. Ostatni argument to struktura parametrów metody. Jeżeli metoda jest przeciążeniem metody klasy nadrzędnej, to typ struktury parametrów określony jest przez klasę nadrzędną. Dla nowych metod typ tej struktury jest określany przez programistę. Niektóre metody mogą posiadać puste struktury parametrów (zawierające wyłącznie identyfikator metody). W tym przypadku trzeci argument może być pominięty.

Większość metod potrzebuje dostępu do danych obiektu. Do otrzymania wskaźnika na te dane używa się makra INST_DATA, zdefiniowanego w pliku <intuition/classes.h>. Oto przykład użycia tego makra:

struct ObjData
{
  LONG JakasWartosc;
  /* ... */
};

IPTR JakasMetoda(Class *cl, Object *obj)
{
  struct ObjData *d = (struct ObjData*)INST_DATA(cl, obj);

  d->JakasWartosc = 14;
  /* ... */
  return 0;
}

Jeżeli metoda jest dziedziczona z klasy nadrzędnej, może być w kodzie metody potrzebne wywołanie tej metody na klasie nadrzędnej. Takie wywołanie nigdy nie jest wykonywane niejawnie. Metoda klasy nadrzędnej musi być zawsze wywołana funkcją DoSuperMethodA():

result = DoSuperMethodA(cl, obj, msg);
result = DoSuperMethod(cl, obj, identyfikator, ...);

Druga forma tej funkcji pozwala na rekonstrukcję struktury parametrów metody z argumentów funkcji. Jest używana, jeżeli parametry metody są modyfikowane przed wywołaniem tej metody w klasie nadrzędnej. Metoda klasy nadrzędnej może być wywołana w dowolnym miejscu metody klasy podrzędnej, może też nie zostać wywołana wcale. Dla standardowych klas i metod MUI zasady wywoływania metody klasy nadrzędnej są opisane w dokumentacji a także innych rozdziałach tego przewodnika. Dla metod nowo zdefiniowanych przez programistę, kwestia wywoływania metod klas nadrzędnych jest całkowicie pozostawiona do jego uznania.

Dispatcher

Funkcja dispatchera klasy pełni rolę tablicy skoków do metod. Gdy jakakolwiek metoda zostanie wywołana na obiekcie (za pomocą DoMethod()), BOOPSI odszukuje dispatchera klasy obiektu i wywołuje go. Dispatcher sprawdza identyfikator metody (identyfikator jest zawsze pierwszym polem struktury parametrów metody) i wywołuje odpowiadającą mu metodę. Jeżeli metoda jest nieznana w danej klasie, dispatcher powinien przekazać ją klasie nadrzędnej poprzez funkcję DoSuperMethod().

Dispatcher jest szczególnym przypadkiem hooka. Pozwala to na wywoływanie go z dowolnego języka programowania, ponieważ sposób przekazania argumentów jest uniezależniony od C/C++. Wadą tego rozwiązania jest przekazywanie argumentów hooka poprzez wirtualne rejestry emulowanego procesora m68k. Z drugiej strony pozwala to na wykorzystanie natywnych klas skompilowanych dla PowerPC przez stare programy m68k, podobnie nowe natywne oprogramowanie może skorzystać ze starszych klas MUI pisanych jeszcze dla m68k. Jako że dispatcher jest hookiem, wymaga zdefiniowania odpowiadającej mu struktury EmulLibEntry zdefiniowanej w pliku nagłówkowym <emul/emulinterface.h>. Struktura ta funkcjonuje jako "bramka" dla agrumentów przekazywanych między natywnym kodem PowerPC, a emulatorem m68k.

const struct EmulLibEntry ClassGate = {TRAP_LIB, 0, (void(*)(void))ClassDispatcher};

Sam dispatcher zdefiniowany jest następująco:

IPTR ClassDispatcher(void)
{
  Class *cl = (Class*)REG_A0;
  Object *obj = (Object*)REG_A2;
  Msg msg = (Msg)REG_A1;

  /* ... */

Argumenty dispatchera są takie same jak argumenty metody, ale są przekazywane przez wirtualne rejestry procesora m68k, zamiast być po prostu argumentami funkcji. Jako wskaźnik na dispatcher funkcja MUI_CreateCustomClass() dostaje adres bramki danych (EmulLibEntry). Bramka danych jest używana nawet wtedy, kiedy natywna aplikacja PowerPC wywołuje również natywną klasę MUI. Wprowadzane przez bramkę danych opóźnienie jest jednak pomijalne. Wielu programistów używa prostych makr do "ukrycia" w kodzie powyższych detali, nie zostały one tu jednak użyte dla lepszego zrozumienia zagadnienia.

Typ Msg jest typem bazowym dla wszystkich struktur parametrów metod. Definiuje on strukturę zawierającą tylko identyfikator metody typu ULONG. Wszystkie pozostałe parametry metody muszą mieć adresy wyrównane zgodnie z wymaganiami stosu procesora, ponieważ funkcja DoMethod() buduje strukturę parametrów na stosie. Dlatego każdy parametr metody musi być zdefiniowany albo jako wskaźnik, albo IPTR, jeżeli jest liczbą.

Po otrzymaniu argumentów dispatcher sprawdza identyfikator metody i skacze do niej. Najczęściej robi się to w instrukcji switch. W przypadku małej ilości metod można też się posłużyć kaskadowym if/else if. Oto typowy przykład:

switch (msg->MethodID)
{
  case OM_NEW:            return MyClassNew(cl, obj, (struct opSet*)msg);
  case OM_DISPOSE:        return MyClassDispose(cl, obj, msg);
  case OM_SET:            return MyClassSet(cl, obj, (struct opSet*)msg);
  case OM_GET:            return MyClassGet(cl, obj, (struct opGet*)msg);
  case MUIM_Draw:         return MyClassDraw(cl, obj, (struct MUIP_Draw*)msg);
  case MUIM_AskMinMax:    return MyClassAskMinMax(cl, obj, (struct MUIP_AskMinMax*)msg);
  /* ... */
  default:                return DoSuperMethodA(cl, obj, msg);
}

Dla każdej metody wskaźnik na strukturę parametrów jest rzutowany na typ struktury parametrów tej konkretnej metody. Są programiści, którzy umieszczają kod metod bezpośrednio wewnątrz wyrażenia switch, zwłaszcza gdy metod jest mało, a ich kod jest krótki. W powyższym przykładzie przeciążono kilka metod klasy Area. Sposób nazywania funkcji implementujących metody jest wyłącznie przykładowy, nie ma tu żadnych narzuconych konwencji. Jednakże dodawanie nazwy klasy do nazw funkcji ma tę zaletę, że nie występują konflikty nazw między różnymi klasami jeżeli funkcje metod nie są zadeklarowane jako statyczne.

Class Creation

Having all components done (methods, dispatcher, gate, object data structure) one can create a MUI class.

struct MUI_CustomClass *MyClass;

MyClass = MUI_CreateCustomClass(NULL, MUIC_Area, NULL, sizeof(struct MyClassData),
 (APTR)&MyClassGate);

The first argument is a library base if the created class is public. Writing MUI public classes will be covered later. Let's say for now, that public classes are implemented as shared libraries, so such a public class has a library base. For private classes this argument should always be NULL.

The next two arguments are used alternatively and specify the superclass. The superclass may be either private (referenced by pointer) or public (referenced by name). Public classes are usually subclassed, so the pointer is set to NULL as in the example. More complex projects may use multilevel subclassing and subclass their own private classes. In this case, a pointer to a private class is passed as the first argument and the second one is NULL.

The fourth argument defines the size of the object data area in bytes. In most cases object data area is defined as a structure, so using the sizeof() operator is the obvious way of determining the size. If the class does not need any per-object data, zero may be passed here.

The last argument is an address of the data gate (EmulLibEntry structure). Programmers experienced on M68k programming may notice that there is a difference – in M68k code only the dispatcher function address is used here. As mentioned above, seamless M68k code support requires that program execution passes through the data gate when going from system code to the dispatcher. That is why the data gate address is placed as this argument, then the data gate contains a real dispatcher address.


Class Disposition

A MUI class is disposed with a call to MUI_DeleteCustomClass().

if (MyClass) MUI_DeleteCustomClass(MyClass);

Some conditions must be fulfilled before calling this function.

  • Call it only if the class was successfully created. Calling it with a NULL class pointer is deadly (hence the checking for NULL in the example).
  • Do not delete a class if it has any remaining subclasses or objects. The best practice is to create all classes before creating the application GUI and to dispose them after the final MUI_DisposeObject() of the main Application object. Classes should be deleted in the reversed order of creation. MUI_DeleteCustomClass() returns a boolean value. It is FALSE when a class cannot be deleted because of potentially orphaned subclasses or objects.