Como desenvolver um módulo do sistema

Este documento apresenta como é o processo de desenvolvimento de um módulo do sistema.

Interface

O primeiro ponto a ser considerado no desenvolvimento de um módulo é a interface. Todo módulo deve implementar pelo menos uma interface e é através da interface que os outros módulos se utilizam dos serviços fornecidos pelo módulo.

O nome das interfaces implementadas por módulos do sistema está num espaço de nomes diferente do das interfaces implementadas por módulos biblioteca. Por isso, são denominadas de interfaces do sistema e podem ser usadas por módulos do usuário e do sistema.

Um módulo do sistema não deve implementar várias interfaces sem uma relação entre elas. Por exemplo, não seria bom ter um módulo que implementasse interfaces de gerenciamento de memória e de compactação de dados. São coisas diferentes que devem ficar em módulos diferentes, pois, assim, fica muito mais fácil atualizar um dos módulos sem interferir nos outros.

Se a interface já existe basta implementá-la.

No caso de não existir uma interface para os serviços que o módulo vai disponibilizar, um estudo precisa ser desenvolvido para criar uma nova interface.

A nova interface tem que ser suficientemente genérica para que as necessidades dos serviços que serão implementados por um módulo sejam satisfeitas. Por exemplo: no desenvolvimento de uma interface para o serviço de digitalização de imagens (scanning) esta não deve ser específica para um determinado modelo de aparelho, mas genérica para que todos os demais aparelhos possam utilizar a mesma interface.

Um exemplo de interface genérica é a BlockDev para dispositivos de bloco. Ela serve tanto para controladores de disquetes, HDs, CD-ROMs e até RAM-disks. Ela não é específica para nenhum dispositivo.

Para que isso aconteça, as pessoas envolvidas no desenvolvimento da interface têm que ter um conhecimento grande da maioria dos dispositivos que possam usar essa interface. É sempre bom utilizar a lista e os foruns de discussão para ter uma interface boa.

O desenvolvimento de uma interface significa especificar os protótipos das funções, o que cada uma espera como entrada, o que cada uma faz e o que cada uma retorna.

Algumas vezes uma interface pode especificar também estruturas usadas pelas funções da interface. Mas lembre-se: estruturas internas usadas pelo módulo não devem ser especificadas na interface nem estruturas que são manipuladas unicamente pelo módulo. Como exemplo veja a interface FSManager. O conteúdo do tipo de dados File não precisa ser conhecido pelos outros módulos.

Outro ponto importante são as funções disponíveis para os módulos do usuário (veja a definição na interface UserModManager). A maioria dos módulos fornece serviços para o sistema e também para os programas. Apesar das funções que são usadas pelos módulos do usuário poderem ser chamadas pelo sistema, muitas vezes elas não podem ser usadas devido as verificações que são feitas.

Por causa dessas verificações, muitas vezes, devem existir funções que são usadas pelo sistema e outras pelos módulos do usuário.

Toda função que é usada por módulos do usuário deve ser identificada com o tipo "usuário" e como padrão o nome deve começar com U. Por exemplo: UExit, ULoadLibrary, etc.

Como a linguagem C é utilizada para desenvolver os módulos, junto com a especificação da interface deve ser criado também um arquivo .h para permitir que outros módulos tenham acesso a interface.

Este é o arquivo .h da interface FSManager:

/*
*****************************************
* 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 */

Como a linguagem C é usada para desenvolver um módulo, cada função, cada tipo de dados e cada #define definido na interface contida no arquivo .h deve ser precedido pelo nome da interface para evitar conflitos entre funções, tipos de dados e #define definidos em outras interfaces.

Os #define FSManager__... são usados por módulos que utilizam a função de carregamento dinâmico de funções definida na interface SysModManager.

Como foi dito anteriormente, estruturas internas não precisam ser expostas e é por isso que existem estas linhas:

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

Elas declaram alguns tipos de dados que são utilizados pelas funções e estruturas de dados da interface, mas revelam apenas parte da estrutura - o começo dela.

Além disso, essas linhas estão entre #ifndef - #endif para que este mesmo arquivo .h possa ser incluído pelo módulo que implementa a interface, mas que define struct _FSManager_File de outra forma. Os tipos entre o #ifndef são usados pelos outros módulos que não têm uma definição própria.

Feita a especificação da interface, é hora de partir para a implementação.

Implementação duma interface

Feita a especificação da interface, é hora de partir para a implementação.

Ao contrário do que pode parecer, os nomes, tipos e estruturas usados por uma implementação não precisam ser os mesmos declarados no arquivo .h da interface.

Assim, apesar do arquivo .h definir o tipo _FSManager_File, a implementação usa o seguinte tipo:

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

Como se nota, na implementação a estrutura do tipo de dados (que é específica da implementação) é revelada e as funções podem ter outros tipos nas suas declarações (mantendo a compatibilidade de tamanho - int * para void *).

As únicas funções que merecem atenção especial são as funções do usuário e as funções de iniciação, se existirem.

As funções de iniciação merecem atenção especial por causa do modo como os módulos do sistema são iniciados.

Como não é possível saber se uma interface/implementação está neste ou naquele módulo, toda interface/implementação utilizada por um módulo do sistema deve ser utilizada na função StartModule da interface SysModManager.

Tipicamente, os módulos utilizados pelas funções da fase 0 são iniciados na função de iniciação da fase 0 já que ela é executada uma única vez. Do mesmo modo, os módulos utilizados pelas funções da fase 0 e da fase 1 são iniciados na função de iniciação da fase 1.

Esta é parte da função de iniciação da fase 1 do módulo que implementa a interface FSManager:

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;
    }

As funções do usuário têm um tratamento especial porque os parâmetros recebidos dos módulos do usuário devem sofrer várias verificações para impedir danos ao sistema.

Por exemplo: numa função do sistema Print(const char *texto); que é chamada por módulos do usuário para imprimir "texto" na tela, o parâmetro "texto" deve ser verificado com a função CheckPointer da interface UserModManager. Essa função vai dizer se "texto" está dentro da área de memória do usuário. Porque se não estiver, dados do sistema podem ser impressos.

Além disso, ao ler "texto" uma exceção de memória pode ocorrer (o ponteiro é inválido). Para tratar isso ou a função trata exceções de memória ou usa a função CopyFromUser da interface UserModManager para copiar os dados do usuário para uma área do sistema.

O mesmo pode ocorrer com ponteiros que são utilizados para passar dados do sistema para o usuário. Nesse caso ou a função trata exceções de memória ou usa a função CopyToUser da interface UserModManager para copiar os dados para a área de memória do usuário.

Outro aspecto importante no desenvolvimento de um módulo é o suporte aos diferentes níveis de depuração: DEBUG e DEBUG_ASSERT.

Para facilitar a implementação desses níveis de depuração, várias macros foram criadas. Essas macros podem ser acessadas usando-se o arquivo debug.h.

Este arquivo define as macros: DEBUG_MSG e DEBUG_MSG_PRE para o nível DEBUG e ASSERT_MSG, ASSERT_MSG_PRE e ASSERT_ERROR para o nível DEBUG_ASSERT. Todas essas macros utilizam a função DebugMsg da interface SysModManager para imprimir a mensagem de depuração.

A macro DEBUG_MSG recebe um único parâmetro e é usada da seguinte maneira:

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

Note que o parâmetro para a macro é: ("Erro de timeout. Var = %x\n", var)

Todo esse parâmetro é passado para a função DebugMsg da interface SysModManager.

A macro DEBUG_MSG_PRE recebe um único parâmetro do tipo inteiro e é usada da seguinte maneira:

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

O parâmetro é um código de erro e será usado numa mensagem padrão.

As macros ASSERT_MSG e ASSERT_MSG_PRE são iguais às macros DEBUG_MSG e DEBUG_MSG_PRE.

A macro ASSERT_ERROR recebe um único parâmetro inteiro e é usada para verificar se ocorreu erro da seguinte maneira:

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

Ela é usada com códigos de retorno de funções que não deveriam falhar.

O nível DEBUG inclui o nível DEBUG_ASSERT e é usado para imprimir informações que ajudem a depurar o módulo. Por exemplo: imprimir as teclas pressionas do teclado, imprimir uma mensagem quando ocorre timeout. Isso deve ser feito da seguinte maneira:

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

Assim, somente quando o módulo for compilado para o nível DEBUG esse código será incluido.

O nível DEBUG_ASSERT é usado para fazer verificações extensas. Verificar tudo que é assumido que vai funcionar. Por exemplo: verificar os parâmetros das funções, verificar o retorno de algumas funções que "não falham" (P, V, etc), etc.

Este é parte do código da função AllocObj da interface MemManager:

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

Neste exemplo, o parâmetro não pode ser NULL, se for algum módulo do sistema fez algo errado.

Esses diferentes níveis foram definidos para permitir compilar o mesmo módulo para finalidades diferentes. Isso facilita muito o desenvolvimento dos módulos, pois permite identificar os erros rapidamente. E quando tudo estiver solucionado basta usar um versão do módulo sem essas verificações.

Se o módulo tem uma implementação não trivial, faça também um arquivo .txt explicando as estruturas, funções internas e decisões tomadas na implementação. Isso ajuda na compreensão do módulo por outras pessoas.

Depois de feita a codificação é preciso criar um arquivo .spec que é usado pelo elf2mod para gerar o binário do módulo.

Agora basta testar e distribuir o módulo.

Outra coisa que deveria ser feita é um programa para testar o módulo. Com isso, sempre que alguma modificação fosse feita seria fácil testar o módulo.