Tratamento de erros em Linguagem C

- por Sergio Prado

Categorias: Linguagem C Tags: , ,

Tratamento de erros (ou tratamento de exceções) é algo essencial em qualquer software, e principalmente em sistemas embarcados, onde precisamos ter a garantia de um software robusto, confiável e a prova de falhas. 

Em C++ temos embutido na linguagem um mecanismo de tratamento de exceções. Já na linguagem C não temos este mesmo mecanismo. Fica sob responsabilidade do programador desenvolver uma forma de identificar e tratar os erros da aplicação.

Existem várias formas de realizar esta tarefa, dependendo da situação, cada uma com seus prós e contras.

Vamos então ver aqui algumas das técnicas de identificação e tratamento de erros em linguagem C.

1. Usando o código de retorno das funções

Este é o mecanismo mais comum de tratamento de erros em linguagem C. Neste método, uma função tem duas responsabilidades extras:

  1. Retornar um código indicando o resultado da sua execução.
  2. Verificar o retorno de todas as funções que ela chama.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int sendCmd(char *buf, int tam)
{
    if (tam < 5)
        return ERR_TAM;
 
    if ((buf = (char *)malloc(tam)) == NULL)
        return ERR_MALLOC;
 
    if (formatCmd(buf, tam))
        return ERR_FORMAT;
 
    if (txCmd(buf, tam))
        return ERR_TX;
 
    free(buf);
    return OK;
}
 
int send()
{
    char buf[10];
 
    return(sendCmd(buf, sizeof(buf)));
}

O que cada função deve retornar fica a cargo do desenvolvedor. Normalmente, eu tenho o costume de retornar “0” em caso de sucesso e um inteiro positivo ou negativo em caso de erro. Você pode também retornar sempre “1” para sucesso e um inteiro negativo em caso de erros. De qualquer forma, o ideal é implementar um arquivo de cabeçalho (Ex: errors.h) e definir os códigos de erro lá dentro, assim evitamos ambiguidades nos códigos de retorno entre as funções.

Este mecanismo é relativamente simples e até elegante, porém tem alguns pontos negativos. Dependendo da complexidade e do tamanho do código, fica difícil de ler e dar manutenção. Imagine uma função que chama outra função, que chama outra, e assim por diante. Pense na complexidade que é gerenciar todas estas chamadas e retornos. Dependendo do caso, o melhor é partir para outras soluções.

2. Retornando erros em variáveis globais

Sou totalmente contra o uso de variáveis globais, e acredito que a qualidade de um software em C é inversamente proporcional à quantidade de variáveis globais declaradas.

De qualquer forma, este é um mecanismo muito usado na GLIBC, biblioteca padrão do sistema GNU/Linux, e que acaba sendo herdado para outras bibliotecas, como a uclibc, usada bastante em sistemas embarcados.

Neste mecanismo, as funções retornam “-1” em caso de erro, e a variável global “errno” é setada com o código do erro. Pode-se usar a função strerror() para imprimir a mensagem de erro. 

1
2
3
if (read(fd, buf, sizeof(buf)) == -1) {
    printf("Read error [%d]: %s", errno, strerror(errno));
}

Esta solução tem alguns problemas, dentre eles é que por usar uma variável global, qualquer função que tentar acessar a variavel “errno” não poderá ser reentrante, a não ser que você proteja o acesso através de um MUTEX.

3. Usando a função assert()

A função assert(), disponível na maioria das bibliotecas C, é um tema que poderia ocupar um post inteiro. Mas basicamente, esta função é uma macro cujo objetivo é ajudar o programador a encontrar bugs quando a aplicação ainda está em desenvolvimento. Ela testa uma determinada condição, e em caso de falha no teste, alerta o programador. Exemplo:

1
2
3
4
5
6
int cfgE2PROM(int reg, char data)
{
    assert(reg > 0);
 
    setE2PROM(E2P_BASE_ADDR + reg, data);
}

No exemplo acima, a função assert() garante que a variável reg seja sempre maior que zero, caso contrário a aplicação aborta e uma mensagem de erro é exibida em stderr (saída padrão de erro). Esta função pode ser utilizada em sistemas que possuem um mecanismo de gerenciamento de streams de I/O, como consoles normalmente presentes em sistemas com Linux Embarcado. Em sistemas mais simples, esta função não tem muita utilidade, apesar de possibilitar a implementação de algo para identificar erros de tempo de compilação.

4. Tratando sinais do sistema operacional

Outra forma de tratamento de erros é através de sinais, que nada mais é do que um mecanismo de comunicaçao entre o sistema operacional e a aplicação. Exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include 
#include 
 
void FPHandler(int signum)
{
    printf("FP error!\n");
    abort();
}
 
main()
{
    int a;
 
    signal(SIGFPE, FPHandler);
 
    a = 10 / 0;
 
    return 0;
}

Configuramos o sinal que queremos capturar com a função signal(). Neste caso queremos que o sistema operacional execute a função FPHandler() em caso de erros de operações aritméticas e ponto flutuante. E quando executamos uma divisão por zero, o erro é capturado pelo sistema operacional e a função FPHandler() é executada, exibindo a mensagem de erro e abortando a aplicação.

5. Usando a palavra-chave “goto”

Já deixei claro aqui no blog que sou totalmente contra o uso de goto em linguagem C. Mas se existe alguma utilidade para a palavra-chave goto, esta utilidade está no tratamento de erros.

Deixei propositalmente um erro no nosso primeiro exemplo, onde faço um malloc() mas em situação de erro retorno da função e não faço um free(). Vamos corrigir este problema agora:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int sendCmd(char *buf, int tam)
{
    if (tam < 5)
        return ERR_TAM;
 
    if ((buf = (char *)malloc(tam)) == NULL)
        return ERR_MALLOC;
 
    if (formatCmd(buf, tam)) {
        free(buf);
        return ERR_FORMAT;
    }
 
    if (txCmd(buf, tam)) {
        free(buf);
        return ERR_TX;
    }
 
    free(buf);
    return OK;
}

Veja um problema aqui: precisamos chamar 3 vezes a função free(), uma para cada ponto de retorno. Se tivessemos mais pontos de retorno, teríamos que chamá-la mais vezes. E o risco de alguem esquecer de fazer isso é muito grande. Memory leak na certa!

Olhe agora uma solução para este problema usando “goto”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int sendCmd(char *buf, int tam)
{
    int ret = OK;
 
    if (tam < 5)
        return ERR_TAM;
 
    if ((buf = (char *)malloc(tam)) == NULL)
        return ERR_MALLOC;
 
    if (formatCmd(buf, tam)) {
        ret = ERR_FORMAT;
        goto fim;
    }
 
    if (txCmd(buf, tam)) {
        ret = ERR_TX;
        goto fim;
    }
 
fim:
    free(buf);
    return ret;
}

6. Usando as funções setjmp() and longjmp()

Citei no começo do post que a linguagem C++ já possui um mecanismo de tratamento de exceções, através das palavras chave try/catch. Tem um artigo sobre tratamento de exceções em C++ aqui. Porém, este mecanismo consome CPU e normalmente trabalha com alocação dinâmica de memória, que pode ser um problema para o desenvolvimento de sistemas embarcados.

E se pudessemos implementar este mesmo mecanismo em C, porém otimizado e sem o uso de malloc? A boa notícia é que isso já foi feito, e existem algumas soluções disponíveis. Uma delas é o cexcept.

Esta implementação usa as funções setjmp() e longjmp(), e define algumas macros para que possamos usar simular as palavras-chave Throw, Try e Catch. Nosso exemplo ficaria assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void sendCmd(char *buf, int tam)
{
    int e;
 
    if (tam < 5)
        Throw ERR_TAM;
 
    if ((buf = (char *)malloc(tam)) == NULL)
        Throw ERR_MALLOC;
 
    Try {
        formatCmd(buf, tam);
        txCmd(buf, tam);
        free(buf);
    }
 
    Catch (e) {
        free(buf);
        Throw e;
    }
}

Os erros são retornados ou “lançados” para a função chamadora com Throw, e capturados com Try e Catch, evitando o uso excessivo de if’s e return’s na aplicação. O código fica bem mais legível e fácil de dar manutenção.

E então, o que vocês acham destas técnicas? Vocês utilizam algum outro método diferente dos apresentados aqui?

Um abraço,

Sergio Prado

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.