General Rules and Purpose of Subclassing

From MorphOS Library

Revision as of 08:09, 31 December 2010 by Krashan (talk | contribs) (Minor style changes.)

Grzegorz Kraszewski


Introduction

Subclassing is one of essential object oriented programming techniques. In MUI subclassing is used for the following purposes:

  • Implementing program functionality as a set of methods. The Application class is usually used for this purpose.
  • Customizing classes by writing methods intentionally left unimplemented (or having some default implementations) in standard MUI classes. The most common example is the List class, but also Numeric one and others.
  • Writing custom drawn gadgets or areas. The Area class is subclassed in this case.

Regardless of the reason of subclassing, it is always done in the same way. A programmer must write new or overridden methods, create a dispatcher function, define an instance data structure (an empty one in some cases), then create the class. It is worth noting, subclassing MUI classes is done the same as subclassing BOOPSI ones. The only difference is that MUI provides own functions for class creation and disposition.


Object Data

Object data are stored in a memory area automatically allocated for every object created. The object data area is used for storing attribute values and for internal variables and buffers. This area is usually defined as a structure. Size of the area is passed to MUI_CreateCustomClass() function. In a class hierarchy, every class may add its own contribution to the object data area. Unlike in C++, a class has no direct access to anything except its own data area. It can't access data defined in the superclass (In C++ it is possible if a field is declared as protected or public). Object data defined in any of superclasses may be only accessed using methods or attributes provided by these superclasses.

There is no limit on object data area size, other than system memory available. The area data is always cleared to all zeros at object creation. If a clas does not need any object instance data it can pass 0 as the area size to MUI_CreateCustomClass().


Writing Methods

A MUI method is just a plain C function, but with partially fixed prototype.

IPTR MethodName(Class *cl, Object *obj, MessageType *msg);

The method return value may be either integer or pointer to anything. That is why it uses IPTR type which has meaning of "integer big enough to hold a pointer". In current MorphOS it is just 32-bit integer (the same as LONG). If a method has no meaningful value to return, it can just return 0. Two first, fixed arguments are: pointer to the class and pointer to the object. The last one is a method message. When a method is being overridden, the type of message is determined by the superclass. For a new method, message type is defined by programmer. Some methods may have empty messages (containing only a method identifier), in this case the third argument may be ommitted.

Most of methods need the access to the object instance data. To get a pointer to the data area, one uses 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 superclass method. There are no implicit super method calls in MUI. The superclass method must be always called explicitly with 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 be not 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 a 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 DoSuperMethod() call.

The dispatcher is a case of hook function. It makes its calling convention independent on programming language. A disadvantage of this is that dispatcher's arguments are passed in virtual M680x0 processor registers. This inconvenience comes with support for legacy M680x0 software and allows for native PowerPC classes to be used by M680x0 applications and old M680x0 classes to be used by native applications. Being a hook, a dispatcher needs a 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 M680x0 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 also used when native application calls a native dispatcher. It introduces some overhead, but a negligible one. Many programmers prefer to hide these details behind a set of preprocessor macros, such macros have been not 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 align, 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 respective method. It is sually implemented as a switch statement. If only a few methods are implemented, it may be also 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 typecasted to a message structure of this particular method. Some programmers place the code of methods inside the switch statement directly, especially if methods are short and only a few.