How to develop a system module

This doc presents how to develop a system module.

Interface

The first thing to consider when developing a module is the interface. Every module must implement at least one interface and is through the use of an interface that modules use the services of a module.

The name of the interfaces implemented by system modules is in a different name space than those implemented by library modules. And so these interfaces are named system interfaces and they can be used by system and user modules.

A system module should not implement interfaces without a relationship between them. For instance, it isn't a good idea to have a module that implements memory management and data compress interfaces. They are different things that should be in different modules, cause, this way, it's easy to update one module without interfering with the other modules.

If an interface already exists you just need to implement it.

In case there isn't an interface for the service that the module will provide a new interface must be developed.

The new interface must be generic so it can satisfy the necessities of a module's services. For instance: an interface for scanning services should not be specific to a device, but generic so that all devices can use it too.

An example is the BlockDev interface for block devices. It is used for floppy disk, HDs, CD-ROMs controllers and even RAM-disks. It isn't a device specific interface.

To achieve this every one developing an interface must have very good knowledge of many devices that will use the interface. It's good to use the mailing lists and forums to share ideas.

The development of an interface is the specification of functions prototypes, what each one expects as input, what each one does and returns.

Some times an interface needs to specify data structures used by its functions too. But remember: internal data structures used by a module shouldn't be specified neither data structures manipulated only by the module. For instance, the FSManager interface doesn't specify File structure contents because other modules don't need to know them.

Another key point in an interface specification are the functions available for user modules (see definition in the UserModManager interface). A lot of modules provide services for the system and for programs too. Although functions that are used by user modules can be called by the system, many times they cannot be used by the system because these functions perform a lot of checks to be sure system won't be damage.

It's because of these checks that many times different functions must exist. One used by the system and other used by user modules.

Every function that is used by user modules must have "user" identification and, as standard, the name should start with U. For instance: UExit, ULoadLibrary, etc.

Because the C language is used to implement a module, a .h file must be generated together with the interface specification. This way other modules can use this interface.

This is the .h file of the FSManager interface:

/*
*****************************************
* Interface FSManager
*
* Author: Luiz Henrique Shigunov
* E-mail: luizshigunov@users.sourceforge.net
* WEB: http://www.sourceforge.net/projects/modulos
* Last change: 16/08/2002
*
* See the interface specification to know what each function does.
*
* Copyright (C) 2000-2002, Luiz Henrique Shigunov
*****************************************
*/

#if !defined(__FSMANAGER_H)
#define __FSMANAGER_H
#include "taskmanager.h"
#define FSManager__CLOSE 0x00
#define FSManager__CLOSEDIR 0x01
#define FSManager__MOUNT 0x02
#define FSManager__OPEN 0x03
#define FSManager__OPENDIR 0x04
#define FSManager__READ 0x05
#define FSManager__READDIR 0x06
#define FSManager__REMOVE 0x07
#define FSManager__REMOVEDIR 0x08
#define FSManager__REWINDDIR 0x09
#define FSManager__SEEK 0x0a
#define FSManager__STATUS 0x0b
#define FSManager__UNMOUNT 0x0c
#define FSManager__WRITE 0x0d
#define FSManager__UCLOSE 0x0e
#define FSManager__UCHANGEDIR 0x0f
#define FSManager__UCHANGEROOT 0x10
#define FSManager__UMOUNT 0x11
#define FSManager__UOPEN 0x12
#define FSManager__UOPENDIR 0x13
#define FSManager__UREAD 0x14
#define FSManager__UREADDIR 0x15
#define FSManager__UREMOVE 0x16
#define FSManager__UREMOVEDIR 0x17
#define FSManager__UREWINDDIR 0x17
#define FSManager__USEEK 0x18
#define FSManager__USTATUS 0x1a
#define FSManager__UUNMOUNT 0x1b
#define FSManager__UWRITE 0x1c
#define FSMANAGER_INAME_LEN 24
#ifndef __NO_FSMANAGER_TYPES
struct _FSManager_File {struct _FileSystem_File *file;};
struct _FSManager_Dir {struct _FileSystem_Dir *dir;};
#endif
typedef struct _FSManager_File FSManager_File;
typedef struct _FSManager_Dir FSManager_Dir;
typedef struct _FSManager_FS FSManager_FS;
typedef struct {unsigned short size;char name[1];} FSManager_DirEntry;
typedef struct {unsigned int mode;unsigned int nlink;unsigned int uid;unsigned int gid;int size;unsigned int blksize;unsigned int blocks;unsigned int atime;unsigned int mtime;unsigned int ctime;} FSManager_EntryStatus;
typedef struct {const char *str;unsigned int len;unsigned int hash;} FSManager_ExStr;
typedef struct FSManager__Dentry {unsigned int count;unsigned int mode;unsigned int flags;struct FSManager__Dentry *parent;FSManager_FS *fs;struct FSManager__Dentry *hashNext;struct FSManager__Dentry *hashPrior;struct FSManager__Dentry *lruNext;struct FSManager__Dentry *lruPrior;struct FSManager__Dentry *mounted;unsigned int version;TaskManager_Semaphore lock;FSManager_ExStr name;void *fsData;char iname[FSMANAGER_INAME_LEN];} FSManager_Dentry;
typedef struct {FSManager_File *file;int flags;void *buf;int count;} FSManager_FileIO;
typedef struct {FSManager_Dir *dir;int flags;FSManager_DirEntry *buf;int size;} FSManager_DirIO;
#include "filesystem.h"
/* system functions */
int FSManager_Close(FSManager_File*);
int FSManager_CloseDir(FSManager_Dir*);
int FSManager_Mount(const char*,const char*,unsigned int,const char*,int,const char*);
int FSManager_Open(const char*,int,FSManager_File**);
int FSManager_OpenDir(const char*,int,FSManager_Dir**);
int FSManager_Read(FSManager_FileIO*);
int FSManager_ReadDir(FSManager_DirIO*);
int FSManager_Remove(const char*);
int FSManager_RemoveDir(const char*);
int FSManager_RewindDir(FSManager_Dir*);
int FSManager_Seek(FSManager_File*,int,int,int*);
int FSManager_Status(const char*,FSManager_EntryStatus*,int);
int FSManager_Unmount(const char*,int);
int FSManager_Write(FSManager_FileIO*);
/* user functions */
int FSManager_UClose(int);
int FSManager_UChangeDir(const char*);
int FSManager_UChangeRoot(const char*);
int FSManager_UMount(const char*,const char*,unsigned int,const char*,int,const char*);
int FSManager_UOpen(const char*,int);
int FSManager_UOpenDir(const char*,int);
int FSManager_URead(int,void*,int);
int FSManager_UReadDir(int,FSManager_DirEntry*,int);
int FSManager_URemove(const char*);
int FSManager_URemoveDir(const char*);
int FSManager_URewindDir(int);
int FSManager_USeek(int,int,int);
int FSManager_UStatus(const char*,FSManager_EntryStatus*,int);
int FSManager_UUnmount(const char*,int);
int FSManager_UWrite(int,void*,int);
#endif /* __FSMANAGER_H */

Because the C language is used to implement a module, each function, data type and #define defined in the .h file must be preceded by the interface name to avoid conflicts with functions, data types and #define defined in other system interfaces.

#define FSManager__... are used by modules that use dynamic load of functions using SysModManager interface.

Like was said before, internal structures don't need to be exposed and that's why these lines exist:

#ifndef __NO_FSMANAGER_TYPES
struct _FSManager_File {struct _FileSystem_File *file;};
struct _FSManager_Dir {struct _FileSystem_Dir *dir;};
#endif

They define data types that are used by functions and data structures of the interface, but they show only part of the structure - the beginning of it.

Besides that, these lines are between #ifndef - #endif so this same .h file can be include by the module that implements this interface, but defines struct _FSManager_File in another way. The data types inside #ifndef are used by other modules that don't have their own definition.

With the interface specification it's time to implement it.

Interface implementation

With the interface specification it's time to implement it.

Differently than it can seems, the names, data types and structures used by an implementation don't need to be the same as defined in the .h file of the interface.

So, also the .h file defines the data type _FSManager_File, the implementation uses the following data type:

struct _FSManager_File {
    FileSystem_File *fsData;
    FSManager_FS *fs;
    int prop;
};

As one can see, in the implementation the data type structure (which is specific of the implementation) is showed and functions can have other data types in their declarations (keeping size compatibility - int * to void *).

Only user and start functions, if they exist, deserve a special attention.

The start functions deserve special attention because of the way system modules are started.

Because it's impossible to know if an interface/implementation is in this or that module, every interface/implementation used by a system module must be used with StartModule of the SysModManager interface.

Usually, all modules used by phase 0 functions are started at the phase 0 start function because it is executed only once. The same way, all modules used by phase 0 and phase 1 functions are started at the phase 1 start function.

This is part of the phase 1 start function of the module that implements the FSManager interface:

int Phase1Start(SysModManager_Module *modID) {
    char fsImp[64], devImp[64], data[64];
    CfgManager_Group *rootGroup;
    unsigned int unit, bufSize;
    int ret;

    ID = modID;
    /* we don't need to start UserModManager because we'll use it if it is started. */
    if ((ret = SysModManager_StartModule((unsigned int)MemMan_Copy)) ||
        (ret = SysModManager_StartModule((unsigned int)MemManager_Alloc)) ||
        (ret = SysModManager_StartModule((unsigned int)CfgManager_OpenGroup)) ||
        (ret = SysModManager_StartModule((unsigned int)TaskManager_P))) {
        DEBUG_MSG_PRE(ret);
        goto returnEnd;
    }

User functions have a special treatment because arguments received from user modules must be checked to prevent system damage.

For instance: in a system function Print(const char *text); that is called by user modules to print "text" in the screen, argument "text" must be checked with CheckPointer function from the UserModManager interface. This function will say if "text" is inside user memory area. Because if it isn't, system data can be printed.

Besides that, when you read data pointed by "text" a memory exception can occur (it's an invalid pointer). To handle this, a function can handle memory exceptions or it can use CopyFromUser function from the UserModManager interface to copy data from user space into system space.

The same can occur with pointers that are used to pass system data to user modules. In this case a function can handle memory exceptions or it can use CopyToUser function from the UserModManager interface to copy data from system space into user space.

Another important thing in a module development is the support of different debug levels: DEBUG and DEBUG_ASSERT.

Some macros were made to make the implementation of these levels easier. These macros can be used by including the debug.h file.

This file defines the macros: DEBUG_MSG and DEBUG_MSG_PRE for DEBUG level and ASSERT_MSG, ASSERT_MSG_PRE and ASSERT_ERROR for DEBUG_ASSERT level. All these macros use the DebugMsg function from SysModManager interface to print the debug message.

Macro DEBUG_MSG receives only one argument and is used this way:

if (timeout) {
    DEBUG_MSG(("Timeout error. Var = %x\n", var));
    return 1;
}

Note that the argument is: ("Timeout error. Var = %x\n", var)

All this argument is passed to function DebugMsg from SysModManager interface.

Macro DEBUG_MSG_PRE receives only one argument of type int and is used this way:

if (!path) {
    DEBUG_MSG_PRE(0);
    return 1;
}
ret = MemManager_i386_Free(ptr);
if (ret) {
    DEBUG_MSG_PRE(ret);
    return 1;
}

The argument is an error code that will be used in a standard message.

Macros ASSERT_MSG and ASSERT_MSG_PRE work the same way as macros DEBUG_MSG and DEBUG_MSG_PRE.

Macro ASSERT_ERROR receives only one argument of type int and is used to check if an error occured this way:

ret = MemManager_i386_Free(ptr);
ASSERT_ERROR(ret);

It's used with return codes of functions that shouldn't fail.

The DEBUG level includes the DEBUG_ASSERT level and is used to print informations that help to debug a module. For instance: print pressed keys of the keyboard, print a message when a timeout occurrs, etc. This should be done this way:

DEBUG_MSG(("Key: %x\n", key));
 ...

So, only when the module is compiled for DEBUG level this code will be included.

The DEBUG_ASSERT level is used to make extense checkings. Check everything that is assumed to work. For instance: check function arguments, check return code of some functions that "don't fail" (P, V, etc), etc.

This is part of the AllocObj function code from MemManager interface:

void *MemManager_AllocObj(MemManager_Cache *cache) {
    MemManager_Slab *slabMgt;
    char **freePtr;
    void *ret;

#ifdef DEBUG_ASSERT
    if (!cache) {
        ASSERT_MSG_PRE(0);
        return NULL;
    }
#endif

In this example, the argument can't be NULL, if it is a module has done something wrong.

These different levels were defined so one could compile the same module for different purposes. This helps a lot the development of modules, cause it allows fast error detection. And when everything is solved one just need to use a version of the module without these checkings.

If a module has an implementation not trivial also make a .txt file explaining its data structures, internal functions and design decision made in the implementation. This helps other people to understand the implementation.

After coding, a .spec file is needed. This file is used by elf2mod to generate a module binary.

Now you just need to test and make it available.

Another thing one should do is a program to test the module. This way, every time something changes would be easy to test the whole module.