Como desenvolver um módulo biblioteca

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

Interface

A primeira coisa a ser considerada no desenvolvimento de um módulo biblioteca é a interface. Todo módulo biblioteca deve implementar pelo menos uma interface e é através da interface que os outros módulos do usuário se utilizam dos serviços fornecidos pelo módulo biblioteca.

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

Um módulo biblioteca 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 biblioteca 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 biblioteca sejam satisfeitos. Por exemplo: no desenvolvimento de uma interface para o serviço de compactação de dados esta não deveria ser específica para um determinado algoritmo, mas genérica para que todos os demais algoritmos possam utilizar a mesma interface.

Para que isso aconteça, as pessoas envolvidas no desenvolvimento da interface têm que ter um bom conhecimento do assunto. É sempre bom utilizar a lista e os foruns de discussões 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. 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 a linguagem utilizada para desenvolver os módulos é a C, junto com a especificação da interface, um arquivo .h deve ser criado para permitir que outros módulos do usuário tenham acesso a interface.

Este é o arquivo .h da interface do usuário Thread:

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

Como a linguagem usada para desenvolver um módulo é a linguagem C, cada função, cada tipo de dados e cada #define definido na interface contida no arquivo .h deve ser precedido por User_, para indicar que é uma interface do usuário, e também pelo nome da interface.

Isso é feito para evitar conflitos entre funções, tipos de dados e #define definidos em outras interfaces do usuário e do sistema.

Os #define User_Thread__... são usados por módulos do usuários que utilizam a função de carregamento dinâmico de funções definida na interface UserModManager.

Como foi dito anteriormente, estruturas internas não precisam ser expostas e é por isso que existe esta linha:

typedef struct {int dummy;} User_Thread_Thread;

Ela declara um tipo de dados que é utilizado pelas funções da interface, mas não revela a sua estrutura.

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 começar 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 User_Thread_Thread, a implementação usa o seguinte tipo:

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

E as funções são assim declaradas:

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

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

Uma função do módulo - e não da interface - merece atenção especial se existir: a função de iniciação.

Essa função merece atenção especial por causa do modo como os módulos biblioteca são iniciados.

Um módulo executável usa a função UGetStartFunctionI da interface UserModManager para iniciar todas a bibliotecas usadas estaticamente. Além disso, toda biblioteca usada dinamicamente por módulos do usuário deve ser iniciada com a função UGetStartFunction da interface UserModManager.

Por causa disso, a função de iniciação pode ser chamada mais de uma vez e um controle deve ser feito para evitar problemas. Esta é a função de iniciação do módulo de sincronização:

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

Como se nota existe um controle para saber se o módulo já foi iniciado. Não é preciso se preocupar com várias linhas de execução (threads) iniciando um módulo porque a iniciação deve ser feita quando o módulo inicia e por apenas uma linha de execução.

Se a iniciação ou a finalização do módulo for bem sucedida, a função deve retornar zero.

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

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. Isso deve ser feito da seguinte maneira:

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

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" (mutex_lock, close, etc), etc. Isso deve ser feito da seguinte maneira:

int MyFunction(int arg1, int arg2, char *p) {
#ifdef DEBUG_ASSERT
    if (!p) {
#ifdef DEBUG_ASSERT_MSG
        TV_Write("Parametro p da funcao MyFunction invalido.\n");
#endif
        return E_BAD_VALUE;
    }
#endif

No nível DEBUG_ASSERT as mensagens de erro devem ser impressas somente quando DEBUG_ASSERT_MSG está definido. Assim é possivel fazer várias verificações sem imprimir erros.

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 biblioteca 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 biblioteca por outras pessoas.

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

Agora basta testar e distribuir o módulo biblioteca.

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.