Class Development Guide

Before reading this guide, please refer to the Module Development Guide and follow the first two steps required for module development. The information on this document assumes that you have a reasonable understanding of module construction, so make sure that you meet this requirement before continuing.

The Importance of Good Design

The most important issue to keep in mind from the very beginning is that the design of your class takes precedence above all else. Failure to come up with a good design structure is something that will always give you problems at a later stage, and mistakes can be easy to make. We tend to place a great deal of emphasis on adhering to good design methodologies as class development is far more complex than function development, which means that exercising care is extremely important. Class construction requires that:

You must be prepared to accept the fact that due to their complexity, your class design will be imperfect in the first phase, and a couple of iterations will be necessary to refine the design structure. If you want the class to be available to third party developers with our official approval, it must first go through a minimum testing and evaluation period of three months before it can be signed off as being stable.

To ease the process, register your class through devsupport@rocklyte.com early on. You will receive an official ID for the class, a suggested category, and in some cases, recommendations on its construction. We tend to take an active interest in class development, so if you keep us regularly informed you will receive further advice on the class' construction so that it blends in well with the existing set of Pandora classes.

Defining the Class Structure

There are two sets of definitions that you need to provide in order to define a class structure - the physical structure that you will use internally, and the virtual structure that will be used to present your class to external programs.

The physical structure is defined by a typical C struct. The following is an example taken from the File class:

   struct File {
      OBJECT_HEADER     /* Class header */
      LONG Position;    /* Current position in file */
      LONG Flags;       /* File opening flags */

      PRIVATE_FIELDS
      LONG Size;
   };

The structure must start with the object header. This is a pre-defined header that all classes must define for object management purposes. If the object header is not defined, then the class will crash any program that attempts to use it. For more information on the object header definition, refer to the Object Management In Depth document. Immediately after the object header, you can define public fields that are directly accessible through GetField() or SetField() functions, or via direct structure access. Direct access means that when a program accesses the field for read or write purposes it does so directly, and is not by-passed through any extra functionality as determined by the class. Public fields must be defined as 32-bit data types or larger (i.e. no BYTE or WORD sized values) because the object kernel functions have been optimised to deal with larger data types.

After the public fields, you can define the private fields that are only accessible by the class source code. In our example we declared the Size field as being private. There are no restrictions on the size of data types used for private fields, because they are used for internal use only.

After defining the physical structure you will need to declare the object definition that external programs will have access to. To do this you need to list all of the fields that are going to be available, including information on each field name, type, permissions and read/write functionality (in that order). The following example is taken from the File class:

   struct FieldArray FileFields[] = {
      { "Position",      0, FDF_LONG|FDF_R,       0,          NULL, NULL },
      { "Flags",         0, FDF_LONGFLAGS|FDF_RI, &FileFlags, NULL, NULL },
      /*** Virtual fields ***/
      { "Comment",       0, FDF_STRING|FDF_RW, 0, GET_Comment, SET_Comment },
      { "Date",          0, FDF_OBJECT|FDF_RW, 0, GET_Date,    SET_Date },
      { "DirectoryList", 0, FDF_POINTER|FDF_R, 0, GET_DirectoryList, NULL },
      { "FileList",      0, FDF_POINTER|FDF_R, 0, GET_FileList,    NULL },
      { "Location",      0, FDF_STRING|FDF_RI, 0, GET_Location,    SET_Location },
      { "Permissions",   0, FDF_LONG|FDF_RW,   0, GET_Permissions, SET_Permissions },
      { "Size",          0, FDF_LONG|FDF_R,    0, GET_Size, NULL },
      END_FIELD
   };

The first two parameters define the field name and the field ID. The field ID is reserved for special use - the object kernel will assign field ID's automatically to each field, so you should set this parameter to 0. The FDF flags define the datatype of each field, their permissions and other extra information. The parameter following these flags is an optional field that is used in conjunction with the type definitions in some cases.

The following tables list all FDF field types and what they are used for:

Data TypesDescription
LONGSet if the field is a 32-bit integer.
LARGESet if the field is a 64-bit integer.
FLOATSet if the field is a 32-bit floating point number.
DOUBLESet if the field is a 64-bit floating point number.
POINTERSet if the field points to a memory address. Note: If the field represents a string, use FDF_STRING rather than FDF_POINTER.
STRINGThis flag must be used if the field points to null-terminated string.
OBJECTIf the field points to an object, set this flag.
OBJECTIDIf the field refers to an object via an object ID, set this flag.
CHILDIf the field points to an object that forms a child of the class, set this flag.
VARIABLE  Set this flag if the field supports multiple data types. You must also elect a 'preferential' data type by specifying one of the aforementioned flags in conjunction with the FDF_VARIABLE setting.

 

Security FlagsDescription
READSet if the field is readable. This flag can be abbreviated to FDF_R, or in conjunction with the next two flags, FDF_RI and FDF_RW.
WRITESet if the field is writeable. This flag can be abbreviated to FDF_W.
INITSet if the field is writeable prior to initialisation. After initialisation, the field will no longer be writeable.

 

Special FlagsDescription
PERCENTAGEThis setting can be used in conjunction with integer and float types that represent percentages.
HEXSet this flag if the field only holds meaning when interpreted as a hexadecimal number.
FLAGSSet this flag if the field will represent a set of flags. You are required to set the option parameter to point to a flag definition list if you use this setting.
LOOKUPSet this flag if the field will represent a value of a lookup table. You are required to set the option parameter to a lookup definition list if you use this setting.

Note: You must set at least one of the data types and one of the security flags for each field definition, or the object kernel will refuse to create the class.

The last two parameters in the field definition list are for re-routing read and write operations through to functions that will manage the fields for your class. These are commonly known as Get and Set routines, because they ca only be called by the GetField() and SetField() function range. You may have noticed that the FieldArray structure that we defined earlier contained the comment "/*** Virtual Fields ***/", which was used to separate the first two fields from the following seven. A virtual field is defined as a field that is only accessible through Get and Set operations. Thus the word 'virtual' refers to the fact that these field types have no discernible physical location to programs that are accessing them, ensuring that they cannot divert past any of the functional restrictions that you need to impose. In many cases, virtual fields are used as an alternative to methods when data retrieval is involved.

The function synopsis for Get and Set routines differ according to their related data types. The following table illustrates the correct synopsis to use in each case:

Data TypeSynopsis
LONG, OBJECTIDERROR GET_Field(OBJECTPTR, LONG *)
ERROR SET_Field(OBJECTPTR, LONG *)
LARGEERROR GET_Field(OBJECTPTR, LARGE *)
ERROR SET_Field(OBJECTPTR, LARGE *)
FLOATERROR GET_Field(OBJECTPTR, FLOAT*)
ERROR SET_Field(OBJECTPTR, FLOAT *)
DOUBLEERROR GET_Field(OBJECTPTR, DOUBLE *)
ERROR SET_Field(OBJECTPTR, DOUBLE *)
POINTER, OBJECT, CHILDERROR GET_Field(OBJECTPTR, APTR *)
ERROR SET_Field(OBJECTPTR, APTR)
STRINGERROR GET_Field(OBJECTPTR, STRING *)
ERROR SET_Field(OBJECTPTR, STRING)
VARIABLE  ERROR GET_Field(OBJECTPTR, struct Variable *)
ERROR SET_Field(OBJECTPTR, struct Variable *)

Your Get and Set routines should always return ERR_Okay unless an error occurs during processing, in which case a relevant error code should be returned. The reasoning behind most of the synopsis are self-explanatory except for the variable type, which is a special case. If a program is trying to update one of your variable fields then it can send you any one of the recognised field types, and it is your job to interpret it. In most cases you will not be able to support every type, so you can exercise discretion in choosing what types you are going to support. Either way, you can find out what field type an external program has sent to you by checking the variable structure's Type field (which contains FDF flags), then read the relative data field from the variable structure and convert it to your preferred format. The variable structure is arranged as follows:

   struct Variable {
      LONG   Type;
      FLOAT  Float;
      LONG   Long;
      DOUBLE Double;
      APTR   Pointer;
   };

If a program attempts to read a variable field then you must return the information in the requested format by reading the Type field, then set either the Float, Long, Double or Pointer field as according to the request.

Returning Allocations From A Field

When a process reads a field using the GetField* range of functions, access to the object is direct and does not result in a resource context switch to that object. This means that if you allocate memory in a 'get' routine, the allocation will be tracked to the process that made the call, not the class object. This design is not consistent with other areas of the system, but is necessary in order to allow you to return resources to the caller and have that resource remain in the system even if the object is destroyed. In some circumstances however, you may need to allocate resources that are private to your object and not intended for return to the caller. In order to track allocated resources back to your object, use the SetContext() function and pass it your object address. After making the necessary allocation, call SetContext() again with the result of the previous function call so that you return the resource tracker to normal.

Note that when returning an allocated resource to the user via a 'get' function, you must provide adequate documentation, so that the developer will know that the resource needs to be freed manually.

Action and Method Support

After defining the class structure you need to consider what actions you are going to support. Generally you will want to support the Free, Init and NewObject actions, which form the back-bone of object management. There are roughly 60 pre-defined actions available, so to consider which of those are appropriate to support for your class type, please refer to the Action Support Guide.

Once you know what actions your class will support, you will need to create an action list. The following example illustrates a code segment taken from the File class:

   struct ActionArray FileActions[] = {
      { AC_AccessObject,  FILE_AccessObject },
      { AC_Free,          FILE_Free },
      { AC_Init,          FILE_Init },
      { AC_NewObject,     FILE_NewObject },
      { AC_Read,          FILE_Read },
      { AC_ReleaseObject, FILE_ReleaseObject },
      { AC_Seek,          FILE_Seek },
      { AC_Write,         FILE_Write },
      { NULL, NULL }
   };

The synopsis used for each action support function is based on the following template:

   ERROR CLASS_ActionName(struct ClassName *Self, APTR Args);

'ClassName' should refer to the structure that defines your class. The 'Args' parameter is defined according to the action, if it supports arguments. For instance, the Read action uses the argument structure 'acRead'. Example:

   ERROR FILE_Read(struct File *Self, struct acRead *Args);

If an action does not provide arguments, set the Args definition to "APTR Void". Note that each support function is required to return an error code. If the routine's execution is successful, return ERR_Okay. If a failure occurs, return one of the values listed in the Error Codes document.

The approach for defining methods is a lot more complex. Each method that you create needs to be defined in the header file associated with the class. This requires that you specify the name and ID for each method, with ID's starting at a value of -1 and working their way down. You will also need to define the argument structures used for each method. The following example from the File class' include header illustrates:

   #define MTF_ReadWord  -1
   #define MTF_ReadLong  -2
   #define MTF_Delete    -3
   #define MTF_MoveFile  -4
   #define MTF_CopyFile  -5

   struct mtReadWord { LONG Result; };
   struct mtReadLong { LONG Result; };
   struct mtMoveFile { STRING Destination; };
   struct mtCopyFile { STRING Destination; };

You also need to define each method as part of your class definition, or the object kernel will not allow external programs to call them. This involves creating a method list, as illustrated here:

   struct MethodArray FileMethods[] = {
      { MTF_CopyFile, FILE_CopyFile, "CopyFile", argsCopyFile, sizeof(struct mtCopyFile) },
      { MTF_Delete,   FILE_Delete,   "Delete",   NULL,         NULL },
      { MTF_MoveFile, FILE_MoveFile, "MoveFile", argsMoveFile, sizeof(struct mtMoveFile) },
      { MTF_ReadLong, FILE_ReadLong, "ReadLong", argsReadLong, sizeof(struct mtReadLong) },
      { MTF_ReadWord, FILE_ReadWord, "ReadWord", argsReadWord, sizeof(struct mtReadWord) },
      { NULL, NULL, NULL, NULL, NULL }
   };

The list arrangement is self-explanatory, but correctly defining the arguments for each method is extremely important. The definition format is identical to describing functions as outlined in the Module Development Guide, only you don't need to define the result type as methods always return a result of type ERROR. The following definitions apply to the methods that we've listed for the File class:

   struct FunctionField argsCopyFile[] = {
      {"Destination", ARG_STR },
      { NULL, NULL }
   };

   struct FunctionField argsMoveFile[] = {
      {"Destination", ARG_STR },
      { NULL, NULL }
   };

   struct FunctionField argsReadLong[] = {
      {"Data", ARG_LONGRESULT },
      { NULL, NULL }
   };

   struct FunctionField argsReadWord[] = {
      {"Data", ARG_LONGRESULT },
      { NULL, NULL }
   };

In this example the definitions were straight-forward, but more care is needed if your methods return or accept arguments that deal with large chunks of data (as an example, the Read and Write actions are routines that are based around buffered data). Designing these particular method types correctly is extremely important, mainly because the ActionMsg() function needs to be able to determine the size of buffers being sent or returned through the messaging system. If you fail to define data-based methods correctly, the messaging system will not interpret them as you might expect.

To create a correct definition for a method that writes data to a buffer provided by the user, you need to organise the arguments so that the buffer pointer argument is immediately followed by an integer that indicates the size of the buffer provided by the user. Take the following synopsis for example:

   ReadInformation(LONG Bytes, APTR Buffer, LONG BufferSize, LONG BytesRead);

You will notice that the BufferSize argument immediately follows the Buffer pointer. The correct argument definition for this synopsis is:

   struct FunctionField argsReadInformation[] = {
      { "Bytes",      ARG_LONG },
      { "Buffer",     ARG_PTRRESULT },
      { "BufferSize", ARG_PTRSIZE },
      { "BytesRead",  ARG_LONGRESULT },
      { NULL, NULL }
   };

To create a correct definition for a method that receives data from the user, the synopsis rules are identical to those already outlined, but the argument definition is slightly different. Example:

   WriteInformation(LONG Bytes, APTR Buffer, LONG BufferSize, LONG BytesRead);

This synopsis would be represented as:

   struct FunctionField argsWriteInformation[] = {
      { "Bytes",      ARG_LONG },
      { "Buffer",     ARG_PTR },
      { "BufferSize", ARG_PTRSIZE },
      { "BytesRead",  ARG_LONGRESULT },
      { NULL, NULL }
   };

If you couldn't spot the difference, it's in the second argument of the argument definition list. While it may seem insignificant, if the definition is incorrect then the messaging system will give your class and the calling program odd results. Therefore, take reasonable care in creating your definitions or you may introduce bugs that can be difficult to detect.

Initialising the Class

By this stage you've defined the class, but you still need to initialise it for the object kernel. Class initialisation is performed in the Init() routine of your module, either by using the CreateObject() function, or through combined use of NewObject() and the Init action. The following code segment illustrates how we might initialise our File class:

   CreateObject(ID_CLASS, NULL, (OBJECTPTR *)&FileClass, NULL,
      FID_BaseClassID, ID_FILE,
      FID_Version,     (LONG)VER_FILE,
      FID_Name,        "File",
      FID_Actions,     FileActions,
      FID_Methods,     FileMethods,
      FID_Fields,      FileFields,
      FID_Size,        sizeof(struct File),
      TAGEND);

You'll also need to free the class in the Expunge sequence of your module. For instance:

   ERROR CMDExpunge(void)
   {
      if (FileClass) { Action(AC_Free, FileClass, NULL); FileClass = NULL; }
      return(ERR_Okay);
   }

For more information on class initialisation, you may want to check the Class Manual to see what fields you can set when creating a new class.

Sub-Class Development

So far we've given you all the information necessary to create base-classes, but we have yet to mention sub-classes. A sub-class is typically created in situations where a base-class exists for generic use within a particular area of development, but the base-class itself only offers support for a specific type of data file or hardware. For instance, the Picture base-class is designed to support all image file types, yet it only supports the IFF file format. In order to support other file formats, sub-classes have to be developed which attach themselves to the Picture class and offer support for file types like PNG, JPEG, GIF and others.

Designing a sub-class is a fairly simple process as you won't need to define a class structure, and in the case of file support, you only need to support actions and methods that are related to data interpretation (typically Init, Query and SaveToObject actions are all that need to be supported). The rest you can leave to be handled by the base-class.

The following code segment shows how the PCX sub-class is initialised:

   CreateObject(ID_CLASS, NULL, (OBJECTPTR *)&PCXClass, NULL,
      FID_BaseClassID,     ID_PICTURE,
      FID_SubClassID,      ID_PCX,
      FID_Name,            "PCX",
      FID_FileExtension,   "pcx",
      FID_FileDescription, "PCX Picture",
      FID_Actions,         PCXActions,
      TAGEND);

Note the BaseClassID and SubClassID settings - you must set these correctly or the base-class will fail to recognise you, and in some cases the object kernel may refuse to initialise your sub-class. If you don't know the ID of your base-class, use the FindClass() function to obtain it, and if you don't have an official ID for the sub-class, allocate a dynamic one using the AllocateClassID() function.

That's all there is to sub-class development. If you'd like to see a working example, refer to the source code for the PCX sub-class.

System Recognition

So you've compiled the class for the first time and now you want to test it. There's one thing left to do - you need to register the class with the object kernel so that it knows where to load it from. Classes are registered in the "system:config/classes.cfg" file. Open the file in a text editor and read it. You'll notice that every class in the system is listed by name, and a location is specified that points directly to each class file. To register your class, all you need to do is add a new entry for it.

At a bare minimum you need to enter the class name and the location, as shown here:

   [MyClass]
   Location = /athene/system/classes/commands/myclass.so

If you've been given an official class ID, you must enter that too:

   [MyClass]
   ClassID  = 6200
   Location = /athene/system/classes/commands/myclass.so

If the class supports a particular file-type, enter the extension and header information as well. The entry for the Picture class illustrates:

   [Picture]
   ClassID   = 200
   Location  = /athene/system/classes/graphics/picture
   Extension = iff;ilbm;pic;picture
   Header    = FORM????ILBM

That's all you need to know for base-classes, but if you are adding a sub-class then you need to specify the ParentID. The following entry is for the JPEG class, which has the Picture class as a parent:

   [JPEG]
   ParentID  = 200
   ClassID   = 4900
   Location  = /athene/system/classes/graphics/jpeg
   Extension = jpg;jpeg;jfif;pic;picture

That's all there is to it. Once you've entered this information correctly, you can write a program to test your class (a DML script is usually sufficient) and continue developing it to a point of completion.

Public Distribution

When releasing your class to the public, the same advice from the Module Development Guide applies. Remember that if you are going for official endorsement then the three month stand-down period will apply from the first release, so you may be asked to make alterations on top of receiving third party suggestions. Once the stand-down period expires the class design will be considered stable enough for everyday use, leaving you to move onto updating the class at periodic intervals.


Copyright (c) 2000-2001 Rocklyte Systems. All rights reserved. Contact us at feedback@rocklyte.com