Tratamento de erros em Linguagem C

Em 12/06/2010, em Linguagem C, por Sergio Prado

Trata­mento de erros (ou trata­mento de exceções) é algo essen­cial em qual­quer soft­ware, e prin­ci­pal­mente em sis­temas embar­ca­dos, onde pre­cisamos ter a garan­tia de um soft­ware robusto, con­fiável e a prova de falhas. 

Em C++ temos embu­tido na lin­guagem um mecan­ismo de trata­mento de exceções. Já na lin­guagem C não temos este mesmo mecan­ismo. Fica sob respon­s­abil­i­dade do pro­gra­mador desen­volver uma forma de iden­ti­ficar e tratar os erros da aplicação.

Exis­tem várias for­mas de realizar esta tarefa, depen­dendo da situ­ação, cada uma com seus prós e contras.

Vamos então ver aqui algu­mas das téc­ni­cas de iden­ti­fi­cação e trata­mento de erros em lin­guagem C.

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

Este é o mecan­ismo mais comum de trata­mento de erros em lin­guagem C. Neste método, uma função tem duas respon­s­abil­i­dades extras:

  1. Retornar um código indi­cando o resul­tado da sua execução.
  2. Ver­i­ficar 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 desen­volve­dor. Nor­mal­mente, eu tenho o cos­tume de retornar “0” em caso de sucesso e um inteiro pos­i­tivo ou neg­a­tivo em caso de erro. Você pode tam­bém retornar sem­pre “1” para sucesso e um inteiro neg­a­tivo em caso de erros. De qual­quer forma, o ideal é imple­men­tar um arquivo de cabeçalho (Ex: errors.h) e definir os códi­gos de erro lá den­tro, assim evi­ta­mos ambigu­idades nos códi­gos de retorno entre as funções.

Este mecan­ismo é rel­a­ti­va­mente sim­ples e até ele­gante, porém tem alguns pon­tos neg­a­tivos. Depen­dendo da com­plex­i­dade e do tamanho do código, fica difí­cil de ler e dar manutenção. Imag­ine uma função que chama outra função, que chama outra, e assim por diante. Pense na com­plex­i­dade que é geren­ciar todas estas chamadas e retornos. Depen­dendo do caso, o mel­hor é par­tir para out­ras soluções.

2. Retor­nando erros em var­iáveis globais

Sou total­mente con­tra o uso de var­iáveis globais, e acred­ito que a qual­i­dade de um soft­ware em C é inver­sa­mente pro­por­cional à quan­ti­dade de var­iáveis globais declaradas.

De qual­quer forma, este é um mecan­ismo muito usado na GLIBC, bib­lioteca padrão do sis­tema GNU/Linux, e que acaba sendo her­dado para out­ras bib­liote­cas, como a uclibc, usada bas­tante em sis­temas embar­ca­dos.

Neste mecan­ismo, as funções retor­nam “-1″ em caso de erro, e a var­iável global “errno” é setada com o código do erro. Pode-se usar a função str­error() para imprimir a men­sagem 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 prob­le­mas, den­tre eles é que por usar uma var­iável global, qual­quer função que ten­tar aces­sar a vari­avel “errno” não poderá ser reen­trante, a não ser que você pro­teja o acesso através de um MUTEX.

3. Usando a função assert()

A função assert(), disponível na maio­ria das bib­liote­cas C, é um tema que pode­ria ocu­par um post inteiro. Mas basi­ca­mente, esta função é uma macro cujo obje­tivo é aju­dar o pro­gra­mador a encon­trar bugs quando a apli­cação ainda está em desen­volvi­mento. Ela testa uma deter­mi­nada condição, e em caso de falha no teste, alerta o pro­gra­mador. Exemplo:

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

No exem­plo acima, a função assert() garante que a var­iável reg seja sem­pre maior que zero, caso con­trário a apli­cação aborta e uma men­sagem de erro é exibida em stderr (saída padrão de erro). Esta função pode ser uti­lizada em sis­temas que pos­suem um mecan­ismo de geren­ci­a­mento de streams de I/O, como con­soles nor­mal­mente pre­sentes em sis­temas com Linux Embar­cado. Em sis­temas mais sim­ples, esta função não tem muita util­i­dade, ape­sar de pos­si­bil­i­tar a imple­men­tação de algo para iden­ti­ficar erros de tempo de compilação.

4. Tratando sinais do sis­tema opera­cional

Outra forma de trata­mento de erros é através de sinais, que nada mais é do que um mecan­ismo de comu­ni­caçao entre o sis­tema opera­cional e a apli­cação. Exemplo:

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

Con­fig­u­ramos o sinal que quer­e­mos cap­turar com a função sig­nal(). Neste caso quer­e­mos que o sis­tema opera­cional exe­cute a função FPHan­dler() em caso de erros de oper­ações arit­méti­cas e ponto flu­tu­ante. E quando exe­cu­ta­mos uma divisão por zero, o erro é cap­turado pelo sis­tema opera­cional e a função FPHan­dler() é exe­cu­tada, exibindo a men­sagem de erro e abor­tando a aplicação.

5. Usando a palavra-chave “goto“

Já deixei claro aqui no blog que sou total­mente con­tra o uso de goto em lin­guagem C. Mas se existe alguma util­i­dade para a palavra-chave goto, esta util­i­dade está no trata­mento de erros.

Deixei proposi­tal­mente um erro no nosso primeiro exem­plo, onde faço um mal­loc() mas em situ­ação de erro retorno da função e não faço um free(). Vamos cor­ri­gir este prob­lema 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 prob­lema aqui: pre­cisamos chamar 3 vezes a função free(), uma para cada ponto de retorno. Se tivesse­mos mais pon­tos de retorno, teríamos que chamá-la mais vezes. E o risco de alguem esque­cer de fazer isso é muito grande. Mem­ory leak na certa!

Olhe agora uma solução para este prob­lema 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 lin­guagem C++ já pos­sui um mecan­ismo de trata­mento de exceções, através das palavras chave try/catch. Tem um artigo sobre trata­mento de exceções em C++ aqui. Porém, este mecan­ismo con­some CPU e nor­mal­mente tra­balha com alo­cação dinâmica de memória, que pode ser um prob­lema para o desen­volvi­mento de sis­temas embar­ca­dos.

E se pudesse­mos imple­men­tar este mesmo mecan­ismo em C, porém otimizado e sem o uso de mal­loc? A boa notí­cia é que isso já foi feito, e exis­tem algu­mas soluções disponíveis. Uma delas é o cex­cept.

Esta imple­men­tação usa as funções setjmp() e longjmp(), e define algu­mas macros para que pos­samos usar sim­u­lar as palavras-chave Throw, Try e Catch. Nosso exem­plo 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 retor­na­dos ou “lança­dos” para a função chamadora com Throw, e cap­tura­dos com Try e Catch, evi­tando o uso exces­sivo de if’s e return’s na apli­caçã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éc­ni­cas? Vocês uti­lizam algum outro método difer­ente dos apre­sen­ta­dos aqui?

Um abraço,

Ser­gio Prado

VN:F [1.9.17_1161]
Rat­ing: 9.6/10 (5 votes cast)
Trata­mento de erros em Lin­guagem C, 9.6 out of 10 based on 5 ratings

Posts rela­ciona­dos:

  1. Otimiza­ção de código em Lin­guagem C — Parte 1
  2. Otimiza­ção de código em Lin­guagem C — Parte 2
  • http://ctaste.blog.com/ Paulo

    Bom dia Ser­gio,
    primeira­mente gostaria de falar que o artigo esta muito legal e adi­cionar um comen­tário sobre esta téc­nica do goto.  Caso o desen­volve­dor use mal­loc mais de uma vez a imple­men­tação teria que ser um pouco difer­ente, porque no momento de lib­erar a memória seria necessário ver­i­ficar quem esta alo­cado, ou criar tags especi­fi­cas para cada goto. É óbvio isto, mas muitas vezes passa des­perce­bido, o que causaria um dou­ble free.
    não pode­ri­amos fazer isto, sem fazer uma val­i­dação.
    fim:

        free(var1);
        free(var2);
        free(var3
    );

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
    • http://www.sergioprado.org ser­gio­prado

      Ótima obser­vação Paulo!

      Um abraço!

      VA:F [1.9.17_1161]
      Rating: 0.0/5 (0 votes cast)
  • Pingback: Você usa goto nos seus códigos em C?

  • http://www.vivaolinux.com.br/~vinipsmaker VinIPS­maker

    Eu escrevi um artigo no vivaolinux que explora um mais pro­fun­da­mente o uso de setjmp/longjmp, acho que pode-lhe inter­es­sar:
    http://www.vivaolinux.com.br/artigo/Tratamento-de-excecoes-na-linguagem-C/
     
    E parabéns pelo post, é bem abrangente.

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • http://universemachine.wordpress.com Tiago Natel de Moura

    Òtimo artigo,
    eu pes­soal­mente tam­bém cos­tumo retornar um código de erro da função e uso goto sem­pre que é necessário oper­ações antes de retornar da função (desa­lo­car memória, fechar arquivos, etc).

    Bom ter fal­ado disso, porque ja peguei algu­mas bib­liote­cas que não tin­ham nen­huma padroniza­ção no trata­mento de excessões …

    Abraço

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)
  • eiji

    Gosto de definir tipos enu­mer­a­dos (enum) para definir as condições de retorno das min­has funções. Desta forma, posso definir enums com nomes que facili­tam a com­preen­são do código, ao invés de usar ape­nas números, char, etc.

    VA:F [1.9.17_1161]
    Rating: 0.0/5 (0 votes cast)