How to develop a library module

This doc presents how to develop a library module.

Interface

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

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

A library 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 library will provide a new interface must be developed.

The new interface must be generic so it can satisfy the necessities of a library's services. For instance: an interface for data compression should be specific to an algorithm, but generic so that all algorithms can use it too.

To achieve this, everyone developing an interface must have very good knowledge of the subject. 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 library shouldn't be specified neither data structures manipulated only by the library.

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

This is the .h file of the Thread user interface:

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

#if !defined(__USER_THREAD_H)
#define __USER_THREAD_H
#define User_Thread__ALLOCTSD 0x03
#define User_Thread__CREATE 0x00
#define User_Thread__DESTROY 0x01
#define User_Thread__FREETSD 0x04
#define User_Thread__GETID 0x02
#define User_Thread__GETTSD 0x05
#define User_Thread__SETTSD 0x06
typedef struct {int dummy;} User_Thread_Thread;
int User_Thread_AllocTSD(unsigned int*,void(*)(void*));
int User_Thread_Create(int(*)(void*),void*,int,User_Thread_Thread**);
int User_Thread_Destroy(User_Thread_Thread*);
int User_Thread_FreeTSD(unsigned int);
User_Thread_Thread *User_Thread_GetID(void);
void *User_Thread_GetTSD(unsigned int);
int User_Thread_SetTSD(unsigned int,void*);
#endif /* __USER_THREAD_H */

Because the language used to implement a module is the C language each function, each data type and each #define defined in the .h file must be preceded by User_, to indicate that this is an user interface, and by the interface name too.

This is done to avoid conflicts with functions, data types and #define defined in other user and system interfaces.

#define User_Thread__... are used by user modules that use dinamic load of functions using UserModManager interface.

Like was said before, internal structures don't need to be exposed and that's why this line exists:

typedef struct {int dummy;} User_Thread_Thread;

It defines a data type that is used by the functions of the interface, but it doesn't show its structure.

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

Interface implementation

With the interface specification done 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 User_Thread_Thread, the implementation uses the following data type:

typedef struct _Thread {
    TaskManager_Thread *thread;
    int (*function)(void*);
    void *arg;
    struct _Thread *next;
    struct _Thread *prev;
    void *tsds[NUM_TSD];
} Thread;

And the functions are this way declared:

int Thread_AllocTSD(unsigned int *key, void *destructor);
int Thread_Create(int (*function)(void*), void *arg, int prop, Thread **id);

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 (*)(void) to void *).

One module's function - not interface's function - deserves special attention if it exists: the start function.

This function deserves special attention because of the way library modules are started.

An executable module uses the function UGetStartFunctionI from the UserModManager interface to start all staticaly used libraries. Besides, all libraries dynamicaly used by user modules must be started with function UGetStartFunction from the UserModManager interface.

So, the start function can be called more than once and some type of control must be done to avoid problems. This is the start function of the synchronization module:

int Start(void) {
    Thread *id;

    if (started)
        return E_OK;
    started = 1;
    stackSize = UserModManager_UGetStackSize();
    Rec_Init(&keysMutex, 0);
    Rec_Init(&threadsMutex, 0);
    /* start first thread struct */
    GET_THREAD_ID(id);
    id->thread = TaskManager_UGetThreadID();
    return E_OK;
}

As one can see, there's a control to know if the module was already started. It isn't necessary to worry about concurrently threads starting a module because the starting must be done when a module start and by only one thread.

If the start or finalization of the module is successfull, the function must return zero.

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

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. This should be done this way:

#ifdef DEBUG
    TV_Write("Key: ");
    ...
#endif

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" (mutex_lock, close, etc), etc. This should be done this way:

int MyFunction(int arg1, int arg2, char *p) {
#ifdef DEBUG_ASSERT
    if (!p) {
#ifdef DEBUG_ASSERT_MSG
        TV_Write("Arg p of function MyFunction invalid.\n");
#endif
        return E_BAD_VALUE;
    }
#endif

With DEBUG_ASSERT level, error messages should only be printed if DEBUG_ASSERT_MSG is defined. This way it's possible to make a lot of checkings without printing error messages.

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 library 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 elf2lmod to generate a library 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.