General Rules and Purpose of Subclassing
From MorphOS Library
Grzegorz Kraszewski
Contents
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 M68k processor registers. This inconvenience comes with support for legacy M68k software and 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 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 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 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. In the example above, some methods of Area class are overridden. The naming scheme of method functions is an example, there are no constraints on this. Prefixing method functions 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 be always 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 subclassed usually, 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 size of the object data area in bytes. In most cases object data area is defined as a structure, so using 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 just dispatcher function address has been 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 when the class was really created. Calling it with NULL class pointer is deadly (hence the NULL check in the example).
- Do not delete a class until it has any 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 orphaned subclasses or objects.