General Rules and Purpose of Subclassing
From MorphOS Library
Grzegorz Kraszewski
This article in other languages: Polish
Contents
Introduction
Subclassing is one of the 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 the Numeric one and others.
- Writing custom drawn gadgets or areas. The Area class is subclassed in this case.
Regardless of the reason for subclassing, it is always done in the same way. A programmer must write new methods or override existing 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 its 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 the 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 that a field is declared as protected or public). Object data defined in any of the superclasses may only be accessed using methods or attributes provided by these superclasses.
Because of internal BOOPSI design, the size of the data instance area is limited to 64 kB. Large buffers should be allocated dynamically in OM_NEW() and freed in OM_DISPOSE(). The area data is always cleared to all zeros at object creation. If a class 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 a 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 the IPTR type which means "integer big enough to hold a pointer". In the current MorphOS it is just a 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 the programmer. Some methods may have empty messages (containing only a method identifier), in this case the third argument may be omitted.
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.