Module Development Guide

So you want to write a module? Before we start, let's cover some background first. A module can basically do two things: It can contain system classes (e.g. the FileSystem module contains the File class), and it can contain functions that perform certain procedures. The majority of existing system modules are focussed around classes, but in this tutorial we will be concentrating on producing a module that exports functions only. If you want to learn how to create class-based modules, we recommend that you read this tutorial first to grasp the basic concepts of module development, then move onto the Class Development Tutorial.

Registration Services

If you're going to write a new module, chances are that you are doing so to fulfil a need that isn't being met by the existing set of available modules. You should have a fair idea of what the module is going to be called, its overall focus and what some of the functions will be. Once you know the direction that you're heading in you're probably itching to get started - but don't charge in so carelessly. It is possible that:

If you're developing a module for private use only - i.e. you have no intention of releasing the specifications for third parties - then the noted issues will not concern you. However, if you are interested in releasing a module for public use and/or if you want an official stamp of approval, it will help if you notify Rocklyte Systems of your work. This basically entails sending a message to devsupport@rocklyte.com with the name of the module, a brief description of the services it will provide, and a short list of function names that may be implemented. You will then receive a message confirming that there are no conflicts with the name of the module and that no known projects exist that might conflict with your work. If any possible issues are detected then an explanation and list of noted conflicts will be provided to you so that you can make alterations or change your direction accordingly. If an identical project exists, consider getting involved with the responsible party rather than duplicating the work. This service is provided free of charge and whether you make the most of it is entirely your choice, but it can potentially save you from problems in the long-run.

Writing the Module

Once you've completed the necessary preparation, you can immediately start to write the module. The quickest way to get started is to use the module template included with the Pandora SDK, which you can find in the "pandora:modules/template/" directory. By using the template you will be able to create a compiled module in under half an hour of development work. From there, it will be just a matter of writing the functions that are going to be exported. As you might have some questions about the module file structure, in this section we will explain some of the finer points of module construction.

The Module Header

If you open the template module, or the source code to any other module in a text editor you should come across a structure such as the following in the first page of text:

   struct ModHeader ModHeader = {
      MODULE_HEADER_V1,
      CMDInit, NULL, CMDOpen, NULL,
      JMP_DEFAULT, JumpTableV1, CPU_PC, VER_ModuleVersion, VER_KERNEL,
      "Rocklyte Systems",
      "Copyright Rocklyte Systems 2001.  All rights reserved.",
      "April 2001",
      "Strings"
   };

This structure, known as the "module header" tells the object kernel how the module should be handled. For your benefit, we will describe the structure in detail using the following table:

FieldDescription
VersionThis is the version of the module header structure - set this to MODULE_HEADER_V1
InitThis field points to the module's initialisation routine (compulsory).
CloseThis field points to the module's close routine (optional).
OpenThis field points to the module's open routine (optional).
ExpungeThis field points to the module's expunge routine (optional).
KernelTableThe function table type to be generated for making calls to the object kernel is specified here. JMP_DEFAULT is sufficient for C/C++ code.
DefaultListThis field points to the default function list that is to be used for generating jump tables for programs that will use your module.
CPUNumberThe CPU that the module is compiled for is specified here, e.g. CPU_I386. Use CPU_PC to reflect the CPU used on your own machine. CPU's are defined in the "system/modules.h" include file.
ModVersionThe current version and revision of the module is specified here in floating point format - e.g. 1.4.
MinKernelVersionThe minimum version number required of the object kernel. If the version you specify is higher than the user's object kernel, the module will not be loaded for reasons of safety.
AuthorEnter your name, or the name of your employer or organisation here.
CopyrightFull copyright details associated with the module are entered here.
DateThe month and year of compilation is entered here, e.g. "April 2001".
NameThe name of the module must be typed in here.

Setting up the module structure correctly is important - when you make changes to it, exercise care or you could introduce subtle bugs that can be difficult to spot when you are developing a module for the first time.

Module Sub-Routines

You will notice that the module header contains four fields that refer to other functions within your module - Init, Open, Close and Expunge. It is compulsory to provide a routine for the Init field, while the other three are optional.

The Init routine is called when the module is loaded for the first time. If the module is loaded consecutive times the Init routine will not be called again as its purpose is to provide initialisation support only. Your Init routine will be passed a pointer to a jump-table that can be used to make function calls to the kernel. Because the jump-table is the only connection that you will have with the object kernel to begin with, you will have to store it so that you can make function calls. The following routine is a typical example of this:

   ERROR CMDInit(OBJECTPTR Module, struct KernelBase *argKernelBase)
   {
      KernelBase = argKernelBase;
      return(ERR_Okay);
   }

The Open routine is called each time the module is opened, including the first time that the module is initialised. You will be passed a pointer to the module object that the calling program used to open your module file. It is recommended that you set the FunctionList field in the Module object to the jump-table requested by the program opening your module. This becomes more important over time, because during the evolution of a module it is likely that functions may be added, as well as altered between versions. To stop backwards compatibility problems, you can assign different jump tables to each version of a module. Thus if a program requests a version 2 jump-table and your module is currently at version 5, you can send a version 2 jump-table back to the program. The following example shows what the Open routine should look like for the first version of your module:

   ERROR CMDOpen(OBJECTPTR Module)
   {
      SetField(Module, FID_FunctionList, FT_POINTER, JumpTableV1);
      return(ERR_Okay);
   }

The Close routine is called each time that the module is closed. You should support this routine only if you are performing extra activity in your Open code that needs to be 'undone' each time that the module is closed. In most cases, you can set this field to NULL.

The Expunge routine is called when the module is being unloaded from memory. You will need to support the expunge sequence if your Init routine allocated memory, created classes or used other resources that need to be destroyed when the module is removed from the system.

Writing the Functions

There are a couple of important factors that you need to be aware of when writing your functions. Firstly, 8 bit and 16 bit integer based arguments (e.g. BYTE and WORD types) are not permitted, although you can use pointers to those types. Floating point numbers are supported as 64-bit values (the DOUBLE type), which means that 32-bit values (the FLOAT type) are not a legal definition in this context. There are no restrictions on the use of 32-bit and 64-bit integers.

As you develop your module's functionality, you will need to add each function to your module's jump table(s). Here is an example of a jump table taken from the Strings module:

   struct Function JumpTableV1[] = {
      { IntToStr,  "IntToStr",  argsIntToStr },
      { StrShrink, "StrShrink", argsStrShrink },
      { StrExpand, "StrExpand", argsStrExpand },
      { StrInsert, "StrInsert", argsStrInsert },
      { NULL, NULL }
   };

When designing a function jump table, the order of the list is unimportant. However, you will need to create a matching header file that describes the jump table structurally, in the same order. In the above case the header file would be located in the "include/modules/strings.h" directory. The matching header file for our aforementioned jump table would be described as follows:

   #ifndef  MODULES_STRINGS_H
   #define  MODULES_STRINGS_H


   #ifndef  MAIN_H
   #include <pandora/main.h>
   #endif

   #ifndef LOCAL_STRINGS

   struct StringsBase {
      LONG   (*IntToStr)(LONG, STRING, LONG);
      LONG   (*StrShrink)(STRING, LONG, LONG);
      LONG   (*StrExpand)(STRING, LONG, LONG);
      void   (*StrInsert)(STRING, STRING, LONG);
   };

   #define IntToStr(a,b,c)  (StringsBase->IntToStr(a,b,c))
   #define StrInsert(a,b,c) (StringsBase->StrInsert(a,b,c))
   #define StrExpand(a,b,c) (StringsBase->StrExpand(a,b,c))
   #define StrShrink(a,b,c) (StringsBase->StrShrink(a,b,c))

   #endif /* LOCAL_STRINGS */
   #endif /* MODULES_STRINGS_H */

When creating the header file for your module, use the one provided in the template directory to get you started, as it will provide you with full instructions. Do not write a header file from scratch, and do not copy and paste information from other module headers.

Finally, some important information on describing your functions. You will notice that the jump table definitions require you to describe the arguments for each function. You are required to do this so that languages specially developed for Pandora can examine the functions provided by the module and build an impression of each function's synopsis. The function descriptions for the previously mentioned functions would be as follows:

   struct FunctionField argsIntToStr[] = {
      { "TotalChars", ARG_LONG },
      { "Integer",    ARG_LONG },
      { "String",     ARG_STRING },
      { "StringSize", ARG_LONG },
      { NULL, NULL }
   };

   struct FunctionField argsStrExpand[] = {
      { "NewLength",  ARG_LONG },
      { "String",     ARG_STRING },
      { "Position",   ARG_LONG },
      { "TotalChars", ARG_LONG },
      { NULL, NULL }
   };

   struct FunctionField argsStrInsert[] = {
      { "Void",        NULL },
      { "Insert",      ARG_STRING },
      { "Destination", ARG_STRING },
      { "Position",    ARG_LONG },
      { NULL, NULL }
   };

   struct FunctionField argsStrShrink[] = {
      { "NewLength",  ARG_LONG },
      { "String",     ARG_STRING },
      { "Position",   ARG_LONG },
      { "TotalChars", ARG_LONG },
      { NULL, NULL }
   };

The first entry in a function description is for the return type. If the function does not return a result, the correct definition is to use a string of "Void" and set the argument flags to NULL. Following the result type are the function arguments, which must match the function synopsis exactly. Failing to make an exact match can potentially crash some programs that might use your module. The available argument types defined by the ARG_ prefix are as follows:

TypeDescription
ARG_LONGA 32-bit integer value ranging from -2,147,483,647 to 2,147,483,648.
ARG_LARGEA 64-bit integer value.
ARG_PTRA standard 32-bit address space pointer.
ARG_STRINGA 32-bit address space pointer that refers to a null-terminated string.
ARG_DOUBLEA 64-bit floating point value with high level of accuracy.
ARG_OBJECTThis flag is sometimes set in conjunction with the ARG_LONG type. It indicates that the argument refers to an object ID.
ARG_PTRSIZEThis argument type can only be used if it follows an ARG_PTR type, and if the argument itself is intended to reflect the size of the buffer referred to by the previous ARG_PTR argument.
ARG_RESULTThis special flag is set in conjunction with the other data-based argument types. Example: If the user is required to supply a pointer to a LONG field in which the function will store a result, the correct argument definition will be ARG_RESULT|ARG_LONG|ARG_PTR. To make the definition of these argument types easier, ARG_PTRRESULT, ARG_LONGRESULT and ARG_FLOATRESULT macros are also available for use.

Writing the Documentation

After developing the module's functionality you'll be ready to write the documentation if you haven't already started. Assuming that your module will be available for other developers to use, the module should be self-documented using Rocklyte's existing standards. To read more about this topic, refer to the Documentation Guidelines section of this manual.

Public Distribution

How you release your module will be dependent on the network that you want to use for the module's distribution. If the module is part of a commercial venture then you may want to restrict its distribution to the commercial products that you are using it for. If it is intended for public use, we recommend that you start by uploading the binary and its accompanying documentation to the Rocklyte FTP Server, under the incoming directory. By doing so, users will be able to grab the binary from the central repository if they need to run a program that requires it. Other Pandora developers will also take a more active interest in the module's availability if it is advertised on the official site.

That ends the last phase of your module's development, bar one important factor which is to keep it updated and bug-free. How often you update the module really depends on a mixture of factors, including its popularity, stability and overall completeness. Generally you should revise the status of a module every 4 months. If you can't revise the module on a regular basis, consider releasing the source code into the public domain so that it can continue to be updated by third parties. This will ensure that developers that have been using the module are not left out in the cold when it needs to be updated. On a separate note, you may also want to release the code so that it can be compiled for other platforms if you want to see it in widespread use. Whether this is appropriate or not will depend on your circumstances, but if commercial interests are not a factor then it is a reasonable action to take.

Next: Class Development Guide


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