Cele i zasady tworzenia klas pochodnych

From MorphOS Library

Revision as of 09:38, 24 January 2011 by Krashan (talk | contribs) (Writing Methods: Translation in progress.)

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.

Most methods need access to the object instance data. To get a pointer to the data area, one uses the INST_DATA macro, defined in <intuition/classes.h>. An example below shows the macro usage:

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

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

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

If a method is an overridden method from a superclass, it may want to perform the superclass method. There are no implicit super method calls in MUI. The superclass method must always be called explicitly with the DoSuperMethodA() call:

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

The second form rebuilds the method message from variable arguments, and is used when the message is modified before calling the superclass method. The super method may be called in any place of the method, or may not be called at all. For MUI standard classes and methods, rules of calling super methods are described in the documentation and will be discussed later in this tutorial. For custom methods the question of calling a super method is up to the application programmer.

The Dispatcher

A dispatcher function is a kind of jump table for methods. When any method is called on an object (with DoMethod()), BOOPSI finds the dispatcher of the object's class and calls it. The dispatcher checks a method identifier, which is always the first field of any method message. Based on the identifier, a method is called. If a method is unknown to the class, the dispatcher should pass it to the superclass with the DoSuperMethod() call.

The dispatcher is a kind of hook function. It makes its calling convention independent of programming language. A disadvantage of this is that the dispatcher's arguments are passed in virtual M68k processor registers. This inconvenience allows support for legacy M68k software and also allows for native PowerPC classes to be used by M68k applications and old M68k classes to be used by native applications. Being a hook, a dispatcher needs an EmulLibEntry structure to be created and filled first. The structure is defined in <emul/emulinterface.h> and acts as a data gate between PowerPC native code and the M68k emulator.

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

Then the dispatcher is defined as follows:

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

  /* ... */

Arguments of the dispatcher are the same as arguments of a method. They are passed in virtual M680x0 processor address registers A0, A1 and A2 instead of being just arguments. The dispatcher's data gate is passed as an argument to MUI_CreateCustomClass(). The data gate is used even when a native application calls a native dispatcher. It introduces some overhead, but it's negligible. Many programmers prefer to hide these details behind a set of preprocessor macros, such macros have not been used here however, for better understanding.

The Msg type is a root type for all method messages. It defines a structure containing only the method identifier field (defined as ULONG). All following parameters have to keep the CPU stack aligned, as DoMethod() builds the message on the stack. It requires that every parameter is defined either as an IPTR or as a pointer.

After receiving arguments the dispatcher checks the method identifier from the message and jumps to the respective method. It is usually implemented as a switch statement. If only a few methods are implemented, it may also be an if/if else cascade. Here is a typical example:

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

For every method a message pointer is typecast to a message structure of this particular method. Some programmers place the method's code inside the switch statement directly, especially if methods are short and only a few. In the example above, some methods of Area class are overridden. The naming scheme used for the method functions is just an example, there are no constraints on this. Although prefixing method function names with a class name has an advantage of avoiding name conflicts between custom classes if method functions are not declared as static.


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.